Merge "Change OutgoingEmail to use a soy instead of string concatenation."
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 974e897..6c9dfef 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -131,6 +131,7 @@
* `proc/cpu/usage`: CPU time used by the Gerrit process.
* `proc/cpu/system_load`: System load average for the last minute.
* `proc/num_open_fds`: Number of open file descriptors.
+* `proc/jvm/memory/allocated`: Total memory allocated by all threads since Gerrit process started.
* `proc/jvm/memory/heap_committed`: Amount of memory guaranteed for user objects.
* `proc/jvm/memory/heap_used`: Amount of memory holding user objects.
* `proc/jvm/memory/non_heap_committed`: Amount of memory guaranteed for classes,
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index bf51252..d9ab64d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -379,6 +379,11 @@
as link:#tracking-id-info[TrackingIdInfo].
--
+[[custom-keyed-values]]
+--
+* `CUSTOM_KEYED_VALUES`: include the custom key-value map
+---
+
.Request
----
GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -6919,6 +6924,11 @@
|==================================
|Field Name ||Description
|`id` ||
+The ID of the change. Subject to a 'GerritBackendFeature__return_new_change_info_id'
+ experiment, the format is either "'<project>\~<_number>'" (new format),
+or `triplet_id`. The experiment is needed to allow callers to migrate.
+'project' and '_number' are URL encoded. The callers must not rely on the format.
+|`triplet_id` ||
The ID of the change in the format "'<project>\~<branch>~<Change-Id>'",
where 'project', 'branch' and 'Change-Id' are URL encoded. For 'branch' the
`refs/heads/` prefix is omitted.
@@ -6939,6 +6949,10 @@
of the account from the attention set.
|`hashtags` |optional|
List of hashtags that are set on the change.
+|`custom_keyed_values` |optional|
+A map that maps custom keys to custom values that are tied to a specific change,
+both in the form of strings. Only set if link:#custom-keyed-values[custom keyed
+values] are requested.
|`change_id` ||The Change-Id of the change.
|`subject` ||
The subject of the change (header line of the commit message).
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 1faffb2..fe9b13c 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -2089,10 +2089,11 @@
|`start_time` ||The start time of the task.
|`delay` ||The remaining delay of the task.
|`command` ||The command of the task.
+|`queue_name` ||The work queue the task is associated with.
|`remote_name`|optional|
The remote name. May only be set for tasks that are associated with a
project.
-|`project` |optional|The project the task is associated with.
+|`project_name` |optional|The project the task is associated with.
|====================================
[[task-summary-info]]
diff --git a/java/Main.java b/java/Main.java
index 09c8c76..c04db2c 100644
--- a/java/Main.java
+++ b/java/Main.java
@@ -16,6 +16,7 @@
public final class Main {
private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
+ private static final Runtime.Version MIN_JAVA_VERSION = Runtime.Version.parse("11.0.10");
// We don't do any real work here because we need to import
// the archive lookup code and we cannot import a class in
@@ -34,11 +35,11 @@
}
private static boolean onSupportedJavaVersion() {
- final String version = System.getProperty("java.specification.version");
- if (1.8 <= parse(version)) {
+ Runtime.Version version = Runtime.version();
+ if (version.compareTo(MIN_JAVA_VERSION) >= 0) {
return true;
}
- System.err.println("fatal: Gerrit Code Review requires Java 8 or later");
+ System.err.println("fatal: Gerrit Code Review requires Java " + MIN_JAVA_VERSION + " or later");
System.err.println(" (trying to run on Java " + version + ")");
return false;
}
@@ -58,22 +59,5 @@
"com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");
}
- private static double parse(String version) {
- if (version == null || version.length() == 0) {
- return 0.0;
- }
-
- try {
- final int fd = version.indexOf('.');
- final int sd = version.indexOf('.', fd + 1);
- if (0 < sd) {
- version = version.substring(0, sd);
- }
- return Double.parseDouble(version);
- } catch (NumberFormatException e) {
- return 0.0;
- }
- }
-
private Main() {}
}
diff --git a/java/com/google/gerrit/auth/BUILD b/java/com/google/gerrit/auth/BUILD
index e844696..f04334d 100644
--- a/java/com/google/gerrit/auth/BUILD
+++ b/java/com/google/gerrit/auth/BUILD
@@ -12,8 +12,6 @@
srcs = glob(
["**/*.java"],
),
- resource_strip_prefix = "resources",
- resources = ["//resources/com/google/gerrit/server"],
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/common:annotations",
diff --git a/java/com/google/gerrit/entities/BUILD b/java/com/google/gerrit/entities/BUILD
index dbbcf71..c0f5de6 100644
--- a/java/com/google/gerrit/entities/BUILD
+++ b/java/com/google/gerrit/entities/BUILD
@@ -10,7 +10,6 @@
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
- "//java/com/google/gerrit/server:project_util",
"//lib:gson",
"//lib:guava",
"//lib:jgit",
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index b43650b..72ca6a9 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.server.ProjectUtil;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
diff --git a/java/com/google/gerrit/server/ProjectUtil.java b/java/com/google/gerrit/entities/ProjectUtil.java
similarity index 97%
rename from java/com/google/gerrit/server/ProjectUtil.java
rename to java/com/google/gerrit/entities/ProjectUtil.java
index e87c8bf..98ce67a 100644
--- a/java/com/google/gerrit/server/ProjectUtil.java
+++ b/java/com/google/gerrit/entities/ProjectUtil.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server;
+package com.google.gerrit.entities;
public class ProjectUtil {
public static String sanitizeProjectName(String name) {
diff --git a/java/com/google/gerrit/extensions/client/ListChangesOption.java b/java/com/google/gerrit/extensions/client/ListChangesOption.java
index f1f7831..de4326e 100644
--- a/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -88,7 +88,10 @@
SKIP_DIFFSTAT(23),
/** Include the evaluated submit requirements for the caller. */
- SUBMIT_REQUIREMENTS(24);
+ SUBMIT_REQUIREMENTS(24),
+
+ /** Include custom keyed values. */
+ CUSTOM_KEYED_VALUES(25);
private final int value;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index dc9bc32..a2e2e8f 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -37,6 +37,8 @@
// protected by any ListChangesOption.
public String id;
+ public String tripletId;
+
public String project;
public String branch;
public String topic;
@@ -49,6 +51,8 @@
public Map<Integer, AttentionSetInfo> removedFromAttentionSet;
+ public Map<String, String> customKeyedValues;
+
public Collection<String> hashtags;
public String changeId;
public String subject;
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index b2173c4..fcf4f0f 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -11,7 +11,6 @@
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/git",
"//java/com/google/gerrit/server",
- "//java/com/google/gerrit/server/api",
"//lib:guava",
"//lib:jgit",
"//lib/auto:auto-factory",
diff --git a/java/com/google/gerrit/gpg/PublicKeyStoreUtil.java b/java/com/google/gerrit/gpg/PublicKeyStoreUtil.java
new file mode 100644
index 0000000..5333826
--- /dev/null
+++ b/java/com/google/gerrit/gpg/PublicKeyStoreUtil.java
@@ -0,0 +1,80 @@
+// 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.gpg;
+
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.common.io.BaseEncoding;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.util.NB;
+
+public class PublicKeyStoreUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final ExternalIds externalIds;
+ private final Provider<PublicKeyStore> storeProvider;
+
+ @Inject
+ public PublicKeyStoreUtil(ExternalIds externalIds, Provider<PublicKeyStore> storeProvider) {
+ this.externalIds = externalIds;
+ this.storeProvider = storeProvider;
+ }
+
+ public static byte[] parseFingerprint(ExternalId gpgKeyExtId) {
+ return BaseEncoding.base16().decode(gpgKeyExtId.key().id());
+ }
+
+ public static long keyIdFromFingerprint(byte[] fp) {
+ return NB.decodeInt64(fp, fp.length - 8);
+ }
+
+ public List<PGPPublicKey> listGpgKeysForUser(Account.Id id) throws PGPException, IOException {
+ List<PGPPublicKey> keys = new ArrayList<>();
+ try (PublicKeyStore store = storeProvider.get()) {
+ for (ExternalId extId : getGpgExtIds(id)) {
+ byte[] fp = parseFingerprint(extId);
+ boolean found = false;
+ for (PGPPublicKeyRing keyRing : store.get(keyIdFromFingerprint(fp))) {
+ if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
+ found = true;
+ keys.add(keyRing.getPublicKey());
+ break;
+ }
+ }
+ if (!found) {
+ logger.atWarning().log(
+ "No public key stored for fingerprint %s", Fingerprint.toString(fp));
+ }
+ }
+ }
+ return keys;
+ }
+
+ public Iterable<ExternalId> getGpgExtIds(Account.Id id) throws IOException {
+ return externalIds.byAccount(id, SCHEME_GPGKEY);
+ }
+}
diff --git a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 6ae0334..57fda5b 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -14,8 +14,6 @@
package com.google.gerrit.gpg.api;
-import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
-
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -70,8 +68,10 @@
return gpgKeys.get().list().apply(account).value();
} catch (PGPException | IOException e) {
throw new GpgException(e);
+ } catch (RestApiException e) {
+ throw e;
} catch (Exception e) {
- throw asRestApiException("Cannot list GPG keys", e);
+ throw RestApiException.wrap("Cannot list GPG keys", e);
}
}
@@ -86,8 +86,10 @@
return postGpgKeys.get().apply(account, in).value();
} catch (PGPException | IOException | ConfigInvalidException e) {
throw new GpgException(e);
+ } catch (RestApiException e) {
+ throw e;
} catch (Exception e) {
- throw asRestApiException("Cannot put GPG keys", e);
+ throw RestApiException.wrap("Cannot put GPG keys", e);
}
}
diff --git a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
index 0ff12e8..2a05f35 100644
--- a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -14,8 +14,6 @@
package com.google.gerrit.gpg.api;
-import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
-
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.Input;
@@ -50,7 +48,7 @@
try {
return get.apply(rsrc).value();
} catch (Exception e) {
- throw asRestApiException("Cannot get GPG key", e);
+ throw RestApiException.wrap("Cannot get GPG key", e);
}
}
@@ -58,8 +56,10 @@
public void delete() throws RestApiException {
try {
delete.apply(rsrc, new Input());
+ } catch (RestApiException e) {
+ throw e;
} catch (PGPException | IOException | ConfigInvalidException e) {
- throw asRestApiException("Cannot delete GPG key", e);
+ throw RestApiException.wrap("Cannot delete GPG key", e);
}
}
}
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index 00a0f57..9fb8286 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -14,13 +14,10 @@
package com.google.gerrit.gpg.server;
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
-import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,10 +33,10 @@
import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.gpg.PublicKeyStoreUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -48,36 +45,35 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
-import org.eclipse.jgit.util.NB;
@Singleton
public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final DynamicMap<RestView<GpgKey>> views;
private final Provider<CurrentUser> self;
+ private final PublicKeyStoreUtil publicKeyStoreUtil;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
- private final ExternalIds externalIds;
@Inject
GpgKeys(
DynamicMap<RestView<GpgKey>> views,
Provider<CurrentUser> self,
+ PublicKeyStoreUtil publicKeyStoreUtil,
Provider<PublicKeyStore> storeProvider,
- GerritPublicKeyChecker.Factory checkerFactory,
- ExternalIds externalIds) {
+ GerritPublicKeyChecker.Factory checkerFactory) {
this.views = views;
this.self = self;
+ this.publicKeyStoreUtil = publicKeyStoreUtil;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
- this.externalIds = externalIds;
}
@Override
@@ -90,10 +86,11 @@
throws ResourceNotFoundException, PGPException, IOException {
checkVisible(self, parent);
- ExternalId gpgKeyExtId = findGpgKey(id.get(), getGpgExtIds(parent));
- byte[] fp = parseFingerprint(gpgKeyExtId);
+ ExternalId gpgKeyExtId =
+ findGpgKey(id.get(), publicKeyStoreUtil.getGpgExtIds(parent.getUser().getAccountId()));
+ byte[] fp = PublicKeyStoreUtil.parseFingerprint(gpgKeyExtId);
try (PublicKeyStore store = storeProvider.get()) {
- long keyId = keyId(fp);
+ long keyId = PublicKeyStoreUtil.keyIdFromFingerprint(fp);
for (PGPPublicKeyRing keyRing : store.get(keyId)) {
PGPPublicKey key = keyRing.getPublicKey();
if (Arrays.equals(key.getFingerprint(), fp)) {
@@ -131,10 +128,6 @@
return gpgKeyExtId;
}
- static byte[] parseFingerprint(ExternalId gpgKeyExtId) {
- return BaseEncoding.base16().decode(gpgKeyExtId.key().id());
- }
-
@Override
public DynamicMap<RestView<GpgKey>> views() {
return views;
@@ -145,29 +138,17 @@
public Response<Map<String, GpgKeyInfo>> apply(AccountResource rsrc)
throws PGPException, IOException, ResourceNotFoundException {
checkVisible(self, rsrc);
- Map<String, GpgKeyInfo> keys = new HashMap<>();
+ List<PGPPublicKey> keys =
+ publicKeyStoreUtil.listGpgKeysForUser(rsrc.getUser().getAccountId());
+ Map<String, GpgKeyInfo> res = new HashMap<>();
try (PublicKeyStore store = storeProvider.get()) {
- for (ExternalId extId : getGpgExtIds(rsrc)) {
- byte[] fp = parseFingerprint(extId);
- boolean found = false;
- for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
- if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
- found = true;
- GpgKeyInfo info =
- toJson(
- keyRing.getPublicKey(), checkerFactory.create(rsrc.getUser(), store), store);
- keys.put(info.id, info);
- info.id = null;
- break;
- }
- }
- if (!found) {
- logger.atWarning().log(
- "No public key stored for fingerprint %s", Fingerprint.toString(fp));
- }
+ for (PGPPublicKey key : keys) {
+ GpgKeyInfo info = toJson(key, checkerFactory.create(rsrc.getUser(), store), store);
+ res.put(info.id, info);
+ info.id = null;
}
}
- return Response.ok(keys);
+ return Response.ok(res);
}
}
@@ -194,14 +175,6 @@
}
}
- private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException {
- return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
- }
-
- private static long keyId(byte[] fp) {
- return NB.decodeInt64(fp, fp.length - 8);
- }
-
static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc)
throws ResourceNotFoundException {
if (!BouncyCastleUtil.havePGP()) {
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 7c7efdc..edd5a58 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -46,6 +46,7 @@
import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.gpg.PublicKeyStoreUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -175,7 +176,8 @@
for (String id : input.delete) {
try {
ExternalId gpgKeyExtId = GpgKeys.findGpgKey(id, existingExtIds);
- fingerprints.put(gpgKeyExtId, new Fingerprint(GpgKeys.parseFingerprint(gpgKeyExtId)));
+ fingerprints.put(
+ gpgKeyExtId, new Fingerprint(PublicKeyStoreUtil.parseFingerprint(gpgKeyExtId)));
} catch (ResourceNotFoundException e) {
// Skip removal.
}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 210ba7b..9056732 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -1615,7 +1615,9 @@
private void checkUserSession(HttpServletRequest req) throws AuthException {
CurrentUser user = globals.currentUser.get();
if (isRead(req)) {
- user.setAccessPath(AccessPath.REST_API);
+ if (user.getAccessPath().equals(AccessPath.UNKNOWN)) {
+ user.setAccessPath(AccessPath.REST_API);
+ }
} else if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
} else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 3ac594e..dac1012 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -62,7 +62,10 @@
@Deprecated static final Schema<ProjectData> V4 = schema(V3);
// Upgrade Lucene to 7.x requires reindexing.
- static final Schema<ProjectData> V5 = schema(V4);
+ @Deprecated static final Schema<ProjectData> V5 = schema(V4);
+
+ // Upgrade Lucene to 8.x requires reindexing.
+ static final Schema<ProjectData> V6 = schema(V5);
/**
* Name of the project index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index f9dc31a..78ee128 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -544,8 +544,7 @@
int realLimit = opts.start() + opts.pageSize();
TopFieldDocs docs =
opts.searchAfter() != null
- ? searcher.searchAfter(
- (ScoreDoc) opts.searchAfter(), query, realLimit, sort, false, false)
+ ? searcher.searchAfter((ScoreDoc) opts.searchAfter(), query, realLimit, sort, false)
: searcher.search(query, realLimit, sort);
ImmutableList.Builder<T> b = ImmutableList.builderWithExpectedSize(docs.scoreDocs.length);
for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 6365260..728606d 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -415,12 +415,7 @@
if (maxRemainingHits > 0) {
TopFieldDocs subIndexHits =
searchers[i].searchAfter(
- searchAfter,
- query,
- maxRemainingHits,
- sort,
- /* doDocScores= */ false,
- /* doMaxScore= */ false);
+ searchAfter, query, maxRemainingHits, sort, /* doDocScores= */ false);
searchAfterHitsCount += subIndexHits.scoreDocs.length;
hits.add(subIndexHits);
searchAfterBySubIndex.put(
diff --git a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
index 09e40c1..301ec85 100644
--- a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -34,6 +34,8 @@
import java.util.concurrent.TimeUnit;
public class ProcMetricModule extends MetricModule {
+ private static final ThreadMXBeanInterface threadMxBean = ThreadMXBeanFactory.create();
+
@Override
protected void configure(MetricMaker metrics) {
buildLabel(metrics);
@@ -167,6 +169,14 @@
objectPendingFinalizationCount.set(memory.getObjectPendingFinalizationCount());
});
+
+ if (threadMxBean.supportsAllocatedBytes()) {
+ metrics.newCallbackMetric(
+ "proc/jvm/memory/allocated",
+ Long.class,
+ new Description("Allocated memory").setCumulative().setUnit(Units.BYTES),
+ () -> getTotalAllocatedBytes());
+ }
}
private void procJvmMemoryPool(MetricMaker metrics) {
@@ -312,4 +322,19 @@
});
}
}
+
+ private static long getTotalAllocatedBytes() {
+ if (!threadMxBean.supportsAllocatedBytes()) {
+ return -1;
+ }
+
+ long[] ids = threadMxBean.getAllThreadIds();
+ long[] allocatedBytes = threadMxBean.getAllThreadsAllocatedBytes(ids);
+ long total = 0;
+
+ for (long a : allocatedBytes) {
+ total += a;
+ }
+ return total;
+ }
}
diff --git a/java/com/google/gerrit/metrics/proc/ThreadMXBeanInterface.java b/java/com/google/gerrit/metrics/proc/ThreadMXBeanInterface.java
index 546924f..03e59a0 100644
--- a/java/com/google/gerrit/metrics/proc/ThreadMXBeanInterface.java
+++ b/java/com/google/gerrit/metrics/proc/ThreadMXBeanInterface.java
@@ -19,4 +19,12 @@
long getCurrentThreadUserTime();
long getCurrentThreadAllocatedBytes();
+
+ boolean supportsAllocatedBytes();
+
+ long getThreadAllocatedBytes(long threadId);
+
+ long[] getAllThreadsAllocatedBytes(long[] threadIds);
+
+ long[] getAllThreadIds();
}
diff --git a/java/com/google/gerrit/metrics/proc/ThreadMXBeanJava.java b/java/com/google/gerrit/metrics/proc/ThreadMXBeanJava.java
index 29dd42a..361aba0 100644
--- a/java/com/google/gerrit/metrics/proc/ThreadMXBeanJava.java
+++ b/java/com/google/gerrit/metrics/proc/ThreadMXBeanJava.java
@@ -37,4 +37,24 @@
public long getCurrentThreadAllocatedBytes() {
return -1;
}
+
+ @Override
+ public boolean supportsAllocatedBytes() {
+ return false;
+ }
+
+ @Override
+ public long getThreadAllocatedBytes(long threadId) {
+ return -1;
+ }
+
+ @Override
+ public long[] getAllThreadsAllocatedBytes(long[] threadIds) {
+ return new long[0];
+ }
+
+ @Override
+ public long[] getAllThreadIds() {
+ return sys.getAllThreadIds();
+ }
}
diff --git a/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java b/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java
index f5555b5..ca750cd 100644
--- a/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java
+++ b/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java
@@ -37,4 +37,24 @@
public long getCurrentThreadAllocatedBytes() {
return sys.getCurrentThreadAllocatedBytes();
}
+
+ @Override
+ public boolean supportsAllocatedBytes() {
+ return true;
+ }
+
+ @Override
+ public long getThreadAllocatedBytes(long threadId) {
+ return sys.getThreadAllocatedBytes(threadId);
+ }
+
+ @Override
+ public long[] getAllThreadsAllocatedBytes(long[] threadIds) {
+ return sys.getThreadAllocatedBytes(threadIds);
+ }
+
+ @Override
+ public long[] getAllThreadIds() {
+ return sys.getAllThreadIds();
+ }
}
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 104a44a..d68f809 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -14,10 +14,6 @@
"account/externalids/testing/ExternalIdTestUtil.java",
]
-PROJECT_UTIL_SRC = [
- "ProjectUtil.java",
-]
-
PROLOG_SRC = ["rules/prolog/*.java"]
java_library(
@@ -26,12 +22,6 @@
visibility = ["//visibility:public"],
)
-java_library(
- name = "project_util",
- srcs = PROJECT_UTIL_SRC,
- visibility = ["//visibility:public"],
-)
-
# Giant kitchen-sink target.
#
# The only reason this hasn't been split up further is because we have too many
@@ -43,14 +33,13 @@
srcs = glob(
["**/*.java"],
exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC + TESTING_SRC +
- PROJECT_UTIL_SRC + PROLOG_SRC,
+ PROLOG_SRC,
),
resource_strip_prefix = "resources",
resources = ["//resources/com/google/gerrit/server"],
visibility = ["//visibility:public"],
deps = [
":constants",
- ":project_util",
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/entities",
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index a1e45e9..dc7fa24 100644
--- a/java/com/google/gerrit/server/args4j/ProjectHandler.java
+++ b/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -18,8 +18,8 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.ProjectUtil;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index bde7404..39cec8b 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -5,8 +5,6 @@
srcs = glob(
["**/*.java"],
),
- resource_strip_prefix = "resources",
- resources = ["//resources/com/google/gerrit/server"],
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/entities",
diff --git a/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java b/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java
index a0526e1..82c2856b 100644
--- a/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java
+++ b/java/com/google/gerrit/server/cache/PerThreadRefDbCache.java
@@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.cache;
-import com.google.gerrit.server.cache.PerThreadCache;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index e5a9534..00673d5 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -123,6 +123,10 @@
changeInfo.removedFromAttentionSet == null
? null
: ImmutableMap.copyOf(changeInfo.removedFromAttentionSet);
+ copy.customKeyedValues =
+ changeInfo.customKeyedValues == null
+ ? null
+ : ImmutableMap.copyOf(changeInfo.customKeyedValues);
copy.hashtags = changeInfo.hashtags;
copy.changeId = changeInfo.changeId;
copy.submitType = changeInfo.submitType;
@@ -147,6 +151,7 @@
copy.unresolvedCommentCount = changeInfo.unresolvedCommentCount;
copy.workInProgress = changeInfo.workInProgress;
copy.id = changeInfo.id;
+ copy.tripletId = changeInfo.tripletId;
copy.cherryPickOfChange = changeInfo.cherryPickOfChange;
copy.cherryPickOfPatchSet = changeInfo.cherryPickOfPatchSet;
return copy;
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index f733a7b..839629e 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -23,6 +23,7 @@
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.CUSTOM_KEYED_VALUES;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
@@ -101,6 +102,8 @@
import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -240,9 +243,11 @@
private AccountLoader accountLoader;
private FixInput fix;
+ private ExperimentFeatures experimentFeatures;
@Inject
ChangeJson(
+ ExperimentFeatures experimentFeatures,
Provider<CurrentUser> user,
PermissionBackend permissionBackend,
ChangeData.Factory cdf,
@@ -259,6 +264,7 @@
@GerritServerConfig Config cfg,
@Assisted Iterable<ListChangesOption> options,
@Assisted Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory) {
+ this.experimentFeatures = experimentFeatures;
this.userProvider = user;
this.changeDataFactory = cdf;
this.permissionBackend = permissionBackend;
@@ -422,10 +428,17 @@
return info;
}
- private static void finish(ChangeInfo info) {
- info.id =
+ private static void finish(ChangeInfo info, ExperimentFeatures experimentFeatures) {
+ info.tripletId =
Joiner.on('~')
.join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId));
+ if (experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_RETURN_NEW_CHANGE_INFO_ID)) {
+ info.id =
+ Joiner.on('~').join(Url.encode(info.project), Url.encode(String.valueOf(info._number)));
+ } else {
+ info.id = info.tripletId;
+ }
}
private static boolean containsAnyOf(
@@ -563,7 +576,7 @@
info.isPrivate = c.isPrivate() ? true : null;
info.workInProgress = c.isWorkInProgress() ? true : null;
info.hasReviewStarted = c.hasReviewStarted();
- finish(info);
+ finish(info, experimentFeatures);
} else {
info._number = result.id().get();
info.problems = result.problems();
@@ -617,6 +630,9 @@
a -> a.account().get(),
a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
}
+ if (has(CUSTOM_KEYED_VALUES)) {
+ out.customKeyedValues = cd.customKeyedValues();
+ }
out.hashtags = cd.hashtags();
out.changeId = in.getKey().get();
if (in.isNew()) {
@@ -727,7 +743,7 @@
if (needMessages) {
out.messages = messages(cd);
}
- finish(out);
+ finish(out, experimentFeatures);
// This block must come after the ChangeInfo is mostly populated, since
// it will be passed to ActionVisitors as-is.
diff --git a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index cd49ea6..093b87c 100644
--- a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -34,6 +34,7 @@
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TotalHits;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexOutput;
@@ -84,10 +85,10 @@
// We don't have much documentation, so we just use MAX_VALUE here and skip paging.
TopDocs results = searcher.search(query, Integer.MAX_VALUE);
ScoreDoc[] hits = results.scoreDocs;
- long totalHits = results.totalHits;
+ TotalHits totalHits = results.totalHits;
List<DocResult> out = new ArrayList<>();
- for (int i = 0; i < totalHits; i++) {
+ for (int i = 0; i < totalHits.value; i++) {
DocResult result = new DocResult();
Document doc = searcher.doc(hits[i].doc);
result.url = doc.get(Constants.URL_FIELD);
diff --git a/java/com/google/gerrit/server/experiments/ConfigExperimentFeatures.java b/java/com/google/gerrit/server/experiments/ConfigExperimentFeatures.java
index 227deb5..8de5db3 100644
--- a/java/com/google/gerrit/server/experiments/ConfigExperimentFeatures.java
+++ b/java/com/google/gerrit/server/experiments/ConfigExperimentFeatures.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.experiments;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
@@ -57,6 +58,11 @@
}
@Override
+ public boolean isFeatureEnabled(String featureFlag, Project.NameKey project) {
+ return isFeatureEnabled(featureFlag);
+ }
+
+ @Override
public ImmutableSet<String> getEnabledExperimentFeatures() {
return enabledExperimentFeatures;
}
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeatures.java b/java/com/google/gerrit/server/experiments/ExperimentFeatures.java
index dc9148a..fd885ed 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeatures.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeatures.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.experiments;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Project;
/**
* Features that can be enabled/disabled on Gerrit (e. g. experiments to research new behavior in
@@ -37,6 +38,12 @@
boolean isFeatureEnabled(String featureFlag);
/**
+ * Same {@link #isFeatureEnabled}, but takes into account {@code project}, when evaluating the
+ * experiment.
+ */
+ boolean isFeatureEnabled(String featureFlag, Project.NameKey project);
+
+ /**
* Returns the names of the features that are enabled on Gerrit instance (either by default or via
* gerrit.config).
*/
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index e294d55..fd7d504 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -29,4 +29,11 @@
/** On BatchUpdate, do not await index completion before returning to the user */
public static String GERRIT_BACKEND_FEATURE_DO_NOT_AWAIT_CHANGE_INDEXING =
"GerritBackendFeature__do_not_await_change_indexing";
+
+ /**
+ * Sets ChangeInfo.id to "'<project>~<_number>'", instead of "'<project>~<branch>~<Change-Id>'",
+ * spearing an index lookup if the id is used in the follow-up API calls.
+ */
+ public static String GERRIT_BACKEND_FEATURE_RETURN_NEW_CHANGE_INFO_ID =
+ "GerritBackendFeature__return_new_change_info_id";
}
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 5cf7de5..5eb913d 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -20,6 +20,7 @@
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.PerThreadRefDbCache;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
index 8e7d964..fd264a1 100644
--- a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -85,7 +85,10 @@
.build();
// Upgrade Lucene to 7.x requires reindexing.
- static final Schema<AccountState> V12 = schema(V11);
+ @Deprecated static final Schema<AccountState> V12 = schema(V11);
+
+ // Upgrade Lucene to 8.x requires reindexing.
+ static final Schema<AccountState> V13 = schema(V12);
/**
* Name of the account index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index e74ce8f..93521f3 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -239,7 +239,7 @@
.build();
/** Remove assignee field. */
- @SuppressWarnings("deprecation")
+ @Deprecated
static final Schema<ChangeData> V82 =
new Schema.Builder<ChangeData>()
.add(V81)
@@ -247,6 +247,10 @@
.remove(ChangeField.ASSIGNEE_FIELD)
.build();
+ /** Upgrade Lucene to 8.x requires reindexing. */
+ @SuppressWarnings("deprecation")
+ static final Schema<ChangeData> V83 = schema(V82);
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index f0f3510..1b87d27 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -69,7 +69,10 @@
@Deprecated static final Schema<InternalGroup> V8 = schema(V7);
// Upgrade Lucene to 7.x requires reindexing.
- static final Schema<InternalGroup> V9 = schema(V8);
+ @Deprecated static final Schema<InternalGroup> V9 = schema(V8);
+
+ // Upgrade Lucene to 8.x requires reindexing.
+ static final Schema<InternalGroup> V10 = schema(V9);
/** Singleton instance of the schema definitions. This is one per JVM. */
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
index 3be55ea..eb1f692 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
@@ -23,6 +23,7 @@
public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
public static final FooterKey FOOTER_CURRENT = new FooterKey("Current");
+ public static final FooterKey FOOTER_CUSTOM_KEYED_VALUE = new FooterKey("Custom-Keyed-Value");
public static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index c75fd29..4a31e23 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -394,6 +394,7 @@
// Lazy defensive copies of mutable ReviewDb types, to avoid polluting the
// ChangeNotesCache from handlers.
+ private ImmutableSortedMap<String, String> customKeyedValues;
private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
private PatchSetApprovals approvals;
private ImmutableSet<Comment.Key> commentKeys;
@@ -477,6 +478,16 @@
return state.allAttentionSetUpdates();
}
+ /** Returns the key-value pairs that are attached to this change */
+ public ImmutableSortedMap<String, String> getCustomKeyedValues() {
+ if (customKeyedValues == null) {
+ ImmutableSortedMap.Builder<String, String> b = ImmutableSortedMap.naturalOrder();
+ b.putAll(state.customKeyedValues());
+ customKeyedValues = b.build();
+ }
+ return customKeyedValues;
+ }
+
/**
* Returns the evaluated submit requirements for the change. We only intend to store submit
* requirements in NoteDb for closed changes. For closed changes, the results represent the state
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 2d3902c..37729b8 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -62,7 +62,7 @@
.weigher(Weigher.class)
.maximumWeight(10 << 20)
.diskLimit(-1)
- .version(5)
+ .version(6)
.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 951a478..95e528b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -22,6 +22,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CUSTOM_KEYED_VALUE;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
@@ -47,6 +48,7 @@
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
@@ -98,6 +100,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -166,6 +169,7 @@
private final Set<PatchSet.Id> deletedPatchSets;
private final Map<PatchSet.Id, PatchSetState> patchSetStates;
private final List<PatchSet.Id> currentPatchSets;
+ private final TreeMap<String, String> customKeyedValues;
private final Map<PatchSetApproval.Key, PatchSetApproval.Builder> approvals;
private final List<PatchSetApproval.Builder> bufferedApprovals;
private final List<ChangeMessage> allChangeMessages;
@@ -234,6 +238,7 @@
deletedPatchSets = new HashSet<>();
patchSetStates = new HashMap<>();
currentPatchSets = new ArrayList<>();
+ customKeyedValues = new TreeMap<>();
}
ChangeNotesState parseAll() throws ConfigInvalidException, IOException {
@@ -264,6 +269,7 @@
checkMandatoryFooters();
}
+ pruneEmptyCustomKeyedValues();
return buildState();
}
@@ -288,6 +294,7 @@
submissionId,
status,
firstNonNull(hashtags, ImmutableSet.of()),
+ ImmutableSortedMap.copyOfSorted(customKeyedValues),
buildPatchSets(),
buildApprovals(),
ReviewerSet.fromTable(Tables.transpose(reviewers)),
@@ -491,6 +498,7 @@
}
parseHashtags(commit);
+ parseCustomKeyedValues(commit);
parseAttentionSetUpdates(commit);
parseSubmission(commit, commitTimestamp);
@@ -722,6 +730,30 @@
}
}
+ private void parseCustomKeyedValues(ChangeNotesCommit commit) throws ConfigInvalidException {
+ for (String customKeyedValueLine : commit.getFooterLineValues(FOOTER_CUSTOM_KEYED_VALUE)) {
+ String[] parts = customKeyedValueLine.split("=", 2);
+ String key = parts[0];
+ String value = parts[1];
+ // Commits are parsed in reverse order and only the last set of values
+ // should be used. An empty value for a key means it's a deletion.
+ customKeyedValues.putIfAbsent(key, value);
+ }
+ }
+
+ private void pruneEmptyCustomKeyedValues() {
+ List<String> toRemove = new ArrayList<>();
+ for (Map.Entry<String, String> entry : customKeyedValues.entrySet()) {
+ if (entry.getValue().length() == 0) {
+ toRemove.add(entry.getKey());
+ }
+ }
+
+ for (String key : toRemove) {
+ customKeyedValues.remove(key);
+ }
+ }
+
private void parseAttentionSetUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
List<String> attentionStrings = commit.getFooterLineValues(FOOTER_ATTENTION);
for (String attentionString : attentionStrings) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 1715b43..304b54d 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -28,6 +28,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
@@ -111,6 +112,7 @@
@Nullable String submissionId,
@Nullable Change.Status status,
Set<String> hashtags,
+ ImmutableSortedMap<String, String> customKeyedValues,
Map<PatchSet.Id, PatchSet> patchSets,
ListMultimap<PatchSet.Id, PatchSetApproval> approvals,
ReviewerSet reviewers,
@@ -163,6 +165,7 @@
.cherryPickOf(cherryPickOf)
.build())
.hashtags(hashtags)
+ .customKeyedValues(customKeyedValues.entrySet())
.serverId(serverId)
.patchSets(patchSets.entrySet())
.approvals(approvals.entries())
@@ -290,6 +293,16 @@
// Other related to this Change.
abstract ImmutableSet<String> hashtags();
+ /*
+ Custom values are small key value pairs. They can be used to associate the
+ change with external, potentially proprietary systems (e.g. Bug trackers)
+ without requiring dedicated fields in Gerrit-core.
+
+ This data is visible to everyone who can see the change. It must not contain
+ personally identify-able information.
+ */
+ abstract ImmutableList<Map.Entry<String, String>> customKeyedValues();
+
@Nullable
abstract String serverId();
@@ -384,6 +397,7 @@
return new AutoValue_ChangeNotesState.Builder()
.changeId(changeId)
.hashtags(ImmutableSet.of())
+ .customKeyedValues(ImmutableList.of())
.patchSets(ImmutableList.of())
.approvals(ImmutableList.of())
.reviewers(ReviewerSet.empty())
@@ -411,6 +425,8 @@
abstract Builder hashtags(Iterable<String> hashtags);
+ abstract Builder customKeyedValues(Iterable<Map.Entry<String, String>> customKeyedValues);
+
abstract Builder patchSets(Iterable<Map.Entry<PatchSet.Id, PatchSet>> patchSets);
abstract Builder approvals(Iterable<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals);
@@ -475,6 +491,14 @@
b.setHasServerId(true);
}
object.hashtags().forEach(b::addHashtag);
+
+ object
+ .customKeyedValues()
+ .forEach(
+ entry -> {
+ b.putCustomKeyedValues(entry.getKey(), entry.getValue());
+ });
+
object
.patchSets()
.forEach(e -> b.addPatchSet(PatchSetProtoConverter.INSTANCE.toProto(e.getValue())));
@@ -614,6 +638,7 @@
.columns(toChangeColumns(changeId, proto.getColumns()))
.serverId(proto.getHasServerId() ? proto.getServerId() : null)
.hashtags(proto.getHashtagList())
+ .customKeyedValues(proto.getCustomKeyedValuesMap().entrySet())
.patchSets(
proto.getPatchSetList().stream()
.map(msg -> PatchSetProtoConverter.INSTANCE.fromProto(msg))
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 0a895fb..be755ea 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -25,6 +25,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CUSTOM_KEYED_VALUE;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
@@ -100,6 +101,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -163,6 +165,7 @@
private Map<Account.Id, AttentionSetUpdate> plannedAttentionSetUpdates;
private boolean ignoreFurtherAttentionSetUpdates;
private Set<String> hashtags;
+ private TreeMap<String, String> customKeyedValues = new TreeMap<>();
private String changeMessage;
private String tag;
private PatchSetState psState;
@@ -462,6 +465,14 @@
this.hashtags = hashtags;
}
+ public void addCustomKeyedValue(String key, String value) {
+ this.customKeyedValues.put(key, value);
+ }
+
+ public void deleteCustomKeyedValue(String key) {
+ this.customKeyedValues.put(key, "");
+ }
+
/**
* Adds attention set updates that should be stored in NoteDb.
*
@@ -764,6 +775,10 @@
addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags));
}
+ for (Map.Entry<String, String> entry : customKeyedValues.entrySet()) {
+ addFooter(msg, FOOTER_CUSTOM_KEYED_VALUE, entry.getKey() + "=" + entry.getValue());
+ }
+
if (tag != null) {
addFooter(msg, FOOTER_TAG, tag);
}
@@ -1159,6 +1174,7 @@
&& submissionId == null
&& submitRecords == null
&& hashtags == null
+ && customKeyedValues.isEmpty()
&& topic == null
&& commit == null
&& psState == null
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 929aa94..46b85b7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -331,6 +331,7 @@
private SubmitTypeRecord submitTypeRecord;
private Boolean mergeable;
private Set<String> hashtags;
+ private Map<String, String> customKeyedValues;
/**
* Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the edit ref for this
* change and a given user.
@@ -1183,6 +1184,16 @@
this.hashtags = hashtags;
}
+ public Map<String, String> customKeyedValues() {
+ if (customKeyedValues == null) {
+ if (!lazyload()) {
+ return Collections.emptyMap();
+ }
+ customKeyedValues = notes().getCustomKeyedValues();
+ }
+ return customKeyedValues;
+ }
+
public ImmutableListMultimap<Account.Id, String> stars() {
if (stars == null) {
if (!lazyload()) {
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 4ce8c42..dd0ec78d 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -21,7 +21,6 @@
"//java/com/google/gerrit/json",
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/server",
- "//java/com/google/gerrit/server:project_util",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
"//java/com/google/gerrit/server/util/time",
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index b8afcb7..343fb72 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -317,10 +317,18 @@
private List<PatchSetData> getChainForCurrentPatchSet(ChangeResource rsrc)
throws PermissionBackendException, IOException {
- return Lists.reverse(
- getRelatedChangesUtil.getAncestors(
- changeDataFactory.create(rsrc.getNotes()),
- patchSetUtil.current(rsrc.getNotes()),
- true));
+ List<PatchSetData> ancestors =
+ Lists.reverse(
+ getRelatedChangesUtil.getAncestors(
+ changeDataFactory.create(rsrc.getNotes()),
+ patchSetUtil.current(rsrc.getNotes()),
+ true));
+ int eldestOpenAncestor = 0;
+ for (PatchSetData ps : ancestors) {
+ if (ps.data().change().isMerged()) {
+ eldestOpenAncestor++;
+ }
+ }
+ return ancestors.subList(eldestOpenAncestor, ancestors.size());
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
index 2f1153e..e30759d 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.base.Strings;
+import com.google.gerrit.entities.ProjectUtil;
import com.google.gerrit.exceptions.InvalidNameException;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
@@ -24,7 +25,6 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectResource;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 8203346..cfdadd9 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.ProjectUtil;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -35,7 +36,6 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index 6174798..f9602bc 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -18,6 +18,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.ProjectUtil;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -32,7 +33,6 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 9250513..14d781d 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -19,6 +19,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
import static com.google.common.flogger.LazyArgs.lazy;
+import static com.google.gerrit.common.UsedAt.Project.GOOGLE;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;
@@ -39,6 +40,7 @@
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
@@ -52,6 +54,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.RefLogIdentityProvider;
@@ -666,9 +669,14 @@
}
}
+ // For upstream implementation, AccessPath.WEB_BROWSER is never set, so the method will always
+ // return false.
+ @UsedAt(GOOGLE)
private boolean indexAsync() {
- return experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_DO_NOT_AWAIT_CHANGE_INDEXING);
+ return user.getAccessPath().equals(AccessPath.WEB_BROWSER)
+ && experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_DO_NOT_AWAIT_CHANGE_INDEXING,
+ project);
}
private void fireRefChangeEvent() {
@@ -751,6 +759,8 @@
}
}
if (indexAsync) {
+ logger.atFine().log(
+ "Asynchronously reindexing changes, %s in project %s", results.keySet(), project.get());
// We want to index asynchronously. However, the callers will await all
// index futures. This allows us to - even in synchronous case -
// parallelize indexing changes.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 118b31b..96c86d4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -246,11 +246,14 @@
private Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
@Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value = "GerritBackendFeature__return_new_change_info_id")
public void get() throws Exception {
PushOneCommit.Result r = createChange();
- String triplet = project.get() + "~master~" + r.getChangeId();
- ChangeInfo c = info(triplet);
- assertThat(c.id).isEqualTo(triplet);
+ String id = project.get() + "~" + r.getChange().getId().get();
+ ChangeInfo c = info(id);
+ assertThat(c.id).isEqualTo(id);
assertThat(c.project).isEqualTo(project.get());
assertThat(c.branch).isEqualTo("master");
assertThat(c.status).isEqualTo(ChangeStatus.NEW);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index fd37fb0..5ecb5a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -986,7 +986,7 @@
public void rebaseChain() throws Exception {
// Create changes with the following hierarchy:
// * HEAD
- // * r1
+ // * r (merged)
// * r2
// * r3
// * r4
@@ -1043,7 +1043,7 @@
final String newContent = "new content";
// Create changes with the following revision hierarchy:
// * HEAD
- // * r1
+ // * r (merged)
// * r2
// * r3/1 r3/2
// * r4
@@ -1079,6 +1079,71 @@
}
@Test
+ public void rebaseChainWithMergedAncestor() throws Exception {
+ final String file = "modified_file.txt";
+ final String newContent = "new content";
+
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r (merged)
+ // * r2.1 r2.2 (merged)
+ // * r3
+ // * r4
+ // *r5
+ PushOneCommit.Result r = createChange();
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+ PushOneCommit.Result r4 = createChange();
+ PushOneCommit.Result r5 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+ testRepo.reset("HEAD~1");
+
+ // Create r2.2
+ gApi.changes()
+ .id(r2.getChangeId())
+ .edit()
+ .modifyFile(file, RawInputUtil.create(newContent.getBytes(UTF_8)));
+ gApi.changes().id(r2.getChangeId()).edit().publish();
+ // Approve and submit r2.2
+ revision = gApi.changes().id(r2.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Add an approval whose score should be copied on trivial rebase
+ gApi.changes().id(r3.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the chain through r4.
+ verifyRebaseChainResponse(gApi.changes().id(r4.getChangeId()).rebaseChain(), false, r3, r4);
+
+ // Only r3 and r4 are rebased.
+ verifyRebaseForChange(r3.getChange().getId(), r2.getChange().getId(), true);
+ verifyRebaseForChange(r4.getChange().getId(), r3.getChange().getId(), false);
+
+ verifyChangeIsUpToDate(r2);
+ verifyChangeIsUpToDate(r3);
+ verifyChangeIsUpToDate(r4);
+
+ // r5 wasn't rebased.
+ assertThat(
+ gApi.changes()
+ .id(r5.getChangeId())
+ .get(CURRENT_REVISION)
+ .getCurrentRevision()
+ ._number)
+ .isEqualTo(1);
+
+ // Rebasing r5
+ verifyRebaseChainResponse(
+ gApi.changes().id(r5.getChangeId()).rebaseChain(), false, r3, r4, r5);
+
+ verifyRebaseForChange(r5.getChange().getId(), r4.getChange().getId(), false);
+ }
+
+ @Test
public void rebaseChainWithConflicts_conflictsForbidden() throws Exception {
PushOneCommit.Result r1 = createChange();
gApi.changes()
diff --git a/javatests/com/google/gerrit/auth/BUILD b/javatests/com/google/gerrit/auth/BUILD
index 6a41d01..fa30f80a 100644
--- a/javatests/com/google/gerrit/auth/BUILD
+++ b/javatests/com/google/gerrit/auth/BUILD
@@ -7,8 +7,6 @@
srcs = glob(
["**/*.java"],
),
- resource_strip_prefix = "resources",
- resources = ["//resources/com/google/gerrit/server"],
tags = ["no_windows"],
visibility = ["//visibility:public"],
runtime_deps = [
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index b5c6149..4821f20 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -25,8 +25,6 @@
["**/*.java"],
exclude = CUSTOM_TRUTH_SUBJECTS,
),
- resource_strip_prefix = "resources",
- resources = ["//resources/com/google/gerrit/server"],
tags = ["no_windows"],
visibility = ["//visibility:public"],
runtime_deps = [
diff --git a/javatests/com/google/gerrit/server/ioutil/BUILD b/javatests/com/google/gerrit/server/ioutil/BUILD
index ac9530f..8654522 100644
--- a/javatests/com/google/gerrit/server/ioutil/BUILD
+++ b/javatests/com/google/gerrit/server/ioutil/BUILD
@@ -6,7 +6,6 @@
srcs = glob(
["**/*.java"],
),
- resource_strip_prefix = "resources",
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/server/ioutil",
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 9a29230..d04964b 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -65,6 +65,7 @@
import java.lang.reflect.Type;
import java.sql.Timestamp;
import java.time.Instant;
+import java.util.AbstractMap.SimpleEntry;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -334,6 +335,24 @@
}
@Test
+ public void serializeCustomKeyedValues() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .customKeyedValues(
+ ImmutableList.of(
+ new SimpleEntry<String, String>("key1", "value1"),
+ new SimpleEntry<String, String>("key2", "value2")))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .putCustomKeyedValues("key1", "value1")
+ .putCustomKeyedValues("key2", "value2")
+ .build());
+ }
+
+ @Test
public void serializePatchSets() throws Exception {
PatchSet ps1 =
PatchSet.builder()
@@ -918,6 +937,9 @@
.put("columns", ChangeColumns.class)
.put("hashtags", new TypeLiteral<ImmutableSet<String>>() {}.getType())
.put(
+ "customKeyedValues",
+ new TypeLiteral<ImmutableList<Map.Entry<String, String>>>() {}.getType())
+ .put(
"patchSets",
new TypeLiteral<ImmutableList<Map.Entry<PatchSet.Id, PatchSet>>>() {}.getType())
.put(
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 15eefcd..69b1870 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -34,6 +34,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
@@ -67,6 +68,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.TreeMap;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -1504,6 +1506,78 @@
}
@Test
+ public void customKeyedValuesCommit() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.addCustomKeyedValue("key1", "value1");
+ update.addCustomKeyedValue("key2", "value2");
+ update.commit();
+ try (RevWalk walk = new RevWalk(repo)) {
+ RevCommit commit = walk.parseCommit(update.getResult());
+ walk.parseBody(commit);
+ assertThat(commit.getFullMessage()).contains("Custom-Keyed-Value: key1=value1\n");
+ assertThat(commit.getFullMessage()).contains("Custom-Keyed-Value: key2=value2\n");
+ }
+ }
+
+ @Test
+ public void customKeyedValuesChangeNotes() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.addCustomKeyedValue("key1", "value\n1");
+ update.addCustomKeyedValue("key2", "value2=value3");
+ update.addCustomKeyedValue("key3", "value3: value4");
+ update.commit();
+
+ TreeMap<String, String> customKeyedValues = new TreeMap<>();
+ customKeyedValues.put("key1", "value 1");
+ customKeyedValues.put("key2", "value2=value3");
+ customKeyedValues.put("key3", "value3: value4");
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getCustomKeyedValues())
+ .isEqualTo(ImmutableSortedMap.copyOfSorted(customKeyedValues));
+ }
+
+ @Test
+ public void customKeyedValuesChangeNotes_Override() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.addCustomKeyedValue("key1", "value1");
+ update.addCustomKeyedValue("key2", "value2");
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.addCustomKeyedValue("key1", "value3");
+ update.commit();
+
+ TreeMap<String, String> customKeyedValues = new TreeMap<>();
+ customKeyedValues.put("key1", "value3");
+ customKeyedValues.put("key2", "value2");
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getCustomKeyedValues())
+ .isEqualTo(ImmutableSortedMap.copyOfSorted(customKeyedValues));
+ }
+
+ @Test
+ public void customKeyedValuesChangeNotes_Deletion() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.addCustomKeyedValue("key1", "value1");
+ update.addCustomKeyedValue("key2", "value2");
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.deleteCustomKeyedValue("key1");
+ update.commit();
+
+ TreeMap<String, String> customKeyedValues = new TreeMap<>();
+ customKeyedValues.put("key2", "value2");
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getCustomKeyedValues())
+ .isEqualTo(ImmutableSortedMap.copyOfSorted(customKeyedValues));
+ }
+
+ @Test
public void topicChangeNotes() throws Exception {
Change c = newChange();
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index e36ecdd..be8e04b 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit e36ecddbc99dc434f3259814d9d84fa96e509772
+Subproject commit be8e04b1a6de091a63c9bc79b56508f2ad56a830
diff --git a/plugins/package.json b/plugins/package.json
index 5323c9c..504fc17 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -13,11 +13,12 @@
"@codemirror/commands": "^6.2.3",
"@codemirror/legacy-modes": "^6.3.2",
"@codemirror/lang-cpp": "^6.0.2",
- "@codemirror/lang-css": "^6.1.1",
+ "@codemirror/lang-css": "^6.2.0",
"@codemirror/lang-html": "^6.4.3",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/lang-json": "^6.0.1",
+ "@codemirror/lang-less": "^6.0.0",
"@codemirror/lang-markdown": "^6.1.1",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-python": "^6.1.2",
@@ -28,9 +29,9 @@
"@codemirror/language": "^6.6.0",
"@codemirror/language-data": "^6.3.0",
"@codemirror/lint": "^6.2.1",
- "@codemirror/search": "^6.3.0",
+ "@codemirror/search": "^6.4.0",
"@codemirror/state": "^6.2.0",
- "@codemirror/view": "^6.9.6",
+ "@codemirror/view": "^6.10.0",
"lit": "^2.2.3",
"rxjs": "^6.6.7",
"sinon": "^13.0.0"
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index cde18ce..3f53453 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -62,14 +62,15 @@
"@codemirror/language" "^6.0.0"
"@lezer/cpp" "^1.0.0"
-"@codemirror/lang-css@^6.0.0", "@codemirror/lang-css@^6.1.1":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.1.1.tgz#8c4414d399df14e796f9891a8152e411264ef535"
- integrity sha512-P6jdNEHyRcqqDgbvHYyC9Wxkek0rnG3a9aVSRi4a7WrjPbQtBTaOmvYpXmm13zZMAatO4Oqpac+0QZs7sy+LnQ==
+"@codemirror/lang-css@^6.0.0", "@codemirror/lang-css@^6.1.1", "@codemirror/lang-css@^6.2.0":
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.2.0.tgz#f84f9da392099432445c75e32fdac63ae572315f"
+ integrity sha512-oyIdJM29AyRPM3+PPq1I2oIk8NpUfEN3kAM05XWDDs6o3gSneIKaVJifT2P+fqONLou2uIgXynFyMUDQvo/szA==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
+ "@lezer/common" "^1.0.2"
"@lezer/css" "^1.0.0"
"@codemirror/lang-html@^6.0.0", "@codemirror/lang-html@^6.4.3":
@@ -116,6 +117,16 @@
"@codemirror/language" "^6.0.0"
"@lezer/json" "^1.0.0"
+"@codemirror/lang-less@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-less/-/lang-less-6.0.0.tgz#47ac36242f45bcc211dbcbce11e10f3b249519c9"
+ integrity sha512-hQVj+AxcUW/LybRkwaOope8K8+U6bjWH91t0tW8MMok33Y65xo+Wx0t1BaXi3Iuo6CgJ4tW7Rz09cfNwloIdNA==
+ dependencies:
+ "@codemirror/lang-css" "^6.2.0"
+ "@codemirror/language" "^6.0.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+
"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.1.1.tgz#ff3cdd339c277f6a02d08eb12f1090977873e771"
@@ -262,10 +273,10 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
-"@codemirror/search@^6.3.0":
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.3.0.tgz#d48b2d636fc7474613e240ba8e56c4c5e6e51822"
- integrity sha512-rBhZxzT34CarfhgCZGhaLBScABDN3iqJxixzNuINp9lrb3lzm0nTpR77G1VrxGO3HOGK7j62jcJftQM7eCOIuw==
+"@codemirror/search@^6.4.0":
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.4.0.tgz#2b256a9e0eaa9317fb48e3cc81eb2735360a59b4"
+ integrity sha512-zMDgaBXah+nMLK2dHz9GdCnGbQu+oaGRXS1qviqNZkvOCv/whp5XZFyoikLp/23PM9RBcbuKUUISUmQHM1eRHw==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
@@ -276,10 +287,10 @@
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2"
integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==
-"@codemirror/view@^6.0.0", "@codemirror/view@^6.2.2", "@codemirror/view@^6.6.0", "@codemirror/view@^6.9.6":
- version "6.9.6"
- resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.9.6.tgz#6bf302608e03566688d5138c50b38d07356e11a1"
- integrity sha512-g68PxS3RkHpxfYS6DTWCy1jeA5/oHzmdWjMVPkOzqQyxpElHEcPncUd4EeMVSa4okt0sS3hNXVaRnJqO/7MeJw==
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.10.0", "@codemirror/view@^6.2.2", "@codemirror/view@^6.6.0":
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.10.0.tgz#40bb39f391955db8960337a9e80fd7564f8915e2"
+ integrity sha512-Oea3rvE4JQLMmLsy2b54yxXQJgJM9xKpUQIpF/LGgKUTH2lA06GAmEtKKWn5OUnbW3jrH1hHeUd8DJEgePMOeQ==
dependencies:
"@codemirror/state" "^6.1.4"
style-mod "^4.0.0"
@@ -348,9 +359,9 @@
"@lezer/lr" "^1.0.0"
"@lezer/javascript@^1.0.0":
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.2.tgz#aee4e18f573b496756294c558965d36320bfca47"
- integrity sha512-77qdAD4zanmImPiAu4ibrMUzRc79UHoccdPa+Ey5iwS891TAkhnMAodUe17T7zV7tnF7e9HXM0pfmjoGEhrppg==
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.3.tgz#f59e764a0578184c6fb86abb5279a9679777c3ba"
+ integrity sha512-k7Eo9z9B1supZ5cCD4ilQv/RZVN30eUQL+gGbr6ybrEY3avBAL5MDiYi2aa23Aj0A79ry4rJRvPAwE2TM8bd+A==
dependencies:
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
@@ -364,9 +375,9 @@
"@lezer/lr" "^1.0.0"
"@lezer/lr@^1.0.0", "@lezer/lr@^1.1.0", "@lezer/lr@^1.3.0", "@lezer/lr@^1.3.1":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.3.tgz#0ac6c889f1235874f33c45a1b9785d7054f60708"
- integrity sha512-JPQe3mwJlzEVqy67iQiiGozhcngbO8QBgpqZM6oL1Wj/dXckrEexpBLeFkq0edtW5IqnPRFxA24BHJni8Js69w==
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.4.tgz#8795bf2ba4f69b998e8fb4b5a7c57ea68753474c"
+ integrity sha512-7o+e4og/QoC/6btozDPJqnzBhUaD1fMfmvnEKQO1wRRiTse1WxaJ3OMEXZJnkgT6HCcTVOctSoXK9jGJw2oe9g==
dependencies:
"@lezer/common" "^1.0.0"
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index fb350fd..ef2f63e 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -10,6 +10,7 @@
STARTED_AS_GUEST = 'Started as guest',
VISIBILILITY_HIDDEN = 'Visibility changed to hidden',
VISIBILILITY_VISIBLE = 'Visibility changed to visible',
+ FOCUS = 'Focus changed',
EXTENSION_DETECTED = 'Extension detected',
PLUGINS_INSTALLED = 'Plugins installed',
PLUGINS_FAILED = 'Some plugins failed to load',
@@ -133,4 +134,6 @@
CHANGE_ACTION_FIRED = 'change-action-fired',
BUTTON_CLICK = 'button-click',
LINK_CLICK = 'link-click',
+ USER_ACTIVE = 'user-active',
+ USER_PASSIVE = 'user-passive',
}
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index 4d0d3f1..e15c240 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -39,8 +39,6 @@
// The name that gets automatically input when a new reference is added.
const NEW_NAME = 'refs/heads/*';
const REFS_NAME = 'refs/';
-const ON_BEHALF_OF = '(On Behalf Of)';
-const LABEL = 'Label';
@customElement('gr-access-section')
export class GrAccessSection extends LitElement {
@@ -360,14 +358,14 @@
labelOptions.push({
id: 'label-' + labelName,
value: {
- name: `${LABEL} ${labelName}`,
+ name: `Label ${labelName}`,
id: 'label-' + labelName,
},
});
labelOptions.push({
id: 'labelAs-' + labelName,
value: {
- name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
+ name: `Label ${labelName} (On Behalf Of)`,
id: 'labelAs-' + labelName,
},
});
@@ -384,11 +382,13 @@
} else if (AccessPermissions[permission.id]) {
return AccessPermissions[permission.id]?.name;
} else if (permission.value.label) {
- let behalfOf = '';
if (permission.id.startsWith('labelAs-')) {
- behalfOf = ON_BEHALF_OF;
+ return `Label ${permission.value.label} (On Behalf Of)`;
+ } else if (permission.id.startsWith('removeLabel-')) {
+ return `Remove Label ${permission.value.label}`;
+ } else {
+ return `Label ${permission.value.label}`;
}
- return `${LABEL} ${permission.value.label}${behalfOf}`;
}
return undefined;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
index 593a1ed..2c397e0 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
@@ -355,7 +355,20 @@
assert.equal(
element.computePermissionName(permission),
- 'Label Code-Review(On Behalf Of)'
+ 'Label Code-Review (On Behalf Of)'
+ );
+
+ permission = {
+ id: 'removeLabel-Code-Review' as GitRef,
+ value: {
+ label: 'Code-Review',
+ rules: {},
+ },
+ };
+
+ assert.equal(
+ element.computePermissionName(permission),
+ 'Remove Label Code-Review'
);
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 4eee3cb..e0c28bc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -42,7 +42,6 @@
BranchName,
ChangeActionDialog,
ChangeInfo,
- ChangeViewChangeInfo,
CherryPickInput,
CommitId,
InheritedBooleanInfo,
@@ -100,7 +99,7 @@
import {changeModelToken} from '../../../models/change/change-model';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html, nothing} from 'lit';
-import {customElement, property, query, state} from 'lit/decorators.js';
+import {customElement, query, state} from 'lit/decorators.js';
import {ifDefined} from 'lit/directives/if-defined.js';
import {assertIsDefined, queryAll, uuid} from '../../../utils/common-util';
import {Interaction} from '../../../constants/reporting';
@@ -113,6 +112,9 @@
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {subscribe} from '../../lit/subscription-controller';
+import {userModelToken} from '../../../models/user/user-model';
+import {ParsedChangeInfo} from '../../../types/types';
+import {configModelToken} from '../../../models/config/config-model';
const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -377,76 +379,54 @@
RevisionActions = RevisionActions;
- @property({type: Object})
- change?: ChangeViewChangeInfo;
+ @state() change?: ParsedChangeInfo;
- @state()
- actions: ActionNameToActionInfoMap = {};
+ @state() actions: ActionNameToActionInfoMap = {};
- @property({type: Array})
- primaryActionKeys: PrimaryActionKey[] = [
+ @state() primaryActionKeys: PrimaryActionKey[] = [
ChangeActions.READY,
RevisionActions.SUBMIT,
];
- @property({type: Boolean})
- disableEdit = false;
-
- // private but used in test
@state() _hideQuickApproveAction = false;
- @property({type: Object})
- account?: AccountInfo;
+ @state() account?: AccountInfo;
- @property({type: String})
- changeNum?: NumericChangeId;
+ @state() changeNum?: NumericChangeId;
- @property({type: String})
- changeStatus?: ChangeStatus;
+ @state() changeStatus?: ChangeStatus;
- @property({type: String})
- commitNum?: CommitId;
+ @state() commitNum?: CommitId;
@state() latestPatchNum?: PatchSetNumber;
- @property({type: String})
- commitMessage = '';
+ @state() commitMessage = '';
- @property({type: Object})
- revisionActions: ActionNameToActionInfoMap = {};
+ @state() revisionActions: ActionNameToActionInfoMap = {};
- @state() private revisionSubmitAction?: ActionInfo | null;
+ @state() revisionSubmitAction?: ActionInfo | null;
- // used as a proprty type so cannot be private
@state() revisionRebaseAction?: ActionInfo | null;
- @property({type: String})
- privateByDefault?: InheritedBooleanInfo;
+ @state() privateByDefault?: InheritedBooleanInfo;
- // private but used in test
@state() loading = true;
- // private but used in test
@state() actionLoadingMessage = '';
- @state() private inProgressActionKeys = new Set<string>();
+ @state() inProgressActionKeys = new Set<string>();
- // _computeAllActions always returns an array
- // private but used in test
@state() allActionValues: UIActionInfo[] = [];
- // private but used in test
@state() topLevelActions?: UIActionInfo[];
- // private but used in test
@state() topLevelPrimaryActions?: UIActionInfo[];
- // private but used in test
@state() topLevelSecondaryActions?: UIActionInfo[];
- @state() private menuActions?: MenuAction[];
+ @state() menuActions?: MenuAction[];
- @state() private overflowActions: OverflowAction[] = [
+ @state() overflowActions: OverflowAction[] = [
{
type: ActionType.CHANGE,
key: ChangeActions.WIP,
@@ -493,29 +473,21 @@
},
];
- @state() private actionPriorityOverrides: ActionPriorityOverride[] = [];
+ @state() actionPriorityOverrides: ActionPriorityOverride[] = [];
- @state() private additionalActions: UIActionInfo[] = [];
+ @state() additionalActions: UIActionInfo[] = [];
- // private but used in test
@state() hiddenActions: string[] = [];
- // private but used in test
@state() disabledMenuActions: string[] = [];
- // private but used in test
- @state()
- editPatchsetLoaded = false;
+ @state() editPatchsetLoaded = false;
- @property({type: Boolean})
- editMode = false;
+ @state() editMode = false;
- // private but used in test
- @state()
- editBasedOnCurrentPatchSet = true;
+ @state() editBasedOnCurrentPatchSet = true;
- @property({type: Boolean})
- loggedIn = false;
+ @state() loggedIn = false;
private readonly restApiService = getAppContext().restApiService;
@@ -523,6 +495,10 @@
private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ private readonly getConfigModel = resolve(this, configModelToken);
+
private readonly getChangeModel = resolve(this, changeModelToken);
private readonly getStorage = resolve(this, storageServiceToken);
@@ -546,6 +522,51 @@
() => this.getChangeModel().patchNum$,
x => (this.editPatchsetLoaded = x === 'edit')
);
+ subscribe(
+ this,
+ () => this.getChangeModel().changeNum$,
+ x => (this.changeNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().change$,
+ x => (this.change = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().status$,
+ x => (this.changeStatus = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().editMode$,
+ x => (this.editMode = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().revision$,
+ rev => (this.commitNum = rev?.commit?.commit)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().latestRevision$,
+ rev => (this.commitMessage = rev?.commit?.message ?? '')
+ );
+ subscribe(
+ this,
+ () => this.getUserModel().account$,
+ x => (this.account = x)
+ );
+ subscribe(
+ this,
+ () => this.getUserModel().loggedIn$,
+ x => (this.loggedIn = x)
+ );
+ subscribe(
+ this,
+ () => this.getConfigModel().repoConfig$,
+ config => (this.privateByDefault = config?.private_by_default)
+ );
}
override connectedCallback() {
@@ -865,7 +886,7 @@
this.revisionActions = revisionActions;
this.sendShowRevisionActions({
- change,
+ change: change as ChangeInfo,
revisionActions,
});
this.handleLoadingComplete();
@@ -1031,18 +1052,7 @@
}
private editStatusChanged() {
- // Hide change edits if not logged in
- if (this.change === undefined || !this.loggedIn) {
- return;
- }
- if (this.disableEdit) {
- delete this.actions.rebaseEdit;
- delete this.actions.publishEdit;
- delete this.actions.deleteEdit;
- delete this.actions.stopEdit;
- delete this.actions.edit;
- return;
- }
+ if (!this.change || !this.loggedIn) return;
if (this.editPatchsetLoaded) {
// Only show actions that mutate an edit if an actual edit patch set
// is loaded.
@@ -1121,7 +1131,7 @@
if (!this.change || !this.change.labels || !this.change.permitted_labels) {
return null;
}
- if (this.change && this.change.status === ChangeStatus.MERGED) {
+ if (this.change?.status === ChangeStatus.MERGED) {
return null;
}
let result;
@@ -1323,18 +1333,18 @@
// private but used in test
canSubmitChange() {
- if (!this.change) {
- return false;
- }
+ if (!this.change) return false;
+ const change = this.change as ChangeInfo;
+ const revision = this.getRevision(change, this.latestPatchNum);
return this.getPluginLoader().jsApiService.canSubmitChange(
- this.change,
- this.getRevision(this.change, this.latestPatchNum)
+ change,
+ revision
);
}
// private but used in test
- getRevision(change: ChangeViewChangeInfo, patchNum?: PatchSetNumber) {
- for (const rev of Object.values(change.revisions)) {
+ getRevision(change: ChangeInfo, patchNum?: PatchSetNumber) {
+ for (const rev of Object.values(change.revisions ?? {})) {
if (rev._number === patchNum) {
return rev;
}
@@ -1805,7 +1815,7 @@
if (dialog.init) dialog.init();
dialog.hidden = false;
assertIsDefined(this.actionsModal, 'actionsModal');
- this.actionsModal.showModal();
+ if (this.actionsModal.isConnected) this.actionsModal.showModal();
whenVisible(dialog, () => {
if (dialog.resetFocus) {
dialog.resetFocus();
@@ -1818,7 +1828,7 @@
// private but used in test
setReviewOnRevert(newChangeId: NumericChangeId) {
const review = this.getPluginLoader().jsApiService.getReviewPostRevert(
- this.change
+ this.change as ChangeInfo
);
if (!review) {
return Promise.resolve(undefined);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 946191b..b628cc1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -129,6 +129,7 @@
element = await fixture<GrChangeActions>(html`
<gr-change-actions></gr-change-actions>
`);
+ element.changeStatus = ChangeStatus.NEW;
element.change = {
...createChangeViewChange(),
actions: {
@@ -155,7 +156,7 @@
await element.reload();
});
- test('render', () => {
+ test('render', async () => {
assert.shadowDom.equal(
element,
/* HTML */ `
@@ -200,6 +201,24 @@
Rebase
</gr-button>
</gr-tooltip-content>
+ <gr-tooltip-content
+ has-tooltip=""
+ position-below=""
+ title="Edit this change"
+ >
+ <gr-button
+ aria-disabled="false"
+ class="edit"
+ data-action-key="edit"
+ data-label="Edit"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ <gr-icon filled="" icon="edit"> </gr-icon>
+ Edit
+ </gr-button>
+ </gr-tooltip-content>
</section>
<gr-button
aria-disabled="false"
@@ -705,29 +724,6 @@
});
suite('change edits', () => {
- test('disableEdit', async () => {
- element.editMode = false;
- element.editBasedOnCurrentPatchSet = false;
- element.change = {
- ...createChangeViewChange(),
- status: ChangeStatus.NEW,
- };
- element.disableEdit = true;
- await element.updateComplete;
-
- assert.isNotOk(
- query(element, 'gr-button[data-action-key="publishEdit"]')
- );
- assert.isNotOk(
- query(element, 'gr-button[data-action-key="rebaseEdit"]')
- );
- assert.isNotOk(
- query(element, 'gr-button[data-action-key="deleteEdit"]')
- );
- assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
- assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
- });
-
test('shows confirm dialog for delete edit', async () => {
element.loggedIn = true;
element.editMode = true;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index b7851a6..1cb25ea 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -82,6 +82,12 @@
import {createChangeUrl} from '../../../models/views/change';
import {getChangeWeblinks} from '../../../utils/weblink-util';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {subscribe} from '../../lit/subscription-controller';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
+import {changeModelToken} from '../../../models/change/change-model';
+import {relatedChangesModelToken} from '../../../models/change/related-changes-model';
const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
@@ -118,54 +124,90 @@
export class GrChangeMetadata extends LitElement {
@query('#webLinks') webLinks?: HTMLElement;
- @property({type: Object}) change?: ParsedChangeInfo;
-
- @property({type: Object}) revertedChange?: ChangeInfo;
-
- @property({type: Object}) account?: AccountDetailInfo;
-
- @property({type: Object}) revision?: RevisionInfo | EditRevisionInfo;
-
- // TODO: Just use `revision.commit` instead.
- @property({type: Object}) commitInfo?: CommitInfoWithRequiredCommit;
-
- @property({type: Object}) serverConfig?: ServerInfo;
-
+ // TODO: Convert to @state. That requires the change model to keep track of
+ // current revision actions. Then we can also get rid of the
+ // `revision-actions-changed` event.
@property({type: Boolean}) parentIsCurrent?: boolean;
- @property({type: Object}) repoConfig?: ConfigInfo;
+ @state() change?: ParsedChangeInfo;
- // private but used in test
+ @state() revertedChange?: ChangeInfo;
+
+ @state() account?: AccountDetailInfo;
+
+ @state() revision?: RevisionInfo | EditRevisionInfo;
+
+ @state() serverConfig?: ServerInfo;
+
+ @state() repoConfig?: ConfigInfo;
+
@state() mutable = false;
- @state() private readonly notCurrentMessage = NOT_CURRENT_MESSAGE;
+ @state() readonly notCurrentMessage = NOT_CURRENT_MESSAGE;
- // private but used in test
@state() topicReadOnly = true;
- // private but used in test
@state() hashtagReadOnly = true;
- @state() private pushCertificateValidation?: PushCertificateValidationInfo;
+ @state() pushCertificateValidation?: PushCertificateValidationInfo;
- // private but used in test
@state() settingTopic = false;
- // private but used in test
@state() currentParents: ParentCommitInfo[] = [];
- @state() private showAllSections = false;
+ @state() showAllSections = false;
- @state() private queryTopic?: AutocompleteQuery;
+ @state() queryTopic?: AutocompleteQuery;
- @state() private queryHashtag?: AutocompleteQuery;
+ @state() queryHashtag?: AutocompleteQuery;
private restApiService = getAppContext().restApiService;
private readonly reporting = getAppContext().reportingService;
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getRelatedChangesModel = resolve(
+ this,
+ relatedChangesModelToken
+ );
+
constructor() {
super();
+ subscribe(
+ this,
+ () => this.getConfigModel().serverConfig$,
+ serverConfig => (this.serverConfig = serverConfig)
+ );
+ subscribe(
+ this,
+ () => this.getConfigModel().repoConfig$,
+ repoConfig => (this.repoConfig = repoConfig)
+ );
+ subscribe(
+ this,
+ () => this.getUserModel().account$,
+ account => (this.account = account)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().change$,
+ change => (this.change = change)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().revision$,
+ revision => (this.revision = revision)
+ );
+ subscribe(
+ this,
+ () => this.getRelatedChangesModel().revertingChange$,
+ revertingChange => (this.revertedChange = revertingChange)
+ );
this.queryTopic = (input: string) => this.getTopicSuggestions(input);
this.queryHashtag = (input: string) => this.getHashtagSuggestions(input);
}
@@ -735,7 +777,10 @@
// private but used in test
computeWebLinks(): WebLinkInfo[] {
- return getChangeWeblinks(this.commitInfo?.web_links, this.serverConfig);
+ return getChangeWeblinks(
+ this.revision?.commit?.web_links,
+ this.serverConfig
+ );
}
private computeStrategy() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index c46fe24..e0e1364 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -9,7 +9,6 @@
import {ChangeRole, GrChangeMetadata} from './gr-change-metadata';
import {
createServerInfo,
- createUserConfig,
createParsedChange,
createAccountWithId,
createCommitInfoWithRequiredCommit,
@@ -61,18 +60,9 @@
let element: GrChangeMetadata;
setup(async () => {
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('getConfig').returns(
- Promise.resolve({
- ...createServerInfo(),
- user: {
- ...createUserConfig(),
- anonymouscowardname: 'test coward name',
- },
- })
- );
element = await fixture(html`<gr-change-metadata></gr-change-metadata>`);
element.change = createParsedChange();
+ element.account = undefined;
await element.updateComplete;
});
@@ -251,16 +241,22 @@
});
test('weblinks hidden when no weblinks', async () => {
- element.commitInfo = createCommitInfoWithRequiredCommit();
+ element.revision = {
+ ...createRevision(),
+ commit: createCommitInfoWithRequiredCommit(),
+ };
element.serverConfig = createServerInfo();
await element.updateComplete;
assert.isNull(element.webLinks);
});
test('weblinks hidden when only gitiles weblink', async () => {
- element.commitInfo = {
- ...createCommitInfoWithRequiredCommit(),
- web_links: [{...createWebLinkInfo(), name: 'gitiles', url: '#'}],
+ element.revision = {
+ ...createRevision(),
+ commit: {
+ ...createCommitInfoWithRequiredCommit(),
+ web_links: [{...createWebLinkInfo(), name: 'gitiles', url: '#'}],
+ },
};
element.serverConfig = createServerInfo();
await element.updateComplete;
@@ -270,9 +266,12 @@
test('weblinks hidden when sole weblink is set as primary', async () => {
const browser = 'browser';
- element.commitInfo = {
- ...createCommitInfoWithRequiredCommit(),
- web_links: [{...createWebLinkInfo(), name: browser, url: '#'}],
+ element.revision = {
+ ...createRevision(),
+ commit: {
+ ...createCommitInfoWithRequiredCommit(),
+ web_links: [{...createWebLinkInfo(), name: browser, url: '#'}],
+ },
};
element.serverConfig = {
...createServerInfo(),
@@ -286,9 +285,12 @@
});
test('weblinks are visible when other weblinks', async () => {
- element.commitInfo = {
- ...createCommitInfoWithRequiredCommit(),
- web_links: [{...createWebLinkInfo(), name: 'test', url: '#'}],
+ element.revision = {
+ ...createRevision(),
+ commit: {
+ ...createCommitInfoWithRequiredCommit(),
+ web_links: [{...createWebLinkInfo(), name: 'test', url: '#'}],
+ },
};
await element.updateComplete;
const webLinks = element.webLinks!;
@@ -297,12 +299,15 @@
});
test('weblinks are visible when gitiles and other weblinks', async () => {
- element.commitInfo = {
- ...createCommitInfoWithRequiredCommit(),
- web_links: [
- {...createWebLinkInfo(), name: 'test', url: '#'},
- {...createWebLinkInfo(), name: 'gitiles', url: '#'},
- ],
+ element.revision = {
+ ...createRevision(),
+ commit: {
+ ...createCommitInfoWithRequiredCommit(),
+ web_links: [
+ {...createWebLinkInfo(), name: 'test', url: '#'},
+ {...createWebLinkInfo(), name: 'gitiles', url: '#'},
+ ],
+ },
};
await element.updateComplete;
const webLinks = element.webLinks!;
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 531994c90..f6a40a2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -63,6 +63,9 @@
@customElement('gr-change-summary')
export class GrChangeSummary extends LitElement {
@state()
+ commentsLoading = true;
+
+ @state()
commentThreads?: CommentThread[];
@state()
@@ -161,6 +164,11 @@
);
subscribe(
this,
+ () => this.getCommentsModel().commentsLoading$,
+ x => (this.commentsLoading = x)
+ );
+ subscribe(
+ this,
() =>
combineLatest([
this.getUserModel().account$,
@@ -530,6 +538,10 @@
<tr>
<td class="key">Comments</td>
<td class="value">
+ ${when(
+ this.commentsLoading,
+ () => html`<span class="loadingSpin"></span>`
+ )}
<gr-comments-summary
.commentThreads=${this.commentThreads}
.draftCount=${this.draftCount}
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 3cb63f9..c3d9774 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -50,6 +50,7 @@
},
discardedDrafts: [],
});
+ element.commentsLoading = false;
element.commentThreads = [
createCommentThread([createComment()]),
createCommentThread([{...createComment(), unresolved: true}]),
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 a64dcd2..02fdb34 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
@@ -335,10 +335,7 @@
@state()
private updateCheckTimerHandle?: number | null;
- // Private but used in tests.
- getEditMode(): boolean {
- return !!this.viewState?.edit || this.patchNum === EDIT;
- }
+ @state() editMode = false;
isSubmitEnabled(): boolean {
return !!(
@@ -664,6 +661,11 @@
);
subscribe(
this,
+ () => this.getChangeModel().editMode$,
+ editMode => (this.editMode = editMode)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().patchNum$,
patchNum => (this.patchNum = patchNum)
);
@@ -1134,23 +1136,16 @@
${this.renderTabHeaders()} ${this.renderTabContent()}
${this.renderChangeLog()}
</div>
- <gr-apply-fix-dialog
- id="applyFixDialog"
- .change=${this.change}
- .changeNum=${this.changeNum}
- ></gr-apply-fix-dialog>
+ <gr-apply-fix-dialog id="applyFixDialog"></gr-apply-fix-dialog>
<dialog id="downloadModal" tabindex="-1">
<gr-download-dialog
id="downloadDialog"
- .change=${this.change}
- .config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
</dialog>
<dialog id="includedInModal" tabindex="-1">
<gr-included-in-dialog
id="includedInDialog"
- .changeNum=${this.changeNum}
@close=${this.handleIncludedInDialogClose}
></gr-included-in-dialog>
</dialog>
@@ -1287,27 +1282,18 @@
}
private renderCommitActions() {
- return html` <div class="commitActions">
- <!-- always show gr-change-actions regardless if logged in or not -->
- <gr-change-actions
- id="actions"
- .change=${this.change}
- .disableEdit=${false}
- .account=${this.account}
- .changeNum=${this.changeNum}
- .changeStatus=${this.change?.status}
- .commitNum=${this.revision?.commit?.commit}
- .commitMessage=${this.latestCommitMessage}
- .editMode=${this.getEditMode()}
- .privateByDefault=${this.projectConfig?.private_by_default}
- .loggedIn=${this.loggedIn}
- @edit-tap=${() => this.handleEditTap()}
- @stop-edit-tap=${() => this.handleStopEditTap()}
- @download-tap=${() => this.handleOpenDownloadDialog()}
- @included-tap=${() => this.handleOpenIncludedInDialog()}
- @revision-actions-changed=${this.handleRevisionActionsChanged}
- ></gr-change-actions>
- </div>`;
+ return html`
+ <div class="commitActions">
+ <gr-change-actions
+ id="actions"
+ @edit-tap=${() => this.handleEditTap()}
+ @stop-edit-tap=${() => this.handleStopEditTap()}
+ @download-tap=${() => this.handleOpenDownloadDialog()}
+ @included-tap=${() => this.handleOpenIncludedInDialog()}
+ @revision-actions-changed=${this.handleRevisionActionsChanged}
+ ></gr-change-actions>
+ </div>
+ `;
}
private renderChangeInfo() {
@@ -1315,20 +1301,13 @@
this.loggedIn,
this.editingCommitMessage,
this.change,
- this.getEditMode()
+ this.editMode
);
return html` <div class="changeInfo">
<div class="changeInfo-column changeMetadata">
<gr-change-metadata
id="metadata"
- .change=${this.change}
- .revertedChange=${this.revertingChange}
- .account=${this.account}
- .revision=${this.revision}
- .commitInfo=${this.revision?.commit}
- .serverConfig=${this.serverConfig}
.parentIsCurrent=${this.isParentCurrent()}
- .repoConfig=${this.projectConfig}
@show-reply-dialog=${this.handleShowReplyDialog}
>
</gr-change-metadata>
@@ -1419,7 +1398,7 @@
)}
${this.pluginTabsHeaderEndpoints.map(
tabHeader => html`
- <paper-tab data-name=${tabHeader}>
+ <paper-tab data-name=${tabHeader} @click=${this.onPaperTabClick}>
<gr-endpoint-decorator name=${tabHeader}>
<gr-endpoint-param name="change" .value=${this.change}>
</gr-endpoint-param>
@@ -1461,7 +1440,7 @@
.changeNum=${this.changeNum}
.commitInfo=${this.revision?.commit}
.changeUrl=${this.computeChangeUrl()}
- .editMode=${this.getEditMode()}
+ .editMode=${this.editMode}
.loggedIn=${this.loggedIn}
.shownFileCount=${this.shownFileCount}
.filesExpanded=${this.fileList?.filesExpanded}
@@ -1475,7 +1454,7 @@
id="fileList"
.change=${this.change}
.changeNum=${this.changeNum}
- .editMode=${this.getEditMode()}
+ .editMode=${this.editMode}
@files-shown-changed=${(e: CustomEvent<{length: number}>) => {
this.shownFileCount = e.detail.length;
}}
@@ -1727,7 +1706,6 @@
const options = {
mergeable: this.mergeable,
- submitEnabled: !!this.isSubmitEnabled(),
revertingChangeStatus: this.revertingChange?.status,
};
return changeStatuses(this.change as ChangeInfo, options);
@@ -1957,7 +1935,7 @@
change: this.change,
patchNum: this.patchNum,
basePatchNum: this.basePatchNum,
- edit: this.getEditMode(),
+ edit: this.editMode,
messageHash: hash,
});
history.replaceState(null, '', url);
@@ -1967,7 +1945,7 @@
async maybeScrollToMessage(hash: string) {
if (hash.startsWith(PREFIX)) {
await waitUntil(() => !!this.messagesList);
- this.messagesList!.scrollToMessage(hash.substr(PREFIX.length));
+ await this.messagesList!.scrollToMessage(hash.substr(PREFIX.length));
}
}
@@ -2405,7 +2383,7 @@
// Private but used in tests.
computeHeaderClass() {
const classes = ['header'];
- if (this.getEditMode()) {
+ if (this.editMode) {
classes.push('editMode');
}
return classes.join(' ');
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 5331558..ae60449 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
@@ -73,7 +73,7 @@
import {Modifier} from '../../../utils/dom-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrCopyLinks} from '../gr-copy-links/gr-copy-links';
-import {ChangeChildView, ChangeViewState} from '../../../models/views/change';
+import {ChangeChildView} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
import {UserModel, userModelToken} from '../../../models/user/user-model';
@@ -1318,10 +1318,9 @@
});
});
- test('header class computation', () => {
+ test('header class computation', async () => {
assert.equal(element.computeHeaderClass(), 'header');
- assertIsDefined(element.viewState);
- element.viewState.edit = true;
+ element.editMode = true;
assert.equal(element.computeHeaderClass(), 'header editMode');
});
@@ -1342,38 +1341,6 @@
assert.equal(scrollStub.lastCall.args[0], 'TEST');
});
- test('computeEditMode', async () => {
- const callCompute = async (viewState: ChangeViewState) => {
- element.viewState = viewState;
- element.patchNum = viewState.patchNum;
- element.basePatchNum = viewState.basePatchNum ?? PARENT;
- await element.updateComplete;
- return element.getEditMode();
- };
- assert.isTrue(
- await callCompute({
- ...createChangeViewState(),
- edit: true,
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- })
- );
- assert.isFalse(
- await callCompute({
- ...createChangeViewState(),
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- })
- );
- assert.isTrue(
- await callCompute({
- ...createChangeViewState(),
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: EDIT,
- })
- );
- });
-
test('file-action-tap handling', async () => {
element.patchNum = 1 as RevisionPatchSetNum;
element.change = {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index fedc377..e421c9c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -15,6 +15,7 @@
import {resolve} from '../../../models/dependency';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {createSearchUrl} from '../../../models/views/search';
+import {ParsedChangeInfo} from '../../../types/types';
const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.';
const INSERT_REASON_STRING = '<INSERT REASONING HERE>';
@@ -170,15 +171,23 @@
return this.revertType === RevertType.REVERT_SUBMISSION;
}
- modifyRevertMsg(change: ChangeInfo, commitMessage: string, message: string) {
+ modifyRevertMsg(
+ change: ParsedChangeInfo,
+ commitMessage: string,
+ message: string
+ ) {
return this.getPluginLoader().jsApiService.modifyRevertMsg(
- change,
+ change as ChangeInfo,
message,
commitMessage
);
}
- populate(change: ChangeInfo, commitMessage: string, changesCount: number) {
+ populate(
+ change: ParsedChangeInfo,
+ commitMessage: string,
+ changesCount: number
+ ) {
this.changesCount = changesCount;
// The option to revert a single change is always available
this.populateRevertSingleChangeMessage(
@@ -190,7 +199,7 @@
}
populateRevertSingleChangeMessage(
- change: ChangeInfo,
+ change: ParsedChangeInfo,
commitMessage: string,
commitHash?: CommitId
) {
@@ -215,18 +224,21 @@
}
private modifyRevertSubmissionMsg(
- change: ChangeInfo,
+ change: ParsedChangeInfo,
msg: string,
commitMessage: string
) {
return this.getPluginLoader().jsApiService.modifyRevertSubmissionMsg(
- change,
+ change as ChangeInfo,
msg,
commitMessage
);
}
- populateRevertSubmissionMessage(change: ChangeInfo, commitMessage: string) {
+ populateRevertSubmissionMessage(
+ change: ParsedChangeInfo,
+ commitMessage: string
+ ) {
// Follow the same convention of the revert
const commitHash = change.current_revision;
if (!commitHash) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
index 904285f..8d71e15 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
@@ -5,7 +5,7 @@
*/
import {fixture, html, assert} from '@open-wc/testing';
import '../../../test/common-test-setup';
-import {createChange} from '../../../test/test-data-generators';
+import {createParsedChange} from '../../../test/test-data-generators';
import {ChangeSubmissionId, CommitId} from '../../../types/common';
import './gr-confirm-revert-dialog';
import {GrConfirmRevertDialog} from './gr-confirm-revert-dialog';
@@ -48,7 +48,7 @@
const alertStub = sinon.stub();
element.addEventListener('show-alert', alertStub);
element.populateRevertSingleChangeMessage(
- createChange(),
+ createParsedChange(),
'not a commitHash in sight',
undefined
);
@@ -58,7 +58,7 @@
test('single line', () => {
assert.isNotOk(element.message);
element.populateRevertSingleChangeMessage(
- createChange(),
+ createParsedChange(),
'one line commit\n\nChange-Id: abcdefg\n',
'abcd123' as CommitId
);
@@ -72,7 +72,7 @@
test('multi line', () => {
assert.isNotOk(element.message);
element.populateRevertSingleChangeMessage(
- createChange(),
+ createParsedChange(),
'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
'abcd123' as CommitId
);
@@ -86,7 +86,7 @@
test('issue above change id', () => {
assert.isNotOk(element.message);
element.populateRevertSingleChangeMessage(
- createChange(),
+ createParsedChange(),
'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
'abcd123' as CommitId
);
@@ -100,7 +100,7 @@
test('revert a revert', () => {
assert.isNotOk(element.message);
element.populateRevertSingleChangeMessage(
- createChange(),
+ createParsedChange(),
'Revert "one line commit"\n\nChange-Id: abcdefg\n',
'abcd123' as CommitId
);
@@ -115,7 +115,7 @@
element.changesCount = 3;
element.populateRevertSubmissionMessage(
{
- ...createChange(),
+ ...createParsedChange(),
submission_id: '5545' as ChangeSubmissionId,
current_revision: 'abcd123' as CommitId,
},
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 11dc890..e1808bf 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -5,7 +5,7 @@
*/
import '../../shared/gr-download-commands/gr-download-commands';
import {changeBaseURL, getRevisionKey} from '../../../utils/change-util';
-import {ChangeInfo, DownloadInfo, PatchSetNum} from '../../../types/common';
+import {DownloadInfo, PatchSetNum} from '../../../types/common';
import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-commands';
import {GrButton} from '../../shared/gr-button/gr-button';
import {copyToClipbard, hasOwnProperty} from '../../../utils/common-util';
@@ -13,13 +13,15 @@
import {fontStyles} from '../../../styles/gr-font-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
-import {customElement, property, state, query} from 'lit/decorators.js';
+import {customElement, state, query} from 'lit/decorators.js';
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
import {ShortcutController} from '../../lit/shortcut-controller';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
+import {ParsedChangeInfo} from '../../../types/types';
+import {configModelToken} from '../../../models/config/config-model';
@customElement('gr-download-dialog')
export class GrDownloadDialog extends LitElement {
@@ -35,27 +37,37 @@
@query('#closeButton') protected closeButton?: GrButton;
- @property({type: Object})
- change: ChangeInfo | undefined;
+ @state() change?: ParsedChangeInfo;
- @property({type: Object})
- config?: DownloadInfo;
+ @state() config?: DownloadInfo;
@state() patchNum?: PatchSetNum;
- @state() private selectedScheme?: string;
+ @state() selectedScheme?: string;
private readonly shortcuts = new ShortcutController(this);
private readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
constructor() {
super();
subscribe(
this,
+ () => this.getChangeModel().change$,
+ x => (this.change = x)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().patchNum$,
x => (this.patchNum = x)
);
+ subscribe(
+ this,
+ () => this.getConfigModel().download$,
+ x => (this.config = x)
+ );
for (const key of ['1', '2', '3', '4', '5']) {
this.shortcuts.addLocal({key}, e => this.handleNumberKey(e));
}
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index e5f40a6..73d7618 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -8,6 +8,7 @@
createChange,
createCommit,
createDownloadInfo,
+ createParsedChange,
createRevision,
} from '../../../test/test-data-generators';
import {
@@ -160,7 +161,7 @@
suite('gr-download-dialog tests with no fetch options', () => {
setup(async () => {
element.change = {
- ...createChange(),
+ ...createParsedChange(),
revisions: {
r1: {
...createRevision(),
@@ -204,7 +205,7 @@
test('computed fields', () => {
element.change = {
- ...createChange(),
+ ...createParsedChange(),
project: 'test/project' as RepoName,
_number: 123 as NumericChangeId,
};
@@ -233,7 +234,7 @@
element.patchNum = 1 as PatchSetNum;
element.change = {
- ...createChange(),
+ ...createParsedChange(),
revisions: {
r1: {...createRevision(), commit: createCommit()},
},
@@ -241,7 +242,7 @@
assert.isTrue(element.computeHidePatchFile());
element.change = {
- ...createChange(),
+ ...createParsedChange(),
revisions: {
r1: {
...createRevision(),
@@ -255,7 +256,7 @@
assert.isFalse(element.computeHidePatchFile());
element.change = {
- ...createChange(),
+ ...createParsedChange(),
revisions: {
r1: {
...createRevision(),
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 62693ea..9665b03 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -86,7 +86,6 @@
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {FileMode, fileModeToString} from '../../../utils/file-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -309,8 +308,6 @@
private readonly getBrowserModel = resolve(this, browserModelToken);
- private readonly flagsService = getAppContext().flagsService;
-
shortcutsController = new ShortcutController(this);
private readonly getNavigation = resolve(this, navigationToken);
@@ -1286,16 +1283,7 @@
private renderFileComments(file: NormalizedFileInfo) {
return html` <div role="gridcell">
<div class="comments desktop">
- ${when(
- this.flagsService.isEnabled(
- KnownExperimentId.COMMENTS_CHIPS_IN_FILE_LIST
- ),
- () => html`<span>${this.renderCommentsChips(file)}</span>`,
- () => html`<span class="drafts"
- >${this.computeDraftsString(file)}</span
- >
- <span>${this.computeCommentsString(file)}</span>`
- )}
+ <span>${this.renderCommentsChips(file)}</span>
<span class="noCommentsScreenReaderText">
<!-- Screen readers read the following content only if 2 other
spans in the parent div is empty. The content is not visible on
@@ -1824,37 +1812,6 @@
}
/**
- * Computes a string with the number of comments and unresolved comments.
- */
- computeCommentsString(file?: NormalizedFileInfo) {
- if (
- this.changeComments === undefined ||
- this.patchRange === undefined ||
- file?.__path === undefined
- ) {
- return '';
- }
- return this.changeComments.computeCommentsString(
- this.patchRange,
- file.__path,
- file
- );
- }
-
- /**
- * Computes a string with the number of drafts.
- */
- computeDraftsString(file?: NormalizedFileInfo) {
- if (this.changeComments === undefined) return '';
- const draftCount = this.changeComments.computeDraftCountForFile(
- this.patchRange,
- file
- );
- if (draftCount === 0) return '';
- return pluralize(Number(draftCount), 'draft');
- }
-
- /**
* Computes a shortened string with the number of drafts.
* Private but used in tests.
*/
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index 8fb5622..939ad06 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -192,7 +192,11 @@
</span>
<div role="gridcell">
<div class="comments desktop">
- <span class="drafts"> </span> <span> </span>
+ <span
+ ><gr-comments-summary
+ emptywhennocomments=""
+ ></gr-comments-summary
+ ></span>
<span class="noCommentsScreenReaderText"> No comments </span>
</div>
<div class="comments mobile">
@@ -719,28 +723,6 @@
element.basePatchNum = PARENT;
element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
- element.computeDraftsString({
- __path: 'unresolved.file',
- size: 0,
- size_delta: 0,
- }),
- '1 draft'
- );
-
- element.basePatchNum = 1 as BasePatchSetNum;
- element.patchNum = 2 as RevisionPatchSetNum;
- assert.equal(
- element.computeDraftsString({
- __path: 'unresolved.file',
- size: 0,
- size_delta: 0,
- }),
- '1 draft'
- );
-
- element.basePatchNum = PARENT;
- element.patchNum = 1 as RevisionPatchSetNum;
- assert.equal(
element.computeDraftsStringMobile({
__path: 'unresolved.file',
size: 0,
@@ -785,28 +767,6 @@
element.basePatchNum = PARENT;
element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
- element.computeDraftsString({
- __path: 'myfile.txt',
- size: 0,
- size_delta: 0,
- }),
- ''
- );
-
- element.basePatchNum = 1 as BasePatchSetNum;
- element.patchNum = 2 as RevisionPatchSetNum;
- assert.equal(
- element.computeDraftsString({
- __path: 'myfile.txt',
- size: 0,
- size_delta: 0,
- }),
- ''
- );
-
- element.basePatchNum = PARENT;
- element.patchNum = 1 as RevisionPatchSetNum;
- assert.equal(
element.computeDraftsStringMobile({
__path: 'myfile.txt',
size: 0,
@@ -851,28 +811,6 @@
element.basePatchNum = PARENT;
element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
- element.computeDraftsString({
- __path: 'file_added_in_rev2.txt',
- size: 0,
- size_delta: 0,
- }),
- ''
- );
-
- element.basePatchNum = 1 as BasePatchSetNum;
- element.patchNum = 2 as RevisionPatchSetNum;
- assert.equal(
- element.computeDraftsString({
- __path: 'file_added_in_rev2.txt',
- size: 0,
- size_delta: 0,
- }),
- ''
- );
-
- element.basePatchNum = PARENT;
- element.patchNum = 1 as RevisionPatchSetNum;
- assert.equal(
element.computeDraftsStringMobile({
__path: 'file_added_in_rev2.txt',
size: 0,
@@ -917,28 +855,6 @@
element.basePatchNum = PARENT;
element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
- element.computeDraftsString({
- __path: '/COMMIT_MSG',
- size: 0,
- size_delta: 0,
- }),
- '2 drafts'
- );
-
- element.basePatchNum = 1 as BasePatchSetNum;
- element.patchNum = 2 as RevisionPatchSetNum;
- assert.equal(
- element.computeDraftsString({
- __path: '/COMMIT_MSG',
- size: 0,
- size_delta: 0,
- }),
- '2 drafts'
- );
-
- element.basePatchNum = PARENT;
- element.patchNum = 1 as RevisionPatchSetNum;
- assert.equal(
element.computeDraftsStringMobile({
__path: '/COMMIT_MSG',
size: 0,
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
index 33dfe82..32da400 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
@@ -10,9 +10,12 @@
import {fontStyles} from '../../../styles/gr-font-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {customElement, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {fireNoBubble} from '../../../utils/event-util';
+import {resolve} from '../../../models/dependency';
+import {changeModelToken} from '../../../models/change/change-model';
+import {subscribe} from '../../lit/subscription-controller';
interface DisplayGroup {
title: string;
@@ -27,19 +30,18 @@
* @event close
*/
- @property({type: Object})
- changeNum?: NumericChangeId;
+ @state() changeNum?: NumericChangeId;
- // private but used in test
@state() includedIn?: IncludedInInfo;
@state() private loaded = false;
- // private but used in test
@state() filterText = '';
private readonly restApiService = getAppContext().restApiService;
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
static override get styles() {
return [
fontStyles,
@@ -93,6 +95,15 @@
];
}
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getChangeModel().changeNum$,
+ changeNum => (this.changeNum = changeNum)
+ );
+ }
+
override render() {
return html`
<header>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index ac087cef..d5da9c9 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -440,7 +440,7 @@
}
async scrollToMessage(messageID: string) {
- await waitUntil(() => this.messages.length > 0);
+ await waitUntil(() => this.messages && this.messages.length > 0);
await this.updateComplete;
const selector = `[data-message-id="${messageID}"]`;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
index 1d2a272..264c6e0 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import {sameOrigin} from '../../../utils/url-util';
+
/**
* 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
@@ -252,17 +254,6 @@
};
}
-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;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 8bcbb2b..4f1ce7a 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -24,7 +24,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {TokenHighlightLayer} from '../../../embed/diff/gr-diff-builder/token-highlight-layer';
import {css, html, LitElement, nothing} from 'lit';
-import {customElement, property, query, state} from 'lit/decorators.js';
+import {customElement, query, state} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
import {subscribe} from '../../lit/subscription-controller';
import {assert} from '../../../utils/common-util';
@@ -39,6 +39,7 @@
import {fireReload} from '../../../utils/event-util';
import {when} from 'lit/directives/when.js';
import {Timing} from '../../../constants/reporting';
+import {changeModelToken} from '../../../models/change/change-model';
interface FilePreview {
filepath: string;
@@ -62,10 +63,10 @@
@query('#nextFix')
nextFix?: GrButton;
- @property({type: Object})
+ @state()
change?: ParsedChangeInfo;
- @property({type: Number})
+ @state()
changeNum?: NumericChangeId;
@state()
@@ -102,6 +103,8 @@
private readonly getUserModel = resolve(this, userModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
private readonly getNavigation = resolve(this, navigationToken);
private readonly reporting = getAppContext().reportingService;
@@ -133,6 +136,16 @@
this.syntaxLayer.setEnabled(!!this.diffPrefs.syntax_highlighting);
}
);
+ subscribe(
+ this,
+ () => this.getChangeModel().change$,
+ change => (this.change = change)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().changeNum$,
+ changeNum => (this.changeNum = changeNum)
+ );
}
static override styles = [
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 bdb634b..0f54ad1 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
@@ -46,7 +46,6 @@
PreferencesInfo,
RepoName,
RevisionPatchSetNum,
- ServerInfo,
CommentMap,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo, WebLinkInfo} from '../../../types/diff';
@@ -80,7 +79,6 @@
import {ShortcutController} from '../../lit/shortcut-controller';
import {subscribe} from '../../lit/subscription-controller';
import {customElement, property, query, state} from 'lit/decorators.js';
-import {configModelToken} from '../../../models/config/config-model';
import {a11yStyles} from '../../../styles/gr-a11y-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {ifDefined} from 'lit/directives/if-defined.js';
@@ -194,9 +192,6 @@
@property({type: Object})
prefs?: DiffPreferencesInfo;
- @state()
- private serverConfig?: ServerInfo;
-
// Private but used in tests.
@state()
userPrefs?: PreferencesInfo;
@@ -241,8 +236,6 @@
private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
- private readonly getConfigModel = resolve(this, configModelToken);
-
private readonly getViewModel = resolve(this, changeViewModelToken);
private throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
@@ -340,13 +333,6 @@
);
subscribe(
this,
- () => this.getConfigModel().serverConfig$,
- config => {
- this.serverConfig = config;
- }
- );
- subscribe(
- this,
() => this.getCommentsModel().changeComments$,
changeComments => {
this.changeComments = changeComments;
@@ -966,12 +952,8 @@
}
private renderDialogs() {
- return html` <gr-apply-fix-dialog
- id="applyFixDialog"
- .change=${this.change}
- .changeNum=${this.changeNum}
- >
- </gr-apply-fix-dialog>
+ return html`
+ <gr-apply-fix-dialog id="applyFixDialog"></gr-apply-fix-dialog>
<gr-diff-preferences-dialog
id="diffPreferencesDialog"
@reload-diff-preference=${this.handleReloadingDiffPreference}
@@ -980,12 +962,10 @@
<dialog id="downloadModal" tabindex="-1">
<gr-download-dialog
id="downloadDialog"
- .change=${this.change}
- .patchNum=${this.patchNum}
- .config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
- </dialog>`;
+ </dialog>
+ `;
}
/**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index e527633..737e964 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -1168,7 +1168,7 @@
});
test(
- '_prefs.manual_review true means set reviewed is not ' +
+ 'prefs.manual_review true means set reviewed is not ' +
'automatically called',
async () => {
const setReviewedFileStatusStub = sinon
@@ -1204,7 +1204,7 @@
}
);
- test('_prefs.manual_review false means set reviewed is called', async () => {
+ test('prefs.manual_review false means set reviewed is called', async () => {
const setReviewedFileStatusStub = sinon
.stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 3082979..76d67af 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -47,7 +47,6 @@
import {changeModelToken} from '../../../models/change/change-model';
import {changeViewModelToken} from '../../../models/views/change';
import {fireNoBubbleNoCompose} from '../../../utils/event-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -126,8 +125,6 @@
private readonly getViewModel = resolve(this, changeViewModelToken);
- private readonly flagsService = getAppContext().flagsService;
-
constructor() {
super();
subscribe(
@@ -326,16 +323,7 @@
}
private computeText(patchNum: PatchSetNum, prefix: string, sha: string) {
- if (
- this.flagsService.isEnabled(KnownExperimentId.COMMENTS_CHIPS_IN_FILE_LIST)
- ) {
- return `${prefix}${patchNum} | ${sha}`;
- }
- return (
- `${prefix}${patchNum}` +
- `${this.computePatchSetCommentsString(patchNum)}` +
- ` | ${sha}`
- );
+ return `${prefix}${patchNum} | ${sha}`;
}
private createDropdownEntry(
@@ -349,17 +337,13 @@
mobileText: this.computeMobileText(patchNum),
bottomText: `${this.computePatchSetDescription(patchNum)}`,
value: patchNum,
- };
- if (
- this.flagsService.isEnabled(KnownExperimentId.COMMENTS_CHIPS_IN_FILE_LIST)
- ) {
- entry.commentThreads = this.changeComments?.computeCommentThreads(
+ commentThreads: this.changeComments?.computeCommentThreads(
{
patchNum,
},
true
- );
- }
+ ),
+ };
const date = this.computePatchSetDate(patchNum);
if (date) {
entry.date = date;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index 6051131..b4ab043 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -147,6 +147,7 @@
mobileText: EDIT,
bottomText: '',
value: EDIT,
+ commentThreads: [],
},
{
disabled: true,
@@ -156,6 +157,7 @@
bottomText: '',
value: 3,
date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ commentThreads: [],
} as DropdownItem,
{
disabled: true,
@@ -165,6 +167,7 @@
bottomText: '',
value: 2,
date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ commentThreads: [],
} as DropdownItem,
{
disabled: true,
@@ -174,6 +177,7 @@
bottomText: '',
value: 1,
date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ commentThreads: [],
} as DropdownItem,
{
text: 'Base',
@@ -287,6 +291,7 @@
mobileText: EDIT,
bottomText: '',
value: EDIT,
+ commentThreads: [],
},
{
disabled: false,
@@ -296,6 +301,7 @@
bottomText: '',
value: 3,
date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ commentThreads: [],
} as DropdownItem,
{
disabled: false,
@@ -305,6 +311,7 @@
bottomText: 'description',
value: 2,
date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ commentThreads: [],
} as DropdownItem,
{
disabled: true,
@@ -314,6 +321,7 @@
bottomText: '',
value: 1,
date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ commentThreads: [],
} as DropdownItem,
];
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 58d7105..75bfca2 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -118,7 +118,7 @@
@state() private version?: string;
- @state() private view?: GerritView;
+ @state() view?: GerritView;
// TODO: Introduce a wrapper element for CHANGE, DIFF, EDIT view.
@state() private childView?: ChangeChildView;
@@ -549,15 +549,21 @@
private renderPluginScreen() {
if (this.view !== GerritView.PLUGIN_SCREEN) return nothing;
+ if (!this.params) return nothing;
const pluginViewState = this.params as PluginViewState;
- return html`
- <gr-endpoint-decorator .name=${this.computePluginScreenName()}>
- <gr-endpoint-param
- name="token"
- .value=${pluginViewState.screen}
- ></gr-endpoint-param>
- </gr-endpoint-decorator>
- `;
+ const pluginScreenName = this.computePluginScreenName();
+
+ return keyed(
+ pluginScreenName,
+ html`
+ <gr-endpoint-decorator .name=${pluginScreenName}>
+ <gr-endpoint-param
+ name="token"
+ .value=${pluginViewState.screen}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `
+ );
}
private renderCLAView() {
diff --git a/polygerrit-ui/app/elements/gr-app-element_test.ts b/polygerrit-ui/app/elements/gr-app-element_test.ts
new file mode 100644
index 0000000..ec415ff
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app-element_test.ts
@@ -0,0 +1,100 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../test/common-test-setup';
+import './gr-app';
+import {fixture, html, assert} from '@open-wc/testing';
+import {GrAppElement} from './gr-app-element';
+import {queryAndAssert} from '../utils/common-util';
+import {GerritView} from '../services/router/router-model';
+import {PluginViewState} from '../models/views/plugin';
+import {GrEndpointDecorator} from './plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+
+suite('gr-app-element tests', () => {
+ let element: GrAppElement;
+
+ setup(async () => {
+ element = await fixture<GrAppElement>(
+ html`<gr-app-element></gr-app-element>`
+ );
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <gr-css-mixins> </gr-css-mixins>
+ <gr-endpoint-decorator name="banner"> </gr-endpoint-decorator>
+ <gr-main-header loggedin id="mainHeader" role="banner">
+ </gr-main-header>
+ <main>
+ <div class="errorView" id="errorView">
+ <div class="errorEmoji"></div>
+ <div class="errorText"></div>
+ <div class="errorMoreInfo"></div>
+ </div>
+ </main>
+ <footer>
+ <div>
+ Powered by
+ <a
+ href="https://www.gerritcodereview.com/"
+ rel="noopener"
+ target="_blank"
+ >
+ Gerrit Code Review
+ </a>
+ ()
+ <gr-endpoint-decorator name="footer-left"> </gr-endpoint-decorator>
+ </div>
+ <div>
+ Press “?” for keyboard shortcuts
+ <gr-endpoint-decorator name="footer-right"> </gr-endpoint-decorator>
+ </div>
+ </footer>
+ <gr-notifications-prompt> </gr-notifications-prompt>
+ <gr-endpoint-decorator name="plugin-overlay"> </gr-endpoint-decorator>
+ <gr-error-manager id="errorManager"> </gr-error-manager>
+ <gr-plugin-host id="plugins"> </gr-plugin-host>
+ `
+ );
+ });
+
+ test('renders plugin screen, changes endpoint instance', async () => {
+ element.view = GerritView.PLUGIN_SCREEN;
+ element.params = {
+ view: GerritView.PLUGIN_SCREEN,
+ screen: 'test-screen-1',
+ plugin: 'test-plugin',
+ } as PluginViewState;
+ await element.updateComplete;
+
+ const main1 = queryAndAssert(element, 'main');
+ const endpoint1 = queryAndAssert<GrEndpointDecorator>(
+ main1,
+ 'gr-endpoint-decorator'
+ );
+ assert.equal(endpoint1.name, 'test-plugin-screen-test-screen-1');
+
+ element.params = {
+ view: GerritView.PLUGIN_SCREEN,
+ screen: 'test-screen-2',
+ plugin: 'test-plugin',
+ } as PluginViewState;
+ await element.updateComplete;
+
+ const main2 = queryAndAssert(element, 'main');
+ const endpoint2 = queryAndAssert<GrEndpointDecorator>(
+ main2,
+ 'gr-endpoint-decorator'
+ );
+ assert.equal(endpoint2.name, 'test-plugin-screen-test-screen-2');
+
+ // Plugin screen endpoints have a variable name. Lit must not re-use the
+ // same element instance. (Issue 16884)
+ assert.isFalse(endpoint1 === endpoint2);
+ });
+});
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index d6a14ed..6589ee8 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -21,6 +21,7 @@
initErrorReporter,
initWebVitals,
initClickReporter,
+ initInteractionReporter,
} from '../services/gr-reporting/gr-reporting_impl';
import {Finalizable} from '../services/registry';
@@ -36,6 +37,7 @@
initWebVitals(reportingService);
initErrorReporter(reportingService);
initClickReporter(reportingService);
+ initInteractionReporter(reportingService);
}
window.GrAnnotation = GrAnnotation;
window.GrPluginActionContext = GrPluginActionContext;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index e230faf..e880531 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -21,7 +21,11 @@
import '../gr-user-suggestion-fix/gr-user-suggestion-fix';
import {KnownExperimentId} from '../../../services/flags/flags';
import {getAppContext} from '../../../services/app-context';
-import {USER_SUGGESTION_INFO_STRING} from '../../../utils/comment-util';
+import {
+ getUserSuggestionFromString,
+ USER_SUGGESTION_INFO_STRING,
+} from '../../../utils/comment-util';
+import {sameOrigin} from '../../../utils/url-util';
/**
* This element optionally renders markdown and also applies some regex
@@ -200,9 +204,8 @@
/* HTML */
`<a
href="${href}"
- target="_blank"
+ ${sameOrigin(href) ? '' : 'target="_blank" rel="noopener"'}
${title ? `title="${title}"` : ''}
- rel="noopener"
>${text}</a
>`;
renderer['image'] = (href: string, _title: string, text: string) =>
@@ -298,9 +301,16 @@
}
private convertCodeToSuggestions() {
- for (const userSuggestionMark of this.renderRoot.querySelectorAll('mark')) {
+ const marks = this.renderRoot.querySelectorAll('mark');
+ if (marks.length > 0) {
+ const userSuggestionMark = marks[0];
const userSuggestion = document.createElement('gr-user-suggestion-fix');
- userSuggestion.textContent = userSuggestionMark.textContent ?? '';
+ // Temporary workaround for bug - tabs replacement
+ if (this.content.includes('\t')) {
+ userSuggestion.textContent = getUserSuggestionFromString(this.content);
+ } else {
+ userSuggestion.textContent = userSuggestionMark.textContent ?? '';
+ }
userSuggestionMark.parentNode?.replaceChild(
userSuggestion,
userSuggestionMark
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 81048a3..9d7f06e 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -502,7 +502,11 @@
});
test('renders inline links into <a> tags', async () => {
- element.content = '[myLink](https://www.google.com)';
+ const origin = window.location.origin;
+ element.content = `[myLink1](https://www.google.com)
+ [myLink2](/destiny)
+ [myLink3](${origin}/destiny)
+ `;
await element.updateComplete;
assert.shadowDom.equal(
@@ -512,8 +516,12 @@
<div slot="markdown-html" class="markdown-html">
<p>
<a href="https://www.google.com" rel="noopener" target="_blank"
- >myLink</a
+ >myLink1</a
>
+ <br />
+ <a href="/destiny">myLink2</a>
+ <br />
+ <a href="${origin}/destiny">myLink3</a>
</p>
</div>
</marked-element>
diff --git a/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts b/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts
index 63a1dc4..fb6372c 100644
--- a/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts
+++ b/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts
@@ -30,6 +30,7 @@
display: inline-block;
vertical-align: top;
line-height: var(--line-height-normal);
+ margin-right: var(--spacing-s);
}
a {
color: var(--link-color);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-binary.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-binary.ts
index 5267f307..cc45e1e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-binary.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-binary.ts
@@ -3,16 +3,13 @@
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
+import {GrDiffBuilder} from './gr-diff-builder';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {createElementDiff} from '../gr-diff/gr-diff-utils';
import {GrDiffGroup} from '../gr-diff/gr-diff-group';
-import {GrDiffLine} from '../gr-diff/gr-diff-line';
-import {GrDiffLineType} from '../../../api/diff';
-import {queryAndAssert} from '../../../utils/common-util';
import {html, render} from 'lit';
-export class GrDiffBuilderBinary extends GrDiffBuilderUnified {
+export class GrDiffBuilderBinary extends GrDiffBuilder {
constructor(
diff: DiffInfo,
prefs: DiffPreferencesInfo,
@@ -25,17 +22,21 @@
const section = createElementDiff('tbody', 'binary-diff');
// Do not create a diff row for 'LOST'.
if (group.lines[0].beforeNumber !== 'FILE') return section;
+ return super.buildSectionElement(group);
+ }
- const line = new GrDiffLine(GrDiffLineType.BOTH, 'FILE', 'FILE');
- const fileRow = this.createRow(line);
- const contentTd = queryAndAssert<HTMLTableCellElement>(
- fileRow,
- 'td.both.file'
- )!;
- const div = document.createElement('div');
- render(html`<span>Difference in binary files</span>`, div);
- contentTd.insertBefore(div, contentTd.firstChild);
- section.appendChild(fileRow);
- return section;
+ public renderBinaryDiff() {
+ render(
+ html`
+ <tbody class="gr-diff binary-diff">
+ <tr class="gr-diff">
+ <td colspan="5" class="gr-diff">
+ <span>Difference in binary files</span>
+ </td>
+ </tr>
+ </tbody>
+ `,
+ this.outputEl
+ );
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 7fa6d72..396e9e2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -5,17 +5,15 @@
*/
import '../gr-diff-processor/gr-diff-processor';
import '../../../elements/shared/gr-hovercard/gr-hovercard';
-import './gr-diff-builder-side-by-side';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {
- DiffBuilder,
- ImageDiffBuilder,
+ GrDiffBuilder,
DiffContextExpandedEventDetail,
isImageDiffBuilder,
+ isBinaryDiffBuilder,
} from './gr-diff-builder';
import {GrDiffBuilderImage} from './gr-diff-builder-image';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
-import {GrDiffBuilderLit} from './gr-diff-builder-lit';
import {CancelablePromise, makeCancelable} from '../../../utils/async-util';
import {BlameInfo, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
@@ -117,7 +115,7 @@
layers: DiffLayer[] = [];
// visible for testing
- builder?: DiffBuilder | ImageDiffBuilder;
+ builder?: GrDiffBuilder;
/**
* All layers, both from the outside and the default ones. See `layers` for
@@ -211,6 +209,8 @@
.then(async () => {
if (isImageDiffBuilder(this.builder)) {
this.builder.renderImageDiff();
+ } else if (isBinaryDiffBuilder(this.builder)) {
+ this.builder.renderBinaryDiff();
}
await this.untilGroupsRendered();
fire(this.diffElement, 'render-content', {});
@@ -261,31 +261,21 @@
this.layersInternal = layers;
}
- getContentTdByLine(lineNumber: LineNumber, side?: Side, root?: Element) {
- if (!this.builder) return null;
- return this.builder.getContentTdByLine(lineNumber, side, root);
+ getContentTdByLine(lineNumber: LineNumber, side?: Side) {
+ if (!this.builder) return undefined;
+ return this.builder.getContentTdByLine(lineNumber, side);
}
- private getDiffRowByChild(child: Element) {
- while (!child.classList.contains('diff-row') && child.parentElement) {
- child = child.parentElement;
- }
- return child;
- }
-
- getContentTdByLineEl(lineEl?: Element): Element | null {
- if (!lineEl) return null;
+ getContentTdByLineEl(lineEl?: Element): Element | undefined {
+ if (!lineEl) return undefined;
const line = getLineNumber(lineEl);
- if (!line) return null;
+ if (!line) return undefined;
const side = getSideByLineEl(lineEl);
- // Performance optimization because we already have an element in the
- // correct row
- const row = this.getDiffRowByChild(lineEl);
- return this.getContentTdByLine(line, side, row);
+ return this.getContentTdByLine(line, side);
}
getLineElByNumber(lineNumber: LineNumber, side?: Side) {
- if (!this.builder) return null;
+ if (!this.builder) return undefined;
return this.builder.getLineElByNumber(lineNumber, side);
}
@@ -407,7 +397,7 @@
}
// visible for testing
- getDiffBuilder(): DiffBuilder {
+ getDiffBuilder(): GrDiffBuilder {
assertIsDefined(this.diff, 'diff');
assertIsDefined(this.diffElement, 'diff table');
if (isNaN(this.prefs.tab_size) || this.prefs.tab_size <= 0) {
@@ -437,14 +427,13 @@
this.useNewImageDiffUi
);
} else if (this.diff.binary) {
- // If the diff is binary, but not an image.
return new GrDiffBuilderBinary(this.diff, localPrefs, this.diffElement);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
this.renderPrefs = {
...this.renderPrefs,
view_mode: DiffViewMode.SIDE_BY_SIDE,
};
- builder = new GrDiffBuilderLit(
+ builder = new GrDiffBuilder(
this.diff,
localPrefs,
this.diffElement,
@@ -456,7 +445,7 @@
...this.renderPrefs,
view_mode: DiffViewMode.UNIFIED,
};
- builder = new GrDiffBuilderLit(
+ builder = new GrDiffBuilder(
this.diff,
localPrefs,
this.diffElement,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
index ba6acfd..d776164 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -12,7 +12,6 @@
import {stubBaseUrl, waitUntil} from '../../../test/test-utils';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
import {
DiffContent,
DiffLayer,
@@ -21,26 +20,23 @@
Side,
} from '../../../api/diff';
import {stubRestApi} from '../../../test/test-utils';
-import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
import {waitForEventOnce} from '../../../utils/event-util';
import {GrDiffBuilderElement} from './gr-diff-builder-element';
import {createDefaultDiffPrefs} from '../../../constants/constants';
import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
-import {BlameInfo} from '../../../types/common';
import {fixture, html, assert} from '@open-wc/testing';
import {GrDiffRow} from './gr-diff-row';
+import {GrDiffBuilder} from './gr-diff-builder';
const DEFAULT_PREFS = createDefaultDiffPrefs();
suite('gr-diff-builder tests', () => {
let element: GrDiffBuilderElement;
- let builder: GrDiffBuilderLegacy;
+ let builder: GrDiffBuilder;
let diffTable: HTMLTableElement;
- const LINE_BREAK_HTML = '<span class="gr-diff br"></span>';
-
const setBuilderPrefs = (prefs: Partial<DiffPreferencesInfo>) => {
- builder = new GrDiffBuilderSideBySide(
+ builder = new GrDiffBuilder(
createEmptyDiff(),
{...createDefaultDiffPrefs(), ...prefs},
diffTable
@@ -63,15 +59,6 @@
setBuilderPrefs({});
});
- test('line_length applied with line break if line_wrapping is false', () => {
- setBuilderPrefs({line_wrapping: false, tab_size: 4, line_length: 50});
- const text = 'a'.repeat(51);
- const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
- const result = builder.createTextEl(null, line(text)).firstElementChild
- ?.firstElementChild?.innerHTML;
- assert.equal(result, expected);
- });
-
[DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE].forEach(mode => {
test(`line_length used for regular files under ${mode}`, () => {
element.path = '/a.txt';
@@ -82,8 +69,8 @@
tab_size: 4,
line_length: 50,
};
- builder = element.getDiffBuilder() as GrDiffBuilderLegacy;
- assert.equal(builder._prefs.line_length, 50);
+ builder = element.getDiffBuilder();
+ assert.equal(builder.prefs.line_length, 50);
});
test(`line_length ignored for commit msg under ${mode}`, () => {
@@ -95,26 +82,11 @@
tab_size: 4,
line_length: 50,
};
- builder = element.getDiffBuilder() as GrDiffBuilderLegacy;
- assert.equal(builder._prefs.line_length, 72);
+ builder = element.getDiffBuilder();
+ assert.equal(builder.prefs.line_length, 72);
});
});
- test('createTextEl linewrap with tabs', () => {
- setBuilderPrefs({tab_size: 4, line_length: 10});
- const text = '\t'.repeat(7) + '!';
- const el = builder.createTextEl(null, line(text));
- assert.equal(el.innerText, text);
- // With line length 10 and tab size 4, there should be a line break
- // after every two tabs.
- const newlineEl = el.querySelector('.contentText .br');
- assert.isOk(newlineEl);
- assert.equal(
- el.querySelector('.contentText .tab:nth-child(2)')?.nextSibling,
- newlineEl
- );
- });
-
test('_handlePreferenceError throws with invalid preference', () => {
element.prefs = {...createDefaultDiffPrefs(), tab_size: 0};
assert.throws(() => element.getDiffBuilder());
@@ -553,95 +525,6 @@
});
});
- suite('rendering', () => {
- let content: DiffContent[];
- let outputEl: HTMLTableElement;
- let keyLocations: KeyLocations;
- let addColumnsStub: sinon.SinonStub;
- let dispatchStub: sinon.SinonStub;
- let builder: GrDiffBuilderSideBySide;
-
- setup(() => {
- const prefs = {...DEFAULT_PREFS};
- content = [
- {
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- },
- {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- },
- ];
- dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
- outputEl = element.diffElement!;
- keyLocations = {left: {}, right: {}};
- sinon.stub(element, 'getDiffBuilder').callsFake(() => {
- builder = new GrDiffBuilderSideBySide(
- {...createEmptyDiff(), content},
- prefs,
- outputEl
- );
- addColumnsStub = sinon.stub(builder, 'addColumns');
- builder.buildSectionElement = function (group) {
- const section = document.createElement('stub');
- section.style.display = 'block';
- section.textContent = group.lines.reduce(
- (acc, line) => acc + line.text,
- ''
- );
- return section;
- };
- return builder;
- });
- element.diff = {...createEmptyDiff(), content};
- element.prefs = prefs;
- element.render(keyLocations);
- });
-
- test('addColumns is called', () => {
- assert.isTrue(addColumnsStub.called);
- });
-
- test('getGroupsByLineRange one line', () => {
- const section = outputEl.querySelector<HTMLElement>(
- 'stub:nth-of-type(3)'
- );
- const groups = builder.getGroupsByLineRange(1, 1, Side.LEFT);
- assert.equal(groups.length, 1);
- assert.strictEqual(groups[0].element, section);
- });
-
- test('getGroupsByLineRange over diff', () => {
- const section = [
- outputEl.querySelector<HTMLElement>('stub:nth-of-type(3)'),
- outputEl.querySelector<HTMLElement>('stub:nth-of-type(4)'),
- ];
- const groups = builder.getGroupsByLineRange(1, 2, Side.LEFT);
- assert.equal(groups.length, 2);
- assert.strictEqual(groups[0].element, section[0]);
- assert.strictEqual(groups[1].element, section[1]);
- });
-
- test('render-start and render-content are fired', async () => {
- await waitUntil(() => dispatchStub.callCount >= 1);
- let firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-start');
-
- await waitUntil(() => dispatchStub.callCount >= 2);
- firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-content');
- });
-
- test('cancel cancels the processor', () => {
- const processorCancelStub = sinon.stub(element.processor, 'cancel');
- element.cleanup();
- assert.isTrue(processorCancelStub.called);
- });
- });
-
suite('context hiding and expanding', () => {
let dispatchStub: sinon.SinonStub;
@@ -747,101 +630,4 @@
assert.include(firedEventTypes, 'render-content');
});
});
-
- suite('blame', () => {
- let mockBlame: BlameInfo[];
-
- setup(() => {
- mockBlame = [
- {
- author: 'test-author',
- time: 314,
- commit_msg: 'test-commit-message',
- id: 'commit 1',
- ranges: [
- {start: 1, end: 2},
- {start: 10, end: 16},
- ],
- },
- {
- author: 'test-author',
- time: 314,
- commit_msg: 'test-commit-message',
- id: 'commit 2',
- ranges: [
- {start: 4, end: 10},
- {start: 17, end: 32},
- ],
- },
- ];
- });
-
- test('setBlame attempts to render each blamed line', () => {
- const getBlameStub = sinon
- .stub(builder, 'getBlameTdByLine')
- .returns(undefined);
- builder.setBlame(mockBlame);
- assert.equal(getBlameStub.callCount, 32);
- });
-
- test('getBlameCommitForBaseLine', () => {
- sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
- builder.setBlame(mockBlame);
- assert.isOk(builder.getBlameCommitForBaseLine(1));
- assert.equal(builder.getBlameCommitForBaseLine(1)?.id, 'commit 1');
-
- assert.isOk(builder.getBlameCommitForBaseLine(11));
- assert.equal(builder.getBlameCommitForBaseLine(11)?.id, 'commit 1');
-
- assert.isOk(builder.getBlameCommitForBaseLine(32));
- assert.equal(builder.getBlameCommitForBaseLine(32)?.id, 'commit 2');
-
- assert.isUndefined(builder.getBlameCommitForBaseLine(33));
- });
-
- test('getBlameCommitForBaseLine w/o blame returns null', () => {
- assert.isUndefined(builder.getBlameCommitForBaseLine(1));
- assert.isUndefined(builder.getBlameCommitForBaseLine(11));
- assert.isUndefined(builder.getBlameCommitForBaseLine(31));
- });
-
- test('createBlameCell', () => {
- const mockBlameInfo = {
- time: 1576155200,
- id: '1234567890',
- author: 'Clark Kent',
- commit_msg: 'Testing Commit',
- ranges: [{start: 4, end: 10}],
- };
- const getBlameStub = sinon
- .stub(builder, 'getBlameCommitForBaseLine')
- .returns(mockBlameInfo);
- const line = new GrDiffLine(GrDiffLineType.BOTH);
- line.beforeNumber = 3;
- line.afterNumber = 5;
-
- const result = builder.createBlameCell(line.beforeNumber);
-
- assert.isTrue(getBlameStub.calledWithExactly(3));
- assert.equal(result.getAttribute('data-line-number'), '3');
- assert.dom.equal(
- result,
- /* HTML */ `
- <span class="gr-diff">
- <a class="blameDate gr-diff" href="/r/q/1234567890"> 12/12/2019 </a>
- <span class="blameAuthor gr-diff">Clark</span>
- <gr-hovercard class="gr-diff">
- <span class="blameHoverCard gr-diff">
- Commit 1234567890<br />
- Author: Clark Kent<br />
- Date: 12/12/2019<br />
- <br />
- Testing Commit
- </span>
- </gr-hovercard>
- </span>
- `
- );
- });
- });
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
index 0ea904a..3bffe08 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
@@ -3,23 +3,19 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
import {ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {RenderPreferences, Side} from '../../../api/diff';
import '../gr-diff-image-viewer/gr-image-viewer';
-import {ImageDiffBuilder} from './gr-diff-builder';
import {html, LitElement, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
+import {GrDiffBuilder} from './gr-diff-builder';
// MIME types for images we allow showing. Do not include SVG, it can contain
// arbitrary JavaScript.
const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp)$/;
-export class GrDiffBuilderImage
- extends GrDiffBuilderSideBySide
- implements ImageDiffBuilder
-{
+export class GrDiffBuilderImage extends GrDiffBuilder {
constructor(
diff: DiffInfo,
prefs: DiffPreferencesInfo,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
deleted file mode 100644
index e786cf4..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ /dev/null
@@ -1,518 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {RenderPreferences} from '../../../api/diff';
-import {fire} from '../../../utils/event-util';
-import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import '../gr-context-controls/gr-context-controls';
-import {
- GrContextControls,
- GrContextControlsShowConfig,
-} from '../gr-context-controls/gr-context-controls';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {DiffViewMode, Side} from '../../../constants/constants';
-import {DiffLayer} from '../../../types/types';
-import {
- createBlameElement,
- createElementDiff,
- createElementDiffWithText,
- formatText,
- getResponsiveMode,
-} from '../gr-diff/gr-diff-utils';
-import {GrDiffBuilder} from './gr-diff-builder';
-import {BlameInfo} from '../../../types/common';
-
-function lineTdSelector(lineNumber: LineNumber, side?: Side): string {
- const sideSelector = side ? `.${side}` : '';
- return `td.lineNum[data-value="${lineNumber}"]${sideSelector}`;
-}
-/**
- * Base class for builders that are creating the DOM elements programmatically
- * by calling `document.createElement()` and such. We are calling such builders
- * "legacy", because we want to create (Lit) component based diff elements.
- *
- * TODO: Do not subclass `GrDiffBuilder`. Use composition and interfaces.
- */
-export abstract class GrDiffBuilderLegacy extends GrDiffBuilder {
- constructor(
- diff: DiffInfo,
- prefs: DiffPreferencesInfo,
- outputEl: HTMLElement,
- layers: DiffLayer[] = [],
- renderPrefs?: RenderPreferences
- ) {
- super(diff, prefs, outputEl, layers, renderPrefs);
- }
-
- override getContentTdByLine(
- lineNumber: LineNumber,
- side?: Side,
- root: Element = this.outputEl
- ): HTMLTableCellElement | null {
- return root.querySelector<HTMLTableCellElement>(
- `${lineTdSelector(lineNumber, side)} ~ td.content`
- );
- }
-
- override getLineElByNumber(
- lineNumber: LineNumber,
- side?: Side
- ): HTMLTableCellElement | null {
- return this.outputEl.querySelector<HTMLTableCellElement>(
- lineTdSelector(lineNumber, side)
- );
- }
-
- override getLineNumberRows() {
- return Array.from(
- this.outputEl.querySelectorAll<HTMLTableRowElement>(
- ':not(.contextControl) > .diff-row'
- ) ?? []
- ).filter(tr => tr.querySelector('button'));
- }
-
- override getLineNumEls(side: Side): HTMLTableCellElement[] {
- return Array.from(
- this.outputEl.querySelectorAll<HTMLTableCellElement>(
- `td.lineNum.${side}`
- ) ?? []
- );
- }
-
- override getBlameTdByLine(lineNum: number): Element | undefined {
- return (
- this.outputEl.querySelector(`td.blame[data-line-number="${lineNum}"]`) ??
- undefined
- );
- }
-
- override getContentByLine(
- lineNumber: LineNumber,
- side?: Side,
- root?: HTMLElement
- ): HTMLElement | null {
- const td = this.getContentTdByLine(lineNumber, side, root);
- return td ? td.querySelector('.contentText') : null;
- }
-
- override renderContentByRange(
- start: LineNumber,
- end: LineNumber,
- side: Side
- ) {
- const lines: GrDiffLine[] = [];
- const elements: HTMLElement[] = [];
- let line;
- let el;
- this.findLinesByRange(start, end, side, lines, elements);
- for (let i = 0; i < lines.length; i++) {
- line = lines[i];
- el = elements[i];
- if (!el || !el.parentElement) {
- // Cannot re-render an element if it does not exist. This can happen
- // if lines are collapsed and not visible on the page yet.
- continue;
- }
- const lineNumberEl = this.getLineNumberEl(el, side);
- const newContent = this.createTextEl(lineNumberEl, line, side)
- .firstChild as HTMLElement;
- // Note that ${el.id} ${newContent.id} might actually mismatch: In unified
- // diff we are rendering the same content twice for all the diff chunk
- // that are unchanged from left to right. TODO: Be smarter about this.
- el.parentElement.replaceChild(newContent, el);
- }
- }
-
- override renderBlameByRange(blame: BlameInfo, start: number, end: number) {
- for (let i = start; i <= end; i++) {
- // TODO(wyatta): this query is expensive, but, when traversing a
- // range, the lines are consecutive, and given the previous blame
- // cell, the next one can be reached cheaply.
- const blameCell = this.getBlameTdByLine(i);
- if (!blameCell) continue;
-
- // Remove the element's children (if any).
- while (blameCell.hasChildNodes()) {
- blameCell.removeChild(blameCell.lastChild!);
- }
- const blameEl = createBlameElement(i, blame);
- if (blameEl) blameCell.appendChild(blameEl);
- }
- }
-
- /**
- * Finds the line number element given the content element by walking up the
- * DOM tree to the diff row and then querying for a .lineNum element on the
- * requested side.
- *
- * TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
- */
- // visible for testing
- getLineNumberEl(content: HTMLElement, side: Side): HTMLElement | null {
- let row: HTMLElement | null = content;
- while (row && !row.classList.contains('diff-row')) row = row.parentElement;
- return row ? (row.querySelector('.lineNum.' + side) as HTMLElement) : null;
- }
-
- /**
- * Adds <tr> table rows to a <tbody> section for allowing the user to expand
- * collapsed of lines. Called by subclasses.
- */
- protected createContextControls(
- section: HTMLElement,
- group: GrDiffGroup,
- viewMode: DiffViewMode
- ) {
- const leftStart = group.lineRange.left.start_line;
- const leftEnd = group.lineRange.left.end_line;
- const firstGroupIsSkipped = !!group.contextGroups[0].skip;
- const lastGroupIsSkipped =
- !!group.contextGroups[group.contextGroups.length - 1].skip;
-
- const containsWholeFile = this.numLinesLeft === leftEnd - leftStart + 1;
- const showAbove =
- (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
- const showBelow = leftEnd < this.numLinesLeft && !lastGroupIsSkipped;
-
- if (showAbove) {
- const paddingRow = this.createContextControlPaddingRow(viewMode);
- paddingRow.classList.add('above');
- section.appendChild(paddingRow);
- }
- section.appendChild(
- this.createContextControlRow(group, showAbove, showBelow, viewMode)
- );
- if (showBelow) {
- const paddingRow = this.createContextControlPaddingRow(viewMode);
- paddingRow.classList.add('below');
- section.appendChild(paddingRow);
- }
- }
-
- /**
- * Creates a context control <tr> table row for with buttons the allow the
- * user to expand collapsed lines. Buttons extend from the gap created by this
- * method up or down into the area of code that they affect.
- */
- private createContextControlRow(
- group: GrDiffGroup,
- showAbove: boolean,
- showBelow: boolean,
- viewMode: DiffViewMode
- ): HTMLElement {
- const row = createElementDiff('tr', 'dividerRow');
- let showConfig: GrContextControlsShowConfig;
- if (showAbove && !showBelow) {
- showConfig = 'above';
- } else if (!showAbove && showBelow) {
- showConfig = 'below';
- } else {
- // Note that !showAbove && !showBelow also intentionally creates
- // "show-both". This means the file is completely collapsed, which is
- // unusual, but at least happens in one test.
- showConfig = 'both';
- }
- row.classList.add(`show-${showConfig}`);
-
- row.appendChild(this.createBlameCell(0));
- if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
- row.appendChild(createElementDiff('td'));
- }
-
- const cell = createElementDiff('td', 'dividerCell');
- // Note that <td> table cells that have `display: none` don't count!
- const colspan = this.renderPrefs?.show_sign_col ? '5' : '3';
- cell.setAttribute('colspan', colspan);
- row.appendChild(cell);
-
- const contextControls = createElementDiff(
- 'gr-context-controls'
- ) as GrContextControls;
- contextControls.diff = this._diff;
- contextControls.renderPreferences = this.renderPrefs;
- contextControls.group = group;
- contextControls.showConfig = showConfig;
- cell.appendChild(contextControls);
- return row;
- }
-
- /**
- * Creates a table row to serve as padding between code and context controls.
- * Blame column, line gutters, and content area will continue visually, but
- * context controls can render over this background to map more clearly to
- * the area of code they expand.
- */
- private createContextControlPaddingRow(viewMode: DiffViewMode) {
- const row = createElementDiff('tr', 'contextBackground');
-
- if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
- row.classList.add('side-by-side');
- row.setAttribute('left-type', GrDiffGroupType.CONTEXT_CONTROL);
- row.setAttribute('right-type', GrDiffGroupType.CONTEXT_CONTROL);
- } else {
- row.classList.add('unified');
- }
-
- row.appendChild(this.createBlameCell(0));
- row.appendChild(createElementDiff('td', 'contextLineNum'));
- if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
- row.appendChild(createElementDiff('td', 'sign'));
- row.appendChild(createElementDiff('td'));
- }
- row.appendChild(createElementDiff('td', 'contextLineNum'));
- if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
- row.appendChild(createElementDiff('td', 'sign'));
- }
- row.appendChild(createElementDiff('td'));
-
- return row;
- }
-
- protected createLineEl(
- line: GrDiffLine,
- number: LineNumber,
- type: GrDiffLineType,
- side: Side
- ) {
- const td = createElementDiff('td');
- td.classList.add(side);
- if (line.type === GrDiffLineType.BLANK) {
- td.classList.add('blankLineNum');
- return td;
- }
- if (line.type === GrDiffLineType.BOTH || line.type === type) {
- td.classList.add('lineNum');
- td.dataset['value'] = number.toString();
-
- if (
- ((this._prefs.show_file_comment_button === false ||
- this.renderPrefs?.show_file_comment_button === false) &&
- number === 'FILE') ||
- number === 'LOST'
- ) {
- return td;
- }
-
- const button = createElementDiff('button');
- td.appendChild(button);
- button.tabIndex = -1;
- button.classList.add('lineNumButton');
- button.classList.add(side);
- button.dataset['value'] = number.toString();
- button.id =
- side === Side.LEFT ? `left-button-${number}` : `right-button-${number}`;
- button.textContent = number === 'FILE' ? 'File' : number.toString();
- if (number === 'FILE') {
- button.setAttribute('aria-label', 'Add file comment');
- }
-
- // Add aria-labels for valid line numbers.
- // For unified diff, this method will be called with number set to 0 for
- // the empty line number column for added/removed lines. This should not
- // be announced to the screenreader.
- if (number !== 'FILE' && number > 0) {
- if (line.type === GrDiffLineType.REMOVE) {
- button.setAttribute('aria-label', `${number} removed`);
- } else if (line.type === GrDiffLineType.ADD) {
- button.setAttribute('aria-label', `${number} added`);
- } else {
- button.setAttribute('aria-label', `${number} unmodified`);
- }
- }
- this.addLineNumberMouseEvents(td, number, side);
- }
- return td;
- }
-
- private addLineNumberMouseEvents(
- el: HTMLElement,
- number: LineNumber,
- side: Side
- ) {
- el.addEventListener('mouseenter', () => {
- fire(el, 'line-mouse-enter', {lineNum: number, side});
- });
- el.addEventListener('mouseleave', () => {
- fire(el, 'line-mouse-leave', {lineNum: number, side});
- });
- }
-
- // visible for testing
- createTextEl(
- lineNumberEl: HTMLElement | null,
- line: GrDiffLine,
- side?: Side,
- twoSlots?: boolean
- ) {
- const td = createElementDiff('td');
- if (line.type !== GrDiffLineType.BLANK) {
- td.classList.add('content');
- }
- if (side) {
- td.classList.add(side);
- }
-
- // If intraline info is not available, the entire line will be
- // considered as changed and marked as dark red / green color
- if (!line.hasIntralineInfo) {
- td.classList.add('no-intraline-info');
- }
- td.classList.add(line.type);
-
- const lineNumber = side ? line.lineNumber(side) : 0;
- if (lineNumber === 'FILE') {
- td.classList.add('file');
- } else if (lineNumber === 'LOST') {
- td.classList.add('lost');
- } else {
- const responsiveMode = getResponsiveMode(this._prefs, this.renderPrefs);
- const contentId =
- side && lineNumber > 0 ? `${side}-content-${lineNumber}` : '';
- const contentText = formatText(
- line.text,
- responsiveMode,
- this._prefs.tab_size,
- this._prefs.line_length,
- contentId
- );
-
- if (side) {
- contentText.setAttribute('data-side', side);
- this.addLineNumberMouseEvents(td, lineNumber, side);
- }
-
- if (lineNumberEl && side) {
- for (const layer of this.layers) {
- if (typeof layer.annotate === 'function') {
- layer.annotate(contentText, lineNumberEl, line, side);
- }
- }
- } else {
- console.error('lineNumberEl or side not set, skipping layer.annotate');
- }
-
- td.appendChild(contentText);
- }
-
- if (side && lineNumber) {
- const threadGroupEl = document.createElement('div');
- threadGroupEl.className = 'thread-group';
- threadGroupEl.setAttribute('data-side', side);
-
- const slot = document.createElement('slot');
- slot.name = `${side}-${lineNumber}`;
- threadGroupEl.appendChild(slot);
-
- // For line.type === BOTH in unified diff we want two slots.
- if (twoSlots) {
- const slot = document.createElement('slot');
- const otherSide = side === Side.LEFT ? Side.RIGHT : Side.LEFT;
- slot.name = `${otherSide}-${line.lineNumber(otherSide)}`;
- threadGroupEl.appendChild(slot);
- }
-
- td.appendChild(threadGroupEl);
- }
-
- return td;
- }
-
- private createMovedLineAnchor(line: number, side: Side) {
- const anchor = createElementDiffWithText('a', `${line}`);
-
- // href is not actually used but important for Screen Readers
- anchor.setAttribute('href', `#${line}`);
- anchor.addEventListener('click', e => {
- e.preventDefault();
- fire(anchor, 'moved-link-clicked', {
- lineNum: line,
- side,
- });
- });
- return anchor;
- }
-
- private createMoveDescriptionDiv(movedIn: boolean, group: GrDiffGroup) {
- const div = createElementDiff('div');
- if (group.moveDetails?.range) {
- const {changed, range} = group.moveDetails;
- const otherSide = movedIn ? Side.LEFT : Side.RIGHT;
- const andChangedLabel = changed ? 'and changed ' : '';
- const direction = movedIn ? 'from' : 'to';
- const textLabel = `Moved ${andChangedLabel}${direction} lines `;
- div.appendChild(createElementDiffWithText('span', textLabel));
- div.appendChild(this.createMovedLineAnchor(range.start, otherSide));
- div.appendChild(createElementDiffWithText('span', ' - '));
- div.appendChild(this.createMovedLineAnchor(range.end, otherSide));
- } else {
- div.appendChild(
- createElementDiffWithText('span', movedIn ? 'Moved in' : 'Moved out')
- );
- }
- return div;
- }
-
- protected buildMoveControls(group: GrDiffGroup) {
- const movedIn = group.adds.length > 0;
- const {
- numberOfCells,
- movedOutIndex,
- movedInIndex,
- lineNumberCols,
- signCols,
- } = this.getMoveControlsConfig();
-
- let controlsClass;
- let descriptionIndex;
- const descriptionTextDiv = this.createMoveDescriptionDiv(movedIn, group);
- if (movedIn) {
- controlsClass = 'movedIn';
- descriptionIndex = movedInIndex;
- } else {
- controlsClass = 'movedOut';
- descriptionIndex = movedOutIndex;
- }
-
- const controls = createElementDiff('tr', `moveControls ${controlsClass}`);
- const cells = [...Array(numberOfCells).keys()].map(() =>
- createElementDiff('td')
- );
- lineNumberCols.forEach(index => {
- cells[index].classList.add('moveControlsLineNumCol');
- });
-
- if (signCols) {
- cells[signCols.left].classList.add('sign', 'left');
- cells[signCols.right].classList.add('sign', 'right');
- }
- const moveRangeHeader = createElementDiff('gr-range-header');
- moveRangeHeader.setAttribute('icon', 'move_item');
- moveRangeHeader.appendChild(descriptionTextDiv);
- cells[descriptionIndex].classList.add('moveHeader');
- cells[descriptionIndex].appendChild(moveRangeHeader);
- cells.forEach(c => {
- controls.appendChild(c);
- });
- return controls;
- }
-
- /**
- * Create a blame cell for the given base line. Blame information will be
- * included in the cell if available.
- */
- // visible for testing
- createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
- const blameTd = createElementDiff('td', 'blame') as HTMLTableCellElement;
- blameTd.setAttribute('data-line-number', lineNumber.toString());
- if (!lineNumber) return blameTd;
-
- const blameInfo = this.getBlameCommitForBaseLine(lineNumber);
- if (!blameInfo) return blameTd;
-
- blameTd.appendChild(createBlameElement(lineNumber, blameInfo));
- return blameTd;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
deleted file mode 100644
index 2a61ef2..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {DiffViewMode, RenderPreferences} from '../../../api/diff';
-import {LineNumber} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup} from '../gr-diff/gr-diff-group';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {Side} from '../../../constants/constants';
-import {DiffLayer, isDefined} from '../../../types/types';
-import {diffClasses} from '../gr-diff/gr-diff-utils';
-import {GrDiffBuilder} from './gr-diff-builder';
-import {BlameInfo} from '../../../types/common';
-import {html, nothing, render} from 'lit';
-import {GrDiffSection} from './gr-diff-section';
-import '../gr-context-controls/gr-context-controls';
-import './gr-diff-section';
-import {GrDiffRow} from './gr-diff-row';
-
-/**
- * Base class for builders that are creating the diff using Lit elements.
- */
-export class GrDiffBuilderLit extends GrDiffBuilder {
- constructor(
- diff: DiffInfo,
- prefs: DiffPreferencesInfo,
- outputEl: HTMLElement,
- layers: DiffLayer[] = [],
- renderPrefs?: RenderPreferences
- ) {
- super(diff, prefs, outputEl, layers, renderPrefs);
- }
-
- override getContentTdByLine(
- lineNumber: LineNumber,
- side?: Side,
- _root: Element = this.outputEl
- ): HTMLTableCellElement | null {
- if (!side) return null;
- const row = this.findRow(lineNumber, side);
- return row?.getContentCell(side) ?? null;
- }
-
- override getLineElByNumber(lineNumber: LineNumber, side: Side) {
- const row = this.findRow(lineNumber, side);
- return row?.getLineNumberCell(side) ?? null;
- }
-
- private findRow(lineNumber?: LineNumber, side?: Side): GrDiffRow | undefined {
- if (!side || !lineNumber) return undefined;
- const group = this.findGroup(side, lineNumber);
- if (!group) return undefined;
- const section = this.findSection(group);
- if (!section) return undefined;
- return section.findRow(side, lineNumber);
- }
-
- private getDiffRows() {
- const sections = [
- ...this.outputEl.querySelectorAll<GrDiffSection>('gr-diff-section'),
- ];
- return sections.map(s => s.getDiffRows()).flat();
- }
-
- override getLineNumberRows(): HTMLTableRowElement[] {
- const rows = this.getDiffRows();
- return rows.map(r => r.getTableRow()).filter(isDefined);
- }
-
- override getLineNumEls(side: Side): HTMLTableCellElement[] {
- const rows = this.getDiffRows();
- return rows.map(r => r.getLineNumberCell(side)).filter(isDefined);
- }
-
- override getBlameTdByLine(lineNumber: number): Element | undefined {
- return this.findRow(lineNumber, Side.LEFT)?.getBlameCell();
- }
-
- override getContentByLine(
- lineNumber: LineNumber,
- side?: Side,
- _root?: HTMLElement
- ): HTMLElement | null {
- const cell = this.getContentTdByLine(lineNumber, side);
- return (cell?.firstChild ?? null) as HTMLElement | null;
- }
-
- /** This is used when layers initiate an update. */
- override renderContentByRange(
- start: LineNumber,
- end: LineNumber,
- side: Side
- ) {
- const groups = this.getGroupsByLineRange(start, end, side);
- for (const group of groups) {
- const section = this.findSection(group);
- for (const row of section?.getDiffRows() ?? []) {
- row.requestUpdate();
- }
- }
- }
-
- private findSection(group?: GrDiffGroup): GrDiffSection | undefined {
- if (!group) return undefined;
- const leftClass = `left-${group.startLine(Side.LEFT)}`;
- const rightClass = `right-${group.startLine(Side.RIGHT)}`;
- return (
- this.outputEl.querySelector<GrDiffSection>(
- `gr-diff-section.${leftClass}.${rightClass}`
- ) ?? undefined
- );
- }
-
- override renderBlameByRange(
- blameInfo: BlameInfo,
- start: number,
- end: number
- ) {
- for (let lineNumber = start; lineNumber <= end; lineNumber++) {
- const row = this.findRow(lineNumber, Side.LEFT);
- if (!row) continue;
- row.blameInfo = blameInfo;
- }
- }
-
- // TODO: Refactor this such that adding the move controls becomes part of the
- // lit element.
- protected override getMoveControlsConfig() {
- return {
- numberOfCells: 6, // How many cells does the diff table have?
- movedOutIndex: 2, // Index of left content column in diff table.
- movedInIndex: 5, // Index of right content column in diff table.
- lineNumberCols: [0, 3], // Indices of line number columns in diff table.
- signCols: {left: 1, right: 4},
- };
- }
-
- protected override buildSectionElement(group: GrDiffGroup) {
- const leftCl = `left-${group.startLine(Side.LEFT)}`;
- const rightCl = `right-${group.startLine(Side.RIGHT)}`;
- const section = html`
- <gr-diff-section
- class="${leftCl} ${rightCl}"
- .group=${group}
- .diff=${this._diff}
- .layers=${this.layers}
- .diffPrefs=${this._prefs}
- .renderPrefs=${this.renderPrefs}
- ></gr-diff-section>
- `;
- // When using Lit's `render()` method it wants to be in full control of the
- // element that it renders into, so we let it render into a temp element.
- // Rendering into the diff table directly would interfere with
- // `clearDiffContent()`for example.
- // TODO: Remove legacy diff builder, then convert <gr-diff> to be fully lit
- // controlled, then this code will become part of the standard `render()` of
- // <gr-diff> as a LitElement.
- const tempEl = document.createElement('div');
- render(section, tempEl);
- const sectionEl = tempEl.firstElementChild as GrDiffSection;
- return sectionEl;
- }
-
- override addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
- const colgroup = html`
- <colgroup>
- <col class=${diffClasses('blame')}></col>
- ${this.renderUnifiedColumns(lineNumberWidth)}
- ${this.renderSideBySideColumns(Side.LEFT, lineNumberWidth)}
- ${this.renderSideBySideColumns(Side.RIGHT, lineNumberWidth)}
- </colgroup>
- `;
- // When using Lit's `render()` method it wants to be in full control of the
- // element that it renders into, so we let it render into a temp element.
- // Rendering into the diff table directly would interfere with
- // `clearDiffContent()`for example.
- // TODO: Remove legacy diff builder, then convert <gr-diff> to be fully lit
- // controlled, then this code will become part of the standard `render()` of
- // <gr-diff> as a LitElement.
- const tempEl = document.createElement('div');
- render(colgroup, tempEl);
- const colgroupEl = tempEl.firstElementChild as HTMLElement;
- outputEl.appendChild(colgroupEl);
- }
-
- private renderUnifiedColumns(lineNumberWidth: number) {
- if (this.renderPrefs?.view_mode !== DiffViewMode.UNIFIED) return nothing;
- return html`
- <col class=${diffClasses()} width=${lineNumberWidth}></col>
- <col class=${diffClasses()} width=${lineNumberWidth}></col>
- <col class=${diffClasses()}></col>
- `;
- }
-
- private renderSideBySideColumns(side: Side, lineNumberWidth: number) {
- if (this.renderPrefs?.view_mode === DiffViewMode.UNIFIED) return nothing;
- return html`
- <col class=${diffClasses(side)} width=${lineNumberWidth}></col>
- <col class=${diffClasses(side, 'sign')}></col>
- <col class=${diffClasses(side)}></col>
- `;
- }
-
- protected override getNextContentOnSide(
- _content: HTMLElement,
- _side: Side
- ): HTMLElement | null {
- // TODO: getNextContentOnSide() is not required by lit based rendering.
- // So let's refactor it to be moved into gr-diff-builder-legacy.
- console.warn('unimplemented method getNextContentOnSide() called');
- return null;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
deleted file mode 100644
index 6ae7d4cd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
-import {DiffViewMode, Side} from '../../../constants/constants';
-import {DiffLayer} from '../../../types/types';
-import {RenderPreferences} from '../../../api/diff';
-import {createElementDiff} from '../gr-diff/gr-diff-utils';
-import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
-
-export class GrDiffBuilderSideBySide extends GrDiffBuilderLegacy {
- constructor(
- diff: DiffInfo,
- prefs: DiffPreferencesInfo,
- outputEl: HTMLElement,
- layers: DiffLayer[] = [],
- renderPrefs?: RenderPreferences
- ) {
- super(diff, prefs, outputEl, layers, renderPrefs);
- }
-
- protected override getMoveControlsConfig() {
- return {
- numberOfCells: 6,
- movedOutIndex: 2,
- movedInIndex: 5,
- lineNumberCols: [0, 3],
- signCols: {left: 1, right: 4},
- };
- }
-
- // visible for testing
- override buildSectionElement(group: GrDiffGroup) {
- const sectionEl = createElementDiff('tbody', 'section');
- sectionEl.classList.add(group.type);
- if (group.isTotal()) {
- sectionEl.classList.add('total');
- }
- if (group.dueToRebase) {
- sectionEl.classList.add('dueToRebase');
- }
- if (group.moveDetails) {
- sectionEl.classList.add('dueToMove');
- if (group.moveDetails.changed) {
- sectionEl.classList.add('changed');
- }
- sectionEl.appendChild(this.buildMoveControls(group));
- }
- if (group.ignoredWhitespaceOnly) {
- sectionEl.classList.add('ignoredWhitespaceOnly');
- }
- if (group.type === GrDiffGroupType.CONTEXT_CONTROL) {
- this.createContextControls(sectionEl, group, DiffViewMode.SIDE_BY_SIDE);
- return sectionEl;
- }
-
- const pairs = group.getSideBySidePairs();
- for (let i = 0; i < pairs.length; i++) {
- sectionEl.appendChild(this.createRow(pairs[i].left, pairs[i].right));
- }
- return sectionEl;
- }
-
- override addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
- const colgroup = document.createElement('colgroup');
-
- // Add the blame column.
- let col = createElementDiff('col', 'blame');
- colgroup.appendChild(col);
-
- // Add left-side line number.
- col = createElementDiff('col', 'left');
- col.setAttribute('width', lineNumberWidth.toString());
- colgroup.appendChild(col);
-
- colgroup.appendChild(createElementDiff('col', 'sign left'));
-
- // Add left-side content.
- colgroup.appendChild(createElementDiff('col', 'left'));
-
- // Add right-side line number.
- col = createElementDiff('col', 'right');
- col.setAttribute('width', lineNumberWidth.toString());
- colgroup.appendChild(col);
-
- colgroup.appendChild(createElementDiff('col', 'sign right'));
-
- // Add right-side content.
- colgroup.appendChild(createElementDiff('col', 'right'));
-
- outputEl.appendChild(colgroup);
- }
-
- private createRow(leftLine: GrDiffLine, rightLine: GrDiffLine) {
- const row = createElementDiff('tr');
- row.classList.add('diff-row', 'side-by-side');
- row.setAttribute('left-type', leftLine.type);
- row.setAttribute('right-type', rightLine.type);
- // TabIndex makes screen reader read a row when navigating with j/k
- row.tabIndex = -1;
- // Before Chrome 102, Chrome was able to compute a11y label from children
- // content. Now Chrome 102 and Firefox are not computing a11y label because
- // tr is not expected to have aria label. Adding aria role button is
- // pushing browser to compute aria even for tr. This can be removed, once
- // browsers will again compute a11y label even for tr when it is focused.
- // TODO: Remove when Chrome 102 is out of date for 1 year.
- row.setAttribute(
- 'aria-labelledby',
- [
- leftLine.beforeNumber ? `left-button-${leftLine.beforeNumber}` : '',
- leftLine.beforeNumber ? `left-content-${leftLine.beforeNumber}` : '',
- rightLine.afterNumber ? `right-button-${rightLine.afterNumber}` : '',
- rightLine.afterNumber ? `right-content-${rightLine.afterNumber}` : '',
- ]
- .join(' ')
- .trim()
- );
-
- row.appendChild(this.createBlameCell(leftLine.beforeNumber));
-
- this.appendPair(row, leftLine, leftLine.beforeNumber, Side.LEFT);
- this.appendPair(row, rightLine, rightLine.afterNumber, Side.RIGHT);
- return row;
- }
-
- private appendPair(
- row: HTMLElement,
- line: GrDiffLine,
- lineNumber: LineNumber,
- side: Side
- ) {
- const lineNumberEl = this.createLineEl(line, lineNumber, line.type, side);
- row.appendChild(lineNumberEl);
- row.appendChild(this.createSignEl(line, side));
- row.appendChild(this.createTextEl(lineNumberEl, line, side));
- }
-
- private createSignEl(line: GrDiffLine, side: Side): HTMLElement {
- const td = createElementDiff('td', 'sign');
- td.classList.add(side);
- if (line.type === GrDiffLineType.BLANK) {
- td.classList.add('blank');
- } else if (line.type === GrDiffLineType.ADD && side === Side.RIGHT) {
- td.classList.add('add');
- td.innerText = '+';
- } else if (line.type === GrDiffLineType.REMOVE && side === Side.LEFT) {
- td.classList.add('remove');
- td.innerText = '-';
- }
- if (!line.hasIntralineInfo) {
- td.classList.add('no-intraline-info');
- }
- return td;
- }
-
- // visible for testing
- override getNextContentOnSide(
- content: HTMLElement,
- side: Side
- ): HTMLElement | null {
- let tr: HTMLElement = content.parentElement!.parentElement!;
- while ((tr = tr.nextSibling as HTMLElement)) {
- const nextContent = tr.querySelector(
- 'td.content .contentText[data-side="' + side + '"]'
- );
- if (nextContent) return nextContent as HTMLElement;
- }
- return null;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
deleted file mode 100644
index ffc2dcd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {DiffViewMode, Side} from '../../../constants/constants';
-import {DiffLayer} from '../../../types/types';
-import {RenderPreferences} from '../../../api/diff';
-import {createElementDiff} from '../gr-diff/gr-diff-utils';
-import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
-
-export class GrDiffBuilderUnified extends GrDiffBuilderLegacy {
- constructor(
- diff: DiffInfo,
- prefs: DiffPreferencesInfo,
- outputEl: HTMLElement,
- layers: DiffLayer[] = [],
- renderPrefs?: RenderPreferences
- ) {
- super(diff, prefs, outputEl, layers, renderPrefs);
- }
-
- protected override getMoveControlsConfig() {
- return {
- numberOfCells: 3,
- movedOutIndex: 2,
- movedInIndex: 2,
- lineNumberCols: [0, 1],
- };
- }
-
- // visible for testing
- override buildSectionElement(group: GrDiffGroup): HTMLElement {
- const sectionEl = createElementDiff('tbody', 'section');
- sectionEl.classList.add(group.type);
- if (group.isTotal()) {
- sectionEl.classList.add('total');
- }
- if (group.dueToRebase) {
- sectionEl.classList.add('dueToRebase');
- }
- if (group.moveDetails) {
- sectionEl.classList.add('dueToMove');
- if (group.moveDetails.changed) {
- sectionEl.classList.add('changed');
- }
- sectionEl.appendChild(this.buildMoveControls(group));
- }
- if (group.ignoredWhitespaceOnly) {
- sectionEl.classList.add('ignoredWhitespaceOnly');
- }
- if (group.type === GrDiffGroupType.CONTEXT_CONTROL) {
- this.createContextControls(sectionEl, group, DiffViewMode.UNIFIED);
- return sectionEl;
- }
-
- for (let i = 0; i < group.lines.length; ++i) {
- const line = group.lines[i];
- // If only whitespace has changed and the settings ask for whitespace to
- // be ignored, only render the right-side line in unified diff mode.
- if (group.ignoredWhitespaceOnly && line.type === GrDiffLineType.REMOVE) {
- continue;
- }
- sectionEl.appendChild(this.createRow(line));
- }
- return sectionEl;
- }
-
- override addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
- const colgroup = document.createElement('colgroup');
-
- // Add the blame column.
- let col = createElementDiff('col', 'blame');
- colgroup.appendChild(col);
-
- // Add left-side line number.
- col = createElementDiff('col');
- col.setAttribute('width', lineNumberWidth.toString());
- colgroup.appendChild(col);
-
- // Add right-side line number.
- col = createElementDiff('col');
- col.setAttribute('width', lineNumberWidth.toString());
- colgroup.appendChild(col);
-
- // Add the content.
- colgroup.appendChild(createElementDiff('col'));
-
- outputEl.appendChild(colgroup);
- }
-
- protected createRow(line: GrDiffLine) {
- const row = createElementDiff('tr', line.type);
- row.classList.add('diff-row', 'unified');
- // TabIndex makes screen reader read a row when navigating with j/k
- row.tabIndex = -1;
- row.appendChild(this.createBlameCell(line.beforeNumber));
- let lineNumberEl = this.createLineEl(
- line,
- line.beforeNumber,
- GrDiffLineType.REMOVE,
- Side.LEFT
- );
- row.appendChild(lineNumberEl);
- lineNumberEl = this.createLineEl(
- line,
- line.afterNumber,
- GrDiffLineType.ADD,
- Side.RIGHT
- );
- row.appendChild(lineNumberEl);
- let side = undefined;
- if (line.type === GrDiffLineType.ADD || line.type === GrDiffLineType.BOTH) {
- side = Side.RIGHT;
- }
- if (line.type === GrDiffLineType.REMOVE) {
- side = Side.LEFT;
- }
-
- // Before Chrome 102, Chrome was able to compute a11y label from children
- // content. Now Chrome 102 and Firefox are not computing a11y label because
- // tr is not expected to have aria label. Adding aria role button is
- // pushing browser to compute aria even for tr. This can be removed, once
- // browsers will again compute a11y label even for tr when it is focused.
- // TODO: Remove when Chrome 102 is out of date for 1 year.
- row.setAttribute(
- 'aria-labelledby',
- [
- line.beforeNumber ? `left-button-${line.beforeNumber}` : '',
- side === Side.LEFT && line.beforeNumber
- ? `left-content-${line.beforeNumber}`
- : '',
- line.afterNumber ? `right-button-${line.afterNumber}` : '',
- side === Side.RIGHT && line.afterNumber
- ? `right-content-${line.afterNumber}`
- : '',
- ]
- .filter(id => !!id)
- .join(' ')
- .trim()
- );
- const twoSlots = line.type === GrDiffLineType.BOTH;
- row.appendChild(this.createTextEl(lineNumberEl, line, side, twoSlots));
- return row;
- }
-
- getNextContentOnSide(content: HTMLElement, side: Side): HTMLElement | null {
- let tr: HTMLElement = content.parentElement!.parentElement!;
- while ((tr = tr.nextSibling as HTMLElement)) {
- // Note that this does not work when there is a "common" chunk in the
- // diff (different content only because of whitespace). Such chunks are
- // rendered with class "add", so these rows will be skipped for the
- // 'left' side.
- // TODO: Fix this when writing a Lit component for unified diff.
- if (
- tr.classList.contains('both') ||
- (side === 'left' && tr.classList.contains('remove')) ||
- (side === 'right' && tr.classList.contains('add'))
- ) {
- return tr.querySelector('.contentText');
- }
- }
- return null;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
deleted file mode 100644
index 8c44727..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
+++ /dev/null
@@ -1,283 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import '../gr-diff/gr-diff-group';
-import './gr-diff-builder';
-import './gr-diff-builder-unified';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
-import {DiffPreferencesInfo} from '../../../api/diff';
-import {createDefaultDiffPrefs} from '../../../constants/constants';
-import {createDiff} from '../../../test/test-data-generators';
-import {queryAndAssert} from '../../../utils/common-util';
-import {assert} from '@open-wc/testing';
-
-suite('GrDiffBuilderUnified tests', () => {
- let prefs: DiffPreferencesInfo;
- let outputEl: HTMLElement;
- let diffBuilder: GrDiffBuilderUnified;
-
- setup(() => {
- prefs = {
- ...createDefaultDiffPrefs(),
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- outputEl = document.createElement('div');
- diffBuilder = new GrDiffBuilderUnified(createDiff(), prefs, outputEl, []);
- });
-
- suite('buildSectionElement for BOTH group', () => {
- let lines: GrDiffLine[];
- let group: GrDiffGroup;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
- new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
- new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World";';
- lines[2].text = ' return True';
-
- group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('both'));
- });
-
- test('creates each unchanged row once', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 3);
-
- assert.equal(
- queryAndAssert(rowEls[0], '.lineNum.left').textContent,
- lines[0].beforeNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[0], '.lineNum.right').textContent,
- lines[0].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[0], '.content').textContent,
- lines[0].text
- );
-
- assert.equal(
- queryAndAssert(rowEls[1], '.lineNum.left').textContent,
- lines[1].beforeNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[1], '.lineNum.right').textContent,
- lines[1].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[1], '.content').textContent,
- lines[1].text
- );
-
- assert.equal(
- queryAndAssert(rowEls[2], '.lineNum.left').textContent,
- lines[2].beforeNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[2], '.lineNum.right').textContent,
- lines[2].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[2], '.content').textContent,
- lines[2].text
- );
- });
- });
-
- suite('buildSectionElement for moved chunks', () => {
- test('creates a moved out group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 15),
- new GrDiffLine(GrDiffLineType.REMOVE, 16),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({
- type: GrDiffGroupType.DELTA,
- lines,
- moveDetails: {changed: false},
- });
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedOut'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved out');
- });
-
- test('creates a moved in group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.ADD, 37),
- new GrDiffLine(GrDiffLineType.ADD, 38),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({
- type: GrDiffGroupType.DELTA,
- lines,
- moveDetails: {changed: false},
- });
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedIn'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved in');
- });
- });
-
- suite('buildSectionElement for DELTA group', () => {
- let lines: GrDiffLine[];
- let group: GrDiffGroup;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 1),
- new GrDiffLine(GrDiffLineType.REMOVE, 2),
- new GrDiffLine(GrDiffLineType.ADD, 2),
- new GrDiffLine(GrDiffLineType.ADD, 3),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- lines[2].text = 'def hello_universe()';
- lines[3].text = ' print "Hello Universe"';
- });
-
- test('creates the section', () => {
- group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('delta'));
- });
-
- test('creates the section with class if ignoredWhitespaceOnly', () => {
- group = new GrDiffGroup({
- type: GrDiffGroupType.DELTA,
- lines,
- ignoredWhitespaceOnly: true,
- });
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
- });
-
- test('creates the section with class if dueToRebase', () => {
- group = new GrDiffGroup({
- type: GrDiffGroupType.DELTA,
- lines,
- dueToRebase: true,
- });
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('dueToRebase'));
- });
-
- test('creates first the removed and then the added rows', () => {
- group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 4);
-
- assert.equal(
- queryAndAssert(rowEls[0], '.lineNum.left').textContent,
- lines[0].beforeNumber.toString()
- );
- assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
- assert.equal(
- queryAndAssert(rowEls[0], '.content').textContent,
- lines[0].text
- );
-
- assert.equal(
- queryAndAssert(rowEls[1], '.lineNum.left').textContent,
- lines[1].beforeNumber.toString()
- );
- assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
- assert.equal(
- queryAndAssert(rowEls[1], '.content').textContent,
- lines[1].text
- );
-
- assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
- assert.equal(
- queryAndAssert(rowEls[2], '.lineNum.right').textContent,
- lines[2].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[2], '.content').textContent,
- lines[2].text
- );
-
- assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
- assert.equal(
- queryAndAssert(rowEls[3], '.lineNum.right').textContent,
- lines[3].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[3], '.content').textContent,
- lines[3].text
- );
- });
-
- test('creates only the added rows if only ignored whitespace', () => {
- group = new GrDiffGroup({
- type: GrDiffGroupType.DELTA,
- lines,
- ignoredWhitespaceOnly: true,
- });
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 2);
-
- assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
- assert.equal(
- queryAndAssert(rowEls[0], '.lineNum.right').textContent,
- lines[2].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[0], '.content').textContent,
- lines[2].text
- );
-
- assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
- assert.equal(
- queryAndAssert(rowEls[1], '.lineNum.right').textContent,
- lines[3].afterNumber.toString()
- );
- assert.equal(
- queryAndAssert(rowEls[1], '.content').textContent,
- lines[3].text
- );
- });
- });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index 5ca5197..f38ba5c 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -3,19 +3,27 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import './gr-diff-section';
+import '../gr-context-controls/gr-context-controls';
import {
ContentLoadNeededEventDetail,
DiffContextExpandedExternalDetail,
+ DiffViewMode,
RenderPreferences,
} from '../../../api/diff';
-import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
+import {LineNumber} from '../gr-diff/gr-diff-line';
import {GrDiffGroup} from '../gr-diff/gr-diff-group';
-import {assert} from '../../../utils/common-util';
-import '../gr-context-controls/gr-context-controls';
import {BlameInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {Side} from '../../../constants/constants';
-import {DiffLayer} from '../../../types/types';
+import {DiffLayer, isDefined} from '../../../types/types';
+import {GrDiffRow} from './gr-diff-row';
+import {GrDiffSection} from './gr-diff-section';
+import {html, render} from 'lit';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+import {when} from 'lit/directives/when.js';
+import {GrDiffBuilderImage} from './gr-diff-builder-image';
+import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
export interface DiffContextExpandedEventDetail
extends DiffContextExpandedExternalDetail {
@@ -32,73 +40,34 @@
}
}
-/**
- * Given that GrDiffBuilder has ~1,000 lines of code, this interface is just
- * making refactorings easier by emphasizing what the public facing "contract"
- * of this class is. There are no plans for adding separate implementations.
- */
-export interface DiffBuilder {
- init(): void;
- cleanup(): void;
- addGroups(groups: readonly GrDiffGroup[]): void;
- clearGroups(): void;
- replaceGroup(
- contextControl: GrDiffGroup,
- groups: readonly GrDiffGroup[]
- ): void;
- findGroup(side: Side, line: LineNumber): GrDiffGroup | undefined;
- addColumns(outputEl: HTMLElement, fontSize: number): void;
- // TODO: Change `null` to `undefined`.
- getContentTdByLine(
- lineNumber: LineNumber,
- side?: Side,
- root?: Element
- ): HTMLTableCellElement | null;
- getLineElByNumber(
- lineNumber: LineNumber,
- side?: Side
- ): HTMLTableCellElement | null;
- getLineNumberRows(): HTMLTableRowElement[];
- getLineNumEls(side: Side): HTMLTableCellElement[];
- setBlame(blame: BlameInfo[]): void;
- updateRenderPrefs(renderPrefs: RenderPreferences): void;
+export function isImageDiffBuilder<T extends GrDiffBuilder>(
+ x: T | GrDiffBuilderImage | undefined
+): x is GrDiffBuilderImage {
+ return !!x && !!(x as GrDiffBuilderImage).renderImageDiff;
}
-export interface ImageDiffBuilder extends DiffBuilder {
- renderImageDiff(): void;
-}
-
-export function isImageDiffBuilder(
- x: DiffBuilder | ImageDiffBuilder | undefined
-): x is ImageDiffBuilder {
- return !!x && !!(x as ImageDiffBuilder).renderImageDiff;
+export function isBinaryDiffBuilder<T extends GrDiffBuilder>(
+ x: T | GrDiffBuilderBinary | undefined
+): x is GrDiffBuilderBinary {
+ return !!x && !!(x as GrDiffBuilderBinary).renderBinaryDiff;
}
/**
- * Base class for different diff builders, like side-by-side, unified etc.
- *
* The builder takes GrDiffGroups, and builds the corresponding DOM elements,
* called sections. Only the builder should add or remove sections from the
* DOM. Callers can use the ...group() methods to modify groups and thus cause
* rendering changes.
- *
- * TODO: Do not subclass `GrDiffBuilder`. Use composition and interfaces.
*/
-export abstract class GrDiffBuilder implements DiffBuilder {
- protected readonly _diff: DiffInfo;
+export class GrDiffBuilder {
+ private readonly diff: DiffInfo;
- protected readonly numLinesLeft: number;
+ readonly prefs: DiffPreferencesInfo;
- // visible for testing
- readonly _prefs: DiffPreferencesInfo;
+ renderPrefs?: RenderPreferences;
- protected renderPrefs?: RenderPreferences;
+ readonly outputEl: HTMLElement;
- protected readonly outputEl: HTMLElement;
-
- protected groups: GrDiffGroup[];
-
- private blameInfo: BlameInfo[] = [];
+ private groups: GrDiffGroup[];
private readonly layerUpdateListener: (
start: LineNumber,
@@ -113,14 +82,8 @@
readonly layers: DiffLayer[] = [],
renderPrefs?: RenderPreferences
) {
- this._diff = diff;
- this.numLinesLeft = this._diff.content
- ? this._diff.content.reduce((sum, chunk) => {
- const left = chunk.a || chunk.ab;
- return sum + (left?.length || chunk.skip || 0);
- }, 0)
- : 0;
- this._prefs = prefs;
+ this.diff = diff;
+ this.prefs = prefs;
this.renderPrefs = renderPrefs;
this.outputEl = outputEl;
this.groups = [];
@@ -141,6 +104,138 @@
this.init();
}
+ getContentTdByLine(
+ lineNumber: LineNumber,
+ side?: Side
+ ): HTMLTableCellElement | undefined {
+ if (!side) return undefined;
+ const row = this.findRow(lineNumber, side);
+ return row?.getContentCell(side);
+ }
+
+ getLineElByNumber(
+ lineNumber: LineNumber,
+ side?: Side
+ ): HTMLTableCellElement | undefined {
+ if (!side) return undefined;
+ const row = this.findRow(lineNumber, side);
+ return row?.getLineNumberCell(side);
+ }
+
+ private findRow(lineNumber?: LineNumber, side?: Side): GrDiffRow | undefined {
+ if (!side || !lineNumber) return undefined;
+ const group = this.findGroup(side, lineNumber);
+ if (!group) return undefined;
+ const section = this.findSection(group);
+ if (!section) return undefined;
+ return section.findRow(side, lineNumber);
+ }
+
+ private getDiffRows() {
+ const sections = [
+ ...this.outputEl.querySelectorAll<GrDiffSection>('gr-diff-section'),
+ ];
+ return sections.map(s => s.getDiffRows()).flat();
+ }
+
+ getLineNumberRows(): HTMLTableRowElement[] {
+ const rows = this.getDiffRows();
+ return rows.map(r => r.getTableRow()).filter(isDefined);
+ }
+
+ getLineNumEls(side: Side): HTMLTableCellElement[] {
+ const rows = this.getDiffRows();
+ return rows.map(r => r.getLineNumberCell(side)).filter(isDefined);
+ }
+
+ /** This is used when layers initiate an update. */
+ renderContentByRange(start: LineNumber, end: LineNumber, side: Side) {
+ const groups = this.getGroupsByLineRange(start, end, side);
+ for (const group of groups) {
+ const section = this.findSection(group);
+ for (const row of section?.getDiffRows() ?? []) {
+ row.requestUpdate();
+ }
+ }
+ }
+
+ private findSection(group: GrDiffGroup): GrDiffSection | undefined {
+ const leftClass = `left-${group.startLine(Side.LEFT)}`;
+ const rightClass = `right-${group.startLine(Side.RIGHT)}`;
+ return (
+ this.outputEl.querySelector<GrDiffSection>(
+ `gr-diff-section.${leftClass}.${rightClass}`
+ ) ?? undefined
+ );
+ }
+
+ buildSectionElement(group: GrDiffGroup): HTMLElement {
+ const leftCl = `left-${group.startLine(Side.LEFT)}`;
+ const rightCl = `right-${group.startLine(Side.RIGHT)}`;
+ const section = html`
+ <gr-diff-section
+ class="${leftCl} ${rightCl}"
+ .group=${group}
+ .diff=${this.diff}
+ .layers=${this.layers}
+ .diffPrefs=${this.prefs}
+ .renderPrefs=${this.renderPrefs}
+ ></gr-diff-section>
+ `;
+ // When using Lit's `render()` method it wants to be in full control of the
+ // element that it renders into, so we let it render into a temp element.
+ // Rendering into the diff table directly would interfere with
+ // `clearDiffContent()`for example.
+ // TODO: Convert <gr-diff> to be fully lit controlled and incorporate this
+ // method into Lit's `render()` cycle.
+ const tempEl = document.createElement('div');
+ render(section, tempEl);
+ const sectionEl = tempEl.firstElementChild as GrDiffSection;
+ return sectionEl;
+ }
+
+ addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
+ const colgroup = html`
+ <colgroup>
+ <col class=${diffClasses('blame')}></col>
+ ${when(
+ this.renderPrefs?.view_mode === DiffViewMode.UNIFIED,
+ () => html` ${this.renderUnifiedColumns(lineNumberWidth)} `,
+ () => html`
+ ${this.renderSideBySideColumns(Side.LEFT, lineNumberWidth)}
+ ${this.renderSideBySideColumns(Side.RIGHT, lineNumberWidth)}
+ `
+ )}
+ </colgroup>
+ `;
+ // When using Lit's `render()` method it wants to be in full control of the
+ // element that it renders into, so we let it render into a temp element.
+ // Rendering into the diff table directly would interfere with
+ // `clearDiffContent()`for example.
+ // TODO: Convert <gr-diff> to be fully lit controlled and incorporate this
+ // method into Lit's `render()` cycle.
+ const tempEl = document.createElement('div');
+ render(colgroup, tempEl);
+ const colgroupEl = tempEl.firstElementChild as HTMLElement;
+ outputEl.appendChild(colgroupEl);
+ }
+
+ private renderUnifiedColumns(lineNumberWidth: number) {
+ return html`
+ <col class=${diffClasses()} width=${lineNumberWidth}></col>
+ <col class=${diffClasses()} width=${lineNumberWidth}></col>
+ <col class=${diffClasses()}></col>
+ `;
+ }
+
+ private renderSideBySideColumns(side: Side, lineNumberWidth: number) {
+ return html`
+ <col class=${diffClasses(side)} width=${lineNumberWidth}></col>
+ <col class=${diffClasses(side, 'sign')}></col>
+ <col class=${diffClasses(side)}></col>
+ `;
+ }
+
/**
* This is meant to be called when the gr-diff component re-connects, or when
* the diff is (re-)rendered.
@@ -172,10 +267,6 @@
}
}
- abstract addColumns(outputEl: HTMLElement, fontSize: number): void;
-
- protected abstract buildSectionElement(group: GrDiffGroup): HTMLElement;
-
addGroups(groups: readonly GrDiffGroup[]) {
for (const group of groups) {
this.groups.push(group);
@@ -238,151 +329,19 @@
.filter(group => group.lines.length > 0);
}
- // TODO: Change `null` to `undefined`.
- abstract getContentTdByLine(
- lineNumber: LineNumber,
- side?: Side,
- root?: Element
- ): HTMLTableCellElement | null;
-
- // TODO: Change `null` to `undefined`.
- abstract getLineElByNumber(
- lineNumber: LineNumber,
- side?: Side
- ): HTMLTableCellElement | null;
-
- abstract getLineNumberRows(): HTMLTableRowElement[];
-
- abstract getLineNumEls(side: Side): HTMLTableCellElement[];
-
- protected abstract getBlameTdByLine(lineNum: number): Element | undefined;
-
- // TODO: Change `null` to `undefined`.
- protected abstract getContentByLine(
- lineNumber: LineNumber,
- side?: Side,
- root?: HTMLElement
- ): HTMLElement | null;
-
- /**
- * Find line elements or line objects by a range of line numbers and a side.
- *
- * @param start The first line number
- * @param end The last line number
- * @param side The side of the range. Either 'left' or 'right'.
- * @param out_lines The output list of line objects.
- * TODO: Change to camelCase.
- * @param out_elements The output list of line elements.
- * TODO: Change to camelCase.
- */
- // visible for testing
- findLinesByRange(
- start: LineNumber,
- end: LineNumber,
- side: Side,
- out_lines: GrDiffLine[],
- out_elements: HTMLElement[]
- ) {
- const groups = this.getGroupsByLineRange(start, end, side);
- for (const group of groups) {
- let content: HTMLElement | null = null;
- for (const line of group.lines) {
- if (
- (side === 'left' && line.type === GrDiffLineType.ADD) ||
- (side === 'right' && line.type === GrDiffLineType.REMOVE)
- ) {
- continue;
- }
- const lineNumber =
- side === 'left' ? line.beforeNumber : line.afterNumber;
- if (lineNumber < start || lineNumber > end) {
- continue;
- }
-
- if (content) {
- content = this.getNextContentOnSide(content, side);
- } else {
- content = this.getContentByLine(lineNumber, side, group.element);
- }
- if (content) {
- // out_lines and out_elements must match. So if we don't have an
- // element to push, then also don't push a line.
- out_lines.push(line);
- out_elements.push(content);
- }
- }
- }
- assert(
- out_lines.length === out_elements.length,
- 'findLinesByRange: lines and elements arrays must have same length'
- );
- }
-
- protected abstract renderContentByRange(
- start: LineNumber,
- end: LineNumber,
- side: Side
- ): void;
-
- protected abstract renderBlameByRange(
- blame: BlameInfo,
- start: number,
- end: number
- ): void;
-
- /**
- * Finds the next DIV.contentText element following the given element, and on
- * the same side. Will only search within a group.
- *
- * TODO: Change `null` to `undefined`.
- */
- protected abstract getNextContentOnSide(
- content: HTMLElement,
- side: Side
- ): HTMLElement | null;
-
- /**
- * Gets configuration for creating move controls for chunks marked with
- * dueToMove
- */
- protected abstract getMoveControlsConfig(): {
- numberOfCells: number;
- movedOutIndex: number;
- movedInIndex: number;
- lineNumberCols: number[];
- signCols?: {left: number; right: number};
- };
-
/**
* Set the blame information for the diff. For any already-rendered line,
* re-render its blame cell content.
*/
setBlame(blame: BlameInfo[]) {
- this.blameInfo = blame;
- for (const commit of blame) {
- for (const range of commit.ranges) {
- this.renderBlameByRange(commit, range.start, range.end);
- }
- }
- }
-
- /**
- * Given a base line number, return the commit containing that line in the
- * current set of blame information. If no blame information has been
- * provided, null is returned.
- *
- * @return The commit information.
- */
- // visible for testing
- getBlameCommitForBaseLine(lineNum: LineNumber): BlameInfo | undefined {
- for (const blameCommit of this.blameInfo) {
- for (const range of blameCommit.ranges) {
- if (range.start <= lineNum && range.end >= lineNum) {
- return blameCommit;
+ for (const blameInfo of blame) {
+ for (const range of blameInfo.ranges) {
+ for (let line = range.start; line <= range.end; line++) {
+ const row = this.findRow(line, Side.LEFT);
+ if (row) row.blameInfo = blameInfo;
}
}
}
- return undefined;
}
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
index 02f2233..69c0f5c 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -44,7 +44,7 @@
* fully blown dependency on GrDiffBuilderElement.
*/
export interface DiffBuilderInterface {
- getContentTdByLineEl(lineEl?: Element): Element | null;
+ getContentTdByLineEl(lineEl?: Element): Element | undefined;
}
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index 3b2fa81..2ec0a2e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -506,16 +506,8 @@
return Promise.resolve();
}
assertIsDefined(this.element);
- // This is a temporary hack while migration to lit based diff rendering:
- // Elements with 'display: contents;' do not have a height, so they
- // won't work as intended with `untilRendered()`.
- const isLitDiff = this.element.tagName === 'GR-DIFF-SECTION';
- if (isLitDiff) {
- await (this.element as LitElement).updateComplete;
- await untilRendered(this.element.firstElementChild as HTMLElement);
- } else {
- await untilRendered(this.element);
- }
+ await (this.element as LitElement).updateComplete;
+ await untilRendered(this.element.firstElementChild as HTMLElement);
}
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index 1c5d594..3d54690 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -413,11 +413,16 @@
.image-diff .right.lineNumButton {
border-left: 1px solid var(--border-color);
}
- .image-diff label,
- .binary-diff label {
+ .image-diff label {
font-family: var(--font-family);
font-style: italic;
}
+ tbody.binary-diff td {
+ font-family: var(--font-family);
+ font-style: italic;
+ text-align: center;
+ padding: var(--spacing-s) 0;
+ }
.diff-row {
outline: none;
user-select: none;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index 227ac2d..f0826ad 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -3142,18 +3142,25 @@
element,
/* HTML */ `
<div class="diffContainer sideBySide">
+ <gr-diff-section class="left-FILE right-FILE"> </gr-diff-section>
+ <gr-diff-row class="left-FILE right-FILE"> </gr-diff-row>
<table class="selected-right" id="diffTable">
<colgroup>
<col class="blame gr-diff" />
- <col class="gr-diff" width="48" />
- <col class="gr-diff" width="48" />
- <col class="gr-diff" />
+ <col class="gr-diff left" width="48" />
+ <col class="gr-diff left sign" />
+ <col class="gr-diff left" />
+ <col class="gr-diff right" width="48" />
+ <col class="gr-diff right sign" />
+ <col class="gr-diff right" />
</colgroup>
<tbody class="binary-diff gr-diff"></tbody>
- <tbody class="binary-diff gr-diff">
+ <tbody class="both gr-diff section">
<tr
- aria-labelledby="left-button-FILE right-button-FILE right-content-FILE"
- class="both diff-row gr-diff unified"
+ aria-labelledby="left-button-FILE left-content-FILE right-button-FILE right-content-FILE"
+ class="diff-row gr-diff side-by-side"
+ left-type="both"
+ right-type="both"
tabindex="-1"
>
<td class="blame gr-diff" data-line-number="FILE"></td>
@@ -3168,6 +3175,14 @@
File
</button>
</td>
+ <td class="gr-diff left no-intraline-info sign"></td>
+ <td
+ class="both content file gr-diff left no-intraline-info"
+ >
+ <div class="thread-group" data-side="left">
+ <slot name="left-FILE"> </slot>
+ </div>
+ </td>
<td class="gr-diff lineNum right" data-value="FILE">
<button
aria-label="Add file comment"
@@ -3179,19 +3194,23 @@
File
</button>
</td>
+ <td class="gr-diff no-intraline-info right sign"></td>
<td
class="both content file gr-diff no-intraline-info right"
>
- <div>
- <span> Difference in binary files </span>
- </div>
<div class="thread-group" data-side="right">
<slot name="right-FILE"> </slot>
- <slot name="left-FILE"> </slot>
</div>
</td>
</tr>
</tbody>
+ <tbody class="binary-diff gr-diff">
+ <tr class="gr-diff">
+ <td class="gr-diff" colspan="5">
+ <span> Difference in binary files </span>
+ </td>
+ </tr>
+ </tbody>
</table>
</div>
`
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 6d111aa..f5d7fef 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -281,6 +281,12 @@
viewModelState?.patchNum || latestPatchN
);
+ /** The user can enter edit mode without an `EDIT` patchset existing yet. */
+ public readonly editMode$ = select(
+ combineLatest([this.viewModel.edit$, this.patchNum$]),
+ ([edit, patchNum]) => !!edit || patchNum === EDIT
+ );
+
/**
* Emits the base patchset number. This is identical to the
* `viewModel.basePatchNum$`, but has some special logic for merges.
diff --git a/polygerrit-ui/app/models/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
index 168e0f4..4c0df60 100644
--- a/polygerrit-ui/app/models/config/config-model.ts
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -34,6 +34,11 @@
configState => configState.serverConfig
);
+ public download$ = select(
+ this.serverConfig$,
+ serverConfig => serverConfig?.download
+ );
+
public mergeabilityComputationBehavior$ = select(
this.serverConfig$,
serverConfig => serverConfig?.change?.mergeability_computation_behavior
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 973960e..7488e79 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -19,5 +19,4 @@
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
SUGGEST_EDIT = 'UiFeature__suggest_edit',
- COMMENTS_CHIPS_IN_FILE_LIST = 'UiFeature__comments_chips_in_file_list',
}
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 49259b4..9b08a6e 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -34,6 +34,7 @@
appStarted(): void;
onVisibilityChange(): void;
+ onFocusChange(): void;
beforeLocationChanged(): void;
locationChanged(page: string): void;
dashboardDisplayed(): void;
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 55bb64c..b8758e2 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -185,6 +185,12 @@
document.addEventListener('visibilitychange', () => {
reportingService.onVisibilityChange();
});
+ window.addEventListener('blur', () => {
+ reportingService.onFocusChange();
+ });
+ window.addEventListener('focus', () => {
+ reportingService.onFocusChange();
+ });
}
export function initClickReporter(reportingService: ReportingService) {
@@ -203,6 +209,62 @@
});
}
+/**
+ * Reports generic user interaction every x seconds to detect, if the user is
+ * present and is using the application somehow. If you just look at
+ * `document.visibilityState`, then the user may have left the browser open
+ * without locking the screen. So it helps to know whether some interaction is
+ * actually happening.
+ */
+export class InteractionReporter implements Finalizable {
+ /** Accumulates event names until the next round of interaction reporting. */
+ private interactionEvents = new Set<string>();
+
+ /** Allows clearing the interval timer. Mostly useful for tests. */
+ private intervalId?: number;
+
+ constructor(
+ private readonly reportingService: ReportingService,
+ private readonly reportingIntervalMs = 10 * 1000
+ ) {
+ const events = ['mousemove', 'scroll', 'wheel', 'keydown', 'pointerdown'];
+ for (const eventName of events) {
+ document.addEventListener(eventName, () =>
+ this.interactionEvents.add(eventName)
+ );
+ }
+
+ this.intervalId = window.setInterval(
+ () => this.report(),
+ this.reportingIntervalMs
+ );
+ }
+
+ finalize() {
+ window.clearInterval(this.intervalId);
+ }
+
+ private report() {
+ const active = this.interactionEvents.size > 0;
+ if (active) {
+ this.reportingService.reportInteraction(Interaction.USER_ACTIVE, {
+ events: [...this.interactionEvents],
+ });
+ } else if (document.visibilityState === 'visible') {
+ this.reportingService.reportInteraction(Interaction.USER_PASSIVE, {});
+ }
+ this.interactionEvents.clear();
+ }
+}
+
+let interactionReporter: InteractionReporter;
+
+export function initInteractionReporter(reportingService: ReportingService) {
+ if (!interactionReporter) {
+ interactionReporter = new InteractionReporter(reportingService);
+ }
+}
+
export function initWebVitals(reportingService: ReportingService) {
function reportWebVitalMetric(name: Timing, metric: Metric) {
let score = metric.value;
@@ -470,6 +532,20 @@
this._reportNavResTimes();
}
+ onFocusChange() {
+ this.reporter(
+ LIFECYCLE.TYPE,
+ LIFECYCLE.CATEGORY.VISIBILITY,
+ LifeCycle.FOCUS,
+ undefined,
+ {
+ isVisible: document.visibilityState === 'visible',
+ hasFocus: document.hasFocus(),
+ },
+ false
+ );
+ }
+
onVisibilityChange() {
this.hiddenDurationTimer.onVisibilityChange();
let eventName;
@@ -486,6 +562,8 @@
undefined,
{
hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
+ isVisible: document.visibilityState === 'visible',
+ hasFocus: document.hasFocus(),
},
false
);
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
index d4efbcc..fd1b88c 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -44,6 +44,9 @@
onVisibilityChange: () => {
log('onVisibilityChange');
},
+ onFocusChange: () => {
+ log('onFocusChange');
+ },
pluginLoaded: () => {},
pluginsLoaded: () => {},
pluginsFailed: () => {},
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
index 9c5e20d..2d3dfe2 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
@@ -8,11 +8,14 @@
GrReporting,
DEFAULT_STARTUP_TIMERS,
initErrorReporter,
+ InteractionReporter,
} from './gr-reporting_impl';
import {getAppContext} from '../app-context';
import {Deduping} from '../../api/reporting';
-import {SinonFakeTimers} from 'sinon';
+import {SinonFakeTimers, SinonStub} from 'sinon';
import {assert} from '@open-wc/testing';
+import {grReportingMock} from './gr-reporting_mock';
+import {Interaction} from '../../constants/reporting';
suite('gr-reporting tests', () => {
// We have to type as any because we access
@@ -563,3 +566,62 @@
});
});
});
+
+suite('InteractionReporter', () => {
+ let interactionReporter: InteractionReporter;
+ let clock: SinonFakeTimers;
+ let stub: SinonStub;
+ let activeCalls: number[] = [];
+ let passiveCalls: number[] = [];
+
+ setup(() => {
+ clock = sinon.useFakeTimers(0);
+ activeCalls = [];
+ passiveCalls = [];
+ const reporting = grReportingMock;
+ stub = sinon
+ .stub(reporting, 'reportInteraction')
+ .callsFake((interaction: string | Interaction) => {
+ if (interaction === Interaction.USER_ACTIVE) {
+ activeCalls.push(clock.now);
+ }
+ if (interaction === Interaction.USER_PASSIVE) {
+ passiveCalls.push(clock.now);
+ }
+ });
+ interactionReporter = new InteractionReporter(reporting, 1000);
+ });
+
+ teardown(() => {
+ clock.restore();
+ interactionReporter.finalize();
+ });
+
+ test('interaction example', () => {
+ clock.tick(500);
+ clock.tick(1000);
+ document.dispatchEvent(new MouseEvent('mousemove'));
+ clock.tick(1000);
+ document.dispatchEvent(new MouseEvent('mousemove'));
+ document.dispatchEvent(new MouseEvent('scroll'));
+ document.dispatchEvent(new MouseEvent('wheel'));
+ clock.tick(1000);
+ clock.tick(1000);
+ clock.tick(1000);
+ document.dispatchEvent(new MouseEvent('mousemove'));
+ clock.tick(1000);
+ document.dispatchEvent(new MouseEvent('mousemove'));
+ clock.tick(1000);
+
+ assert.sameOrderedMembers(activeCalls, [2000, 3000, 6000, 7000]);
+ assert.sameOrderedMembers(passiveCalls, [1000, 4000, 5000]);
+
+ assert.isUndefined(stub.getCall(0).args[1].events);
+ assert.sameMembers(stub.getCall(1).args[1].events, ['mousemove']);
+ assert.sameMembers(stub.getCall(2).args[1].events, [
+ 'mousemove',
+ 'scroll',
+ 'wheel',
+ ]);
+ });
+});
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 4490afa..7de5e7e 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -16,10 +16,8 @@
import {ParsedChangeInfo} from '../types/types';
import {getUserId, isServiceUser} from './account-util';
-// This can be wrong! See WARNING above
interface ChangeStatusesOptions {
- mergeable: boolean; // This can be wrong! See WARNING above
- submitEnabled: boolean; // This can be wrong! See WARNING above
+ mergeable: boolean;
/** Is there a reverting change and if so, what status has it? */
revertingChangeStatus?: ChangeStatus;
}
@@ -190,11 +188,9 @@
return states;
}
- // If no missing requirements, either active or ready to submit.
- if (change.submittable && options.submitEnabled) {
+ if (change.submittable) {
states.push(ChangeStates.READY_TO_SUBMIT);
} else {
- // Otherwise it is active.
states.push(ChangeStates.ACTIVE);
}
return states;
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
index f768145..1782d80 100644
--- a/polygerrit-ui/app/utils/change-util_test.ts
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -66,29 +66,24 @@
assert.deepEqual(statuses, []);
change.submittable = false;
- statuses = changeStatuses(change, {mergeable: true, submitEnabled: false});
+ statuses = changeStatuses(change, {mergeable: true});
assert.deepEqual(statuses, [ChangeStates.ACTIVE]);
- // With no missing labels but no submitEnabled option.
change.submittable = true;
- statuses = changeStatuses(change, {mergeable: true, submitEnabled: false});
- assert.deepEqual(statuses, [ChangeStates.ACTIVE]);
-
- // Without missing labels and enabled submit
- statuses = changeStatuses(change, {mergeable: true, submitEnabled: true});
+ statuses = changeStatuses(change, {mergeable: true});
assert.deepEqual(statuses, [ChangeStates.READY_TO_SUBMIT]);
change.mergeable = false;
change.submittable = true;
- statuses = changeStatuses(change, {mergeable: false, submitEnabled: false});
+ statuses = changeStatuses(change, {mergeable: false});
assert.deepEqual(statuses, [ChangeStates.MERGE_CONFLICT]);
change.mergeable = true;
- statuses = changeStatuses(change, {mergeable: true, submitEnabled: true});
+ statuses = changeStatuses(change, {mergeable: true});
assert.deepEqual(statuses, [ChangeStates.READY_TO_SUBMIT]);
change.submittable = true;
- statuses = changeStatuses(change, {mergeable: false, submitEnabled: false});
+ statuses = changeStatuses(change, {mergeable: false});
assert.deepEqual(statuses, [ChangeStates.MERGE_CONFLICT]);
});
@@ -141,7 +136,6 @@
changeStatuses(change, {
revertingChangeStatus: ChangeStatus.NEW,
mergeable: true,
- submitEnabled: true,
}),
[ChangeStates.MERGED, ChangeStates.REVERT_CREATED]
);
@@ -149,7 +143,6 @@
changeStatuses(change, {
revertingChangeStatus: ChangeStatus.MERGED,
mergeable: true,
- submitEnabled: true,
}),
[ChangeStates.MERGED, ChangeStates.REVERT_SUBMITTED]
);
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 4646bf6..ee1a44c 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -476,13 +476,17 @@
return comment.message?.includes(USER_SUGGESTION_START_PATTERN) ?? false;
}
+export function getUserSuggestionFromString(content: string) {
+ const start =
+ content.indexOf(USER_SUGGESTION_START_PATTERN) +
+ USER_SUGGESTION_START_PATTERN.length;
+ const end = content.indexOf('\n```', start);
+ return content.substring(start, end);
+}
+
export function getUserSuggestion(comment: Comment) {
if (!comment.message) return;
- const start =
- comment.message.indexOf(USER_SUGGESTION_START_PATTERN) +
- USER_SUGGESTION_START_PATTERN.length;
- const end = comment.message.indexOf('\n```', start);
- return comment.message.substring(start, end);
+ return getUserSuggestionFromString(comment.message);
}
export function getContentInCommentRange(
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 4fa0e63..6ceaa4f 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -149,3 +149,9 @@
export function generateAbsoluteUrl(url: string) {
return new URL(url, window.location.href).toString();
}
+
+export function sameOrigin(href: string) {
+ if (!href) return false;
+ const url = new URL(href, window.location.origin);
+ return url.origin === window.location.origin;
+}
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index c82cd70..e2ff837 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -14,6 +14,7 @@
toSearchParams,
getPatchRangeExpression,
PatchRangeParams,
+ sameOrigin,
} from './url-util';
import {assert} from '@open-wc/testing';
@@ -69,6 +70,12 @@
});
});
+ test('sameOrigin', () => {
+ assert.isTrue(sameOrigin('/asdf'));
+ assert.isTrue(sameOrigin(window.location.origin + '/asdf'));
+ assert.isFalse(sameOrigin('http://www.goole.com/asdf'));
+ });
+
test('toPathname', () => {
assert.equal(toPathname('asdf'), 'asdf');
assert.equal(toPathname('asdf?qwer=zxcv'), 'asdf');
diff --git a/proto/cache.proto b/proto/cache.proto
index 7063ee5..79bfa9e 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -78,7 +78,7 @@
// Instead, we just take the tedious yet simple approach of having a "has_foo"
// field for each nullable field "foo", indicating whether or not foo is null.
//
-// Next ID: 28
+// Next ID: 29
message ChangeNotesStateProto {
// Effectively required, even though the corresponding ChangeNotesState field
// is optional, since the field is only absent when NoteDb is disabled, in
@@ -142,6 +142,8 @@
repeated string hashtag = 5;
+ map<string, string> custom_keyed_values = 28;
+
repeated devtools.gerritcodereview.PatchSet patch_set = 6;
repeated devtools.gerritcodereview.PatchSetApproval approval = 7;
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 7f26ef3..a03ccf0 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -171,24 +171,24 @@
sha1 = GUAVA_TESTLIB_BIN_SHA1,
)
- GUICE_VERS = "5.0.1"
+ GUICE_VERS = "5.1.0"
maven_jar(
name = "guice-library",
artifact = "com.google.inject:guice:" + GUICE_VERS,
- sha1 = "0dae7556b441cada2b4f0a2314eb68e1ff423429",
+ sha1 = "da25056c694c54ba16e78e4fc35f17fc60f0d1b4",
)
maven_jar(
name = "guice-assistedinject",
artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
- sha1 = "62e02f2aceb7d90ba354584dacc018c1e94ff01c",
+ sha1 = "58a8956f00d6939978d7da735f393d7af7db5c02",
)
maven_jar(
name = "guice-servlet",
artifact = "com.google.inject.extensions:guice-servlet:" + GUICE_VERS,
- sha1 = "f527009d51f172a2e6937bfb55fcb827e2e2386b",
+ sha1 = "cb89ddec4246a469698a3461e69de1f245016c5d",
)
# Keep this version of Soy synchronized with the version used in Gitiles.
@@ -243,36 +243,36 @@
sha1 = "64cba89cf87c1d84cb8c81d06f0b9c482f10b4dc",
)
- LUCENE_VERS = "7.7.3"
+ LUCENE_VERS = "8.11.2"
maven_jar(
name = "lucene-core",
artifact = "org.apache.lucene:lucene-core:" + LUCENE_VERS,
- sha1 = "5faa5ae56f7599019fce6184accc6c968b7519e7",
+ sha1 = "57438c3f31e0e440de149294890eee88e030ea6d",
)
maven_jar(
name = "lucene-analyzers-common",
artifact = "org.apache.lucene:lucene-analyzers-common:" + LUCENE_VERS,
- sha1 = "0a76cbf5e21bbbb0c2d6288b042450236248214e",
+ sha1 = "07a74c5c2dd082b08c644a9016bc6ff66c8f27cc",
)
maven_jar(
name = "backward-codecs",
artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS,
- sha1 = "40207d0dd023a0e2868a23dd87d72f1a3cdbb893",
+ sha1 = "a5d0f0db405d607cc13265819b8d2ef0c81c0819",
)
maven_jar(
name = "lucene-misc",
artifact = "org.apache.lucene:lucene-misc:" + LUCENE_VERS,
- sha1 = "3aca078edf983059722fe61a81b7b7bd5ecdb222",
+ sha1 = "9c7204f923465a96a20ac9e49cdca0cfcde64851",
)
maven_jar(
name = "lucene-queryparser",
artifact = "org.apache.lucene:lucene-queryparser:" + LUCENE_VERS,
- sha1 = "685fc6166d29eb3e3441ae066873bb442aa02df1",
+ sha1 = "1886e3a27a8d4a73eb8fad54ea93a160b099bc60",
)
# JGit's transitive dependencies