Merge "Only fire text-changed for gr-textarea."
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index e2afbf5..114aa3a 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -1274,38 +1274,6 @@
----
-[[isarray]]
-isarray
-
-* isarray
-
-[[isarray_license]]
-----
-(MIT)
-
-Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>;
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
[[marked]]
marked
@@ -1363,7 +1331,7 @@
[[page]]
page
-* page
+* polygerrit-gr-page
[[page_license]]
----
@@ -1393,38 +1361,6 @@
----
-[[path-to-regexp]]
-path-to-regexp
-
-* path-to-regexp
-
-[[path-to-regexp_license]]
-----
-The MIT License (MIT)
-
-Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-----
-
-
[[resemblejs]]
resemblejs
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 8ccbcab..f8ca85b 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -4178,38 +4178,6 @@
----
-[[isarray]]
-isarray
-
-* isarray
-
-[[isarray_license]]
-----
-(MIT)
-
-Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>;
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
[[marked]]
marked
@@ -4267,7 +4235,7 @@
[[page]]
page
-* page
+* polygerrit-gr-page
[[page_license]]
----
@@ -4297,38 +4265,6 @@
----
-[[path-to-regexp]]
-path-to-regexp
-
-* path-to-regexp
-
-[[path-to-regexp_license]]
-----
-The MIT License (MIT)
-
-Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-----
-
-
[[resemblejs]]
resemblejs
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 9d237af..3c7ec2b 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -63,6 +63,7 @@
"//java/com/google/gerrit/pgm/util",
"//java/com/google/gerrit/truth",
"//java/com/google/gerrit/acceptance/config",
+ "//java/com/google/gerrit/acceptance/testsuite/group",
"//java/com/google/gerrit/acceptance/testsuite/project",
"//java/com/google/gerrit/server/fixes/testing",
"//java/com/google/gerrit/server/data",
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/BUILD b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
new file mode 100644
index 0000000..d4f1175
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
@@ -0,0 +1,25 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(default_testonly = 1)
+
+java_library(
+ name = "group",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/acceptance:function",
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/entities",
+ "//java/com/google/gerrit/exceptions",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib:jgit",
+ "//lib:jgit-junit",
+ "//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
+ "//lib/commons:lang3",
+ "//lib/guice",
+ ],
+)
diff --git a/java/com/google/gerrit/entities/KeyUtil.java b/java/com/google/gerrit/entities/KeyUtil.java
index 0f14cd9..4aec7ac 100644
--- a/java/com/google/gerrit/entities/KeyUtil.java
+++ b/java/com/google/gerrit/entities/KeyUtil.java
@@ -66,7 +66,13 @@
return r.toString();
}
- public static String decode(final String key) {
+ public static String decode(String key) {
+ // URLs use percentage encoding which replaces unsafe ASCII characters with a '%' followed by
+ // two hexadecimal digits. If there is '%' that is not followed by two hexadecimal digits
+ // the code below fails with an IllegalArgumentException. To prevent this replace any '%'
+ // that is not followed by two hexadecimal digits by "%25", which is the URL encoding for '%'.
+ key = key.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
+
if (key.indexOf('%') < 0) {
return key.replace('+', ' ');
}
diff --git a/java/com/google/gerrit/extensions/restapi/IdString.java b/java/com/google/gerrit/extensions/restapi/IdString.java
index b2538fa..a69919f 100644
--- a/java/com/google/gerrit/extensions/restapi/IdString.java
+++ b/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -38,7 +38,16 @@
/** Returns the decoded value of the string. */
public String get() {
- return Url.decode(urlEncoded);
+ String data = urlEncoded;
+
+ // URLs use percentage encoding which replaces unsafe ASCII characters with a '%' followed by
+ // two hexadecimal digits. If there is '%' that is not followed by two hexadecimal digits
+ // Url.decode(String) fails with an IllegalArgumentException. To prevent this replace any '%'
+ // that is not followed by two hexadecimal digits by "%25", which is the URL encoding for '%',
+ // before calling Url.decode(String).
+ data = data.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
+
+ return Url.decode(data);
}
/** Returns true if the string is the empty string. */
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
index b0e270c..2f063f6 100644
--- a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -103,6 +103,13 @@
memberships.put(user, membership);
}
+ /**
+ * Remove a the memberships of the given user. No-op if the user does not have any memberships.
+ */
+ public void removeMembershipsOf(Account.Id user) {
+ memberships.remove(user);
+ }
+
@Override
public boolean handles(AccountGroup.UUID uuid) {
if (uuid != null) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 622c4bf..03cdfaa 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -1291,7 +1291,8 @@
AccountGroup.UUID groupId = g.getUUID();
GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
- if (!(groupDescription instanceof GroupDescription.Internal)) {
+ if (!(groupDescription instanceof GroupDescription.Internal)
+ || containsExernalSubGroups((GroupDescription.Internal) groupDescription)) {
return new OwnerinPredicate(args.userFactory, groupId);
}
@@ -1314,7 +1315,8 @@
AccountGroup.UUID groupId = g.getUUID();
GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
- if (!(groupDescription instanceof GroupDescription.Internal)) {
+ if (!(groupDescription instanceof GroupDescription.Internal)
+ || containsExernalSubGroups((GroupDescription.Internal) groupDescription)) {
return new UploaderinPredicate(args.userFactory, groupId);
}
@@ -1687,6 +1689,22 @@
return Predicate.and(predicates);
}
+ private boolean containsExernalSubGroups(GroupDescription.Internal internalGroup)
+ throws IOException {
+ for (AccountGroup.UUID subGroupUuid : internalGroup.getSubgroups()) {
+ GroupDescription.Basic subGroupDescription = args.groupBackend.get(subGroupUuid);
+ if (!(subGroupDescription instanceof GroupDescription.Internal)) {
+ return true;
+ }
+ boolean containsExernalSubGroups =
+ containsExernalSubGroups((GroupDescription.Internal) subGroupDescription);
+ if (containsExernalSubGroups) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private Set<Account.Id> getMembers(AccountGroup.UUID g) throws IOException {
Set<Account.Id> accounts;
Set<Account.Id> allMembers =
@@ -1765,7 +1783,7 @@
return value;
}
- /** Returns {@link Account.Id} of the identified calling user. */
+ /** Returns {@link com.google.gerrit.entities.Account.Id} of the identified calling user. */
public Account.Id self() throws QueryParseException {
return args.getIdentifiedUser().getAccountId();
}
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 861fa00..e5234fe 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -15,6 +15,7 @@
runtime_deps = ["//java/com/google/gerrit/index/testing"],
deps = [
"//java/com/google/gerrit/acceptance/config",
+ "//java/com/google/gerrit/acceptance/testsuite/group",
"//java/com/google/gerrit/acceptance/testsuite/project",
"//java/com/google/gerrit/auth",
"//java/com/google/gerrit/common:annotations",
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 936b448..e86fd09 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -21,6 +21,8 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
import com.google.gerrit.auth.AuthModule;
@@ -279,6 +281,7 @@
install(new ConfigExperimentFeaturesModule());
bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
+ bind(GroupOperations.class).to(GroupOperationsImpl.class);
bind(TestGroupBackend.class).in(SINGLETON);
DynamicSet.bind(binder(), GroupBackend.class).to(TestGroupBackend.class);
}
diff --git a/javatests/com/google/gerrit/acceptance/BUILD b/javatests/com/google/gerrit/acceptance/BUILD
index 75c90f2..fe451c4 100644
--- a/javatests/com/google/gerrit/acceptance/BUILD
+++ b/javatests/com/google/gerrit/acceptance/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["**/*.java"]),
deps = [
"//java/com/google/gerrit/acceptance:lib",
+ "//java/com/google/gerrit/acceptance/testsuite/group",
"//java/com/google/gerrit/server/util/time",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//java/com/google/gerrit/truth",
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
index 06e24ab..779d8eb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
@@ -19,6 +19,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.restapi.IdString;
import org.junit.Test;
public class ChangeIdIT extends AbstractDaemonTest {
@@ -47,6 +48,13 @@
}
@Test
+ public void invalidProjectChangeNumberReturnsNotFound() throws Exception {
+ RestResponse res =
+ adminRestSession.get(changeDetail(IdString.fromDecoded("<%=FOO%>~1").encoded()));
+ res.assertNotFound();
+ }
+
+ @Test
public void changeNumberReturnsChange() throws Exception {
PushOneCommit.Result c = createChange();
RestResponse res = adminRestSession.get(changeDetail(getNumericChangeId(c.getChangeId())));
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index b94ea37..ca7c3c5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -109,4 +109,13 @@
.fromJson(r.getReader(), new TypeToken<Map<String, ProjectAccessInfo>>() {}.getType());
assertThat(infoByProject.keySet()).containsExactly(project.get());
}
+
+ @Test
+ public void listAccess_invalidProject() throws Exception {
+ String invalidProject = "<%=FOO%>";
+ RestResponse r =
+ adminRestSession.get("/access/?project=" + IdString.fromDecoded(invalidProject));
+ r.assertNotFound();
+ assertThat(r.getEntityContent()).isEqualTo(invalidProject);
+ }
}
diff --git a/javatests/com/google/gerrit/extensions/restapi/IdStringTest.java b/javatests/com/google/gerrit/extensions/restapi/IdStringTest.java
new file mode 100644
index 0000000..3a92864
--- /dev/null
+++ b/javatests/com/google/gerrit/extensions/restapi/IdStringTest.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 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.extensions.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/** Unit tests for {@link IdString}. */
+public class IdStringTest {
+ @Test
+ public void decodeStringWithPercentageThatIsNotFollowedByTwoHexadecimalDigits() throws Exception {
+ String s = "<%=FOO%>";
+ assertThat(IdString.fromUrl(s).get()).isEqualTo(s);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index fbf9c87..153c62c 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -51,6 +51,7 @@
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.FakeSubmitRule;
import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.RawInputUtil;
@@ -58,6 +59,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
@@ -204,6 +206,7 @@
@Inject protected AuthRequest.Factory authRequestFactory;
@Inject protected ExternalIdFactory externalIdFactory;
@Inject protected ProjectOperations projectOperations;
+ @Inject protected GroupOperations groupOperations;
@Inject private ProjectConfig.Factory projectConfigFactory;
@@ -783,6 +786,41 @@
assertQuery("ownerin:\"Registered Users\"", change3, change2, change1);
assertQuery("ownerin:\"Registered Users\" project:repo", change3, change2, change1);
assertQuery("ownerin:\"Registered Users\" status:merged", change3);
+
+ GroupDescription.Basic externalGroup = testGroupBackend.create("External Group");
+ try {
+ testGroupBackend.setMembershipsOf(
+ user2, new ListGroupMembership(ImmutableList.of(externalGroup.getGroupUUID())));
+
+ assertQuery(
+ "ownerin:\"" + TestGroupBackend.PREFIX + externalGroup.getName() + "\"",
+ change3,
+ change2);
+
+ String nameOfGroupThatContainsExternalGroupAsSubgroup = "test-group-1";
+ AccountGroup.UUID uuidOfGroupThatContainsExternalGroupAsSubgroup =
+ groupOperations
+ .newGroup()
+ .name(nameOfGroupThatContainsExternalGroupAsSubgroup)
+ .addSubgroup(externalGroup.getGroupUUID())
+ .create();
+ assertQuery(
+ "ownerin:\"" + nameOfGroupThatContainsExternalGroupAsSubgroup + "\"", change3, change2);
+
+ String nameOfGroupThatContainsExternalGroupAsSubSubgroup = "test-group-2";
+ groupOperations
+ .newGroup()
+ .name(nameOfGroupThatContainsExternalGroupAsSubSubgroup)
+ .addSubgroup(uuidOfGroupThatContainsExternalGroupAsSubgroup)
+ .create();
+ assertQuery(
+ "ownerin:\"" + nameOfGroupThatContainsExternalGroupAsSubSubgroup + "\"",
+ change3,
+ change2);
+ } finally {
+ testGroupBackend.removeMembershipsOf(user2);
+ testGroupBackend.remove(externalGroup.getGroupUUID());
+ }
}
@Test
@@ -799,6 +837,37 @@
assertQuery("uploaderin:Administrators");
assertQuery("uploaderin:\"Registered Users\"", change1);
+
+ GroupDescription.Basic externalGroup = testGroupBackend.create("External Group");
+ try {
+ testGroupBackend.setMembershipsOf(
+ user2, new ListGroupMembership(ImmutableList.of(externalGroup.getGroupUUID())));
+
+ assertQuery(
+ "uploaderin:\"" + TestGroupBackend.PREFIX + externalGroup.getName() + "\"", change1);
+
+ String nameOfGroupThatContainsExternalGroupAsSubgroup = "test-group-1";
+ AccountGroup.UUID uuidOfGroupThatContainsExternalGroupAsSubgroup =
+ groupOperations
+ .newGroup()
+ .name(nameOfGroupThatContainsExternalGroupAsSubgroup)
+ .addSubgroup(externalGroup.getGroupUUID())
+ .create();
+ assertQuery("uploaderin:\"" + nameOfGroupThatContainsExternalGroupAsSubgroup + "\"", change1);
+
+ String nameOfGroupThatContainsExternalGroupAsSubSubgroup = "test-group-2";
+ groupOperations
+ .newGroup()
+ .name(nameOfGroupThatContainsExternalGroupAsSubSubgroup)
+ .addSubgroup(uuidOfGroupThatContainsExternalGroupAsSubgroup)
+ .create();
+ assertQuery(
+ "uploaderin:\"" + nameOfGroupThatContainsExternalGroupAsSubSubgroup + "\"", change1);
+
+ } finally {
+ testGroupBackend.removeMembershipsOf(user2);
+ testGroupBackend.remove(externalGroup.getGroupUUID());
+ }
}
@Test
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 32a646e..57a3c4b 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -19,6 +19,7 @@
deps = [
"//java/com/google/gerrit/acceptance:lib",
"//java/com/google/gerrit/acceptance/config",
+ "//java/com/google/gerrit/acceptance/testsuite/group",
"//java/com/google/gerrit/acceptance/testsuite/project",
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index ae4aad9..be86863 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -93,6 +93,8 @@
FID = 'FID',
// WebVitals - Largest Contentful Paint (LCP): measures loading performance.
LCP = 'LCP',
+ // WebVitals - Interaction to Next Paint (INP): measures responsiveness
+ INP = 'INP',
}
export enum Interaction {
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 d4627e2..371e5b5 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
@@ -1259,7 +1259,7 @@
@click=${(e: MouseEvent) => {
// We don't want to handle clicks on the star or the <a> link.
// Calling `stopPropagation()` from the click handler of <a> is not an
- // option, because then the click does not reach the top-level page.js
+ // option, because then the click does not reach the top-level gr-page
// click handler and would result is a full page reload.
if ((e.target as HTMLElement)?.nodeName !== 'GR-BUTTON') return;
this.copyLinksDropdown?.toggleDropdown();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
new file mode 100644
index 0000000..3af8207
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
@@ -0,0 +1,365 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * This file was originally a copy of https://github.com/visionmedia/page.js.
+ * It was converted to TypeScript and stripped off lots of code that we don't
+ * need in Gerrit. Thus we reproduce the original LICENSE in js_licenses.txt.
+ */
+
+/**
+ * This is what registered routes have to provide, see `registerRoute()` and
+ * `registerExitRoute()`.
+ * `context` provides information about the matched parameters in the URL.
+ * Then you can decide to handle the route exclusively (not calling `next()`),
+ * or to pass it on to other registered routes. Normally you would not call
+ * `next()`, because your regex matching the URL was specific enough.
+ */
+export type PageCallback = (
+ context: PageContext,
+ next: PageNextCallback
+) => void;
+
+/** See comment on `PageCallback` above. */
+export type PageNextCallback = () => void;
+
+/** Options for starting the router. */
+export interface PageOptions {
+ /**
+ * Should the router inspect the current URL and dispatch it when the router
+ * is started? Default is `true`, but can be turned off for testing.
+ */
+ dispatch: boolean;
+
+ /**
+ * The base path of the application. For Gerrit this must be set to
+ * getBaseUrl().
+ */
+ base: string;
+}
+
+/**
+ * The browser `History` API allows `pushState()` to contain an arbitrary state
+ * object. Our router only sets `path` on the state and inspects it when
+ * handling `popstate` events. This interface is internal only.
+ */
+interface PageState {
+ path?: string;
+}
+
+const clickEvent = document.ontouchstart ? 'touchstart' : 'click';
+
+export class Page {
+ /**
+ * When a new URL is dispatched all these routes are called one after another.
+ * If a route decides that it wants to handle a URL, then it does not call
+ * next().
+ */
+ private entryRoutes: PageCallback[] = [];
+
+ /**
+ * Before a new URL is dispatched exit routes for the previous URL are called.
+ * They can clean up some state for example. But they could also prevent the
+ * user from navigating away (from within the app), if they don't call next().
+ */
+ private exitRoutes: PageCallback[] = [];
+
+ /**
+ * The path that is currently being dispatched. This is used, so that we can
+ * check whether a context is still valid, i.e. ctx.path === currentPath.
+ */
+ private currentPath = '';
+
+ /**
+ * The base path of the application. For Gerrit this must be set to
+ * getBaseUrl(). For example https://gerrit.wikimedia.org/ uses r/ as its
+ * base path.
+ */
+ private base = '';
+
+ /**
+ * Is set at the beginning of start() and stop(), so that you cannot start
+ * the routing twice.
+ */
+ private running = false;
+
+ /**
+ * Keeping around the previous context for being able to call exit routes
+ * after creating a new context.
+ */
+ private prevPageContext?: PageContext;
+
+ /**
+ * We don't want to handle popstate events before the document is loaded.
+ */
+ private documentLoaded = false;
+
+ start(options: PageOptions = {dispatch: true, base: ''}) {
+ if (this.running) return;
+ this.running = true;
+ this.base = options.base;
+
+ window.document.addEventListener(clickEvent, this.clickHandler);
+ window.addEventListener('load', this.loadHandler);
+ window.addEventListener('popstate', this.popStateHandler);
+ if (document.readyState === 'complete') this.documentLoaded = true;
+
+ if (options.dispatch) {
+ const loc = window.location;
+ this.replace(loc.pathname + loc.search + loc.hash);
+ }
+ }
+
+ stop() {
+ if (!this.running) return;
+ this.currentPath = '';
+ this.running = false;
+
+ window.document.removeEventListener(clickEvent, this.clickHandler);
+ window.removeEventListener('popstate', this.popStateHandler);
+ window.removeEventListener('load', this.loadHandler);
+ }
+
+ show(path: string, push = true) {
+ const ctx = new PageContext(path, {}, this.base);
+ const prev = this.prevPageContext;
+ this.prevPageContext = ctx;
+ this.currentPath = ctx.path;
+ this.dispatch(ctx, prev);
+ if (push && !ctx.preventPush) ctx.pushState();
+ }
+
+ redirect(to: string) {
+ setTimeout(() => this.replace(to), 0);
+ }
+
+ replace(path: string, state: PageState = {}, dispatch = true) {
+ const ctx = new PageContext(path, state, this.base);
+ const prev = this.prevPageContext;
+ this.prevPageContext = ctx;
+ this.currentPath = ctx.path;
+ ctx.replaceState(); // replace before dispatching, which may redirect
+ if (dispatch) this.dispatch(ctx, prev);
+ }
+
+ dispatch(ctx: PageContext, prev?: PageContext) {
+ let j = 0;
+ const nextExit = () => {
+ const fn = this.exitRoutes[j++];
+ // First call the exit routes of the previous context. Then proceed
+ // to the entry routes for the new context.
+ if (!fn) {
+ nextEnter();
+ return;
+ }
+ fn(prev!, nextExit);
+ };
+
+ let i = 0;
+ const nextEnter = () => {
+ const fn = this.entryRoutes[i++];
+
+ // Concurrency protection. The context is not valid anymore.
+ // Stop calling any further route handlers.
+ if (ctx.path !== this.currentPath) {
+ ctx.preventPush = true;
+ return;
+ }
+
+ // You must register a route that handles everything (.*) and does not
+ // call next().
+ if (!fn) throw new Error('No route has handled the URL.');
+
+ fn(ctx, nextEnter);
+ };
+
+ if (prev) {
+ nextExit();
+ } else {
+ nextEnter();
+ }
+ }
+
+ registerRoute(re: RegExp, fn: PageCallback) {
+ this.entryRoutes.push(createRoute(re, fn));
+ }
+
+ registerExitRoute(re: RegExp, fn: PageCallback) {
+ this.exitRoutes.push(createRoute(re, fn));
+ }
+
+ loadHandler = () => {
+ setTimeout(() => (this.documentLoaded = true), 0);
+ };
+
+ clickHandler = (e: MouseEvent | TouchEvent) => {
+ if ((e as MouseEvent).button !== 0) return;
+ if (e.metaKey || e.ctrlKey || e.shiftKey) return;
+ if (e.defaultPrevented) return;
+
+ let el = e.target as HTMLAnchorElement;
+ const eventPath = e.composedPath();
+ if (eventPath) {
+ for (let i = 0; i < eventPath.length; i++) {
+ const pathEl = eventPath[i] as HTMLAnchorElement;
+ if (!pathEl.nodeName) continue;
+ if (pathEl.nodeName.toUpperCase() !== 'A') continue;
+ if (!pathEl.href) continue;
+
+ el = pathEl;
+ break;
+ }
+ }
+
+ while (el && 'A' !== el.nodeName.toUpperCase())
+ el = el.parentNode as HTMLAnchorElement;
+ if (!el || 'A' !== el.nodeName.toUpperCase()) return;
+
+ if (el.hasAttribute('download') || el.getAttribute('rel') === 'external')
+ return;
+ const link = el.getAttribute('href');
+ if (samePath(el) && (el.hash || '#' === link)) return;
+ if (link && link.indexOf('mailto:') > -1) return;
+ if (el.target) return;
+ if (!sameOrigin(el.href)) return;
+
+ let path = el.pathname + el.search + (el.hash ?? '');
+ path = path[0] !== '/' ? '/' + path : path;
+
+ const orig = path;
+ if (path.indexOf(this.base) === 0) {
+ path = path.substr(this.base.length);
+ }
+ if (this.base && orig === path && window.location.protocol !== 'file:') {
+ return;
+ }
+ e.preventDefault();
+ this.show(orig);
+ };
+
+ popStateHandler = (e: PopStateEvent) => {
+ if (!this.documentLoaded) return;
+ if (e.state) {
+ const path = e.state.path;
+ this.replace(path, e.state);
+ } else {
+ const loc = window.location;
+ this.show(loc.pathname + loc.search + loc.hash, /* push */ false);
+ }
+ };
+}
+
+function sameOrigin(href: string) {
+ if (!href) return false;
+ const url = new URL(href, window.location.toString());
+ const loc = window.location;
+ return (
+ loc.protocol === url.protocol &&
+ loc.hostname === url.hostname &&
+ loc.port === url.port
+ );
+}
+
+function samePath(url: HTMLAnchorElement) {
+ const loc = window.location;
+ return url.pathname === loc.pathname && url.search === loc.search;
+}
+
+function escapeRegExp(s: string) {
+ return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
+}
+
+function decodeURIComponentString(val: string | undefined | null) {
+ if (!val) return '';
+ return decodeURIComponent(val.replace(/\+/g, ' '));
+}
+
+export class PageContext {
+ /**
+ * Includes everything: base, path, query and hash.
+ */
+ canonicalPath = '';
+
+ /**
+ * Does not include base path.
+ * Does not include hash.
+ * Includes query string.
+ */
+ path = '';
+
+ /** Does not include hash. */
+ querystring = '';
+
+ hash = '';
+
+ /**
+ * Regular expression matches of capturing groups. The first entry params[0]
+ * corresponds to the first capturing group. The entire matched string is not
+ * returned in this array.
+ */
+ params: string[] = [];
+
+ /**
+ * Prevents `show()` from eventually calling `pushState()`. For example if
+ * the current context is not "valid" anymore, i.e. the URL has changed in the
+ * meantime.
+ *
+ * This is router internal state. Do not use it from routes.
+ */
+ preventPush = false;
+
+ private title = '';
+
+ constructor(
+ path: string,
+ private readonly state: PageState = {},
+ pageBase = ''
+ ) {
+ this.title = window.document.title;
+
+ if ('/' === path[0] && 0 !== path.indexOf(pageBase)) path = pageBase + path;
+ this.canonicalPath = path;
+ const re = new RegExp('^' + escapeRegExp(pageBase));
+ this.path = path.replace(re, '') || '/';
+ this.state.path = path;
+
+ const i = path.indexOf('?');
+ this.querystring =
+ i !== -1 ? decodeURIComponentString(path.slice(i + 1)) : '';
+
+ // Does the path include a hash? If yes, then remove it from path and
+ // querystring.
+ if (this.path.indexOf('#') === -1) return;
+ const parts = this.path.split('#');
+ this.path = parts[0];
+ this.hash = decodeURIComponentString(parts[1]) || '';
+ this.querystring = this.querystring.split('#')[0];
+ }
+
+ pushState() {
+ window.history.pushState(this.state, this.title, this.canonicalPath);
+ }
+
+ replaceState() {
+ window.history.replaceState(this.state, this.title, this.canonicalPath);
+ }
+}
+
+function createRoute(re: RegExp, fn: Function) {
+ return (ctx: PageContext, next: Function) => {
+ const qsIndex = ctx.path.indexOf('?');
+ const pathname = qsIndex !== -1 ? ctx.path.slice(0, qsIndex) : ctx.path;
+ const matches = re.exec(decodeURIComponent(pathname));
+ if (matches) {
+ ctx.params = matches
+ .slice(1)
+ .map(match => decodeURIComponentString(match));
+ fn(ctx, next);
+ } else {
+ next();
+ }
+ };
+}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
new file mode 100644
index 0000000..d194bf55
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {html, assert, fixture, waitUntil} from '@open-wc/testing';
+import './gr-router';
+import {Page, PageContext} from './gr-page';
+
+suite('gr-page tests', () => {
+ let page: Page;
+
+ setup(() => {
+ page = new Page();
+ page.start({dispatch: false, base: ''});
+ });
+
+ teardown(() => {
+ page.stop();
+ });
+
+ test('click handler', async () => {
+ const spy = sinon.spy();
+ page.registerRoute(/\/settings/, spy);
+ const link = await fixture<HTMLAnchorElement>(
+ html`<a href="/settings"></a>`
+ );
+ link.click();
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('register route and exit', () => {
+ const handleA = sinon.spy();
+ const handleAExit = sinon.stub();
+ page.registerRoute(/\/A/, handleA);
+ page.registerExitRoute(/\/A/, handleAExit);
+
+ page.show('/A');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleAExit.callCount, 0);
+
+ page.show('/B');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleAExit.callCount, 1);
+ });
+
+ test('register, show, replace', () => {
+ const handleA = sinon.spy();
+ const handleB = sinon.spy();
+ page.registerRoute(/\/A/, handleA);
+ page.registerRoute(/\/B/, handleB);
+
+ page.show('/A');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleB.callCount, 0);
+
+ page.show('/B');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleB.callCount, 1);
+
+ page.replace('/A');
+ assert.equal(handleA.callCount, 2);
+ assert.equal(handleB.callCount, 1);
+
+ page.replace('/B');
+ assert.equal(handleA.callCount, 2);
+ assert.equal(handleB.callCount, 2);
+ });
+
+ test('popstate browser back', async () => {
+ const handleA = sinon.spy();
+ const handleB = sinon.spy();
+ page.registerRoute(/\/A/, handleA);
+ page.registerRoute(/\/B/, handleB);
+
+ page.show('/A');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleB.callCount, 0);
+
+ page.show('/B');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleB.callCount, 1);
+
+ window.history.back();
+ await waitUntil(() => window.location.href.includes('/A'));
+ assert.equal(handleA.callCount, 2);
+ assert.equal(handleB.callCount, 1);
+ });
+
+ test('register pattern, check context', async () => {
+ let context: PageContext;
+ const handler = (ctx: PageContext) => (context = ctx);
+ page.registerRoute(/\/asdf\/(.*)\/qwer\/(.*)\//, handler);
+ page.stop();
+ page.start({dispatch: false, base: '/base'});
+
+ page.show('/base/asdf/1234/qwer/abcd/');
+
+ await waitUntil(() => !!context);
+ assert.equal(context!.canonicalPath, '/base/asdf/1234/qwer/abcd/');
+ assert.equal(context!.path, '/asdf/1234/qwer/abcd/');
+ assert.equal(context!.querystring, '');
+ assert.equal(context!.hash, '');
+ assert.equal(context!.params[0], '1234');
+ assert.equal(context!.params[1], 'abcd');
+
+ page.show('/asdf//qwer////?a=b#go');
+
+ await waitUntil(() => !!context);
+ assert.equal(context!.canonicalPath, '/base/asdf//qwer////?a=b#go');
+ assert.equal(context!.path, '/asdf//qwer////?a=b');
+ assert.equal(context!.querystring, 'a=b');
+ assert.equal(context!.hash, 'go');
+ assert.equal(context!.params[0], '');
+ assert.equal(context!.params[1], '//');
+ });
+});
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 c0433dd2..4f7d56f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -3,12 +3,7 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {
- Options,
- page,
- PageContext,
- PageNextCallback,
-} from '../../../utils/page-wrapper-utils';
+import {Page, PageOptions, PageContext, PageNextCallback} from './gr-page';
import {NavigationService} from '../gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
import {
@@ -108,7 +103,7 @@
// TODO: Move all patterns to view model files and use the `Route` interface,
// which will enforce using `RegExp` in its `urlPattern` property.
const RoutePattern = {
- ROOT: '/',
+ ROOT: /^\/$/,
DASHBOARD: /^\/dashboard\/(.+)$/,
CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
@@ -302,7 +297,7 @@
private view?: GerritView;
- readonly page = page.create();
+ readonly page = new Page();
constructor(
private readonly reporting: ReportingService,
@@ -340,12 +335,7 @@
}
if (browserUrl.toString() !== stateUrl.toString()) {
- this.page.replace(
- stateUrl.toString(),
- null,
- /* init: */ false,
- /* dispatch: */ false
- );
+ this.page.replace(stateUrl.toString(), {}, /* dispatch: */ false);
}
}),
this.routerModel.routerView$.subscribe(view => (this.view = view)),
@@ -429,13 +419,13 @@
*/
redirectToLogin(returnUrl: string) {
const basePath = getBaseUrl() || '';
- this.page(
+ this.setUrl(
'/login/' + encodeURIComponent(returnUrl.substring(basePath.length))
);
}
/**
- * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
+ * Hashes parsed by gr-page exclude "inner" hashes, so a URL like "/a#b#c"
* is parsed to have a hash of "b" rather than "b#c". Instead, this method
* parses hashes correctly. Will return an empty string if there is no hash.
*
@@ -464,18 +454,18 @@
* @return A promise yielding the original route ctx
* (if it resolves).
*/
- redirectIfNotLoggedIn(ctx: PageContext) {
+ redirectIfNotLoggedIn(path: string) {
return this.restApiService.getLoggedIn().then(loggedIn => {
if (loggedIn) {
return Promise.resolve();
} else {
- this.redirectToLogin(ctx.canonicalPath);
+ this.redirectToLogin(path);
return Promise.reject(new Error());
}
});
}
- /** Page.js middleware that warms the REST API's logged-in cache line. */
+ /** gr-page middleware that warms the REST API's logged-in cache line. */
private loadUserMiddleware(_: PageContext, next: PageNextCallback) {
this.restApiService.getLoggedIn().then(() => {
next();
@@ -485,11 +475,10 @@
/**
* Map a route to a method on the router.
*
- * @param pattern The page.js pattern for the route.
+ * @param pattern The regex pattern for the route.
* @param handlerName The method name for the handler. If the
* route is matched, the handler will be executed with `this` referring
- * to the component. Its return value will be discarded so that it does
- * not interfere with page.js.
+ * to the component. Its return value will be discarded.
* TODO: Get rid of this parameter. This is really not something that the
* router wants to be concerned with. The reporting service and the view
* models should figure that out between themselves.
@@ -499,24 +488,23 @@
* redirect specifies the matched URL to be used after successful auth.
*/
mapRoute(
- pattern: string | RegExp,
+ pattern: RegExp,
handlerName: string,
handler: (ctx: PageContext) => void,
authRedirect?: boolean
) {
- this.page(
- pattern,
- (ctx, next) => this.loadUserMiddleware(ctx, next),
- ctx => {
- this.reporting.locationChanged(handlerName);
- const promise = authRedirect
- ? this.redirectIfNotLoggedIn(ctx)
- : Promise.resolve();
- promise.then(() => {
- handler(ctx);
- });
- }
+ this.page.registerRoute(pattern, (ctx, next) =>
+ this.loadUserMiddleware(ctx, next)
);
+ this.page.registerRoute(pattern, ctx => {
+ this.reporting.locationChanged(handlerName);
+ const promise = authRedirect
+ ? this.redirectIfNotLoggedIn(ctx.canonicalPath)
+ : Promise.resolve();
+ promise.then(() => {
+ handler(ctx);
+ });
+ });
}
/**
@@ -583,16 +571,11 @@
}
_testOnly_startRouter() {
- this.startRouter({dispatch: false, popstate: false});
+ this.startRouter({dispatch: false, base: getBaseUrl()});
}
- startRouter(opts: Options = {}) {
- const base = getBaseUrl();
- if (base) {
- this.page.base(base);
- }
-
- this.page.exit('*', (_, next) => {
+ startRouter(opts: PageOptions = {dispatch: true, base: getBaseUrl()}) {
+ this.page.registerExitRoute(/(.*)/, (_, next) => {
if (!this._isRedirecting) {
this.reporting.beforeLocationChanged();
}
@@ -603,7 +586,7 @@
// Remove the tracking param 'usp' (User Source Parameter) from the URL,
// just to have users look at cleaner URLs.
- this.page((ctx, next) => {
+ this.page.registerRoute(/(.*)/, (ctx, next) => {
if (window.URLSearchParams) {
const pathname = toPathname(ctx.canonicalPath);
const searchParams = toSearchParams(ctx.canonicalPath);
@@ -619,7 +602,7 @@
});
// Middleware
- this.page((ctx, next) => {
+ this.page.registerRoute(/(.*)/, (ctx, next) => {
document.body.scrollTop = 0;
if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
@@ -937,7 +920,7 @@
// For backward compatibility with GWT links.
if (hash) {
// In certain login flows the server may redirect to a hash without
- // a leading slash, which page.js doesn't handle correctly.
+ // a leading slash, which gr-page doesn't handle correctly.
if (hash[0] !== '/') {
hash = '/' + hash;
}
@@ -1087,7 +1070,7 @@
const state: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.GROUPS,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
openCreateModal:
!ctx.params[1] && !ctx.params[2] && ctx.hash === 'create',
@@ -1184,7 +1167,7 @@
view: GerritView.REPO,
detail: RepoDetailView.BRANCHES,
repo: ctx.params[0] as RepoName,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
};
// Note that router model view must be updated before view models.
@@ -1197,7 +1180,7 @@
view: GerritView.REPO,
detail: RepoDetailView.TAGS,
repo: ctx.params[0] as RepoName,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
};
// Note that router model view must be updated before view models.
@@ -1209,7 +1192,7 @@
const state: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.REPOS,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
openCreateModal:
!ctx.params[1] && !ctx.params[2] && ctx.hash === 'create',
@@ -1239,7 +1222,7 @@
const state: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.PLUGINS,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
};
// Note that router model view must be updated before view models.
@@ -1251,9 +1234,8 @@
const state: SearchViewState = {
view: GerritView.SEARCH,
query: ctx.params[0],
- offset: ctx.params[2],
+ offset: ctx.params[2] || '0',
loading: false,
- changes: [],
};
// Note that router model view must be updated before view models.
this.setState(state as AppElementParams);
@@ -1267,9 +1249,8 @@
const state: SearchViewState = {
view: GerritView.SEARCH,
query: ctx.params[0],
- offset: undefined,
+ offset: '0',
loading: false,
- changes: [],
};
// Note that router model view must be updated before view models.
this.setState(state as AppElementParams);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 87d100c..c4cb465 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -5,7 +5,7 @@
*/
import '../../../test/common-test-setup';
import './gr-router';
-import {Page, PageContext} from '../../../utils/page-wrapper-utils';
+import {Page, PageContext} from './gr-page';
import {
stubBaseUrl,
stubRestApi,
@@ -125,7 +125,6 @@
const requiresAuth: any = {};
const doesNotRequireAuth: any = {};
sinon.stub(page, 'start');
- sinon.stub(page, 'base');
sinon
.stub(router, 'mapRoute')
.callsFake((_pattern, methodName, _method, usesAuth) => {
@@ -212,20 +211,8 @@
test('redirectIfNotLoggedIn while logged in', () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(true));
- const ctx = {
- save() {},
- handled: true,
- canonicalPath: '',
- path: '',
- querystring: '',
- pathname: '',
- state: '',
- title: '',
- hash: '',
- params: {test: 'test'},
- };
const redirectStub = sinon.stub(router, 'redirectToLogin');
- return router.redirectIfNotLoggedIn(ctx).then(() => {
+ return router.redirectIfNotLoggedIn('somepath').then(() => {
assert.isFalse(redirectStub.called);
});
});
@@ -233,21 +220,9 @@
test('redirectIfNotLoggedIn while logged out', () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
const redirectStub = sinon.stub(router, 'redirectToLogin');
- const ctx = {
- save() {},
- handled: true,
- canonicalPath: '',
- path: '',
- querystring: '',
- pathname: '',
- state: '',
- title: '',
- hash: '',
- params: {test: 'test'},
- };
return new Promise(resolve => {
router
- .redirectIfNotLoggedIn(ctx)
+ .redirectIfNotLoggedIn('somepath')
.then(() => {
assert.isTrue(false, 'Should never execute');
})
@@ -321,17 +296,6 @@
await waitUntilCalled(handlePassThroughRoute, 'handlePassThroughRoute');
}
- function createPageContext(): PageContext {
- return {
- canonicalPath: '',
- path: '',
- querystring: '',
- pathname: '',
- hash: '',
- params: {},
- };
- }
-
setup(() => {
stubRestApi('setInProjectLookup');
redirectStub = sinon.stub(router, 'redirect');
@@ -395,9 +359,8 @@
) => {
onExit = _onExit;
};
- sinon.stub(page, 'exit').callsFake(onRegisteringExit);
+ sinon.stub(page, 'registerExitRoute').callsFake(onRegisteringExit);
sinon.stub(page, 'start');
- sinon.stub(page, 'base');
router._testOnly_startRouter();
router.handleDefaultRoute();
@@ -474,7 +437,10 @@
suite('ROOT', () => {
test('closes for closeAfterLogin', () => {
- const ctx = {...createPageContext(), querystring: 'closeAfterLogin'};
+ const ctx = {
+ querystring: 'closeAfterLogin',
+ canonicalPath: '',
+ } as PageContext;
const closeStub = sinon.stub(window, 'close');
const result = router.handleRootRoute(ctx);
assert.isNotOk(result);
@@ -595,7 +561,7 @@
adminView: AdminChildView.GROUPS,
offset: '0',
openCreateModal: false,
- filter: null,
+ filter: '',
};
await checkUrlToState('/admin/groups', defaultState);
@@ -1029,7 +995,7 @@
view: GerritView.DOCUMENTATION_SEARCH,
filter: 'asdf',
});
- // Percent decoding works fine. page.js decodes twice, so the only problem
+ // Percent decoding works fine. gr-page decodes twice, so the only problem
// is having `%25` in the URL, because the first decoding pass will yield
// `%`, and then the second decoding pass will throw `URI malformed`.
await checkUrlToState('/Documentation/q/filter:as%20%2fdf', {
diff --git a/polygerrit-ui/app/elements/integration_test.ts b/polygerrit-ui/app/elements/integration_test.ts
new file mode 100644
index 0000000..a6e02be
--- /dev/null
+++ b/polygerrit-ui/app/elements/integration_test.ts
@@ -0,0 +1,74 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../test/common-test-setup';
+import './gr-app-element';
+import {testResolver} from '../test/common-test-setup';
+import {fixture, html, assert} from '@open-wc/testing';
+import {GrRouter, routerToken} from './core/gr-router/gr-router';
+import {
+ queryAndAssert,
+ queryAll,
+ stubRestApi,
+ waitQueryAndAssert,
+} from '../test/test-utils';
+import {GrAppElement} from './gr-app-element';
+import {LitElement} from 'lit';
+import {createSearchUrl} from '../models/views/search';
+import {createChange} from '../test/test-data-generators';
+import {NumericChangeId} from '../api/rest-api';
+import {createSettingsUrl} from '../models/views/settings';
+
+suite('integration tests', () => {
+ let appElement: GrAppElement;
+ let router: GrRouter;
+
+ const assertView = async function <T extends LitElement>(tagName: string) {
+ await appElement.updateComplete;
+ const view = await waitQueryAndAssert<T>(appElement, tagName);
+ assert.isOk(view);
+ return view;
+ };
+
+ const assertItems = function (el: HTMLElement) {
+ const list = queryAndAssert(el, 'gr-change-list');
+ const section = queryAndAssert(list, 'gr-change-list-section');
+ return queryAll(section, 'gr-change-list-item');
+ };
+
+ setup(async () => {
+ appElement = await fixture<GrAppElement>(
+ html`<gr-app-element id="app-element"></gr-app-element>`
+ );
+ router = testResolver(routerToken);
+ router._testOnly_startRouter();
+ await appElement.updateComplete;
+ });
+
+ teardown(async () => {
+ router.finalize();
+ });
+
+ test('navigate from search view page to settings page and back', async () => {
+ stubRestApi('getChanges').returns(
+ Promise.resolve([
+ createChange({_number: 1 as NumericChangeId}),
+ createChange({_number: 2 as NumericChangeId}),
+ createChange({_number: 3 as NumericChangeId}),
+ ])
+ );
+
+ router.setUrl(createSearchUrl({query: 'asdf'}));
+ let view = await assertView('gr-change-list-view');
+ assert.equal(assertItems(view).length, 3);
+
+ router.setUrl(createSettingsUrl());
+ await assertView('gr-settings-view');
+
+ window.history.back();
+ view = await assertView('gr-change-list-view');
+ assert.equal(assertItems(view).length, 3);
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
index abc7f3c..2730fcf 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -160,6 +160,12 @@
.status .value {
white-space: pre-wrap;
}
+ /* Make sure that users cannot break the layout with super long
+ "About Me" texts. */
+ div.status {
+ max-height: 8em;
+ overflow-y: auto;
+ }
`,
];
}
@@ -273,7 +279,7 @@
return html`
<div class="status">
<span class="title">About me:</span>
- <span class="value">${this.account.status}</span>
+ <span class="value">${this.account.status.trim()}</span>
</div>
`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
index a1ac781..bd47ec8 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -38,7 +38,7 @@
email: 'kermit@gmail.com' as EmailAddress,
username: 'kermit',
name: 'Kermit The Frog',
- status: 'I am a frog',
+ status: ' I am a frog ',
_account_id: 31415926535 as AccountId,
};
diff --git a/polygerrit-ui/app/models/views/admin_test.ts b/polygerrit-ui/app/models/views/admin_test.ts
index c9b7801..b6089af 100644
--- a/polygerrit-ui/app/models/views/admin_test.ts
+++ b/polygerrit-ui/app/models/views/admin_test.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '@open-wc/testing';
+import {PageContext} from '../../elements/core/gr-router/gr-page';
import {GerritView} from '../../services/router/router-model';
import '../../test/common-test-setup';
import {AdminChildView, PLUGIN_LIST_ROUTE} from './admin';
@@ -20,7 +21,7 @@
assert.isFalse(pattern.test('//admin/plugins?'));
assert.isFalse(pattern.test('/admin/plugins//'));
- assert.deepEqual(createState({}), {
+ assert.deepEqual(createState(new PageContext('')), {
view: GerritView.ADMIN,
adminView: AdminChildView.PLUGINS,
});
diff --git a/polygerrit-ui/app/models/views/base.ts b/polygerrit-ui/app/models/views/base.ts
index 72bec33..5c388f4 100644
--- a/polygerrit-ui/app/models/views/base.ts
+++ b/polygerrit-ui/app/models/views/base.ts
@@ -3,6 +3,7 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {PageContext} from '../../elements/core/gr-router/gr-page';
import {GerritView} from '../../services/router/router-model';
export interface ViewState {
@@ -10,22 +11,10 @@
}
/**
- * While we are using page.js this interface will normally be implemented by
- * PageContext, but it helps testing and independence to have our own type
- * here.
- */
-export interface UrlInfo {
- querystring?: string;
- hash?: string;
- /** What the regular expression matching returns. */
- params?: {[paramIndex: string]: string};
-}
-
-/**
* Based on `urlPattern` knows whether a URL matches and if so, then
* `createState()` can produce a `ViewState` from the matched URL.
*/
export interface Route<T extends ViewState> {
urlPattern: RegExp;
- createState: (info: UrlInfo) => T;
+ createState: (ctx: PageContext) => T;
}
diff --git a/polygerrit-ui/app/models/views/search.ts b/polygerrit-ui/app/models/views/search.ts
index 48775ce..f3b803b 100644
--- a/polygerrit-ui/app/models/views/search.ts
+++ b/polygerrit-ui/app/models/views/search.ts
@@ -64,8 +64,13 @@
/**
* The search results for the current query.
+ * `undefined` must be allowed here, because updating state with a partial
+ * state without `changes` must be possible without overwriting existing
+ * changes.
+ * TODO: We should consider moving `changes` to a another model. This is not
+ * really "view" state. View state must directly correlate to the URL.
*/
- changes: ChangeInfo[];
+ changes?: ChangeInfo[];
}
export interface SearchUrlOptions {
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index b5b313e..c65e7a31 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -377,6 +377,10 @@
license: SharedLicenses.Polymer2018,
},
{
+ name: 'polygerrit-gr-page',
+ license: SharedLicenses.Page,
+ },
+ {
name: 'web-vitals',
license: {
name: 'web-vitals',
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 392b5a8..0df1eda 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -41,14 +41,13 @@
"highlightjs-structured-text": "https://github.com/highlightjs/highlightjs-structured-text",
"immer": "^9.0.5",
"lit": "^2.2.3",
- "page": "^1.11.6",
"polymer-bridges": "file:../../polymer-bridges/",
"polymer-resin": "^2.0.1",
"resemblejs": "^4.0.0",
"rxjs": "^6.6.7",
"safevalues": "^0.3.1",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^3.0.0"
},
"license": "Apache-2.0",
"private": true
-}
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
index be60a63..bc9d7a8 100644
--- a/polygerrit-ui/app/rollup.config.js
+++ b/polygerrit-ui/app/rollup.config.js
@@ -73,14 +73,15 @@
customResolveOptions: {
// By default, it tries to use page.mjs file instead of page.js
// when importing 'page/page'.
+ // TODO: page.was removed. Is something obsolete here?
extensions: ['.js'],
moduleDirectory: 'external/ui_npm/node_modules',
},
}),
define({
- replacements: {
- 'process.env.NODE_ENV': JSON.stringify('production'),
- },
+ replacements: {
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ },
}),
importLocalFontMetaUrlResolver()],
};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index dadf9e4..b81f6d5 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -16,7 +16,7 @@
LifeCycle,
Timing,
} from '../../constants/reporting';
-import {getCLS, getFID, getLCP, Metric} from 'web-vitals';
+import {onCLS, onFID, onLCP, Metric, onINP} from 'web-vitals';
// Latency reporting constants.
@@ -198,9 +198,10 @@
);
}
- getCLS(metric => reportWebVitalMetric(Timing.CLS, metric));
- getFID(metric => reportWebVitalMetric(Timing.FID, metric));
- getLCP(metric => reportWebVitalMetric(Timing.LCP, metric));
+ onCLS(metric => reportWebVitalMetric(Timing.CLS, metric));
+ onFID(metric => reportWebVitalMetric(Timing.FID, metric));
+ onLCP(metric => reportWebVitalMetric(Timing.LCP, metric));
+ onINP(metric => reportWebVitalMetric(Timing.INP, metric));
}
// Calculates the time of Gerrit being in a background tab. When Gerrit reports
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 6dbda3e..659ec9b 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -415,8 +415,8 @@
--diff-trailing-whitespace-indicator: #ff9ad2;
--focused-line-outline-color: var(--blue-700);
--coverage-covered-line-num-color: var(--deemphasized-text-color);
- --coverage-covered: #e0f2f1;
- --coverage-not-covered: #ffd1a4;
+ --coverage-covered: var(--cyan-100);
+ --coverage-not-covered: var(--orange-100);
--ranged-comment-hint-text-color: var(--orange-900);
--token-highlighting-color: #fffd54;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index c6884a6..5e414f0 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -241,9 +241,9 @@
--diff-tab-indicator-color: var(--deemphasized-text-color);
--diff-trailing-whitespace-indicator: #ff9ad2;
--focused-line-outline-color: var(--blue-200);
- --coverage-covered: #37674a;
+ --coverage-covered: var(--cyan-tonal);
--coverage-covered-line-num-color: var(--gray-200);
- --coverage-not-covered: #6b3600;
+ --coverage-not-covered: var(--orange-tonal);
--ranged-comment-hint-text-color: var(--blue-50);
--token-highlighting-color: var(--yellow-tonal);
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index b480bfe..b0f0dc0 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -415,7 +415,7 @@
};
}
-export function createChange(): ChangeInfo {
+export function createChange(partial: Partial<ChangeInfo> = {}): ChangeInfo {
return {
id: TEST_CHANGE_INFO_ID,
project: TEST_PROJECT_NAME,
@@ -431,6 +431,7 @@
owner: createAccountWithId(),
// This is documented as optional, but actually always set.
reviewers: createReviewers(),
+ ...partial,
};
}
@@ -749,9 +750,8 @@
return {
view: GerritView.SEARCH,
query: '',
- offset: undefined,
+ offset: '0',
loading: false,
- changes: [],
};
}
@@ -767,7 +767,7 @@
view: GerritView.ADMIN,
adminView: AdminChildView.REPOS,
offset: '0',
- filter: null,
+ filter: '',
openCreateModal: false,
};
}
@@ -777,7 +777,7 @@
view: GerritView.ADMIN,
adminView: AdminChildView.PLUGINS,
offset: '0',
- filter: null,
+ filter: '',
};
}
@@ -799,7 +799,7 @@
view: GerritView.REPO,
detail: RepoDetailView.BRANCHES,
offset: '0',
- filter: null,
+ filter: '',
};
}
@@ -808,7 +808,7 @@
view: GerritView.REPO,
detail: RepoDetailView.TAGS,
offset: '0',
- filter: null,
+ filter: '',
};
}
diff --git a/polygerrit-ui/app/utils/page-wrapper-utils.ts b/polygerrit-ui/app/utils/page-wrapper-utils.ts
deleted file mode 100644
index 58bb024..0000000
--- a/polygerrit-ui/app/utils/page-wrapper-utils.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-// @ts-ignore: Bazel is not yet configured to download the types
-import pagejs from 'page';
-
-// Reexport page.js. To make it work rollup patches page.js and replace "this"
-// to "window". Otherwise, it can't assign global property. We can't import
-// page.mjs because typescript doesn't support mjs extensions
-export interface Page {
- (pattern: string | RegExp, ...pageCallback: PageCallback[]): void;
- (pageCallback: PageCallback): void;
- show(url: string): void;
- redirect(url: string): void;
- replace(path: string, state: null, init: boolean, dispatch: boolean): void;
- base(url: string): void;
- start(opts: Options): void;
- stop(): void;
- exit(pattern: string | RegExp, ...pageCallback: PageCallback[]): void;
-}
-
-export interface Options {
- popstate?: boolean;
- dispatch?: boolean;
-}
-
-// See https://visionmedia.github.io/page.js/ for details
-export interface PageContext {
- canonicalPath: string;
- path: string;
- querystring: string;
- pathname: string;
- hash: string;
- params: {[paramIndex: string]: string};
-}
-
-export type PageNextCallback = () => void;
-
-export type PageCallback = (
- context: PageContext,
- next: PageNextCallback
-) => void;
-
-// Must only be used by gr-router!
-// TODO: Move this into gr-router. Note that there is a Google import rule
-// that would need to be modified.
-export const page = pagejs as unknown as {create(): Page};
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index 355e54b..183671f 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -64,7 +64,7 @@
export function convertToPatchSetNum(
patchset: string | undefined
): PatchSetNum | undefined {
- if (patchset === undefined) return patchset;
+ if (!patchset) return undefined;
if (!isPatchSetNum(patchset)) {
console.error('string is not of type PatchSetNum');
}
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 22a9721..5e294cb 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -86,7 +86,7 @@
* encodeURIComponent() with some tweaks.
*/
export function encodeURL(url: string): string {
- // page.js decodes the entire URL, and then decodes once more the
+ // gr-page decodes the entire URL, and then decodes once more the
// individual regex matching groups. It uses `decodeURIComponent()`, which
// will choke on singular `%` chars without two trailing digits. We prefer
// to not double encode *everything* (just for readaiblity and simplicity),
@@ -127,7 +127,7 @@
output = output.replace(/%40/g, '@');
output = output.replace(/%2F/g, '/');
- // page.js replaces `+` by ` ` in addition to calling `decodeURIComponent()`.
+ // gr-page replaces `+` by ` ` in addition to calling `decodeURIComponent()`.
// So we can use `+` to increase readability.
output = output.replace(/%20/g, '+');
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 5a91fc0..2edce7e 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -677,11 +677,6 @@
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-isarray@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
- integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -813,25 +808,11 @@
dependencies:
wrappy "1"
-page@^1.11.6:
- version "1.11.6"
- resolved "https://registry.yarnpkg.com/page/-/page-1.11.6.tgz#5ef4efc7073749b8085ccdaa0dcd7c9e0de12fe3"
- integrity sha512-P6e2JfzkBrPeFCIPplLP7vDDiU84RUUZMrWdsH4ZBGJ8OosnwFkcUkBHp1DTIjuipLliw9yQn/ZJsXZvarsO+g==
- dependencies:
- path-to-regexp "~1.2.1"
-
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-path-to-regexp@~1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.2.1.tgz#b33705c140234d873c8721c7b9fd8b541ed3aff9"
- integrity sha1-szcFwUAjTYc8hyHHuf2LVB7Tr/k=
- dependencies:
- isarray "0.0.1"
-
"polymer-bridges@file:../../polymer-bridges":
version "1.0.0"
@@ -993,10 +974,10 @@
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
-web-vitals@^2.1.4:
- version "2.1.4"
- resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
- integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
+web-vitals@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-3.1.1.tgz#bb124a03df7a135617f495c5bb7dbc30ecf2cce3"
+ integrity sha512-qvllU+ZeQChqzBhZ1oyXmWsjJ8a2jHYpH8AMaVuf29yscOPZfTQTjQFRX6+eADTdsDE8IanOZ0cetweHMs8/2A==
webidl-conversions@^3.0.0:
version "3.0.1"
diff --git a/resources/com/google/gerrit/server/commit-msg_test.sh b/resources/com/google/gerrit/server/commit-msg_test.sh
index 2c256ff..0cc3da0 100755
--- a/resources/com/google/gerrit/server/commit-msg_test.sh
+++ b/resources/com/google/gerrit/server/commit-msg_test.sh
@@ -43,6 +43,31 @@
fi
}
+function test_empty_with_cutoff {
+ rm -f input
+ cat << EOF > input
+# Please enter the commit message for your changes.
+# ------------------------ >8 ------------------------
+# Do not modify or remove the line above.
+# Everything below it will be ignored.
+diff --git a/file.txt b/file.txt
+index 625fd613d9..03aeba3b21 100755
+--- a/file.txt
++++ b/file.txt
+@@ -38,6 +38,7 @@
+ context
+ line
+
++hello, world
+
+ context
+ line
+EOF
+ if ${hook} input ; then
+ fail "must fail on empty message"
+ fi
+}
+
function test_keep_cutoff_line {
if ! prereq_modern_git ; then
echo "old version of Git detected; skipping scissors test."
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index d9fd1f1..0154d43 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -50,7 +50,7 @@
trap 'rm -f "$dest" "$dest-2"' EXIT
-if ! git stripspace --strip-comments < "$1" > "${dest}" ; then
+if ! cat "$1" | sed -e '/>8/q' | git stripspace --strip-comments > "${dest}" ; then
echo "cannot strip comments from $1"
exit 1
fi
diff --git a/tools/node_tools/node_modules_licenses/licenses-map.ts b/tools/node_tools/node_modules_licenses/licenses-map.ts
index 7dfb23e..642a749 100644
--- a/tools/node_tools/node_modules_licenses/licenses-map.ts
+++ b/tools/node_tools/node_modules_licenses/licenses-map.ts
@@ -97,6 +97,9 @@
*/
public generateMap(nodeModulesFiles: ReadonlyArray<string>): LicensesMap {
const installedPackages = this.getInstalledPackages(nodeModulesFiles);
+ // Static packages that are not inside `node_modules` directories.
+ // gr-page.ts was derived from page.js, so we reproduce the original LICENSE.
+ installedPackages.push({name: 'polygerrit-gr-page', version: 'current', rootPath: 'polygerrit-ui/app/elements/core/gr-router/', files: ['gr-page.ts']});
const licensedFilesGroupedByLicense = this.getLicensedFilesGroupedByLicense(installedPackages);
const result: LicensesMap = {};