Merge "Create class to diff ChangeInfos for the REST API"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 91fe24b..23720460 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5189,6 +5189,13 @@
+
By default 50.
+[[suggest.skipServiceUsers]]suggest.skipServiceUsers::
++
+If link:access-control.html#service_users[service users] should be skipped when
+suggesting reviewers.
++
+By default true.
+
[[tracing]]
=== Section tracing
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 9d29980..8197550 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -41,14 +41,8 @@
[[java]]
=== Java
-==== MacOS
-
-On MacOS, ensure that "Java for MacOS X 10.5 Update 4" (or higher) is installed
-and that `JAVA_HOME` is set to the
-link:install.html#Requirements[required Java version].
-
-Java installations can typically be found in
-"/System/Library/Frameworks/JavaVM.framework/Versions".
+Ensure that the link:install.html#Requirements[required Java version]
+is installed and that `JAVA_HOME` is set to it.
To check the installed version of Java, open a terminal window and run:
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
index fec9c97..a28e230 100644
--- a/Documentation/dev-rest-api.txt
+++ b/Documentation/dev-rest-api.txt
@@ -50,6 +50,11 @@
curl -X PUT --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json; charset=UTF-8" http://localhost:8080/a/projects/myproject/description
----
+[[pretty-json]]
+=== Pretty JSON
+
+By default any JSON in responses is compacted. To get pretty-printed JSON add `pp=1` to the request.
+
=== Authentication
To test APIs that require authentication, the username and password must be specified on
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 94a576c..6e1a9bd 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -10,39 +10,6 @@
+
Gerrit is not yet compatible with Java 13 or newer at this time.
-[[cryptography]]
-== Configure Java for Strong Cryptography
-
-Support for extra strength cryptographic ciphers: _AES128CTR_, _AES256CTR_,
-_ARCFOUR256_, and _ARCFOUR128_ can be enabled by downloading the _Java
-Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files_
-from Oracle and installing them into your JRE.
-
-[NOTE]
-Installing JCE extensions is optional and export restrictions may apply.
-
-. Download the unlimited strength JCE policy files.
-+
-- link:http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html[JDK7 JCE policy files,role=external,window=_blank]
-- link:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html[JDK8 JCE policy files,role=external,window=_blank]
-. Uncompress and extract the downloaded file.
-+
-The downloaded file contains the following files:
-+
-[cols="2"]
-|===
-|README.txt
-|Information about JCE and installation guide
-
-|local_policy.jar
-|Unlimited strength local policy file
-
-|US_export_policy.jar
-|Unlimited strength US export policy file
-|===
-. Install the unlimited strength policy JAR files by following instructions
-found in `README.txt`.
-
[[download]]
== Download Gerrit
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 48e729f..7ef9473 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -247,6 +247,38 @@
----
+[[DefinitelyTyped]]
+DefinitelyTyped
+
+* @types/resize-observer-browser
+
+[[DefinitelyTyped_license]]
+----
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+----
+
+
[[Polymer-2014]]
Polymer-2014
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index b7cdf8a..5f0fb65 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3204,6 +3204,38 @@
----
+[[DefinitelyTyped]]
+DefinitelyTyped
+
+* @types/resize-observer-browser
+
+[[DefinitelyTyped_license]]
+----
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+----
+
+
[[Polymer-2014]]
Polymer-2014
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 3a5fb96..f66d430 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6718,6 +6718,10 @@
A list of link:#context-line[ContextLine] containing the lines of the source
file where the comment was written. Available only if the "enable-context"
parameter (see link:#list-change-comments[List Change Comments]) is set.
+|`source_content_type` |optional|
+Mime type of the file where the comment is written. Available only if the
+"enable-context" parameter (see link:#list-change-comments[List Change Comments])
+is set.
|===========================
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 9764c8a..a8d9b3d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1839,6 +1839,8 @@
Whether to enable the web UI for editing GPG keys.
|`report_bug_url` |optional|
link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
+|`instance_id` |optional|
+link:config-gerrit.html#gerrit.instanceId[Short identifier for this Gerrit installation].
|=================================
[[index-config-info]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index e02dc21..52c282e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -190,6 +190,13 @@
+
Changes occurring in projects starting with 'PREFIX'.
+[[parentof]]
+parentof:'ID'::
+Changes which are parent to the change specified by 'ID'. Change 'ID' can be
+specified as a legacy numerical 'ID' such as 15183, or a Change-Id that can be
+picked from the commit message. This operator will return immediate parents
+and will not return grand parents or higher level ancestors of the given change.
+
[[parentproject]]
parentproject:'PROJECT'::
+
diff --git a/java/com/google/gerrit/common/data/PatchScript.java b/java/com/google/gerrit/common/data/PatchScript.java
index e3c0ba6..1ba5592 100644
--- a/java/com/google/gerrit/common/data/PatchScript.java
+++ b/java/com/google/gerrit/common/data/PatchScript.java
@@ -34,7 +34,17 @@
public enum FileMode {
FILE,
SYMLINK,
- GITLINK
+ GITLINK;
+
+ public static FileMode fromJgitFileMode(org.eclipse.jgit.lib.FileMode jgitFileMode) {
+ PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
+ if (jgitFileMode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
+ fileMode = FileMode.SYMLINK;
+ } else if (jgitFileMode == org.eclipse.jgit.lib.FileMode.GITLINK) {
+ fileMode = FileMode.GITLINK;
+ }
+ return fileMode;
+ }
}
public static class PatchScriptFileInfo {
diff --git a/java/com/google/gerrit/entities/CachedProjectConfig.java b/java/com/google/gerrit/entities/CachedProjectConfig.java
index 0b755b7..2a94bc8 100644
--- a/java/com/google/gerrit/entities/CachedProjectConfig.java
+++ b/java/com/google/gerrit/entities/CachedProjectConfig.java
@@ -14,19 +14,17 @@
package com.google.gerrit.entities;
-import static com.google.common.base.Preconditions.checkState;
-
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -37,6 +35,8 @@
*/
@AutoValue
public abstract class CachedProjectConfig {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public abstract Project getProject();
public abstract ImmutableMap<AccountGroup.UUID, GroupReference> getGroups();
@@ -126,34 +126,10 @@
public abstract ImmutableMap<String, String> getPluginConfigs();
- /**
- * Returns the {@link Config} that got parsed from the specified {@code fileName} on {@code
- * refs/meta/config}. The returned instance is a defensive copy of the cached value.
- *
- * @param fileName the name of the file. Must end in {@code .config}.
- * @return an {@link Optional} of the {@link Config}. {@link Optional#empty()} if the file was not
- * found or could not be parsed. {@link com.google.gerrit.server.project.ProjectConfig} will
- * surface validation errors in case of a parsing issue.
- */
- public Optional<Config> getProjectLevelConfig(String fileName) {
- checkState(fileName.endsWith(".config"), "file name must end in .config");
- if (getProjectLevelConfigs().containsKey(fileName)) {
- Config config = new Config();
- try {
- config.fromText(getProjectLevelConfigs().get(fileName));
- } catch (ConfigInvalidException e) {
- // This is OK to propagate as IllegalStateException because it's a programmer error.
- // The config was converted to a String using Config#toText. So #fromText must not
- // throw a ConfigInvalidException
- throw new IllegalStateException("invalid config for " + fileName, e);
- }
- return Optional.of(config);
- }
- return Optional.empty();
- }
-
public abstract ImmutableMap<String, String> getProjectLevelConfigs();
+ public abstract ImmutableMap<String, ImmutableConfig> getParsedProjectLevelConfigs();
+
public static Builder builder() {
return new AutoValue_CachedProjectConfig.Builder();
}
@@ -235,8 +211,15 @@
abstract ImmutableMap.Builder<String, String> projectLevelConfigsBuilder();
+ abstract ImmutableMap.Builder<String, ImmutableConfig> parsedProjectLevelConfigsBuilder();
+
public Builder addProjectLevelConfig(String configFileName, String config) {
projectLevelConfigsBuilder().put(configFileName, config);
+ try {
+ parsedProjectLevelConfigsBuilder().put(configFileName, ImmutableConfig.parse(config));
+ } catch (ConfigInvalidException e) {
+ logger.atInfo().withCause(e).log("Config for " + configFileName + " not parsable");
+ }
return this;
}
diff --git a/java/com/google/gerrit/entities/CommentContext.java b/java/com/google/gerrit/entities/CommentContext.java
index c8c8a76..68d779c 100644
--- a/java/com/google/gerrit/entities/CommentContext.java
+++ b/java/com/google/gerrit/entities/CommentContext.java
@@ -20,15 +20,24 @@
/** An entity class representing all context lines of a comment. */
@AutoValue
public abstract class CommentContext {
- private static final CommentContext EMPTY = new AutoValue_CommentContext(ImmutableMap.of());
+ private static final CommentContext EMPTY = new AutoValue_CommentContext(ImmutableMap.of(), "");
- public static CommentContext create(ImmutableMap<Integer, String> lines) {
- return new AutoValue_CommentContext(lines);
+ public static CommentContext create(ImmutableMap<Integer, String> lines, String contentType) {
+ return new AutoValue_CommentContext(lines, contentType);
}
/** Map of {line number, line text} of the context lines of a comment */
public abstract ImmutableMap<Integer, String> lines();
+ /**
+ * Content type of the source file. Useful for syntax highlighting.
+ *
+ * @return text/x-gerrit-commit-message if the file is a commit message.
+ * <p>text/x-gerrit-merge-list if the file is a merge list.
+ * <p>The content/mime type, e.g. text/x-c++src otherwise.
+ */
+ public abstract String contentType();
+
public static CommentContext empty() {
return EMPTY;
}
diff --git a/java/com/google/gerrit/entities/ImmutableConfig.java b/java/com/google/gerrit/entities/ImmutableConfig.java
new file mode 100644
index 0000000..a5efc14
--- /dev/null
+++ b/java/com/google/gerrit/entities/ImmutableConfig.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Immutable parsed representation of a {@link org.eclipse.jgit.lib.Config} that can be cached.
+ * Supports only a limited set of operations.
+ */
+public class ImmutableConfig {
+ public static final ImmutableConfig EMPTY = new ImmutableConfig("", new Config());
+
+ private final String stringCfg;
+ private final Config cfg;
+
+ private ImmutableConfig(String stringCfg, Config cfg) {
+ this.stringCfg = stringCfg;
+ this.cfg = cfg;
+ }
+
+ public static ImmutableConfig parse(String stringCfg) throws ConfigInvalidException {
+ Config cfg = new Config();
+ cfg.fromText(stringCfg);
+ return new ImmutableConfig(stringCfg, cfg);
+ }
+
+ /** Returns a mutable copy of this config. */
+ public Config mutableCopy() {
+ Config cfg = new Config();
+ try {
+ cfg.fromText(this.cfg.toText());
+ } catch (ConfigInvalidException e) {
+ // Can't happen as we used JGit to format that config.
+ throw new IllegalStateException(e);
+ }
+ return cfg;
+ }
+
+ /** @see Config#getSections() */
+ public Set<String> getSections() {
+ return cfg.getSections();
+ }
+
+ /** @see Config#getNames(String) */
+ public Set<String> getNames(String section) {
+ return cfg.getNames(section);
+ }
+
+ /** @see Config#getNames(String, String) */
+ public Set<String> getNames(String section, String subsection) {
+ return cfg.getNames(section, subsection);
+ }
+
+ /** @see Config#getStringList(String, String, String) */
+ public String[] getStringList(String section, String subsection, String name) {
+ return cfg.getStringList(section, subsection, name);
+ }
+
+ /** @see Config#getSubsections(String) */
+ public Set<String> getSubsections(String section) {
+ return cfg.getSubsections(section);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ImmutableConfig)) {
+ return false;
+ }
+ return ((ImmutableConfig) o).stringCfg.equals(stringCfg);
+ }
+
+ @Override
+ public int hashCode() {
+ return stringCfg.hashCode();
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/CommentInfo.java b/java/com/google/gerrit/extensions/common/CommentInfo.java
index fcce2b3..35587a0 100644
--- a/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -30,6 +30,9 @@
*/
public List<ContextLineInfo> contextLines;
+ /** Mime type of the underlying source file. Only available if context lines are requested. */
+ public String sourceContentType;
+
@Override
public boolean equals(Object o) {
if (super.equals(o)) {
diff --git a/java/com/google/gerrit/extensions/common/GerritInfo.java b/java/com/google/gerrit/extensions/common/GerritInfo.java
index 2ae6703..3265a00 100644
--- a/java/com/google/gerrit/extensions/common/GerritInfo.java
+++ b/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -23,4 +23,5 @@
public Boolean editGpgKeys;
public String reportBugUrl;
public String primaryWeblinkName;
+ public String instanceId;
}
diff --git a/java/com/google/gerrit/server/ApprovalInference.java b/java/com/google/gerrit/server/ApprovalInference.java
index 572ae7a..d77427a 100644
--- a/java/com/google/gerrit/server/ApprovalInference.java
+++ b/java/com/google/gerrit/server/ApprovalInference.java
@@ -25,6 +25,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
@@ -109,10 +110,10 @@
PatchSetApproval psa,
PatchSet.Id psId,
ChangeKind kind,
- PatchList patchList) {
+ LabelType type,
+ @Nullable PatchList patchList) {
int n = psa.key().patchSetId().get();
checkArgument(n != psId.get());
- LabelType type = project.getLabelTypes().byLabel(psa.labelId());
if (type == null) {
logger.atFine().log(
@@ -367,12 +368,18 @@
logger.atFine().log(
"change kind for patch set %d of change %d against prior patch set %s is %s",
ps.id().get(), ps.id().changeId().get(), priorPatchSet.getValue().id().changeId(), kind);
- PatchList patchList = getPatchList(project, ps, priorPatchSet);
+ PatchList patchList = null;
+ LabelTypes labelTypes = project.getLabelTypes();
for (PatchSetApproval psa : priorApprovals) {
if (resultByUser.contains(psa.label(), psa.accountId())) {
continue;
}
- if (!canCopy(project, psa, ps.id(), kind, patchList)) {
+ LabelType type = labelTypes.byLabel(psa.labelId());
+ // Only compute patchList if there is a relevant label, since this is expensive.
+ if (patchList == null && type != null && type.isCopyAllScoresIfListOfFilesDidNotChange()) {
+ patchList = getPatchList(project, ps, priorPatchSet);
+ }
+ if (!canCopy(project, psa, ps.id(), kind, type, patchList)) {
continue;
}
resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(ps.id()));
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index a3136d4a..761b57d 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -32,7 +32,6 @@
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
@@ -47,20 +46,17 @@
private final ChangeData.Factory changeDataFactory;
private final ApprovalsUtil approvalsUtil;
private final AccountLoader.Factory accountLoaderFactory;
- private final SubmitRuleEvaluator submitRuleEvaluator;
@Inject
ReviewerJson(
PermissionBackend permissionBackend,
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
- AccountLoader.Factory accountLoaderFactory,
- SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
+ AccountLoader.Factory accountLoaderFactory) {
this.permissionBackend = permissionBackend;
this.changeDataFactory = changeDataFactory;
this.approvalsUtil = approvalsUtil;
this.accountLoaderFactory = accountLoaderFactory;
- submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
}
public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
@@ -123,7 +119,7 @@
if (ps != null) {
PermissionBackend.ForChange perm = permissionBackend.absentUser(reviewerAccountId).change(cd);
- for (SubmitRecord rec : submitRuleEvaluator.evaluate(cd)) {
+ for (SubmitRecord rec : cd.submitRecords(SubmitRuleOptions.defaults())) {
if (rec.labels == null) {
continue;
}
diff --git a/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java b/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
index 3d75349..f44b075 100644
--- a/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
+++ b/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
@@ -68,7 +68,7 @@
@Override
protected void configure() {
persist(CACHE_NAME, CommentContextKey.class, CommentContext.class)
- .version(2)
+ .version(3)
.diskLimit(1 << 30) // limit the total cache size to 1 GB
.maximumWeight(1 << 23) // Limit the size of the in-memory cache to 8 MB
.weigher(CommentContextWeigher.class)
@@ -149,6 +149,7 @@
@Override
public byte[] serialize(CommentContext commentContext) {
AllCommentContextProto.Builder allBuilder = AllCommentContextProto.newBuilder();
+ allBuilder.setContentType(commentContext.contentType());
commentContext
.lines()
@@ -165,9 +166,10 @@
@Override
public CommentContext deserialize(byte[] in) {
ImmutableMap.Builder<Integer, String> contextLinesMap = ImmutableMap.builder();
- Protos.parseUnchecked(AllCommentContextProto.parser(), in).getContextList().stream()
+ AllCommentContextProto proto = Protos.parseUnchecked(AllCommentContextProto.parser(), in);
+ proto.getContextList().stream()
.forEach(c -> contextLinesMap.put(c.getLineNumber(), c.getContextLine()));
- return CommentContext.create(contextLinesMap.build());
+ return CommentContext.create(contextLinesMap.build(), proto.getContentType());
}
}
diff --git a/java/com/google/gerrit/server/comment/CommentContextLoader.java b/java/com/google/gerrit/server/comment/CommentContextLoader.java
index c93f4b1..4a6c956 100644
--- a/java/com/google/gerrit/server/comment/CommentContextLoader.java
+++ b/java/com/google/gerrit/server/comment/CommentContextLoader.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.stream.Collectors.groupingBy;
import com.google.auto.value.AutoValue;
@@ -23,15 +24,22 @@
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.CommentContext;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ContextLineInfo;
+import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.patch.ComparisonType;
import com.google.gerrit.server.patch.Text;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import eu.medsea.mimeutil.MimeType;
+import eu.medsea.mimeutil.MimeUtil2;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
@@ -52,17 +60,25 @@
public class CommentContextLoader {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final FileTypeRegistry registry;
private final GitRepositoryManager repoManager;
private final Project.NameKey project;
+ private final ProjectState projectState;
public interface Factory {
CommentContextLoader create(Project.NameKey project);
}
@Inject
- CommentContextLoader(GitRepositoryManager repoManager, @Assisted Project.NameKey project) {
+ CommentContextLoader(
+ FileTypeRegistry registry,
+ GitRepositoryManager repoManager,
+ ProjectCache projectCache,
+ @Assisted Project.NameKey project) {
+ this.registry = registry;
this.repoManager = repoManager;
this.project = project;
+ projectState = projectCache.get(project).orElseThrow(illegalState(project));
}
/**
@@ -122,7 +138,8 @@
ObjectReader reader, RevCommit commit, Range commentRange, int contextPadding)
throws IOException {
Text text = Text.forCommit(reader, commit);
- return createContext(text, commentRange, contextPadding);
+ return createContext(
+ text, commentRange, contextPadding, FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE);
}
private CommentContext getContextForMergeList(
@@ -130,7 +147,8 @@
throws IOException {
ComparisonType cmp = ComparisonType.againstParent(1);
Text text = Text.forMergeList(cmp, reader, commit);
- return createContext(text, commentRange, contextPadding);
+ return createContext(
+ text, commentRange, contextPadding, FileContentUtil.TEXT_X_GERRIT_MERGE_LIST);
}
private CommentContext getContextForFilePath(
@@ -151,11 +169,23 @@
}
ObjectId id = tw.getObjectId(0);
Text src = new Text(repo.open(id, Constants.OBJ_BLOB));
- return createContext(src, commentRange, contextPadding);
+ String contentType = getContentType(tw, filePath, src);
+ return createContext(src, commentRange, contextPadding, contentType);
}
}
- private static CommentContext createContext(Text src, Range commentRange, int contextPadding) {
+ private String getContentType(TreeWalk tw, String filePath, Text src) {
+ PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(tw.getFileMode(0));
+ String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString();
+ if (src.size() > 0 && PatchScript.FileMode.SYMLINK != fileMode) {
+ MimeType registryMimeType = registry.getMimeType(filePath, src.getContent());
+ mimeType = registryMimeType.toString();
+ }
+ return FileContentUtil.resolveContentType(projectState, filePath, fileMode, mimeType);
+ }
+
+ private static CommentContext createContext(
+ Text src, Range commentRange, int contextPadding, String contentType) {
if (commentRange.start() < 1 || commentRange.end() - 1 > src.size()) {
// TODO(ghareeb): We should throw an exception in this case. See
// https://bugs.chromium.org/p/gerrit/issues/detail?id=14102 which is an example where the
@@ -168,7 +198,7 @@
for (int i = commentRange.start(); i < commentRange.end(); i++) {
context.put(i, src.getString(i - 1));
}
- return CommentContext.create(context.build());
+ return CommentContext.create(context.build(), contentType);
}
/**
diff --git a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
index d507531..6e640f3 100644
--- a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
@@ -34,6 +35,8 @@
* issues. Note that autogenerated change messages are not subject to validation.
*/
public class CommentCumulativeSizeValidator implements CommentValidator {
+ public static final int DEFAULT_CUMULATIVE_COMMENT_SIZE_LIMIT = 3 << 20;
+
private final int maxCumulativeSize;
private final ChangeNotes.Factory notesFactory;
@@ -41,7 +44,9 @@
CommentCumulativeSizeValidator(
@GerritServerConfig Config serverConfig, ChangeNotes.Factory notesFactory) {
this.notesFactory = notesFactory;
- maxCumulativeSize = serverConfig.getInt("change", "cumulativeCommentSizeLimit", 3 << 20);
+ maxCumulativeSize =
+ serverConfig.getInt(
+ "change", "cumulativeCommentSizeLimit", DEFAULT_CUMULATIVE_COMMENT_SIZE_LIMIT);
}
@Override
@@ -55,7 +60,13 @@
notes.getRobotComments().values().stream())
.mapToInt(Comment::getApproximateSize)
.sum()
- + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
+ + notes.getChangeMessages().stream()
+ // Auto-generated change messages are not counted for the limit. This method is not
+ // called when those change messages are created, but we should also skip them when
+ // counting the size for unrelated messages.
+ .filter(cm -> !ChangeMessagesUtil.isAutogenerated(cm.getTag()))
+ .mapToInt(cm -> cm.getMessage().length())
+ .sum();
int newCumulativeSize =
comments.stream().mapToInt(CommentForValidation::getApproximateSize).sum();
ImmutableList.Builder<CommentValidationFailure> failures = ImmutableList.builder();
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index fe915c5..ac37411 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -109,6 +109,7 @@
ActionType.GIT_UPDATE,
"createAutoMerge",
() -> createAutoMergeCommit(repo, rw, ins, merge, mergeStrategy))
+ .defaultTimeoutMultiplier(2)
.call();
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
diff --git a/java/com/google/gerrit/server/patch/ComparisonType.java b/java/com/google/gerrit/server/patch/ComparisonType.java
index 260c507..eca2658 100644
--- a/java/com/google/gerrit/server/patch/ComparisonType.java
+++ b/java/com/google/gerrit/server/patch/ComparisonType.java
@@ -16,34 +16,40 @@
import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
-import static java.util.Objects.requireNonNull;
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.server.cache.proto.Cache.FileDiffOutputProto;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.Optional;
-public class ComparisonType {
+/** Relation between the old and new commits used in the diff. */
+@AutoValue
+public abstract class ComparisonType {
- /** 1-based parent */
- private final Integer parentNum;
+ /**
+ * 1-based parent. Available if the old commit is the parent of the new commit and old commit is
+ * not the auto-merge.
+ */
+ abstract Optional<Integer> parentNum();
- private final boolean autoMerge;
+ abstract boolean autoMerge();
public static ComparisonType againstOtherPatchSet() {
- return new ComparisonType(null, false);
+ return new AutoValue_ComparisonType(Optional.empty(), false);
}
public static ComparisonType againstParent(int parentNum) {
- return new ComparisonType(parentNum, false);
+ return new AutoValue_ComparisonType(Optional.of(parentNum), false);
}
public static ComparisonType againstAutoMerge() {
- return new ComparisonType(null, true);
+ return new AutoValue_ComparisonType(Optional.empty(), true);
}
- private ComparisonType(Integer parentNum, boolean autoMerge) {
- this.parentNum = parentNum;
- this.autoMerge = autoMerge;
+ private static ComparisonType create(Optional<Integer> parent, boolean automerge) {
+ return new AutoValue_ComparisonType(parent, automerge);
}
public boolean isAgainstParentOrAutoMerge() {
@@ -51,27 +57,43 @@
}
public boolean isAgainstParent() {
- return parentNum != null;
+ return parentNum().isPresent();
}
public boolean isAgainstAutoMerge() {
- return autoMerge;
+ return autoMerge();
}
- public int getParentNum() {
- requireNonNull(parentNum);
- return parentNum;
+ public Optional<Integer> getParentNum() {
+ return parentNum();
}
void writeTo(OutputStream out) throws IOException {
- writeVarInt32(out, parentNum != null ? parentNum : 0);
- writeVarInt32(out, autoMerge ? 1 : 0);
+ writeVarInt32(out, isAgainstParent() ? parentNum().get() : 0);
+ writeVarInt32(out, autoMerge() ? 1 : 0);
}
static ComparisonType readFrom(InputStream in) throws IOException {
int p = readVarInt32(in);
- Integer parentNum = p > 0 ? p : null;
+ Optional<Integer> parentNum = p > 0 ? Optional.of(p) : Optional.empty();
boolean autoMerge = readVarInt32(in) != 0;
- return new ComparisonType(parentNum, autoMerge);
+ return create(parentNum, autoMerge);
+ }
+
+ public FileDiffOutputProto.ComparisonType toProto() {
+ FileDiffOutputProto.ComparisonType.Builder builder =
+ FileDiffOutputProto.ComparisonType.newBuilder().setAutoMerge(autoMerge());
+ if (parentNum().isPresent()) {
+ builder.setParentNum(parentNum().get());
+ }
+ return builder.build();
+ }
+
+ public static ComparisonType fromProto(FileDiffOutputProto.ComparisonType proto) {
+ Optional<Integer> parentNum = Optional.empty();
+ if (proto.hasField(FileDiffOutputProto.ComparisonType.getDescriptor().findFieldByNumber(1))) {
+ parentNum = Optional.of(proto.getParentNum());
+ }
+ return create(parentNum, proto.getAutoMerge());
}
}
diff --git a/java/com/google/gerrit/server/patch/DiffOperations.java b/java/com/google/gerrit/server/patch/DiffOperations.java
index 8b90531..93aefff 100644
--- a/java/com/google/gerrit/server/patch/DiffOperations.java
+++ b/java/com/google/gerrit/server/patch/DiffOperations.java
@@ -74,8 +74,9 @@
/**
* Returns the diff for a single file between a patchset commit against its parent or the
* auto-merge commit. For deleted files, the {@code fileName} parameter should contain the old
- * name of the file. This method will return {@link FileDiffOutput#empty(String)} if the requested
- * file identified by {@code fileName} has unchanged content or does not exist at both commits.
+ * name of the file. This method will return {@link FileDiffOutput#empty(String, ObjectId,
+ * ObjectId)} if the requested file identified by {@code fileName} has unchanged content or does
+ * not exist at both commits.
*
* @param project a project name representing a git repository.
* @param newCommit 20 bytes SHA-1 of the new commit used in the diff.
@@ -98,8 +99,8 @@
/**
* Returns the diff for a single file between two patchset commits. For deleted files, the {@code
* fileName} parameter should contain the old name of the file. This method will return {@link
- * FileDiffOutput#empty(String)} if the requested file identified by {@code fileName} has
- * unchanged content or does not exist at both commits.
+ * FileDiffOutput#empty(String, ObjectId, ObjectId)} if the requested file identified by {@code
+ * fileName} has unchanged content or does not exist at both commits.
*
* @param project a project name representing a git repository.
* @param oldCommit 20 bytes SHA-1 of the old commit used in the diff.
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 16bd135..efb64bc 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -125,7 +125,9 @@
FileDiffCacheKey key =
createFileDiffCacheKey(project, diffParams.baseCommit(), newCommit, fileName, whitespace);
Map<String, FileDiffOutput> result = getModifiedFilesForKeys(ImmutableList.of(key));
- return result.containsKey(fileName) ? result.get(fileName) : FileDiffOutput.empty(fileName);
+ return result.containsKey(fileName)
+ ? result.get(fileName)
+ : FileDiffOutput.empty(fileName, key.oldCommit(), key.newCommit());
} catch (IOException e) {
throw new DiffNotAvailableException(
"Failed to evaluate the parent/base commit for commit " + newCommit, e);
@@ -143,7 +145,9 @@
FileDiffCacheKey key =
createFileDiffCacheKey(project, oldCommit, newCommit, fileName, whitespace);
Map<String, FileDiffOutput> result = getModifiedFilesForKeys(ImmutableList.of(key));
- return result.containsKey(fileName) ? result.get(fileName) : FileDiffOutput.empty(fileName);
+ return result.containsKey(fileName)
+ ? result.get(fileName)
+ : FileDiffOutput.empty(fileName, oldCommit, newCommit);
}
private Map<String, FileDiffOutput> getModifiedFiles(
diff --git a/java/com/google/gerrit/server/patch/MagicFile.java b/java/com/google/gerrit/server/patch/MagicFile.java
index aa6b11f..e42dd8c 100644
--- a/java/com/google/gerrit/server/patch/MagicFile.java
+++ b/java/com/google/gerrit/server/patch/MagicFile.java
@@ -93,7 +93,7 @@
}
default:
int uninterestingParent =
- comparisonType.isAgainstParent() ? comparisonType.getParentNum() : 1;
+ comparisonType.isAgainstParent() ? comparisonType.getParentNum().get() : 1;
b.append("Merge List:\n\n");
for (RevCommit commit : MergeListBuilder.build(rw, c, uninterestingParent)) {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index c6f7acf..ae30113 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -405,12 +405,7 @@
if (mode == FileMode.MISSING) {
displayMethod = DisplayMethod.NONE;
}
- PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
- if (mode == FileMode.SYMLINK) {
- fileMode = PatchScript.FileMode.SYMLINK;
- } else if (mode == FileMode.GITLINK) {
- fileMode = PatchScript.FileMode.GITLINK;
- }
+ PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(mode);
return new PatchSide(
treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode);
}
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index 9f58aaf..18d532b 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -30,7 +30,9 @@
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.prettify.common.SparseFileContent.Accessor;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gerrit.server.git.validators.CommentCumulativeSizeValidator;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -41,6 +43,7 @@
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.lib.Config;
/**
* This class is used on submit to compute the diff between the latest approved patch-set, and the
@@ -58,15 +61,22 @@
private final ProjectCache projectCache;
private final PatchScriptFactory.Factory patchScriptFactoryFactory;
private final PatchListCache patchListCache;
+ private final int maxCumulativeSize;
@Inject
SubmitWithStickyApprovalDiff(
ProjectCache projectCache,
PatchScriptFactory.Factory patchScriptFactoryFactory,
- PatchListCache patchListCache) {
+ PatchListCache patchListCache,
+ @GerritServerConfig Config serverConfig) {
this.projectCache = projectCache;
this.patchScriptFactoryFactory = patchScriptFactoryFactory;
this.patchListCache = patchListCache;
+ maxCumulativeSize =
+ serverConfig.getInt(
+ "change",
+ "cumulativeCommentSizeLimit",
+ CommentCumulativeSizeValidator.DEFAULT_CUMULATIVE_COMMENT_SIZE_LIMIT);
}
public String apply(ChangeNotes notes, CurrentUser currentUser)
@@ -88,8 +98,10 @@
// If the latest approved patchset is the current patchset, no need to return anything.
return "";
}
- String diff =
- String.format("\n\n%d is the latest approved patch-set.\n", latestApprovedPatchsetId.get());
+ StringBuilder diff =
+ new StringBuilder(
+ String.format(
+ "\n\n%d is the latest approved patch-set.\n", latestApprovedPatchsetId.get()));
PatchList patchList =
getPatchList(
notes.getProjectName(),
@@ -103,19 +115,29 @@
.collect(Collectors.toList());
if (patchListEntryList.isEmpty()) {
- diff +=
- "No files were changed between the latest approved patch-set and the submitted one.\n";
- return diff;
+ diff.append(
+ "No files were changed between the latest approved patch-set and the submitted one.\n");
+ return diff.toString();
}
- diff += "The change was submitted with unreviewed changes in the following files:\n\n";
+ diff.append("The change was submitted with unreviewed changes in the following files:\n\n");
for (PatchListEntry patchListEntry : patchListEntryList) {
- diff +=
+ diff.append(
getDiffForFile(
- notes, currentPatchset.id(), latestApprovedPatchsetId, patchListEntry, currentUser);
+ notes, currentPatchset.id(), latestApprovedPatchsetId, patchListEntry, currentUser));
}
- return diff;
+ if (diff.length() > maxCumulativeSize) {
+ // The diff length is not counted as part of the limit (for technical reasons, since we'd
+ // have to call CommentCumulativeSizeValidator), but it's best not to post an extra large
+ // change message here.
+ return String.format(
+ "\n\n%d is the latest approved patch-set.\nThe change was submitted "
+ + "with many unreviewed changes (the diff is too large to show). Please review the "
+ + "diff.",
+ latestApprovedPatchsetId.get());
+ }
+ return diff.toString();
}
private String getDiffForFile(
@@ -126,12 +148,13 @@
CurrentUser currentUser)
throws AuthException, InvalidChangeOperationException, IOException,
PermissionBackendException {
- String diff =
- String.format(
- "The name of the file: %s\nInsertions: %d, Deletions: %d.\n\n",
- patchListEntry.getNewName(),
- patchListEntry.getInsertions(),
- patchListEntry.getDeletions());
+ StringBuilder diff =
+ new StringBuilder(
+ String.format(
+ "The name of the file: %s\nInsertions: %d, Deletions: %d.\n\n",
+ patchListEntry.getNewName(),
+ patchListEntry.getInsertions(),
+ patchListEntry.getDeletions()));
DiffPreferencesInfo diffPreferencesInfo = createDefaultDiffPreferencesInfo();
PatchScriptFactory patchScriptFactory =
patchScriptFactoryFactory.create(
@@ -145,66 +168,66 @@
try {
patchScript = patchScriptFactory.call();
} catch (LargeObjectException exception) {
- diff += "The file content is too large for showing the full diff. \n\n";
- return diff;
+ diff.append("The file content is too large for showing the full diff. \n\n");
+ return diff.toString();
}
if (patchScript.getChangeType() == ChangeType.RENAMED) {
- diff +=
+ diff.append(
String.format(
"The file %s was renamed to %s\n",
- patchListEntry.getOldName(), patchListEntry.getNewName());
+ patchListEntry.getOldName(), patchListEntry.getNewName()));
}
SparseFileContent.Accessor fileA = patchScript.getA().createAccessor();
SparseFileContent.Accessor fileB = patchScript.getB().createAccessor();
boolean editsExist = false;
if (patchScript.getEdits().stream().anyMatch(e -> e.getType() != Edit.Type.EMPTY)) {
- diff += "```\n";
+ diff.append("```\n");
editsExist = true;
}
for (Edit edit : patchScript.getEdits()) {
- diff += getDiffForEdit(fileA, fileB, edit);
+ diff.append(getDiffForEdit(fileA, fileB, edit));
}
if (editsExist) {
- diff += "```\n";
+ diff.append("```\n");
}
- return diff;
+ return diff.toString();
}
private String getDiffForEdit(Accessor fileA, Accessor fileB, Edit edit) {
- String diff = "";
+ StringBuilder diff = new StringBuilder();
Edit.Type type = edit.getType();
switch (type) {
case INSERT:
- diff += String.format("@@ +%d:%d @@\n", edit.getBeginB(), edit.getEndB());
- diff += getModifiedLines(fileB, edit.getBeginB(), edit.getEndB(), '+');
- diff += "\n";
+ diff.append(String.format("@@ +%d:%d @@\n", edit.getBeginB(), edit.getEndB()));
+ diff.append(getModifiedLines(fileB, edit.getBeginB(), edit.getEndB(), '+'));
+ diff.append("\n");
break;
case DELETE:
- diff += String.format("@@ -%d:%d @@\n", edit.getBeginA(), edit.getEndA());
- diff += getModifiedLines(fileA, edit.getBeginA(), edit.getEndA(), '-');
- diff += "\n";
+ diff.append(String.format("@@ -%d:%d @@\n", edit.getBeginA(), edit.getEndA()));
+ diff.append(getModifiedLines(fileA, edit.getBeginA(), edit.getEndA(), '-'));
+ diff.append("\n");
break;
case REPLACE:
- diff +=
+ diff.append(
String.format(
"@@ -%d:%d, +%d:%d @@\n",
- edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB());
- diff += getModifiedLines(fileA, edit.getBeginA(), edit.getEndA(), '-');
- diff += getModifiedLines(fileB, edit.getBeginB(), edit.getEndB(), '+');
- diff += "\n";
+ edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB()));
+ diff.append(getModifiedLines(fileA, edit.getBeginA(), edit.getEndA(), '-'));
+ diff.append(getModifiedLines(fileB, edit.getBeginB(), edit.getEndB(), '+'));
+ diff.append("\n");
break;
case EMPTY:
// do nothing since there is no change here.
}
- return diff;
+ return diff.toString();
}
private String getModifiedLines(Accessor file, int begin, int end, char modificationType) {
- String diff = "";
+ StringBuilder diff = new StringBuilder();
for (int i = begin; i < end; i++) {
- diff += String.format("%c %s\n", modificationType, file.get(i));
+ diff.append(String.format("%c %s\n", modificationType, file.get(i)));
}
- return diff;
+ return diff.toString();
}
private DiffPreferencesInfo createDefaultDiffPreferencesInfo() {
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index 63f311b..1bb407d 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -93,6 +93,7 @@
persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
.maximumWeight(10 << 20)
.weigher(FileDiffWeigher.class)
+ .version(2)
.keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
.valueSerializer(FileDiffOutput.Serializer.INSTANCE)
.loader(FileDiffLoader.class);
@@ -219,19 +220,16 @@
RawTextComparator cmp = comparatorFor(key.whitespace());
ComparisonType comparisonType =
getComparisonType(rw, reader, key.oldCommit(), key.newCommit());
- RevCommit aCommit =
- comparisonType.isAgainstParentOrAutoMerge()
- ? null
- : DiffUtil.getRevCommit(rw, key.oldCommit());
+ RevCommit aCommit = DiffUtil.getRevCommit(rw, key.oldCommit());
RevCommit bCommit = DiffUtil.getRevCommit(rw, key.newCommit());
return magicPath == MagicPath.COMMIT
- ? createCommitEntry(reader, aCommit, bCommit, cmp, key.diffAlgorithm())
+ ? createCommitEntry(reader, aCommit, bCommit, comparisonType, cmp, key.diffAlgorithm())
: createMergeListEntry(
reader, aCommit, bCommit, comparisonType, cmp, key.diffAlgorithm());
} catch (IOException e) {
logger.atWarning().log("Failed to compute commit entry for key %s", key);
}
- return FileDiffOutput.empty(key.newFilePath());
+ return FileDiffOutput.empty(key.newFilePath(), key.oldCommit(), key.newCommit());
}
private static RawTextComparator comparatorFor(Whitespace ws) {
@@ -255,13 +253,24 @@
ObjectReader reader,
RevCommit oldCommit,
RevCommit newCommit,
+ ComparisonType comparisonType,
RawTextComparator rawTextComparator,
GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
throws IOException {
- Text aText = oldCommit != null ? Text.forCommit(reader, oldCommit) : Text.EMPTY;
+ Text aText =
+ comparisonType.isAgainstParentOrAutoMerge()
+ ? Text.EMPTY
+ : Text.forCommit(reader, oldCommit);
Text bText = Text.forCommit(reader, newCommit);
return createMagicFileDiffOutput(
- rawTextComparator, oldCommit, aText, bText, Patch.COMMIT_MSG, diffAlgorithm);
+ oldCommit,
+ newCommit,
+ comparisonType,
+ rawTextComparator,
+ aText,
+ bText,
+ Patch.COMMIT_MSG,
+ diffAlgorithm);
}
private FileDiffOutput createMergeListEntry(
@@ -273,20 +282,31 @@
GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
throws IOException {
Text aText =
- oldCommit != null ? Text.forMergeList(comparisonType, reader, oldCommit) : Text.EMPTY;
+ comparisonType.isAgainstParentOrAutoMerge()
+ ? Text.EMPTY
+ : Text.forMergeList(comparisonType, reader, oldCommit);
Text bText = Text.forMergeList(comparisonType, reader, newCommit);
return createMagicFileDiffOutput(
- rawTextComparator, oldCommit, aText, bText, Patch.MERGE_LIST, diffAlgorithm);
+ oldCommit,
+ newCommit,
+ comparisonType,
+ rawTextComparator,
+ aText,
+ bText,
+ Patch.MERGE_LIST,
+ diffAlgorithm);
}
private static FileDiffOutput createMagicFileDiffOutput(
+ ObjectId oldCommit,
+ ObjectId newCommit,
+ ComparisonType comparisonType,
RawTextComparator rawTextComparator,
- RevCommit aCommit,
Text aText,
Text bText,
String fileName,
GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm) {
- byte[] rawHdr = getRawHeader(aCommit != null, fileName);
+ byte[] rawHdr = getRawHeader(!comparisonType.isAgainstParentOrAutoMerge(), fileName);
byte[] aContent = aText.getContent();
byte[] bContent = bText.getContent();
long size = bContent.length;
@@ -298,6 +318,9 @@
FileHeader fileHeader = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
Patch.ChangeType changeType = FileHeaderUtil.getChangeType(fileHeader);
return FileDiffOutput.builder()
+ .oldCommitId(oldCommit)
+ .newCommitId(newCommit)
+ .comparisonType(comparisonType)
.oldPath(FileHeaderUtil.getOldPath(fileHeader))
.newPath(FileHeaderUtil.getNewPath(fileHeader))
.changeType(changeType)
@@ -370,8 +393,13 @@
mainGitDiff.newPath().get())
: 0;
+ ObjectId oldCommit = augmentedKey.key().oldCommit();
+ ObjectId newCommit = augmentedKey.key().newCommit();
FileDiffOutput fileDiff =
FileDiffOutput.builder()
+ .oldCommitId(oldCommit)
+ .newCommitId(newCommit)
+ .comparisonType(getComparisonType(rw, reader, oldCommit, newCommit))
.changeType(mainGitDiff.changeType())
.patchType(mainGitDiff.patchType())
.oldPath(mainGitDiff.oldPath())
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
index 3348033..e7f47ef 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
@@ -24,16 +24,28 @@
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.proto.Cache.FileDiffOutputProto;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
+import com.google.gerrit.server.patch.ComparisonType;
import com.google.protobuf.Descriptors.FieldDescriptor;
import java.io.Serializable;
import java.util.Optional;
import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
/** File diff for a single file path. Produced as output of the {@link FileDiffCache}. */
@AutoValue
public abstract class FileDiffOutput implements Serializable {
private static final long serialVersionUID = 1L;
+ /** The 20 bytes SHA-1 object ID of the old git commit used in the diff. */
+ public abstract ObjectId oldCommitId();
+
+ /** The 20 bytes SHA-1 object ID of the new git commit used in the diff. */
+ public abstract ObjectId newCommitId();
+
+ /** Comparison type of old and new commits: against another patchset, parent or auto-merge. */
+ public abstract ComparisonType comparisonType();
+
/**
* The file path at the old commit. Returns an empty Optional if {@link #changeType()} is equal to
* {@link ChangeType#ADDED}.
@@ -95,8 +107,11 @@
}
/** Returns an entity representing an unchanged file between two commits. */
- public static FileDiffOutput empty(String filePath) {
+ public static FileDiffOutput empty(String filePath, ObjectId oldCommitId, ObjectId newCommitId) {
return builder()
+ .oldCommitId(oldCommitId)
+ .newCommitId(newCommitId)
+ .comparisonType(ComparisonType.againstOtherPatchSet()) // not important
.oldPath(Optional.empty())
.newPath(Optional.of(filePath))
.changeType(ChangeType.MODIFIED)
@@ -124,6 +139,8 @@
if (newPath().isPresent()) {
result += stringSize(newPath().get());
}
+ result += 20 + 20; // old and new commit IDs
+ result += 4; // comparison type
result += 4; // changeType
if (patchType().isPresent()) {
result += 4;
@@ -140,6 +157,12 @@
@AutoValue.Builder
public abstract static class Builder {
+ public abstract Builder oldCommitId(ObjectId value);
+
+ public abstract Builder newCommitId(ObjectId value);
+
+ public abstract Builder comparisonType(ComparisonType value);
+
public abstract Builder oldPath(Optional<String> value);
public abstract Builder newPath(Optional<String> value);
@@ -173,8 +196,12 @@
@Override
public byte[] serialize(FileDiffOutput fileDiff) {
+ ObjectIdConverter idConverter = ObjectIdConverter.create();
FileDiffOutputProto.Builder builder =
FileDiffOutputProto.newBuilder()
+ .setOldCommit(idConverter.toByteString(fileDiff.oldCommitId().toObjectId()))
+ .setNewCommit(idConverter.toByteString(fileDiff.newCommitId().toObjectId()))
+ .setComparisonType(fileDiff.comparisonType().toProto())
.setSize(fileDiff.size())
.setSizeDelta(fileDiff.sizeDelta())
.addAllHeaderLines(fileDiff.headerLines())
@@ -212,9 +239,13 @@
@Override
public FileDiffOutput deserialize(byte[] in) {
+ ObjectIdConverter idConverter = ObjectIdConverter.create();
FileDiffOutputProto proto = Protos.parseUnchecked(FileDiffOutputProto.parser(), in);
FileDiffOutput.Builder builder = FileDiffOutput.builder();
builder
+ .oldCommitId(idConverter.fromByteString(proto.getOldCommit()))
+ .newCommitId(idConverter.fromByteString(proto.getNewCommit()))
+ .comparisonType(ComparisonType.fromProto(proto.getComparisonType()))
.size(proto.getSize())
.sizeDelta(proto.getSizeDelta())
.headerLines(proto.getHeaderLinesList().stream().collect(ImmutableList.toImmutableList()))
diff --git a/java/com/google/gerrit/server/project/ProjectLevelConfig.java b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
index 4825233..d82a318 100644
--- a/java/com/google/gerrit/server/project/ProjectLevelConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
@@ -16,14 +16,15 @@
import static java.util.stream.Collectors.toList;
-import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
+import com.google.gerrit.entities.ImmutableConfig;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -73,19 +74,17 @@
private final String fileName;
private final ProjectState project;
- private Config cfg;
+ private final ImmutableConfig immutableConfig;
- public ProjectLevelConfig(String fileName, ProjectState project, Config cfg) {
+ public ProjectLevelConfig(
+ String fileName, ProjectState project, @Nullable ImmutableConfig immutableConfig) {
this.fileName = fileName;
this.project = project;
- this.cfg = cfg;
+ this.immutableConfig = immutableConfig == null ? ImmutableConfig.EMPTY : immutableConfig;
}
public Config get() {
- if (cfg == null) {
- cfg = new Config();
- }
- return cfg;
+ return immutableConfig.mutableCopy();
}
public Config getWithInheritance() {
@@ -105,58 +104,61 @@
* @return a combined config.
*/
public Config getWithInheritance(boolean merge) {
- Config cfgWithInheritance = new Config();
- try {
- cfgWithInheritance.fromText(get().toText());
- } catch (ConfigInvalidException e) {
- // cannot happen
- }
- ProjectState parent = Iterables.getFirst(project.parents(), null);
- if (parent != null) {
- Config parentCfg = parent.getConfig(fileName).getWithInheritance();
- for (String section : parentCfg.getSections()) {
- Set<String> allNames = get().getNames(section);
- for (String name : parentCfg.getNames(section)) {
- String[] parentValues = parentCfg.getStringList(section, null, name);
- if (!allNames.contains(name)) {
- cfgWithInheritance.setStringList(section, null, name, Arrays.asList(parentValues));
- } else if (merge) {
- cfgWithInheritance.setStringList(
- section,
- null,
- name,
- Stream.concat(
- Arrays.stream(cfg.getStringList(section, null, name)),
- Arrays.stream(parentValues))
- .sorted()
- .distinct()
- .collect(toList()));
- }
- }
+ Config cfg = new Config();
+ // Traverse from All-Projects down to the current project
+ StreamSupport.stream(project.treeInOrder().spliterator(), false)
+ .forEach(
+ parent -> {
+ ImmutableConfig levelCfg = parent.getConfig(fileName).immutableConfig;
+ for (String section : levelCfg.getSections()) {
+ Set<String> allNames = cfg.getNames(section);
+ for (String name : levelCfg.getNames(section)) {
+ String[] levelValues = levelCfg.getStringList(section, null, name);
+ if (allNames.contains(name) && merge) {
+ cfg.setStringList(
+ section,
+ null,
+ name,
+ Stream.concat(
+ Arrays.stream(cfg.getStringList(section, null, name)),
+ Arrays.stream(levelValues))
+ .sorted()
+ .distinct()
+ .collect(toList()));
+ } else {
+ cfg.setStringList(section, null, name, Arrays.asList(levelValues));
+ }
+ }
- for (String subsection : parentCfg.getSubsections(section)) {
- allNames = get().getNames(section, subsection);
- for (String name : parentCfg.getNames(section, subsection)) {
- String[] parentValues = parentCfg.getStringList(section, subsection, name);
- if (!allNames.contains(name)) {
- cfgWithInheritance.setStringList(
- section, subsection, name, Arrays.asList(parentValues));
- } else if (merge) {
- cfgWithInheritance.setStringList(
- section,
- subsection,
- name,
- Streams.concat(
- Arrays.stream(cfg.getStringList(section, subsection, name)),
- Arrays.stream(parentValues))
- .sorted()
- .distinct()
- .collect(toList()));
- }
- }
- }
- }
- }
- return cfgWithInheritance;
+ for (String subsection : levelCfg.getSubsections(section)) {
+ allNames = cfg.getNames(section, subsection);
+
+ Set<String> allNamesLevelCfg = levelCfg.getNames(section, subsection);
+ if (allNamesLevelCfg.isEmpty()) {
+ // Set empty subsection.
+ cfg.setString(section, subsection, null, null);
+ } else {
+ for (String name : allNamesLevelCfg) {
+ String[] levelValues = levelCfg.getStringList(section, subsection, name);
+ if (allNames.contains(name) && merge) {
+ cfg.setStringList(
+ section,
+ subsection,
+ name,
+ Streams.concat(
+ Arrays.stream(cfg.getStringList(section, subsection, name)),
+ Arrays.stream(levelValues))
+ .sorted()
+ .distinct()
+ .collect(toList()));
+ } else {
+ cfg.setStringList(section, subsection, name, Arrays.asList(levelValues));
+ }
+ }
+ }
+ }
+ }
+ });
+ return cfg;
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 8c024ef..249eb35 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.PermissionRule.Action.ALLOW;
import static java.util.Comparator.comparing;
@@ -176,8 +177,9 @@
}
public ProjectLevelConfig getConfig(String fileName) {
- Optional<Config> rawConfig = cachedConfig.getProjectLevelConfig(fileName);
- return new ProjectLevelConfig(fileName, this, rawConfig.orElse(new Config()));
+ checkState(fileName.endsWith(".config"), "file name must end in .config. is: " + fileName);
+ return new ProjectLevelConfig(
+ fileName, this, cachedConfig.getParsedProjectLevelConfigs().get(fileName));
}
public long getMaxObjectSizeLimit() {
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index f047543..bf56000 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -872,36 +872,25 @@
}
public List<SubmitRecord> submitRecords(SubmitRuleOptions options) {
- List<SubmitRecord> records = getCachedSubmitRecord(options);
+ // If the change is not submitted yet, 'strict' and 'lenient' both have the same result. If the
+ // change is submitted, SubmitRecord requested with 'strict' will contain just a single entry
+ // that with status=CLOSED. The latter is cheap to evaluate as we don't have to run any actual
+ // evaluation.
+ List<SubmitRecord> records = submitRecords.get(options);
if (records == null) {
if (!lazyLoad) {
return Collections.emptyList();
}
records = submitRuleEvaluatorFactory.create(options).evaluate(this);
submitRecords.put(options, records);
+ if (!change().isClosed() && submitRecords.size() == 1) {
+ // Cache the SubmitRecord with allowClosed = !allowClosed as the SubmitRecord are the same.
+ submitRecords.put(options.toBuilder().allowClosed(!options.allowClosed()).build(), records);
+ }
}
return records;
}
- @Nullable
- public List<SubmitRecord> getSubmitRecords(SubmitRuleOptions options) {
- return getCachedSubmitRecord(options);
- }
-
- private List<SubmitRecord> getCachedSubmitRecord(SubmitRuleOptions options) {
- List<SubmitRecord> records = submitRecords.get(options);
- if (records != null) {
- return records;
- }
-
- if (options.allowClosed() && change != null && change.getStatus().isOpen()) {
- SubmitRuleOptions openSubmitRuleOptions = options.toBuilder().allowClosed(false).build();
- return submitRecords.get(openSubmitRuleOptions);
- }
-
- return null;
- }
-
public void setSubmitRecords(SubmitRuleOptions options, List<SubmitRecord> records) {
submitRecords.put(options, records);
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 68a90d2..4e3edcd 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -173,6 +173,7 @@
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_OWNER = "owner";
public static final String FIELD_OWNERIN = "ownerin";
+ public static final String FIELD_PARENTOF = "parentof";
public static final String FIELD_PARENTPROJECT = "parentproject";
public static final String FIELD_PATH = "path";
public static final String FIELD_PENDING_REVIEWER = "pendingreviewer";
@@ -735,6 +736,16 @@
}
@Operator
+ public Predicate<ChangeData> parentof(String value) throws QueryParseException {
+ List<ChangeData> changes = parseChangeData(value);
+ List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
+ for (ChangeData c : changes) {
+ or.add(new ParentOfPredicate(value, c, args.repoManager));
+ }
+ return Predicate.or(or);
+ }
+
+ @Operator
public Predicate<ChangeData> parentproject(String name) {
return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
}
@@ -1560,14 +1571,18 @@
}
private List<Change> parseChange(String value) throws QueryParseException {
+ return asChanges(parseChangeData(value));
+ }
+
+ private List<ChangeData> parseChangeData(String value) throws QueryParseException {
if (PAT_LEGACY_ID.matcher(value).matches()) {
Optional<Change.Id> id = Change.Id.tryParse(value);
if (!id.isPresent()) {
throw error("Invalid change id " + value);
}
- return asChanges(args.queryProvider.get().byLegacyChangeId(id.get()));
+ return args.queryProvider.get().byLegacyChangeId(id.get());
} else if (PAT_CHANGE_ID.matcher(value).matches()) {
- List<Change> changes = asChanges(args.queryProvider.get().byKeyPrefix(parseChangeId(value)));
+ List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value));
if (changes.isEmpty()) {
throw error("Change " + value + " not found");
}
diff --git a/java/com/google/gerrit/server/query/change/ParentOfPredicate.java b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
new file mode 100644
index 0000000..e48d586
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class ParentOfPredicate extends OperatorPredicate<ChangeData>
+ implements Matchable<ChangeData> {
+ protected final Set<RevCommit> parents;
+
+ public ParentOfPredicate(String value, ChangeData change, GitRepositoryManager repoManager) {
+ super(ChangeQueryBuilder.FIELD_PARENTOF, value);
+ this.parents = getParents(change, repoManager);
+ }
+
+ @Override
+ public boolean match(ChangeData changeData) {
+ return changeData.patchSets().stream().anyMatch(ps -> parents.contains(ps.commitId()));
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+
+ protected Set<RevCommit> getParents(ChangeData change, GitRepositoryManager repoManager) {
+ PatchSet ps = change.currentPatchSet();
+ try (Repository repo = repoManager.openRepository(change.project());
+ RevWalk walk = new RevWalk(repo)) {
+ RevCommit c = walk.parseCommit(ps.commitId());
+ return Sets.newHashSet(c.getParents());
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format(
+ "Loading commit %s for ps %d of change %d failed.",
+ ps.commitId(), ps.id().get(), ps.id().changeId().get()),
+ e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 77b58c6..edc8fcf 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -171,7 +171,10 @@
allComments.stream().map(this::createCommentContextKey).collect(toList());
ImmutableMap<CommentContextKey, CommentContext> allContext = commentContextCache.getAll(keys);
for (T c : allComments) {
- c.contextLines = toContextLineInfoList(allContext.get(createCommentContextKey(c)));
+ CommentContextKey contextKey = createCommentContextKey(c);
+ CommentContext commentContext = allContext.get(contextKey);
+ c.contextLines = toContextLineInfoList(commentContext);
+ c.sourceContentType = commentContext.contentType();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 562bdf8..73b38b2 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -1228,9 +1228,10 @@
}
private boolean isReviewer(ChangeContext ctx) {
- ChangeData cd = changeDataFactory.create(ctx.getNotes());
- ReviewerSet reviewers = cd.reviewers();
- return reviewers.byState(REVIEWER).contains(ctx.getAccountId());
+ return approvalsUtil
+ .getReviewers(ctx.getNotes())
+ .byState(REVIEWER)
+ .contains(ctx.getAccountId());
}
private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 2a55e41..38be27e 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -51,6 +51,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -130,6 +131,7 @@
private final IndexConfig indexConfig;
private final AccountControl.Factory accountControlFactory;
private final Provider<CurrentUser> self;
+ private final ServiceUserClassifier serviceUserClassifier;
@Inject
ReviewersUtil(
@@ -143,7 +145,8 @@
AccountIndexCollection accountIndexes,
IndexConfig indexConfig,
AccountControl.Factory accountControlFactory,
- Provider<CurrentUser> self) {
+ Provider<CurrentUser> self,
+ ServiceUserClassifier serviceUserClassifier) {
this.accountLoaderFactory = accountLoaderFactory;
this.accountQueryBuilder = accountQueryBuilder;
this.accountIndexRewriter = accountIndexRewriter;
@@ -155,6 +158,7 @@
this.indexConfig = indexConfig;
this.accountControlFactory = accountControlFactory;
this.self = self;
+ this.serviceUserClassifier = serviceUserClassifier;
}
public interface VisibilityControl {
@@ -200,13 +204,17 @@
reviewerState, changeNotes, suggestReviewers, projectState, candidateList);
logger.atFine().log("Sorted recommendations: %s", sortedRecommendations);
- // Filter accounts by visibility and enforce limit
+ // Filter accounts by visibility, skip service users and enforce limit
List<Account.Id> filteredRecommendations = new ArrayList<>();
try (Timer0.Context ctx = metrics.filterVisibility.start()) {
for (Account.Id reviewer : sortedRecommendations) {
if (filteredRecommendations.size() >= limit) {
break;
}
+ if (suggestReviewers.isSkipServiceUsers()
+ && serviceUserClassifier.isServiceUser(reviewer)) {
+ continue;
+ }
// Check if change is visible to reviewer and if the current user can see reviewer
if (visibilityControl.isVisibleTo(reviewer)
&& accountControlFactory.get().canSee(reviewer)) {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index e071c89..71ff493 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -31,6 +31,8 @@
private static final int DEFAULT_MAX_SUGGESTED = 10;
+ private static final boolean DEFAULT_SKIP_SERVICE_USERS = true;
+
protected final ReviewersUtil reviewersUtil;
private final boolean suggestAccounts;
@@ -39,6 +41,7 @@
protected int limit;
protected String query;
protected final int maxSuggestedReviewers;
+ protected boolean skipServiceUsers;
@Option(
name = "--limit",
@@ -78,6 +81,10 @@
return maxAllowedWithoutConfirmation;
}
+ public boolean isSkipServiceUsers() {
+ return skipServiceUsers;
+ }
+
@Inject
public SuggestReviewers(
AccountVisibility av, @GerritServerConfig Config cfg, ReviewersUtil reviewersUtil) {
@@ -100,6 +107,9 @@
ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
logger.atFine().log("AccountVisibility: %s", av.name());
+
+ this.skipServiceUsers =
+ cfg.getBoolean("suggest", "skipServiceUsers", DEFAULT_SKIP_SERVICE_USERS);
}
public static GerritConfigListener configListener() {
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 5459ede..0a5692e 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -304,6 +304,7 @@
info.editGpgKeys =
toBoolean(enableSignedPush && config.getBoolean("gerrit", null, "editGpgKeys", true));
info.primaryWeblinkName = config.getString("gerrit", null, "primaryWeblinkName");
+ info.instanceId = config.getString("gerrit", null, "instanceId");
return info;
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 871d8d2..f486650 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -189,7 +189,7 @@
// date by this point.
ChangeData cd = requireNonNull(changes.get(id), () -> String.format("ChangeData for %s", id));
return requireNonNull(
- cd.getSubmitRecords(submitRuleOptions(allowClosed)),
+ cd.submitRecords(submitRuleOptions(allowClosed)),
"getSubmitRecord only valid after submit rules are evalutated");
}
@@ -549,8 +549,8 @@
.listener(retryTracker)
// Up to the entire submit operation is retried, including possibly many projects.
// Multiply the timeout by the number of projects we're actually attempting to
- // submit.
- .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size())
+ // submit. Times 2 to retry more persistently, to increase success rate.
+ .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
// By default, we only retry lock failures. Here it's better to also retry unexpected
// runtime exceptions.
.retryOn(t -> t instanceof RuntimeException)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
index eba2634..5ca7310 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
@@ -21,8 +21,10 @@
import static com.google.gerrit.server.project.testing.TestLabels.value;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Change;
@@ -30,6 +32,7 @@
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.server.patch.filediff.Edit;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.inject.Inject;
@@ -144,6 +147,56 @@
}
@Test
+ @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "1k")
+ public void autoGeneratedPostSubmitDiffIsNotPartOfTheCommentSizeLimit() throws Exception {
+ Change.Id changeId =
+ changeOperations.newChange().project(project).file("file").content("content").create();
+ gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
+ String content = new String(new char[800]).replace("\0", "a");
+ changeOperations.change(changeId).newPatchset().file("file").content(content).create();
+
+ // Post a submit diff that is almost the cumulativeCommentSizeLimit
+ gApi.changes().id(changeId.get()).current().submit();
+ assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
+ .doesNotContain("many unreviewed changes");
+
+ // unrelated comment and change message posting works fine, since the post submit diff is not
+ // counted towards the cumulativeCommentSizeLimit for unrelated follow-up comments.
+ // 800 + 400 + 400 > 1k, but 400 + 400 < 1k, hence these comments are accepted (the original
+ // 800 is not counted).
+ String message = new String(new char[400]).replace("\0", "a");
+ ReviewInput reviewInput = new ReviewInput().message(message);
+ CommentInput commentInput = new CommentInput();
+ commentInput.line = 1;
+ commentInput.message = message;
+ commentInput.path = "file";
+ reviewInput.comments = ImmutableMap.of("file", ImmutableList.of(commentInput));
+
+ gApi.changes().id(changeId.get()).current().review(reviewInput);
+ }
+
+ @Test
+ @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "1k")
+ public void postSubmitDiffCannotBeTooBig() throws Exception {
+ Change.Id changeId =
+ changeOperations.newChange().project(project).file("file").content("content").create();
+ gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
+
+ String content = new String(new char[1100]).replace("\0", "a");
+
+ changeOperations.change(changeId).newPatchset().file("file").content(content).create();
+
+ // Post submit diff is over the cumulativeCommentSizeLimit, so we shorten the message.
+ gApi.changes().id(changeId.get()).current().submit();
+ assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
+ .isEqualTo(
+ "Change has been successfully merged\n\n1 is the latest approved patch-set.\nThe "
+ + "change was submitted "
+ + "with many unreviewed changes (the diff is too large to show). Please review the "
+ + "diff.");
+ }
+
+ @Test
public void diffChangeMessageOnSubmitWithStickyVote_addedFile() throws Exception {
Change.Id changeId = changeOperations.newChange().project(project).create();
gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index 64bd25c..5b18d02 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -27,7 +27,6 @@
labels = [
"docker",
"elastic",
- "exclusive",
"pgm",
"no_windows",
],
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 888878f..ffde622 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -450,6 +450,23 @@
}
@Test
+ public void suggestNoServiceAccounts() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ String changeIdReviewed = createChangeFromApi();
+ String changeId = createChangeFromApi();
+
+ String name = name("foo");
+ TestAccount foo = accountCreator.create(name);
+ reviewChange(changeIdReviewed, foo);
+
+ assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo), ImmutableList.of());
+
+ groupOperations.group(serviceUsersUUID()).forUpdate().addMember(foo.id()).update();
+
+ assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(), ImmutableList.of());
+ }
+
+ @Test
public void suggestNoExistingReviewers() throws Exception {
requestScopeOperations.setApiUser(user.id());
String changeId = createChangeFromApi();
@@ -608,6 +625,13 @@
return user(name, fullName, name);
}
+ private AccountGroup.UUID serviceUsersUUID() {
+ return groupCache
+ .get(AccountGroup.nameKey("Service Users"))
+ .orElseThrow(() -> new IllegalStateException("unable to find 'Service Users'"))
+ .getGroupUUID();
+ }
+
private void reviewChange(String changeId, TestAccount reviewer) throws RestApiException {
gApi.changes().id(changeId).addReviewer(reviewer.id().toString());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 0a84db4..4738f64 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -74,6 +74,7 @@
@GerritConfig(name = "gerrit.allProjects", value = "Root")
@GerritConfig(name = "gerrit.allUsers", value = "Users")
@GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report")
+ @GerritConfig(name = "gerrit.instanceId", value = "devops-instance")
// suggest
@GerritConfig(name = "suggest.from", value = "3")
@@ -116,6 +117,7 @@
assertThat(i.gerrit.allProjects).isEqualTo("Root");
assertThat(i.gerrit.allUsers).isEqualTo("Users");
assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
+ assertThat(i.gerrit.instanceId).isEqualTo("devops-instance");
// plugin
assertThat(i.plugin.jsResourcePaths).isEmpty();
@@ -184,6 +186,7 @@
assertThat(i.gerrit.allProjects).isEqualTo(AllProjectsNameProvider.DEFAULT);
assertThat(i.gerrit.allUsers).isEqualTo(AllUsersNameProvider.DEFAULT);
assertThat(i.gerrit.reportBugUrl).isNull();
+ assertThat(i.gerrit.instanceId).isNull();
// plugin
assertThat(i.plugin.jsResourcePaths).isEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index 5fd55ec..ffdbd8e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -22,16 +22,23 @@
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import java.util.Arrays;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
public class ProjectLevelConfigIT extends AbstractDaemonTest {
+ private static final String PLUGIN_NAME = "test-plugin";
+
@Inject private ProjectOperations projectOperations;
+ @Inject private PluginConfigFactory pluginConfigFactory;
@Before
public void setUp() throws Exception {
@@ -185,4 +192,27 @@
ProjectState state = projectCache.get(project).get();
assertThat(state.getConfig(configName).get().toText()).isEmpty();
}
+
+ @Test
+ public void emptySubSectionsCanBeRead() throws Exception {
+ updatePluginConfig(project, "[section \"subsection\"]");
+ Config cfg = pluginConfigFactory.getProjectPluginConfigWithInheritance(project, PLUGIN_NAME);
+ assertThat(cfg.getSubsections("section")).containsExactly("subsection");
+ }
+
+ private void updatePluginConfig(Project.NameKey project, String pluginConfig) throws Exception {
+ try (TestRepository<Repository> testRepo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ Ref ref = testRepo.getRepository().exactRef(RefNames.REFS_CONFIG);
+ RevCommit head = testRepo.getRevWalk().parseCommit(ref.getObjectId());
+ testRepo.update(
+ RefNames.REFS_CONFIG,
+ testRepo
+ .commit()
+ .parent(head)
+ .message("Configure plugin")
+ .add(PLUGIN_NAME + ".config", pluginConfig));
+ }
+ projectCache.evict(project);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
index 548e3fe..20b9882 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
@@ -31,6 +31,7 @@
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.ContextLineInfo;
+import com.google.gerrit.server.change.FileContentUtil;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@@ -319,6 +320,82 @@
IntStream.range(200, 301).boxed().collect(ImmutableList.toImmutableList()));
}
+ @Test
+ public void commentContextReturnsCorrectContentTypeForCommitMessage() throws Exception {
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
+ String changeId = result.getChangeId();
+ String ps1 = result.getCommit().name();
+
+ CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, Side.REVISION, 7, "comment", false);
+ CommentsUtil.addComments(gApi, changeId, ps1, comment);
+
+ List<CommentInfo> comments =
+ gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
+
+ assertThat(comments).hasSize(1);
+ assertThat(comments.get(0).path).isEqualTo(COMMIT_MSG);
+ assertThat(comments.get(0).sourceContentType)
+ .isEqualTo(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE);
+ }
+
+ @Test
+ public void commentContextReturnsCorrectContentType_Java() throws Exception {
+ String javaContent =
+ "public class Main {\n"
+ + " public static void main(String[]args){\n"
+ + " if(args==null){\n"
+ + " System.err.println(\"Something\");\n"
+ + " }\n"
+ + " }\n"
+ + " }";
+ String fileName = "src.java";
+ String changeId = createChangeWithContent(fileName, javaContent, /* line= */ 4);
+
+ List<CommentInfo> comments =
+ gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
+
+ assertThat(comments).hasSize(1);
+ assertThat(comments.get(0).path).isEqualTo(fileName);
+ assertThat(comments.get(0).contextLines)
+ .isEqualTo(createContextLines("4", " System.err.println(\"Something\");"));
+ assertThat(comments.get(0).sourceContentType).isEqualTo("text/x-java");
+ }
+
+ @Test
+ public void commentContextReturnsCorrectContentType_Cpp() throws Exception {
+ String cppContent =
+ "#include <iostream>\n"
+ + "\n"
+ + "int main() {\n"
+ + " std::cout << \"Hello World!\";\n"
+ + " return 0;\n"
+ + "}";
+ String fileName = "src.cpp";
+ String changeId = createChangeWithContent(fileName, cppContent, /* line= */ 4);
+
+ List<CommentInfo> comments =
+ gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
+
+ assertThat(comments).hasSize(1);
+ assertThat(comments.get(0).path).isEqualTo(fileName);
+ assertThat(comments.get(0).contextLines)
+ .isEqualTo(createContextLines("4", " std::cout << \"Hello World!\";"));
+ assertThat(comments.get(0).sourceContentType).isEqualTo("text/x-c++src");
+ }
+
+ private String createChangeWithContent(String fileName, String fileContent, int line)
+ throws Exception {
+ PushOneCommit.Result result =
+ createChange(testRepo, "master", SUBJECT, fileName, fileContent, "topic");
+ String changeId = result.getChangeId();
+ String ps1 = result.getCommit().name();
+
+ CommentInput comment = CommentsUtil.newComment(fileName, Side.REVISION, line, "comment", false);
+ CommentsUtil.addComments(gApi, changeId, ps1, comment);
+ return changeId;
+ }
+
private String createChangeWithComment(int startLine, int endLine) throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index be35d5a..3036811 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -55,7 +55,6 @@
ELASTICSEARCH_TAGS = [
"docker",
"elastic",
- "exclusive",
]
[junit_tests(
diff --git a/javatests/com/google/gerrit/server/cache/serialize/CommentContextSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/CommentContextSerializerTest.java
index 643c7b7..8fe7662 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/CommentContextSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/CommentContextSerializerTest.java
@@ -14,7 +14,7 @@
@Test
public void roundTripValue() {
CommentContext commentContext =
- CommentContext.create(ImmutableMap.of(1, "line_1", 2, "line_2"));
+ CommentContext.create(ImmutableMap.of(1, "line_1", 2, "line_2"), "text/x-java");
byte[] serialized = INSTANCE.serialize(commentContext);
CommentContext deserialized = INSTANCE.deserialize(serialized);
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
index 44ea55a..17fd959 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
@@ -19,10 +19,12 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.Patch.PatchType;
+import com.google.gerrit.server.patch.ComparisonType;
import com.google.gerrit.server.patch.filediff.Edit;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.patch.filediff.TaggedEdit;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
public class FileDiffOutputSerializerTest {
@@ -35,6 +37,9 @@
FileDiffOutput fileDiff =
FileDiffOutput.builder()
+ .oldCommitId(ObjectId.fromString("dd4d2a1498870ca5fe415b33f65d052d69d9eaf5"))
+ .newCommitId(ObjectId.fromString("0cfaab3f2ba76f71798da0a2651f41be8d45f842"))
+ .comparisonType(ComparisonType.againstOtherPatchSet())
.oldPath(Optional.of("old_file_path.txt"))
.newPath(Optional.empty())
.changeType(ChangeType.DELETED)
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 7efcb4b..48bd321 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -705,6 +705,24 @@
}
@Test
+ public void byParentOf() throws Exception {
+ TestRepository<Repo> repo1 = createProject("repo1");
+ RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
+ Change change1 = insert(repo1, newChangeForCommit(repo1, commit1));
+ RevCommit commit2 = repo1.parseBody(repo1.commit(commit1));
+ Change change2 = insert(repo1, newChangeForCommit(repo1, commit2));
+ RevCommit commit3 = repo1.parseBody(repo1.commit(commit1, commit2));
+ Change change3 = insert(repo1, newChangeForCommit(repo1, commit3));
+
+ assertQuery("parentof:" + change1.getId().get());
+ assertQuery("parentof:" + change1.getKey().get());
+ assertQuery("parentof:" + change2.getId().get(), change1);
+ assertQuery("parentof:" + change2.getKey().get(), change1);
+ assertQuery("parentof:" + change3.getId().get(), change2, change1);
+ assertQuery("parentof:" + change3.getKey().get(), change2, change1);
+ }
+
+ @Test
public void byParentProject() throws Exception {
TestRepository<Repo> repo1 = createProject("repo1");
TestRepository<Repo> repo2 = createProject("repo2", "repo1");
diff --git a/modules/jgit b/modules/jgit
index 4560bdf..9bfb0f3 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 4560bdf7e2e3c16a7c7bb3f2fcf067bb1eee26fb
+Subproject commit 9bfb0f3a4ec856dcbebb477a1ee8803a3c47c194
diff --git a/plugins/replication b/plugins/replication
index ab80790..14766e7 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit ab8079055a92fa4068a2982306c11425f347e12f
+Subproject commit 14766e75f91886ab48951035d59a78c8c3f07471
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index 799d1f7..4a5ef7e 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -247,7 +247,7 @@
checkName: string | undefined,
/** Identical to 'name' property of Action entity. */
actionName: string
-) => Promise<ActionResult>;
+) => Promise<ActionResult> | undefined;
export interface ActionResult {
/** An empty errorMessage means success. */
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 5305cea..f2b4c89 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -400,7 +400,7 @@
}
return '';
}
- // TODO(TS): The following condtion seems always false, because params
+ // TODO(TS): The following condition seems always false, because params
// never has detailType property. Remove it.
if (
((params as unknown) as AdminSubsectionLink).detailType &&
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
index b0065d9..9cc6357 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
@@ -192,7 +192,7 @@
};
element.section = 'refs/*';
- // Typically called on ready since elements will have properies defined
+ // Typically called on ready since elements will have properties defined
// by the parent element.
element._setupValues(element.rule);
flush();
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
index 9812933..add7ca5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
@@ -36,7 +36,7 @@
width: 10em;
}
#graphic iron-icon {
- color: #9e9e9e;
+ color: var(--gray-foreground);
height: 5em;
width: 5em;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index 9240905..a34bd63 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -58,7 +58,7 @@
import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
-const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
+const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
export interface GrDashboardView {
@@ -380,6 +380,19 @@
e.detail.change._number,
e.detail.starred
);
+ // When a change is updated the same change may appear elsewhere in the
+ // dashboard (but is not the same object), so we must update other
+ // occurrences of the same change.
+ this._results?.forEach((dashboardChange, dashboardIndex) =>
+ dashboardChange.results.forEach((change, changeIndex) => {
+ if (change.id === e.detail.change.id) {
+ this.set(
+ `_results.${dashboardIndex}.results.${changeIndex}.starred`,
+ e.detail.starred
+ );
+ }
+ })
+ );
}
_handleToggleReviewed(e: CustomEvent<ChangeListToggleReviewedDetail>) {
@@ -387,6 +400,19 @@
e.detail.change._number,
e.detail.reviewed
);
+ // When a change is updated the same change may appear elsewhere in the
+ // dashboard (but is not the same object), so we must update other
+ // occurrences of the same change.
+ this._results?.forEach((dashboardChange, dashboardIndex) =>
+ dashboardChange.results.forEach((change, changeIndex) => {
+ if (change.id === e.detail.change.id) {
+ this.set(
+ `_results.${dashboardIndex}.results.${changeIndex}.reviewed`,
+ e.detail.reviewed
+ );
+ }
+ })
+ );
}
/**
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index 47f885b..a5de72b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -332,6 +332,56 @@
});
});
+ test('toggling star will update change everywhere', () => {
+ // It is important that the same change is represented by multiple objects
+ // and all are updated.
+ const change = {id: '5', starred: false};
+ const sameChange = {id: '5', starred: false};
+ const differentChange = {id: '4', starred: false};
+ element._results = [
+ {query: 'has:draft', results: [change]},
+ {query: 'is:open', results: [sameChange, differentChange]},
+ ];
+
+ element._handleToggleStar(
+ new CustomEvent('toggle-star', {
+ detail: {
+ change,
+ starred: true,
+ },
+ })
+ );
+
+ assert.isTrue(change.starred);
+ assert.isTrue(sameChange.starred);
+ assert.isFalse(differentChange.starred);
+ });
+
+ test('toggling reviewed will update change everywhere', () => {
+ // It is important that the same change is represented by multiple objects
+ // and all are updated.
+ const change = {id: '5', reviewed: false};
+ const sameChange = {id: '5', reviewed: false};
+ const differentChange = {id: '4', reviewed: false};
+ element._results = [
+ {query: 'has:draft', results: [change]},
+ {query: 'is:open', results: [sameChange, differentChange]},
+ ];
+
+ element._handleToggleReviewed(
+ new CustomEvent('toggle-reviewed', {
+ detail: {
+ change,
+ reviewed: true,
+ },
+ })
+ );
+
+ assert.isTrue(change.reviewed);
+ assert.isTrue(sameChange.reviewed);
+ assert.isFalse(differentChange.reviewed);
+ });
+
test('_showNewUserHelp', () => {
element._loading = false;
element._showNewUserHelp = false;
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 c3858d8..87b09c7 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
@@ -2068,10 +2068,10 @@
*
*/
_waitForChangeReachable(changeNum: NumericChangeId) {
- let attempsRemaining = AWAIT_CHANGE_ATTEMPTS;
+ let attemptsRemaining = AWAIT_CHANGE_ATTEMPTS;
return new Promise(resolve => {
const check = () => {
- attempsRemaining--;
+ attemptsRemaining--;
// Pass a no-op error handler to avoid the "not found" error toast.
this.restApiService
.getChange(changeNum, () => {})
@@ -2082,7 +2082,7 @@
return;
}
- if (attempsRemaining) {
+ if (attemptsRemaining) {
this.async(check, AWAIT_CHANGE_TIMEOUT_MS);
} else {
resolve(false);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index 6c9a27d..fbb70b7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -2110,7 +2110,7 @@
element = basicFixture.instantiate();
// getChangeRevisionActions is not called without
- // set the following properies
+ // set the following properties
element.change = {};
element.changeNum = '42';
element.latestPatchNum = '2';
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 ef4d323..68e2368 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
@@ -210,6 +210,8 @@
restApiService = appContext.restApiService;
+ private readonly reporting = appContext.reportingService;
+
/** @override */
ready() {
super.ready();
@@ -567,6 +569,10 @@
_onShowAllClick() {
this._showAllSections = !this._showAllSections;
+ this.reporting.reportInteraction('toggle show all button', {
+ sectionName: 'metadata',
+ toState: this._showAllSections ? 'Show all' : 'Show less',
+ });
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index c5c73c5..59bf8ad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -78,7 +78,7 @@
}
.icon.help,
.icon.notTrusted {
- color: #ffa62f;
+ color: var(--warning-foreground);
}
.icon.invalid {
color: var(--negative-red-text-color);
@@ -87,7 +87,7 @@
color: var(--positive-green-text-color);
}
.parentList.notCurrent.nonMerge #parentNotCurrentMessage {
- --arrow-color: #ffa62f;
+ --arrow-color: var(--warning-foreground);
display: inline-block;
}
.separatedSection {
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
index c0e87f3..e3fdf7a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
@@ -37,6 +37,7 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {appContext} from '../../../services/app-context';
import {KnownExperimentId} from '../../../services/flags/flags';
+import {labelCompare} from '../../../utils/label-util';
interface ChangeRequirement extends Requirement {
satisfied: boolean;
@@ -93,6 +94,8 @@
KnownExperimentId.NEW_CHANGE_SUMMARY_UI
);
+ private readonly reporting = appContext.reportingService;
+
_computeShowWip(change: ChangeInfo) {
return change.work_in_progress;
}
@@ -136,7 +139,7 @@
const labels = labelsRecord.base || {};
const allLabels: Label[] = [];
- for (const label of Object.keys(labels).sort()) {
+ for (const label of Object.keys(labels).sort(labelCompare)) {
allLabels.push({
labelName: label,
icon: this._computeLabelIcon(labels[label]),
@@ -192,6 +195,10 @@
_handleShowHide() {
this._showOptionalLabels = !this._showOptionalLabels;
+ this.reporting.reportInteraction('toggle show all button', {
+ sectionName: 'optional labels',
+ toState: this._showOptionalLabels ? 'Show all' : 'Show less',
+ });
}
_computeSubmitRequirementEndpoint(item: ChangeRequirement | ChangeWIP) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
index e02b337..a502949 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
@@ -23,7 +23,7 @@
width: 100%;
}
.status {
- color: #ffa62f;
+ color: var(--warning-foreground);
display: inline-block;
text-align: center;
vertical-align: top;
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 4af51a9..427924e 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
@@ -22,7 +22,8 @@
import {KnownExperimentId} from '../../../services/flags/flags';
import {
allRuns$,
- aPluginHasRegistered,
+ aPluginHasRegistered$,
+ someProvidersAreLoading$,
} from '../../../services/checks/checks-model';
import {
Category,
@@ -56,7 +57,7 @@
import {notUndefined} from '../../../types/types';
import {uniqueDefinedAvatar} from '../../../utils/account-util';
import {PrimaryTab} from '../../../constants/constants';
-import {CommentTabState} from '../../../types/events';
+import {ChecksTabState, CommentTabState} from '../../../types/events';
export enum SummaryChipStyles {
INFO = 'info',
@@ -76,6 +77,8 @@
@property()
category?: CommentTabState;
+ private readonly reporting = appContext.reportingService;
+
static get styles() {
return [
sharedStyles,
@@ -110,13 +113,6 @@
.summaryChip.check iron-icon {
color: var(--gray-foreground);
}
- .summaryChip.info {
- border-color: var(--info-deemphasized-foreground;
- background-color: var(--info-deemphasized-background);
- }
- .summaryChip.info iron-icon {
- color: var(--info-deemphasized-foreground);
- }
`,
];
}
@@ -137,6 +133,9 @@
private handleClick(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
+ this.reporting.reportInteraction('comment chip click', {
+ category: this.category,
+ });
fireShowPrimaryTab(this, PrimaryTab.COMMENT_THREADS, true, {
commentTab: this.category,
});
@@ -229,19 +228,13 @@
const chipClass = `checksChip font-small ${this.icon}`;
const grIcon = `gr-icons:${this.icon}`;
return html`
- <div class="${chipClass}" role="button" @click="${this.handleClick}">
+ <div class="${chipClass}" role="button">
<iron-icon icon="${grIcon}"></iron-icon>
<div class="text">${this.text}</div>
<slot></slot>
</div>
`;
}
-
- private handleClick(e: MouseEvent) {
- e.stopPropagation();
- e.preventDefault();
- fireShowPrimaryTab(this, PrimaryTab.CHECKS);
- }
}
/** What is the maximum number of expanded checks chips? */
@@ -268,13 +261,17 @@
@property()
showChecksSummary = false;
+ @property()
+ someProvidersAreLoading = false;
+
/** Is reset when rendering beings and decreases while chips are rendered. */
private detailsQuota = DETAILS_QUOTA;
constructor() {
super();
this.subscribe('runs', allRuns$);
- this.subscribe('showChecksSummary', aPluginHasRegistered);
+ this.subscribe('showChecksSummary', aPluginHasRegistered$);
+ this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
}
static get styles() {
@@ -318,14 +315,15 @@
renderChecksZeroState() {
if (this.runs.some(isRunningOrHasCompleted)) return;
- return html`<span class="font-small zeroState">No results</span>`;
+ const msg = this.someProvidersAreLoading ? 'Loading...' : 'No results';
+ return html`<span class="font-small zeroState">${msg}</span>`;
}
renderChecksChipForCategory(category: Category) {
const icon = iconForCategory(category);
const runs = this.runs.filter(run => hasResultsOf(run, category));
const count = (run: CheckRun) => getResultsOf(run, category);
- return this.renderChecksChip(icon, runs, count);
+ return this.renderChecksChip(icon, runs, category, count);
}
renderChecksChipForStatus(
@@ -334,12 +332,13 @@
) {
const icon = iconForStatus(status);
const runs = this.runs.filter(filter);
- return this.renderChecksChip(icon, runs, () => []);
+ return this.renderChecksChip(icon, runs, status, () => []);
}
renderChecksChip(
icon: string,
runs: CheckRun[],
+ statusOrCategory: RunStatus | Category,
resultFilter: (run: CheckRun) => CheckResult[]
) {
if (runs.length === 0) {
@@ -359,9 +358,10 @@
class="${icon}"
.icon="${icon}"
.text="${text}"
+ @click="${() => this.onChipClick({checkName: run.checkName})}"
>${links.map(
link => html`
- <a href="${link.url}" target="_blank" @click="${this.onClick}"
+ <a href="${link.url}" target="_blank" @click="${this.onLinkClick}"
><iron-icon class="launch" icon="gr-icons:launch"></iron-icon
></a>
`
@@ -380,11 +380,18 @@
class="${icon}"
.icon="${icon}"
.text="${sum}"
+ @click="${() => this.onChipClick({statusOrCategory})}"
></gr-checks-chip>`;
}
- private onClick(e: MouseEvent) {
- // Prevents handleClick() from reacting to <a> link clicks.
+ private onChipClick(state: ChecksTabState) {
+ fireShowPrimaryTab(this, PrimaryTab.CHECKS, true, {
+ checksTab: state,
+ });
+ }
+
+ private onLinkClick(e: MouseEvent) {
+ // Prevents onChipClick() from reacting to <a> link clicks.
e.stopPropagation();
}
@@ -436,7 +443,6 @@
><gr-summary-chip
styleType=${SummaryChipStyles.WARNING}
category=${CommentTabState.UNRESOLVED}
- icon="message"
?hidden=${!countUnresolvedComments}
>
${unresolvedAuthors.map(
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 3d7646f..ab1c48f 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
@@ -171,7 +171,7 @@
import {fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
import {takeUntil} from 'rxjs/operators';
-import {aPluginHasRegistered} from '../../../services/checks/checks-model';
+import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
import {Subject} from 'rxjs';
import {GrRelatedChangesListExperimental} from '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
@@ -274,11 +274,13 @@
* @event show-auth-required
*/
- reporting = appContext.reportingService;
+ private readonly reporting = appContext.reportingService;
- flagsService = appContext.flagsService;
+ private readonly flagsService = appContext.flagsService;
- readonly jsAPI = appContext.jsApiService;
+ private readonly jsAPI = appContext.jsApiService;
+
+ private readonly changeService = appContext.changeService;
/**
* URL params passed from the router.
@@ -594,7 +596,7 @@
/** @override */
ready() {
super.ready();
- aPluginHasRegistered.pipe(takeUntil(this.disconnected$)).subscribe(b => {
+ aPluginHasRegistered$.pipe(takeUntil(this.disconnected$)).subscribe(b => {
this._showChecksTab = b;
});
this._isNewChangeSummaryUiEnabled = this.flagsService.isEnabled(
@@ -823,8 +825,11 @@
paperTabs.scrollIntoView();
}
if (paperTabs.selected !== activeIndex) {
+ // paperTabs.selected is undefined during rendering
+ if (paperTabs.selected !== undefined) {
+ this.reporting.reportInteraction('show-tab', {tabName});
+ }
paperTabs.selected = activeIndex;
- this.reporting.reportInteraction('show-tab', {tabName});
}
return tabName;
}
@@ -2022,6 +2027,7 @@
this._lineHeight = Number(lineHeight.slice(0, lineHeight.length - 2));
this._change = change;
+ this.changeService.updateChange(change);
if (
!this._patchRange ||
!this._patchRange.patchNum ||
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index f3fb860..08c04e8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -690,7 +690,10 @@
is="dom-if"
if="[[_isTabActive(_constants.PrimaryTab.CHECKS, _activeTabs)]]"
>
- <gr-checks-tab id="checksTab"></gr-checks-tab>
+ <gr-checks-tab
+ id="checksTab"
+ tab-state="[[_tabState.checksTab]]"
+ ></gr-checks-tab>
</template>
<template
is="dom-if"
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 99e5356..ae446cd 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
@@ -105,6 +105,7 @@
import {GerritView} from '../../../services/router/router-model';
import {ParsedChangeInfo} from '../../../types/types';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
+import {appContext} from '../../../services/app-context';
const pluginApi = _testOnly_initGerritPluginApi();
const fixture = fixtureFromElement('gr-change-view');
@@ -2766,7 +2767,7 @@
element._change = {...change};
element._patchRange = {patchNum: 4 as PatchSetNum};
element._mergeable = true;
- const showStub = sinon.stub(element.jsAPI, 'handleEvent');
+ const showStub = sinon.stub(appContext.jsApiService, 'handleEvent');
element._sendShowChangeEvent();
assert.isTrue(showStub.calledOnce);
assert.equal(showStub.lastCall.args[0], EventType.SHOW_CHANGE);
@@ -2968,11 +2969,11 @@
test("don't report changeDisplayed on reply", done => {
const changeDisplayStub = sinon.stub(
- element.reporting,
+ appContext.reportingService,
'changeDisplayed'
);
const changeFullyLoadedStub = sinon.stub(
- element.reporting,
+ appContext.reportingService,
'changeFullyLoaded'
);
element._handleReplySent();
@@ -2985,11 +2986,11 @@
test('report changeDisplayed on _paramsChanged', done => {
const changeDisplayStub = sinon.stub(
- element.reporting,
+ appContext.reportingService,
'changeDisplayed'
);
const changeFullyLoadedStub = sinon.stub(
- element.reporting,
+ appContext.reportingService,
'changeFullyLoaded'
);
element._paramsChanged({
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index 225e3e9..ebcabbf 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -38,6 +38,7 @@
import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
import {HttpMethod, ChangeStatus} from '../../../constants/constants';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {fireEvent} from '../../../utils/event-util';
const SUGGESTIONS_LIMIT = 15;
const CHANGE_SUBJECT_LIMIT = 50;
@@ -264,10 +265,12 @@
_handlecherryPickSingleChangeClicked() {
this._cherryPickType = CherryPickType.SINGLE_CHANGE;
+ fireEvent(this, 'iron-resize');
}
_handlecherryPickTopicClicked() {
this._cherryPickType = CherryPickType.TOPIC;
+ fireEvent(this, 'iron-resize');
}
@observe('changeStatus', 'commitNum', 'commitMessage')
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
index 4de395c..6c0099e 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
@@ -46,11 +46,12 @@
}
.cherryPickTopicLayout {
display: flex;
+ align-items: center;
+ margin-bottom: var(--spacing-m);
}
.cherryPickSingleChange,
.cherryPickTopic {
margin-left: var(--spacing-m);
- margin-bottom: var(--spacing-m);
}
.cherry-pick-topic-message {
margin-bottom: var(--spacing-m);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
index 99094d2..29b3752 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
@@ -25,7 +25,7 @@
margin-bottom: var(--spacing-l);
}
.warningBeforeSubmit {
- color: var(--error-text-color);
+ color: var(--warning-foreground);
vertical-align: top;
margin-right: var(--spacing-s);
}
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 a5c0624..c2947a6 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
@@ -803,7 +803,7 @@
}
/**
- * Handle all events from the file list dom-repeat so event handleers don't
+ * Handle all events from the file list dom-repeat so event handlers don't
* have to get registered for potentially very long lists.
*/
_handleFileListClick(e: MouseEvent) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index 23718fa..c57a2d5 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -154,7 +154,7 @@
padding-left: var(--spacing-s);
}
.drafts {
- color: #c62828;
+ color: var(--error-foreground);
font-weight: var(--font-weight-bold);
}
.show-hide-icon:focus {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 285b73f..80d8729 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -1244,7 +1244,7 @@
// are no deletions.
assert.equal(element._computeBarAdditionWidth(file, stats), 30);
- // If there are no insetions, there is no width.
+ // If there are no insertions, there is no width.
stats.maxInserted = 0;
assert.equal(element._computeBarAdditionWidth(file, stats), 0);
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index a966186..661cd1a 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -37,6 +37,7 @@
} from '../gr-label-score-row/gr-label-score-row';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {appContext} from '../../../services/app-context';
+import {labelCompare} from '../../../utils/label-util';
@customElement('gr-label-scores')
export class GrLabelScores extends GestureEventListeners(
@@ -147,7 +148,7 @@
if (!labelRecord?.base) return [];
const labelsObj = labelRecord.base;
return Object.keys(labelsObj)
- .sort()
+ .sort(labelCompare)
.map(key => {
return {
name: key,
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 2fe409a..f913459 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -54,7 +54,7 @@
} from '../../../utils/patch-set-util';
import {isServiceUser} from '../../../utils/account-util';
-const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?(?:P|p)atch (?:S|s)et \d+:\s*(.*)/;
+const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?[Pp]atch [Ss]et \d+:\s*(.*)/;
const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.]?$/;
const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
@@ -484,7 +484,7 @@
_handleAnchorClick(e: Event) {
e.preventDefault();
// The element which triggers _handleAnchorClick is rendered only if
- // message.id defined: the elemenet is wrapped in dom-if if="[[message.id]]"
+ // message.id defined: the element is wrapped in dom-if if="[[message.id]]"
const detail: MessageAnchorTapDetail = {
id: this.message!.id,
};
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index b93040b..c018443 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -61,7 +61,7 @@
font-weight: var(--font-weight-bold);
}
.message {
- --gr-formatted-text-prose-max-width: 80ch;
+ --gr-formatted-text-prose-max-width: 120ch;
}
.collapsed .message {
max-width: none;
@@ -305,7 +305,7 @@
</gr-button>
</template>
<template is="dom-if" if="[[message._revision_number]]">
- <span class="patchset">[[message._revision_number]]</span>
+ <span class="patchset">[[message._revision_number]] |</span>
</template>
<template is="dom-if" if="[[!message.id]]">
<span class="date">
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 9877a95..bc3f167 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
@@ -118,7 +118,7 @@
/**
* If messages have the same tag, then that influences grouping and whether
- * a message is initally hidden or not, see isImportant(). So we are applying
+ * a message is initially hidden or not, see isImportant(). So we are applying
* some "magic" rules here in order to hide exactly the right messages.
*
* 1. If a message does not have a tag, but is associated with robot comments,
@@ -263,7 +263,7 @@
_combinedMessages: CombinedMessage[] = [];
@property({type: Object, computed: '_computeLabelExtremes(labels.*)'})
- _labelExtremes: {[lableName: string]: VotingRangeInfo} = {};
+ _labelExtremes: {[labelName: string]: VotingRangeInfo} = {};
private readonly reporting = appContext.reportingService;
@@ -466,7 +466,7 @@
LabelNameToInfoMap
>
) {
- const extremes: {[lableName: string]: VotingRangeInfo} = {};
+ const extremes: {[labelName: string]: VotingRangeInfo} = {};
const labels = labelRecord.base;
if (!labels) {
return extremes;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
index 3ed545e..7b698f1 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
@@ -77,10 +77,10 @@
margin-left: var(--spacing-xs);
}
.notCurrent {
- color: #e65100;
+ color: var(--warning-foreground);
}
.indirectAncestor {
- color: #33691e;
+ color: var(--indirect-ancestor-text-color);
}
.submittableCheck {
padding-left: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
index e921979..214271af 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
@@ -429,6 +429,8 @@
@property()
length = 0;
+ private readonly reporting = appContext.reportingService;
+
static get styles() {
return [
sharedStyles,
@@ -509,6 +511,10 @@
private toggle(e: MouseEvent) {
e.stopPropagation();
this.showAll = !this.showAll;
+ this.reporting.reportInteraction('toggle show all button', {
+ sectionName: this.title,
+ toState: this.showAll ? 'Show all' : 'Show less',
+ });
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
index 9941fa9..2f53319 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
@@ -66,10 +66,10 @@
margin-left: var(--spacing-xs);
}
.notCurrent {
- color: #e65100;
+ color: var(--warning-foreground);
}
.indirectAncestor {
- color: #33691e;
+ color: var(--indirect-ancestor-text-color);
}
.submittableCheck {
padding-left: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 705a402..a8e89b6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -784,7 +784,7 @@
}
_handle400Error(r?: Response | null) {
- if (!r) throw new Error('Reponse is empty.');
+ if (!r) throw new Error('Response is empty.');
let response: Response = r;
// A call to _saveReview could fail with a server error if erroneous
// reviewers were requested. This is signalled with a 400 Bad Request
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 6682bfb..0575239 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -1276,7 +1276,7 @@
'Send and Start review');
});
- test('_handle400Error reviewrs and CCs', done => {
+ test('_handle400Error reviewers and CCs', done => {
const error1 = 'error 1';
const error2 = 'error 2';
const error3 = 'error 3';
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index e448374..ef2430c 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -24,7 +24,14 @@
query,
} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
-import {Category, CheckRun, Link, RunStatus, Tag} from '../../api/checks';
+import {
+ Category,
+ CheckRun,
+ Link,
+ LinkIcon,
+ RunStatus,
+ Tag,
+} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
import {RunResult} from '../../services/checks/checks-model';
import {
@@ -35,6 +42,7 @@
} from '../../services/checks/checks-util';
import {assertIsDefined} from '../../utils/common-util';
import {whenVisible} from '../../utils/dom-util';
+import {durationString} from '../../utils/date-util';
@customElement('gr-result-row')
class GrResultRow extends GrLitElement {
@@ -295,6 +303,8 @@
@property()
runs: CheckRun[] = [];
+ private isSectionExpanded = new Map<Category | 'SUCCESS', boolean>();
+
static get styles() {
return [
sharedStyles,
@@ -312,23 +322,36 @@
margin-top: var(--spacing-l);
margin-left: var(--spacing-l);
text-transform: capitalize;
+ cursor: default;
}
- .categoryHeader iron-icon {
+ .categoryHeader .expandIcon {
+ width: var(--line-height-h3);
+ height: var(--line-height-h3);
+ margin-right: var(--spacing-s);
+ }
+ .categoryHeader .statusIcon {
position: relative;
- top: 1px;
+ top: 2px;
}
- .categoryHeader iron-icon.error {
+ .categoryHeader .statusIcon.error {
color: var(--error-foreground);
}
- .categoryHeader iron-icon.warning {
+ .categoryHeader .statusIcon.warning {
color: var(--warning-foreground);
}
- .categoryHeader iron-icon.info {
+ .categoryHeader .statusIcon.info {
color: var(--info-foreground);
}
- .categoryHeader iron-icon.success {
+ .categoryHeader .statusIcon.success {
color: var(--success-foreground);
}
+ .collapsed table {
+ display: none;
+ }
+ .collapsed {
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: var(--spacing-m);
+ }
.noCompleted {
margin-top: var(--spacing-l);
}
@@ -354,7 +377,7 @@
${this.renderFilter()} ${this.renderNoCompleted()}
${this.renderSection(Category.ERROR)}
${this.renderSection(Category.WARNING)}
- ${this.renderSection(Category.INFO)} ${this.renderSuccess()}
+ ${this.renderSection(Category.INFO)} ${this.renderSection('SUCCESS')}
`;
}
@@ -384,36 +407,62 @@
return html`<div class="noCompleted">${text}</div>`;
}
- renderSection(category: Category) {
+ renderSection(category: Category | 'SUCCESS') {
const catString = category.toString().toLowerCase();
- const runs = this.runs.filter(r =>
- (r.results ?? []).some(res => res.category === category)
- );
+ let runs = this.runs;
+ if (category === 'SUCCESS') {
+ runs = runs
+ .filter(hasCompletedWithoutResults)
+ .filter(r => this.filterRegExp.test(r.checkName));
+ } else {
+ runs = runs.filter(r =>
+ (r.results ?? []).some(res => res.category === category)
+ );
+ }
if (runs.length === 0) return;
+ const expanded = this.isSectionExpanded.get(category) ?? true;
+ const expandedClass = expanded ? 'expanded' : 'collapsed';
+ const icon = expanded ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
return html`
- <h3 class="categoryHeader heading-3">
- <iron-icon
- icon="gr-icons:${iconForCategory(category)}"
- class="${catString}"
- ></iron-icon>
- ${catString}
- </h3>
- <table class="resultsTable">
- <thead>
- <tr class="headerRow">
- <th class="iconCol"></th>
- <th class="nameCol">Run</th>
- <th class="summaryCol">Summary</th>
- <th class="expanderCol"></th>
- </tr>
- </thead>
- <tbody>
- ${runs.map(run => this.renderRun(category, run))}
- </tbody>
- </table>
+ <div class="${expandedClass}">
+ <h3
+ class="categoryHeader heading-3"
+ @click="${() => this.toggleExpanded(category)}"
+ >
+ <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
+ <iron-icon
+ icon="gr-icons:${iconForCategory(category)}"
+ class="statusIcon ${catString}"
+ ></iron-icon>
+ ${catString}
+ </h3>
+ <table class="resultsTable">
+ <thead>
+ <tr class="headerRow">
+ <th class="iconCol"></th>
+ <th class="nameCol">Run</th>
+ <th class="summaryCol">Summary</th>
+ <th class="expanderCol"></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${runs.map(run =>
+ category === 'SUCCESS'
+ ? this.renderSuccessfulRun(run)
+ : this.renderRun(category, run)
+ )}
+ </tbody>
+ </table>
+ </div>
`;
}
+ toggleExpanded(category: Category | 'SUCCESS') {
+ const expanded = this.isSectionExpanded.get(category) ?? true;
+ this.isSectionExpanded.set(category, !expanded);
+ this.requestUpdate();
+ }
+
renderRun(category: Category, run: CheckRun) {
return html`${run.results
?.filter(result => result.category === category)
@@ -428,38 +477,30 @@
)}`;
}
- renderSuccess() {
- const runs = this.runs
- .filter(hasCompletedWithoutResults)
- .filter(r => this.filterRegExp.test(r.checkName));
- if (runs.length === 0) return;
- return html`
- <h3 class="categoryHeader heading-3">
- <iron-icon
- icon="gr-icons:check-circle-outline"
- class="success"
- ></iron-icon>
- Success
- </h3>
- <table class="resultsTable">
- <tr class="headerRow">
- <th class="iconCol"></th>
- <th class="nameCol">Run</th>
- <th class="summaryCol">Summary</th>
- <th class="expanderCol"></th>
- </tr>
- ${runs.map(run => this.renderSuccessfulRun(run))}
- </table>
- `;
- }
-
renderSuccessfulRun(run: CheckRun) {
const adaptedRun: RunResult = {
category: Category.INFO, // will not be used, but is required
summary: run.statusDescription ?? '',
- message: 'Completed without results.',
...run,
};
+ if (!run.statusDescription) {
+ const start = run.scheduledTimestamp ?? run.startedTimestamp;
+ const end = run.finishedTimestamp;
+ let duration = '';
+ if (start && end) {
+ duration = ` in ${durationString(start, end, true)}`;
+ }
+ adaptedRun.message = `Completed without results${duration}.`;
+ }
+ if (run.statusLink) {
+ adaptedRun.links = [
+ {
+ url: run.statusLink,
+ primary: true,
+ icon: LinkIcon.EXTERNAL,
+ },
+ ];
+ }
return html`<gr-result-row .result="${adaptedRun}"></gr-result-row>`;
}
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 71ad041..31f17724 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {html} from 'lit-html';
+import {html, nothing} from 'lit-html';
import {classMap} from 'lit-html/directives/class-map';
import {
css,
@@ -29,8 +29,10 @@
import {
compareByWorstCategory,
fireActionTriggered,
+ iconForCategory,
iconForRun,
primaryRunAction,
+ worstCategory,
} from '../../services/checks/checks-util';
import {
allRuns$,
@@ -41,7 +43,7 @@
fakeRun4,
updateStateSetResults,
} from '../../services/checks/checks-model';
-import {assertIsDefined, toggleSetMembership} from '../../utils/common-util';
+import {assertIsDefined} from '../../utils/common-util';
import {whenVisible} from '../../utils/dom-util';
export interface RunSelectedEventDetail {
@@ -111,16 +113,19 @@
.chip.placeholder {
border-left: var(--thick-border) solid var(--border-color);
}
- .chip.error iron-icon {
+ .chip.placeholder iron-icon {
+ display: none;
+ }
+ iron-icon.error {
color: var(--error-foreground);
}
- .chip.warning iron-icon {
+ iron-icon.warning {
color: var(--warning-foreground);
}
- .chip.info-outline iron-icon {
+ iron-icon.info-outline {
color: var(--info-foreground);
}
- .chip.check-circle-outline iron-icon {
+ iron-icon.check-circle-outline {
color: var(--success-foreground);
}
/* Additional 'div' for increased specificity. */
@@ -195,7 +200,8 @@
return html`
<div @click="${this.handleChipClick}" class="${classMap(classes)}">
<div class="left">
- <iron-icon icon="gr-icons:${icon}"></iron-icon>
+ <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ ${this.renderAdditionalIcon()}
<span class="name">${this.run.checkName}</span>
</div>
<div class="right">
@@ -212,6 +218,20 @@
`;
}
+ /**
+ * For RUNNING we also want to render an icon representing the worst result
+ * that has been reported until now - if there are any results already.
+ */
+ renderAdditionalIcon() {
+ if (this.run.status !== RunStatus.RUNNING) return nothing;
+ const category = worstCategory(this.run);
+ if (!category) return nothing;
+ const icon = iconForCategory(category);
+ return html`
+ <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ `;
+ }
+
private handleChipClick(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
@@ -236,7 +256,10 @@
@property()
runs: CheckRun[] = [];
- private selectedRuns = new Set<string>();
+ @property()
+ selectedRuns: string[] = [];
+
+ private isSectionExpanded = new Map<RunStatus, boolean>();
constructor() {
super();
@@ -251,9 +274,24 @@
display: block;
padding: var(--spacing-xl);
}
- .statusHeader {
+ .expandIcon {
+ width: var(--line-height-h3);
+ height: var(--line-height-h3);
+ }
+ .sectionHeader {
padding-top: var(--spacing-l);
text-transform: capitalize;
+ cursor: default;
+ }
+ .sectionHeader h3 {
+ display: inline-block;
+ }
+ .collapsed .sectionRuns {
+ display: none;
+ }
+ .collapsed {
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: var(--spacing-m);
}
input#filterInput {
margin-top: var(--spacing-s);
@@ -344,29 +382,40 @@
.filter(r => this.filterRegExp.test(r.checkName))
.sort(compareByWorstCategory);
if (runs.length === 0) return;
+ const expanded = this.isSectionExpanded.get(status) ?? true;
+ const expandedClass = expanded ? 'expanded' : 'collapsed';
+ const icon = expanded ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
return html`
- <div class="${status.toLowerCase()}">
- <h3 class="statusHeader heading-3">${status.toLowerCase()}</h3>
- ${runs.map(run => this.renderRun(run))}
+ <div class="${status.toLowerCase()} ${expandedClass}">
+ <div
+ class="sectionHeader"
+ @click="${() => this.toggleExpanded(status)}"
+ >
+ <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
+ <h3 class="heading-3">${status.toLowerCase()}</h3>
+ </div>
+ <div class="sectionRuns">
+ ${runs.map(run => this.renderRun(run))}
+ </div>
</div>
`;
}
+ toggleExpanded(status: RunStatus) {
+ const expanded = this.isSectionExpanded.get(status) ?? true;
+ this.isSectionExpanded.set(status, !expanded);
+ this.requestUpdate();
+ }
+
renderRun(run: CheckRun) {
- const selected = this.selectedRuns.has(run.checkName);
- const deselected = !selected && this.selectedRuns.size > 0;
+ const selected = this.selectedRuns.includes(run.checkName);
+ const deselected = !selected && this.selectedRuns.length > 0;
return html`<gr-checks-run
.run="${run}"
.selected="${selected}"
.deselected="${deselected}"
- @run-selected="${this.handleRunSelected}"
></gr-checks-run>`;
}
-
- handleRunSelected(e: RunSelectedEvent) {
- toggleSetMembership(this.selectedRuns, e.detail.checkName);
- this.requestUpdate();
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 0ce81ed..c072f2c 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -15,28 +15,42 @@
* limitations under the License.
*/
import {html} from 'lit-html';
-import {css, customElement, property} from 'lit-element';
+import {
+ css,
+ customElement,
+ internalProperty,
+ property,
+ PropertyValues,
+} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
import {Action, CheckResult, CheckRun} from '../../api/checks';
import {
allActions$,
allResults$,
allRuns$,
+ checksPatchsetNumber$,
+ someProvidersAreLoading$,
} from '../../services/checks/checks-model';
import './gr-checks-runs';
import './gr-checks-results';
import {sharedStyles} from '../../styles/shared-styles';
-import {changeNum$, currentPatchNum$} from '../../services/change/change-model';
-import {NumericChangeId, PatchSetNum} from '../../types/common';
+import {changeNum$, latestPatchNum$} from '../../services/change/change-model';
+import {NumericChangeId, PatchSetNumber} from '../../types/common';
import {
ActionTriggeredEvent,
fireActionTriggered,
} from '../../services/checks/checks-util';
import {
+ assertIsDefined,
+ check,
checkRequiredProperty,
- toggleSetMembership,
} from '../../utils/common-util';
import {RunSelectedEvent} from './gr-checks-runs';
+import {ChecksTabState} from '../../types/events';
+import {fireAlert} from '../../utils/event-util';
+import {appContext} from '../../services/app-context';
+import {from, timer} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
/**
* The "Checks" tab on the Gerrit change page. Gets its data from plugins that
@@ -52,20 +66,34 @@
actions: Action[] = [];
@property()
- currentPatchNum: PatchSetNum | undefined = undefined;
+ tabState?: ChecksTabState;
+
+ @property()
+ checksPatchsetNumber: PatchSetNumber | undefined = undefined;
+
+ @property()
+ latestPatchsetNumber: PatchSetNumber | undefined = undefined;
@property()
changeNum: NumericChangeId | undefined = undefined;
- private selectedRuns = new Set<string>();
+ @property()
+ someProvidersAreLoading = false;
+
+ @internalProperty()
+ selectedRuns: string[] = [];
+
+ private readonly checksService = appContext.checksService;
constructor() {
super();
this.subscribe('runs', allRuns$);
this.subscribe('actions', allActions$);
this.subscribe('results', allResults$);
- this.subscribe('currentPatchNum', currentPatchNum$);
+ this.subscribe('checksPatchsetNumber', checksPatchsetNumber$);
+ this.subscribe('latestPatchsetNumber', latestPatchNum$);
this.subscribe('changeNum', changeNum$);
+ this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
this.addEventListener('action-triggered', (e: ActionTriggeredEvent) =>
this.handleActionTriggered(e.detail.action, e.detail.run)
@@ -105,22 +133,20 @@
}
render() {
- const ps = `Patchset ${this.currentPatchNum} (Latest)`;
const filteredRuns = this.runs.filter(
- r => this.selectedRuns.size === 0 || this.selectedRuns.has(r.checkName)
+ r =>
+ this.selectedRuns.length === 0 ||
+ this.selectedRuns.includes(r.checkName)
);
return html`
<div class="header">
<div class="left">
<gr-dropdown-list
- value="${ps}"
- .items="${[
- {
- value: `${ps}`,
- text: `${ps}`,
- },
- ]}"
+ value="${this.checksPatchsetNumber}"
+ .items="${this.createPatchsetDropdownItems()}"
+ @value-change="${this.onPatchsetSelected}"
></gr-dropdown-list>
+ <span ?hidden="${!this.someProvidersAreLoading}">Loading...</span>
</div>
<div class="right">
${this.actions.map(this.renderAction)}
@@ -130,6 +156,7 @@
<gr-checks-runs
class="runs"
.runs="${this.runs}"
+ .selectedRuns="${this.selectedRuns}"
@run-selected="${this.handleRunSelected}"
></gr-checks-runs>
<gr-checks-results
@@ -140,6 +167,35 @@
`;
}
+ private onPatchsetSelected(e: CustomEvent<{value: string}>) {
+ const patchset = Number(e.detail.value);
+ check(!isNaN(patchset), 'selected patchset must be a number');
+ this.checksService.setPatchset(patchset as PatchSetNumber);
+ }
+
+ private createPatchsetDropdownItems() {
+ if (!this.latestPatchsetNumber) return [];
+ return Array.from(Array(this.latestPatchsetNumber), (_, i) => {
+ assertIsDefined(this.latestPatchsetNumber, 'latestPatchsetNumber');
+ const index = this.latestPatchsetNumber - i;
+ const postfix = index === this.latestPatchsetNumber ? ' (latest)' : '';
+ return {
+ value: `${index}`,
+ text: `Patchset ${index}${postfix}`,
+ };
+ });
+ }
+
+ protected updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+ if (changedProperties.has('tabState')) {
+ const check = this.tabState?.checkName;
+ if (check) {
+ this.selectedRuns = [check];
+ }
+ }
+ }
+
renderAction(action: Action) {
return html`<gr-checks-top-level-action
.action="${action}"
@@ -148,23 +204,46 @@
handleActionTriggered(action: Action, run?: CheckRun) {
if (!this.changeNum) return;
- if (!this.currentPatchNum) return;
- // TODO(brohlfs): The callback is supposed to be returning a promise.
- // A toast should be displayed until the promise completes. And then the
- // data should be updated.
- action.callback(
+ if (!this.checksPatchsetNumber) return;
+ const promise = action.callback(
this.changeNum,
- this.currentPatchNum as number,
+ this.checksPatchsetNumber,
run?.attempt,
run?.externalId,
run?.checkName,
action.name
);
+ // Plugins *should* return a promise, but you never know ...
+ if (promise?.then) {
+ const prefix = `Triggering action '${action.name}'`;
+ fireAlert(this, `${prefix} ...`);
+ from(promise)
+ // If the action takes longer than 5 seconds, then most likely the
+ // user is either not interested or the result not relevant anymore.
+ .pipe(takeUntil(timer(5000)))
+ .subscribe(result => {
+ if (result.errorMessage) {
+ fireAlert(this, `${prefix} failed with ${result.errorMessage}.`);
+ } else {
+ fireAlert(this, `${prefix} successful.`);
+ this.checksService.reloadForCheck(run?.checkName);
+ }
+ });
+ } else {
+ fireAlert(this, `Action '${action.name}' triggered.`);
+ }
}
handleRunSelected(e: RunSelectedEvent) {
- toggleSetMembership(this.selectedRuns, e.detail.checkName);
- this.requestUpdate();
+ this.toggleSelected(e.detail.checkName);
+ }
+
+ toggleSelected(checkName: string) {
+ if (this.selectedRuns.includes(checkName)) {
+ this.selectedRuns = this.selectedRuns.filter(r => r !== checkName);
+ } else {
+ this.selectedRuns = [...this.selectedRuns, checkName];
+ }
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index b43b3b0..bed07a6 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -304,7 +304,7 @@
if (!config) {
throw new Error('getConfig returned undefined');
}
- this._retreiveFeedbackURL(config);
+ this._retrieveFeedbackURL(config);
this._retrieveRegisterURL(config);
return getDocsBaseUrl(config, this.restApiService);
})
@@ -325,7 +325,7 @@
});
}
- _retreiveFeedbackURL(config: ServerInfo) {
+ _retrieveFeedbackURL(config: ServerInfo) {
if (config.gerrit?.report_bug_url) {
this._feedbackURL = config.gerrit.report_bug_url;
}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index 7430cc0..d9c43d6 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -488,7 +488,7 @@
report_bug_url: url,
},
};
- element._retreiveFeedbackURL(config);
+ element._retrieveFeedbackURL(config);
await flush();
assert.equal(element._feedbackURL, url);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index f2ee838..632ce4c 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -249,7 +249,7 @@
export interface GenerateUrlChangeViewParameters {
view: GerritView.CHANGE;
- // TODO(TS): NumericChangeId - not sure about it, may be it can be removeds
+ // TODO(TS): NumericChangeId - not sure about it, may be it can be removed
changeNum: NumericChangeId;
project: RepoName;
patchNum?: PatchSetNum;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 3a76112..676ef7b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -308,15 +308,6 @@
private readonly restApiService = appContext.restApiService;
- private readonly changeService = appContext.changeService;
-
- constructor() {
- super();
- // TODO: This is just an artificical dependdency such that the service is
- // instantiated and its observables subscribed. Remove this later.
- this.changeService.dontDoAnything();
- }
-
start() {
if (!this._app) {
return;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 43f8a2b..32e0083 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -40,10 +40,12 @@
// Possible static search options for auto complete, without negations.
const SEARCH_OPERATORS: ReadonlyArray<string> = [
'added:',
+ 'after:',
'age:',
'age:1week', // Give an example age
'assignee:',
'author:',
+ 'before:',
'branch:',
'bug:',
'cc:',
@@ -77,6 +79,7 @@
'is:assigned',
'is:closed',
'is:ignored',
+ 'is:merge',
'is:merged',
'is:open',
'is:owner',
@@ -88,11 +91,14 @@
'is:watched',
'is:wip',
'label:',
+ 'mergedafter:',
+ 'mergedbefore:',
'message:',
'onlyexts:',
'onlyextensions:',
'owner:',
'ownerin:',
+ 'parentof:',
'parentproject:',
'project:',
'projects:',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 829038b..4943298 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -33,7 +33,7 @@
/**
* In JS, unicode code points above 0xFFFF occupy two elements of a string.
- * For example '𐀏'.length is 2. An occurence of such a code point is called a
+ * For example '𐀏'.length is 2. An occurrence of such a code point is called a
* surrogate pair.
*
* This regex segments a string along tabs ('\t') and surrogate pairs, since
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
new file mode 100644
index 0000000..42268e9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -0,0 +1,304 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+ query,
+} from 'lit-element';
+import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+
+import {Dimensions, fitToFrame, Point, Rect} from './util';
+
+/**
+ * Displays a scaled-down version of an image with a draggable frame for
+ * choosing a portion of the image to be magnified by other components.
+ *
+ * Slotted content can be arbitrary elements, but should be limited to images or
+ * stacks of image-like elements (e.g. for overlays) with limited interactivity,
+ * to prevent confusion, as the component only captures a limited set of events.
+ * Slotted content is scaled to fit the bounds of the component, with
+ * letterboxing if aspect ratios differ. For slotted content smaller than the
+ * component, it will cap the scale at 1x and also apply letterboxing.
+ */
+@customElement('gr-overview-image')
+export class GrOverviewImage extends LitElement {
+ @property({type: Object})
+ frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
+
+ @internalProperty() protected contentStyle: StyleInfo = {};
+
+ @internalProperty() protected contentTransformStyle: StyleInfo = {};
+
+ @internalProperty() protected frameStyle: StyleInfo = {};
+
+ @internalProperty() protected overlayStyle: StyleInfo = {};
+
+ @internalProperty() protected dragging = false;
+
+ @query('.content-box') protected contentBox!: HTMLDivElement;
+
+ @query('.content') protected content!: HTMLDivElement;
+
+ @query('.content-transform') protected contentTransform!: HTMLDivElement;
+
+ @query('.frame') protected frame!: HTMLDivElement;
+
+ private contentBounds: Dimensions = {width: 0, height: 0};
+
+ private imageBounds: Dimensions = {width: 0, height: 0};
+
+ private scale = 1;
+
+ // When grabbing the frame to drag it around, this stores the offset of the
+ // cursor from the center of the frame at the start of the drag.
+ private grabOffset: Point = {x: 0, y: 0};
+
+ private readonly resizeObserver = new ResizeObserver(
+ (entries: ResizeObserverEntry[]) => {
+ for (const entry of entries) {
+ if (entry.target === this.contentBox) {
+ this.contentBounds = {
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ };
+ }
+ if (entry.target === this.contentTransform) {
+ this.imageBounds = {
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ };
+ }
+ this.updateScale();
+ }
+ }
+ );
+
+ static styles = css`
+ :host {
+ --overview-image-background-color: #000;
+ --overview-image-frame-color: #f00;
+ display: flex;
+ }
+ * {
+ box-sizing: border-box;
+ }
+ ::slotted(*) {
+ display: block;
+ }
+ .content-box {
+ border: 1px solid var(--overview-image-background-color);
+ background-color: var(--overview-iamge-background-color);
+ width: 100%;
+ position: relative;
+ }
+ .content {
+ position: absolute;
+ cursor: pointer;
+ }
+ .content-transform {
+ position: absolute;
+ transform-origin: top left;
+ will-change: transform;
+ }
+ .frame {
+ border: 1px solid var(--overview-image-frame-color);
+ position: absolute;
+ will-change: transform;
+ }
+ .overlay {
+ position: absolute;
+ z-index: 10000;
+ cursor: grabbing;
+ }
+ `;
+
+ render() {
+ return html`
+ <div class="content-box">
+ <div
+ class="content"
+ style="${styleMap({
+ ...this.contentStyle,
+ })}"
+ @mousemove="${this.maybeDragFrame}"
+ @mousedown=${this.clickOverview}
+ @mouseup="${this.releaseFrame}"
+ >
+ <div
+ class="content-transform"
+ style="${styleMap(this.contentTransformStyle)}"
+ >
+ <slot></slot>
+ </div>
+ <div
+ class="frame"
+ style="${styleMap({
+ ...this.frameStyle,
+ cursor: this.dragging ? 'grabbing' : 'grab',
+ })}"
+ @mousedown="${this.grabFrame}"
+ ></div>
+ </div>
+ <div
+ class="overlay"
+ style="${styleMap({
+ ...this.overlayStyle,
+ display: this.dragging ? 'block' : 'none',
+ })}"
+ @mousemove="${this.overlayMouseMove}"
+ @mouseleave="${this.releaseFrame}"
+ @mouseup="${this.releaseFrame}"
+ ></div>
+ </div>
+ `;
+ }
+
+ firstUpdated() {
+ this.resizeObserver.observe(this.contentBox);
+ this.resizeObserver.observe(this.contentTransform);
+ }
+
+ updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('frameRect')) {
+ this.updateFrameStyle();
+ }
+ }
+
+ clickOverview(event: MouseEvent) {
+ event.preventDefault();
+
+ this.updateOverlaySize();
+
+ this.dragging = true;
+ const rect = this.content.getBoundingClientRect();
+ this.notifyNewCenter({
+ x: (event.clientX - rect.left) / this.scale,
+ y: (event.clientY - rect.top) / this.scale,
+ });
+ }
+
+ grabFrame(event: MouseEvent) {
+ event.preventDefault();
+ // Do not bubble up into clickOverview().
+ event.stopPropagation();
+
+ this.updateOverlaySize();
+
+ this.dragging = true;
+ const rect = this.frame.getBoundingClientRect();
+ const frameCenterX = rect.x + rect.width / 2;
+ const frameCenterY = rect.y + rect.height / 2;
+ this.grabOffset = {
+ x: event.clientX - frameCenterX,
+ y: event.clientY - frameCenterY,
+ };
+ }
+
+ maybeDragFrame(event: MouseEvent) {
+ event.preventDefault();
+ if (!this.dragging) return;
+ const rect = this.content.getBoundingClientRect();
+ const center = {
+ x: (event.clientX - rect.left - this.grabOffset.x) / this.scale,
+ y: (event.clientY - rect.top - this.grabOffset.y) / this.scale,
+ };
+ this.notifyNewCenter(center);
+ }
+
+ releaseFrame(event: MouseEvent) {
+ event.preventDefault();
+ this.dragging = false;
+ this.grabOffset = {x: 0, y: 0};
+ }
+
+ overlayMouseMove(event: MouseEvent) {
+ event.preventDefault();
+ this.maybeDragFrame(event);
+ }
+
+ private updateScale() {
+ const fitted = fitToFrame(this.imageBounds, this.contentBounds);
+ this.scale = fitted.scale;
+
+ this.contentStyle = {
+ ...this.contentStyle,
+ top: `${fitted.top}px`,
+ left: `${fitted.left}px`,
+ width: `${fitted.width}px`,
+ height: `${fitted.height}px`,
+ };
+
+ this.contentTransformStyle = {
+ transform: `scale(${this.scale})`,
+ };
+
+ this.updateFrameStyle();
+ }
+
+ private updateFrameStyle() {
+ const x = this.frameRect.origin.x * this.scale;
+ const y = this.frameRect.origin.y * this.scale;
+ const width = this.frameRect.dimensions.width * this.scale;
+ const height = this.frameRect.dimensions.height * this.scale;
+ this.frameStyle = {
+ ...this.frameStyle,
+ transform: `translate(${x}px, ${y}px)`,
+ width: `${width}px`,
+ height: `${height}px`,
+ };
+ }
+
+ private updateOverlaySize() {
+ const rect = this.contentBox.getBoundingClientRect();
+ // Create a whole-page overlay to capture mouse events, so that the drag
+ // interaction continues until the user releases the mouse button. Since
+ // innerWidth and innerHeight include scrollbars, we subtract 20 pixels each
+ // to prevent the overlay from extending offscreen under any existing
+ // scrollbar and causing the scrollbar for the other dimension to show up
+ // unnecessarily.
+ const width = window.innerWidth - 20;
+ const height = window.innerHeight - 20;
+ this.overlayStyle = {
+ ...this.overlayStyle,
+ top: `-${rect.top + 1}px`,
+ left: `-${rect.left + 1}px`,
+ width: `${width}px`,
+ height: `${height}px`,
+ };
+ }
+
+ private notifyNewCenter(center: Point) {
+ this.dispatchEvent(
+ new CustomEvent('center-updated', {
+ detail: {...center},
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-overview-image': GrOverviewImage;
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
new file mode 100644
index 0000000..a14a9cc
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
@@ -0,0 +1,95 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+} from 'lit-element';
+import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+import {Rect} from './util';
+
+/**
+ * Displays its slotted content at a given scale, centered over a given point,
+ * while ensuring the content always fills the container. The content does not
+ * have to be a single image, it can be arbitrary HTML. To prevent user
+ * confusion, it should ideally be image-like, i.e. have limited or no
+ * interactivity, as the component does not prevent events or focus from
+ * reaching the slotted content.
+ */
+@customElement('gr-zoomed-image')
+export class GrZoomedImage extends LitElement {
+ @property({type: Number}) scale = 1;
+
+ @property({type: Object})
+ frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
+
+ @internalProperty() protected imageStyles: StyleInfo = {};
+
+ static styles = css`
+ :host {
+ display: block;
+ }
+ ::slotted(*) {
+ display: block;
+ }
+ #clip {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ }
+ #transform {
+ position: absolute;
+ transform-origin: top left;
+ will-change: transform;
+ }
+ `;
+
+ render() {
+ return html`
+ <div id="clip">
+ <div id="transform" style="${styleMap(this.imageStyles)}">
+ <slot></slot>
+ </div>
+ </div>
+ `;
+ }
+
+ updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('scale') || changedProperties.has('frameRect')) {
+ this.updateImageStyles();
+ }
+ }
+
+ private updateImageStyles() {
+ const {x, y} = this.frameRect.origin;
+ this.imageStyles = {
+ 'image-rendering': this.scale >= 1 ? 'pixelated' : 'auto',
+ transform: `translate(${-x}px, ${-y}px) scale(${this.scale})`,
+ };
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-zoomed-image': GrZoomedImage;
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
new file mode 100644
index 0000000..b42eea9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
@@ -0,0 +1,236 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface Dimensions {
+ width: number;
+ height: number;
+}
+
+export interface Rect {
+ origin: Point;
+ dimensions: Dimensions;
+}
+
+export interface FittedContent {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+ scale: number;
+}
+
+function clamp(value: number, min: number, max: number) {
+ return Math.max(min, Math.min(value, max));
+}
+
+/**
+ * Fits content of the given dimensions into the given frame, maintaining the
+ * aspect ratio of the content and applying letterboxing / pillarboxing as
+ * needed.
+ */
+export function fitToFrame(
+ content: Dimensions,
+ frame: Dimensions
+): FittedContent {
+ const contentAspectRatio = content.width / content.height;
+ const frameAspectRatio = frame.width / frame.height;
+ // If the content is wider than the frame, it will be letterboxed, otherwise
+ // it will be pillarboxed. When letterboxed, content and frame width will
+ // match exactly, when pillarboxed, content and frame height will match
+ // exactly.
+ const isLetterboxed = contentAspectRatio > frameAspectRatio;
+ let width: number;
+ let height: number;
+ if (isLetterboxed) {
+ width = Math.min(frame.width, content.width);
+ height = content.height * (width / content.width);
+ } else {
+ height = Math.min(frame.height, content.height);
+ width = content.width * (height / content.height);
+ }
+ const top = (frame.height - height) / 2;
+ const left = (frame.width - width) / 2;
+ const scale = width / content.width;
+ return {top, left, width, height, scale};
+}
+
+function ensureInBounds(part: Rect, bounds: Dimensions): Rect {
+ const x =
+ part.dimensions.width <= bounds.width
+ ? clamp(part.origin.x, 0, bounds.width - part.dimensions.width)
+ : (bounds.width - part.dimensions.width) / 2;
+ const y =
+ part.dimensions.height <= bounds.height
+ ? clamp(part.origin.y, 0, bounds.height - part.dimensions.height)
+ : (bounds.height - part.dimensions.height) / 2;
+ return {origin: {x, y}, dimensions: part.dimensions};
+}
+
+/**
+ * Maintains a given frame inside given bounds, adjusting requested positions
+ * for the frame as needed. This supports the non-destructive application of a
+ * scaling factor, so that e.g. the magnification of an image can be changed
+ * easily while keeping the frame centered over the same spot. Changing bounds
+ * or frame size also keeps the frame position when possible.
+ */
+export class FrameConstrainer {
+ private center: Point = {x: 0, y: 0};
+
+ private frameSize: Dimensions = {width: 0, height: 0};
+
+ private bounds: Dimensions = {width: 0, height: 0};
+
+ private scale = 1;
+
+ private unscaledFrame: Rect = {
+ origin: {x: 0, y: 0},
+ dimensions: {width: 0, height: 0},
+ };
+
+ private scaledFrame: Rect = {
+ origin: {x: 0, y: 0},
+ dimensions: {width: 0, height: 0},
+ };
+
+ getCenter(): Point {
+ return {...this.center};
+ }
+
+ /**
+ * Returns the frame at its original size, positioned within the given bounds
+ * at the given scale; its origin will be in scaled bounds coordinates.
+ *
+ * Ex: for given bounds 100x50 and frame size 30x20, centered over (50, 25),
+ * all at 1x scale, when setting scale to 2, this will return a frame of size
+ * 30x20, centered over (100, 50), within bounds 200x100.
+ *
+ * Useful for positioning a viewport of fixed size over a magnified image.
+ */
+ getUnscaledFrame(): Rect {
+ return {
+ origin: {...this.unscaledFrame.origin},
+ dimensions: {...this.unscaledFrame.dimensions},
+ };
+ }
+
+ /**
+ * Returns the scaled down frame–a scale of 2 will result in frame dimensions
+ * being halved—position within the given bounds at 1x scale; its origin will
+ * be in unscaled bounds coordinates.
+ *
+ * Ex: for given bounds 100x50 and frame size 30x20, centered over (50, 25),
+ * all at 1x scale, when setting scale to 2, this will return a frame of size
+ * 15x10, centered over (50, 25), within bounds 100x50.
+ *
+ * Useful for highlighting the magnified portion of an image as determined by
+ * getUnscaledFrame() in an overview image of fixed size.
+ */
+ getScaledFrame(): Rect {
+ return {
+ origin: {...this.scaledFrame.origin},
+ dimensions: {...this.scaledFrame.dimensions},
+ };
+ }
+
+ /**
+ * Requests the frame to be centered over the given point, in unscaled bounds
+ * coordinates. This will keep the frame within the given bounds, also when
+ * requesting a center point fully outside the given bounds.
+ */
+ requestCenter(center: Point) {
+ this.center = {...center};
+
+ this.ensureFrameInBounds();
+ }
+
+ /**
+ * Sets the frame size, while keeping the frame within the given bounds, and
+ * maintaining the current center if possible.
+ */
+ setFrameSize(frameSize: Dimensions) {
+ if (frameSize.width <= 0 || frameSize.height <= 0) return;
+ this.frameSize = {...frameSize};
+
+ this.ensureFrameInBounds();
+ }
+
+ /**
+ * Sets the bounds, while keeping the frame within them, and maintaining the
+ * current center if possible.
+ */
+ setBounds(bounds: Dimensions) {
+ if (bounds.width <= 0 || bounds.height <= 0) return;
+ this.bounds = {...bounds};
+
+ this.ensureFrameInBounds();
+ }
+
+ /**
+ * Sets the applied scale, while keeping the frame within the given bounds,
+ * and maintaining the current center if possible (both relevant moving from
+ * a larger scale to a smaller scale).
+ */
+ setScale(scale: number) {
+ if (!scale || scale <= 0) return;
+ this.scale = scale;
+
+ this.ensureFrameInBounds();
+ }
+
+ private ensureFrameInBounds() {
+ const scaledCenter = {
+ x: this.center.x * this.scale,
+ y: this.center.y * this.scale,
+ };
+ const scaledBounds = {
+ width: this.bounds.width * this.scale,
+ height: this.bounds.height * this.scale,
+ };
+ const scaledFrameSize = {
+ width: this.frameSize.width / this.scale,
+ height: this.frameSize.height / this.scale,
+ };
+
+ const requestedUnscaledFrame = {
+ origin: {
+ x: scaledCenter.x - this.frameSize.width / 2,
+ y: scaledCenter.y - this.frameSize.height / 2,
+ },
+ dimensions: this.frameSize,
+ };
+ const requestedScaledFrame = {
+ origin: {
+ x: this.center.x - scaledFrameSize.width / 2,
+ y: this.center.y - scaledFrameSize.height / 2,
+ },
+ dimensions: scaledFrameSize,
+ };
+
+ this.unscaledFrame = ensureInBounds(requestedUnscaledFrame, scaledBounds);
+ this.scaledFrame = ensureInBounds(requestedScaledFrame, this.bounds);
+
+ this.center = {
+ x: this.scaledFrame.origin.x + this.scaledFrame.dimensions.width / 2,
+ y: this.scaledFrame.origin.y + this.scaledFrame.dimensions.height / 2,
+ };
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util_test.js b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util_test.js
new file mode 100644
index 0000000..80cfa36
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util_test.js
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../test/common-test-setup-karma.js';
+import {FrameConstrainer} from './util.js';
+
+suite('FrameConstrainer tests', () => {
+ let constrainer;
+
+ setup(() => {
+ constrainer = new FrameConstrainer();
+ constrainer.setBounds({width: 100, height: 100});
+ constrainer.setFrameSize({width: 50, height: 50});
+ constrainer.requestCenter({x: 50, y: 50});
+ });
+
+ suite('changing center', () => {
+ test('moves frame to requested position', () => {
+ constrainer.requestCenter({x: 30, y: 30});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 5, y: 5}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('keeps frame in bounds for top left corner', () => {
+ constrainer.requestCenter({x: 5, y: 5});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 0, y: 0}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('keeps frame in bounds for bottom right corner', () => {
+ constrainer.requestCenter({x: 95, y: 95});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center left', () => {
+ constrainer.requestCenter({x: -5, y: 50});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 0, y: 25}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center right', () => {
+ constrainer.requestCenter({x: 105, y: 50});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 50, y: 25}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center top', () => {
+ constrainer.requestCenter({x: 50, y: -5});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 25, y: 0}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center bottom', () => {
+ constrainer.requestCenter({x: 50, y: 105});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 25, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+ });
+
+ suite('changing frame size', () => {
+ test('maintains center when decreased', () => {
+ constrainer.setFrameSize({width: 10, height: 10});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 45, y: 45}, dimensions: {width: 10, height: 10}});
+ });
+
+ test('maintains center when increased', () => {
+ constrainer.setFrameSize({width: 80, height: 80});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 10, y: 10}, dimensions: {width: 80, height: 80}});
+ });
+
+ test('updates center to remain in bounds when increased', () => {
+ constrainer.setFrameSize({width: 10, height: 10});
+ constrainer.requestCenter({x: 95, y: 95});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
+
+ constrainer.setFrameSize({width: 20, height: 20});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 80, y: 80}, dimensions: {width: 20, height: 20}});
+ });
+ });
+
+ suite('changing scale', () => {
+ suite('for unscaled frame', () => {
+ test('adjusts origin to maintain center when zooming in', () => {
+ constrainer.setScale(2);
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 75, y: 75}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('adjusts origin to maintain center when zooming out', () => {
+ constrainer.setFrameSize({width: 20, height: 20});
+ constrainer.setScale(0.5);
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 15, y: 15}, dimensions: {width: 20, height: 20}});
+ });
+
+ test('keeps frame in bounds when zooming out', () => {
+ constrainer.setScale(5);
+ constrainer.requestCenter({x: 100, y: 100});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 450, y: 450}, dimensions: {width: 50, height: 50}});
+
+ constrainer.setScale(1);
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+ });
+
+ suite('for scaled frame', () => {
+ test('decreases frame size and maintains center when zooming in', () => {
+ constrainer.setScale(2);
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 37.5, y: 37.5}, dimensions: {width: 25, height: 25}});
+ });
+
+ test('increases frame size and maintains center when zooming out', () => {
+ constrainer.setFrameSize({width: 20, height: 20});
+ constrainer.setScale(0.5);
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 30, y: 30}, dimensions: {width: 40, height: 40}});
+ });
+
+ test('keeps frame in bounds when zooming out', () => {
+ constrainer.setScale(5);
+ constrainer.requestCenter({x: 100, y: 100});
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
+
+ constrainer.setScale(1);
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index cfe2cfe..60f2853 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -60,17 +60,17 @@
this.restApiService.savePreferences({diff_view: newMode});
}
this.mode = newMode;
- let annoucement;
+ let announcement;
if (this.isUnifiedSelected(newMode)) {
- annoucement = 'Changed diff view to unified';
+ announcement = 'Changed diff view to unified';
} else if (this.isSideBySideSelected(newMode)) {
- annoucement = 'Changed diff view to side by side';
+ announcement = 'Changed diff view to side by side';
}
- if (annoucement) {
+ if (announcement) {
this.fire(
'iron-announce',
{
- text: annoucement,
+ text: announcement,
},
{bubbles: true}
);
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 088d9cf..2c4b8f6 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
@@ -1465,9 +1465,9 @@
) {
let patchNum = patchRange.patchNum;
- const comparedAgainsParent = patchRange.basePatchNum === 'PARENT';
+ const comparedAgainstParent = patchRange.basePatchNum === 'PARENT';
- if (isBase && !comparedAgainsParent) {
+ if (isBase && !comparedAgainstParent) {
patchNum = patchRange.basePatchNum;
}
@@ -1475,7 +1475,7 @@
changeBaseURL(project, changeNum, patchNum) +
`/files/${encodeURIComponent(path)}/download`;
- if (isBase && comparedAgainsParent) {
+ if (isBase && comparedAgainstParent) {
url += '?parent=1';
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
index 66ac065..0ca929a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
@@ -88,7 +88,7 @@
// TODO: This type should be exposed to gr-diff clients in a separate type file.
// For Gerrit these are instances of GrCommentThread, but other gr-diff users
// have different HTML elements in use for comment threads.
-// TODO: Also document the required HTML attritbutes that thread elements must
+// TODO: Also document the required HTML attributes that thread elements must
// have, e.g. 'diff-side', 'range', 'line-num', 'data-value'.
export interface GrDiffThreadElement extends HTMLElement {
rootId: string;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
index 52465b3..5ab8449 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
@@ -35,7 +35,6 @@
text-transform: none;
font-family: var(--font-family);
}
- --trigger-hover-color: rgba(0, 0, 0, 0.6);
}
@media screen and (max-width: 50em) {
.filesWeblinks {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
index da29b85..1150674 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
@@ -492,7 +492,7 @@
* with code it shouldn't AND to avoid executing regexes as much as
* possible.
* * These tests should document the issue clearly enough that the test can
- * be condidently removed when the issue is solved in HLJS.
+ * be confidently removed when the issue is solved in HLJS.
* * These tests should rewrite the line of code to have the same number of
* characters. This method rewrites the string that gets parsed, but NOT
* the string that gets displayed and highlighted. Thus, the positions
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index b36edd4..bc153ee 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -277,7 +277,8 @@
return this.restApiService
.queryChangeFiles(this.change._number, this.patchNum, input)
.then(res => {
- if (!res) throw new Error('Failed to retrieve files. Reponse not set.');
+ if (!res)
+ throw new Error('Failed to retrieve files. Response not set.');
return res.map(file => {
return {name: file};
});
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
index be6ffc4..bbf4790 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
@@ -50,14 +50,14 @@
suite('edit button CUJ', () => {
let navStubs;
- let openAutoCcmplete;
+ let openAutoComplete;
setup(() => {
navStubs = [
sinon.stub(GerritNav, 'getEditUrlForDiff'),
sinon.stub(GerritNav, 'navigateToRelativeUrl'),
];
- openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
+ openAutoComplete = element.$.openDialog.querySelector('gr-autocomplete');
});
test('_isValidPath', () => {
@@ -77,9 +77,9 @@
assert.isFalse(queryStub.called);
// Setup _focused manually - in headless mode Chrome sometimes don't
// setup focus. flush and/or flushAsynchronousOperations don't help
- openAutoCcmplete._focused = true;
- openAutoCcmplete.noDebounce = true;
- openAutoCcmplete.text = 'src/test.cpp';
+ openAutoComplete._focused = true;
+ openAutoComplete.noDebounce = true;
+ openAutoComplete.text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isFalse(element.$.openDialog.disabled);
MockInteractions.tap(element.$.openDialog.shadowRoot
@@ -95,8 +95,8 @@
MockInteractions.tap(element.shadowRoot.querySelector('#open'));
return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.openDialog.disabled);
- openAutoCcmplete.noDebounce = true;
- openAutoCcmplete.text = 'src/test.cpp';
+ openAutoComplete.noDebounce = true;
+ openAutoComplete.text = 'src/test.cpp';
assert.isFalse(element.$.openDialog.disabled);
MockInteractions.tap(element.$.openDialog.shadowRoot
.querySelector('gr-button'));
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 1864598..1e08a5c 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -200,7 +200,7 @@
}
_handlePathChanged(e: CustomEvent<string>) {
- // TODO(TS) could be cleand up, it was added for type requirements
+ // TODO(TS) could be cleaned up, it was added for type requirements
if (this._changeNum === undefined || !this._path) {
return Promise.reject(new Error('changeNum or path undefined'));
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
index 897be67..7a91c68 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.ts
@@ -16,6 +16,7 @@
*/
import {EventType, PluginApi} from '../../../api/plugin';
import {AdminPluginApi, MenuLink} from '../../../api/admin';
+import {appContext} from '../../../services/app-context';
/**
* GrAdminApi class.
@@ -26,15 +27,20 @@
// TODO(TS): maybe define as enum if its a limited set
private menuLinks: MenuLink[] = [];
+ private readonly reporting = appContext.reportingService;
+
constructor(private readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'admin', 'constructor');
this.plugin.on(EventType.ADMIN_MENU_LINKS, this);
}
addMenuLink(text: string, url: string, capability?: string) {
+ this.reporting.trackApi(this.plugin, 'admin', 'addMenuLink');
this.menuLinks.push({text, url, capability: capability || null});
}
getMenuLinks(): MenuLink[] {
+ this.reporting.trackApi(this.plugin, 'admin', 'getMenuLinks');
return this.menuLinks.slice(0);
}
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
index e0b4ee9..ab2ce6a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
@@ -15,14 +15,20 @@
* limitations under the License.
*/
import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
+import {PluginApi} from '../../../api/plugin';
+import {appContext} from '../../../services/app-context';
export class GrAttributeHelper implements AttributeHelperPluginApi {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _promises = new Map<string, Promise<any>>();
+ private readonly reporting = appContext.reportingService;
+
// TODO(TS): Change any to something more like HTMLElement.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- constructor(public element: any) {}
+ constructor(readonly plugin: PluginApi, public element: any) {
+ this.reporting.trackApi(this.plugin, 'attribute', 'constructor');
+ }
_getChangedEventName(name: string): string {
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + '-changed';
@@ -52,6 +58,7 @@
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
bind(name: string, callback: (value: any) => void) {
+ this.reporting.trackApi(this.plugin, 'attribute', 'bind');
const attributeChangedEventName = this._getChangedEventName(name);
const changedHandler = (e: CustomEvent) =>
this._reportValue(callback, e.detail.value);
@@ -72,6 +79,7 @@
* to be initialized if it isn't defined.
*/
get(name: string): Promise<unknown> {
+ this.reporting.trackApi(this.plugin, 'attribute', 'get');
if (this._elementHasProperty(name)) {
return Promise.resolve(this.element[name]);
}
@@ -93,6 +101,7 @@
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(name: string, value: any) {
+ this.reporting.trackApi(this.plugin, 'attribute', 'set');
this.element[name] = value;
this.element.dispatchEvent(
new CustomEvent(this._getChangedEventName(name), {detail: {value}})
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
index ea7bdc3..2d83012 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
@@ -17,10 +17,10 @@
import '../../../test/common-test-setup-karma.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {GrAttributeHelper} from './gr-attribute-helper.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
Polymer({
- is: 'gr-attrubute-helper-some-element',
+ is: 'gr-attribute-helper-some-element',
properties: {
fooBar: {
type: Object,
@@ -29,15 +29,20 @@
},
});
-const basicFixture = fixtureFromElement('gr-attrubute-helper-some-element');
+const basicFixture = fixtureFromElement('gr-attribute-helper-some-element');
+
+const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-attribute-helper tests', () => {
let element;
let instance;
setup(() => {
+ let plugin;
+ pluginApi.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
element = basicFixture.instantiate();
- instance = new GrAttributeHelper(element);
+ instance = plugin.attributeHelper(element);
});
test('resolved on value change from undefined', () => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts
index a03c5dc..bbb58fc 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.ts
@@ -17,15 +17,19 @@
import {PluginApi} from '../../../api/plugin';
import {ChangeMetadataPluginApi} from '../../../api/change-metadata';
import {HookApi} from '../../../api/hook';
+import {appContext} from '../../../services/app-context';
export class GrChangeMetadataApi implements ChangeMetadataPluginApi {
private hook: HookApi | null;
public plugin: PluginApi;
+ private readonly reporting = appContext.reportingService;
+
constructor(plugin: PluginApi) {
this.plugin = plugin;
this.hook = null;
+ this.reporting.trackApi(this.plugin, 'metadata', 'constructor');
}
_createHook() {
@@ -33,6 +37,7 @@
}
onLabelsChanged(callback: (value: unknown) => void) {
+ this.reporting.trackApi(this.plugin, 'metadata', 'onLabelsChanged');
if (!this.hook) {
this._createHook();
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
index 404fc71..39d3c8b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
@@ -43,13 +43,19 @@
private readonly checksService = appContext.checksService;
- constructor(readonly plugin: PluginApi) {}
+ private readonly reporting = appContext.reportingService;
+
+ constructor(readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'checks', 'constructor');
+ }
announceUpdate() {
+ this.reporting.trackApi(this.plugin, 'checks', 'announceUpdate');
this.checksService.reload(this.plugin.getPluginName());
}
register(provider: ChecksProvider, config?: ChecksApiConfig): void {
+ this.reporting.trackApi(this.plugin, 'checks', 'register');
if (this.state === State.REGISTERED)
throw new Error('Only one provider can be registered per plugin.');
this.state = State.REGISTERED;
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
index 4b34d56..0c2f412 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
@@ -18,6 +18,8 @@
EventHelperPluginApi,
UnsubscribeCallback,
} from '../../../api/event-helper';
+import {PluginApi} from '../../../api/plugin';
+import {appContext} from '../../../services/app-context';
export interface ListenOptions {
event?: string;
@@ -25,13 +27,18 @@
}
export class GrEventHelper implements EventHelperPluginApi {
- constructor(readonly element: HTMLElement) {}
+ private readonly reporting = appContext.reportingService;
+
+ constructor(readonly plugin: PluginApi, readonly element: HTMLElement) {
+ this.reporting.trackApi(this.plugin, 'event', 'constructor');
+ }
/**
* Add a callback to arbitrary event.
* The callback may return false to prevent event bubbling.
*/
on(event: string, callback: (event: Event) => boolean) {
+ this.reporting.trackApi(this.plugin, 'event', 'on');
return this._listen(this.element, callback, {event});
}
@@ -39,6 +46,7 @@
* Alias for @see onClick
*/
onTap(callback: (event: Event) => boolean) {
+ this.reporting.trackApi(this.plugin, 'event', 'onTap');
return this.onClick(callback);
}
@@ -47,6 +55,7 @@
* The callback may return false to prevent event bubbling.
*/
onClick(callback: (event: Event) => boolean) {
+ this.reporting.trackApi(this.plugin, 'event', 'onClick');
return this._listen(this.element, callback);
}
@@ -54,6 +63,7 @@
* Alias for @see captureClick
*/
captureTap(callback: (event: Event) => boolean) {
+ this.reporting.trackApi(this.plugin, 'event', 'captureTap');
return this.captureClick(callback);
}
@@ -64,6 +74,7 @@
* The callback may return false to cancel regular event listeners.
*/
captureClick(callback: (event: Event) => boolean) {
+ this.reporting.trackApi(this.plugin, 'event', 'captureClick');
const parent = this.element.parentElement!;
return this._listen(parent, callback, {capture: true});
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
index 25c0d43..547b575 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
@@ -17,8 +17,8 @@
import '../../../test/common-test-setup-karma.js';
import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
-import {GrEventHelper} from './gr-event-helper.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
Polymer({
is: 'gr-event-helper-some-element',
@@ -33,13 +33,18 @@
const basicFixture = fixtureFromElement('gr-event-helper-some-element');
+const pluginApi = _testOnly_initGerritPluginApi();
+
suite('gr-event-helper tests', () => {
let element;
let instance;
setup(() => {
+ let plugin;
+ pluginApi.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
element = basicFixture.instantiate();
- instance = new GrEventHelper(element);
+ instance = plugin.eventHelper(element);
});
test('onTap()', done => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
index dcabc80..13d18b5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
@@ -19,6 +19,7 @@
import {GrPluginPopup} from './gr-plugin-popup';
import {PluginApi} from '../../../api/plugin';
import {PopupPluginApi} from '../../../api/popup';
+import {appContext} from '../../../services/app-context';
interface CustomPolymerPluginEl extends HTMLElement {
plugin: PluginApi;
@@ -35,10 +36,14 @@
private popup: GrPluginPopup | null = null;
+ private readonly reporting = appContext.reportingService;
+
constructor(
readonly plugin: PluginApi,
private moduleName: string | null = null
- ) {}
+ ) {
+ this.reporting.trackApi(this.plugin, 'popup', 'constructor');
+ }
_getElement() {
// TODO(TS): maybe consider removing this if no one is using
@@ -52,6 +57,7 @@
* if it was provided with constructor.
*/
open(): Promise<PopupPluginApi> {
+ this.reporting.trackApi(this.plugin, 'popup', 'open');
if (!this.openingPromise) {
this.openingPromise = this.plugin
.hook('plugin-overlay')
@@ -76,6 +82,7 @@
* Hides the popup.
*/
close() {
+ this.reporting.trackApi(this.plugin, 'popup', 'close');
if (!this.popup) {
return;
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts
index 0418edb..51e9112 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.ts
@@ -19,6 +19,7 @@
import {PluginApi} from '../../../api/plugin';
import {RepoCommandCallback, RepoPluginApi} from '../../../api/repo';
import {HookApi} from '../../../api/hook';
+import {appContext} from '../../../services/app-context';
/**
* Parameters provided on repo-command endpoint
@@ -31,7 +32,11 @@
export class GrRepoApi implements RepoPluginApi {
private hook?: HookApi;
- constructor(readonly plugin: PluginApi) {}
+ private readonly reporting = appContext.reportingService;
+
+ constructor(readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'repo', 'constructor');
+ }
// TODO(TS): should mark as public since used in gr-change-metadata-api
_createHook(title: string) {
@@ -43,6 +48,7 @@
}
createCommand(title: string, callback: RepoCommandCallback) {
+ this.reporting.trackApi(this.plugin, 'repo', 'createCommand');
if (this.hook) {
console.warn('Already set up.');
return this;
@@ -57,6 +63,7 @@
}
onTap(callback: (event: Event) => boolean) {
+ this.reporting.trackApi(this.plugin, 'repo', 'onTap');
if (!this.hook) {
console.warn('Call createCommand first.');
return this;
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts
index 4bdd40e..3f75c0a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.ts
@@ -18,6 +18,7 @@
import '../../settings/gr-settings-view/gr-settings-menu-item';
import {PluginApi} from '../../../api/plugin';
import {SettingsPluginApi} from '../../../api/settings';
+import {appContext} from '../../../services/app-context';
export class GrSettingsApi implements SettingsPluginApi {
private _token: string;
@@ -26,27 +27,34 @@
private _moduleName?: string;
+ private readonly reporting = appContext.reportingService;
+
constructor(readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'settings', 'constructor');
// Generate default screen URL token, specific to plugin, and unique(ish).
this._token = plugin.getPluginName() + Math.random().toString(36).substr(5);
}
title(newTitle: string) {
+ this.reporting.trackApi(this.plugin, 'settings', 'title');
this._title = newTitle;
return this;
}
token(newToken: string) {
+ this.reporting.trackApi(this.plugin, 'settings', 'token');
this._token = newToken;
return this;
}
module(newModuleName: string) {
+ this.reporting.trackApi(this.plugin, 'settings', 'module');
this._moduleName = newModuleName;
return this;
}
build() {
+ this.reporting.trackApi(this.plugin, 'settings', 'build');
if (!this._moduleName) {
throw new Error('Settings screen custom element not defined!');
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.ts b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.ts
index a91b8d3..9a15bf5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.ts
@@ -15,6 +15,8 @@
* limitations under the License.
*/
import {StyleObject, StylesPluginApi} from '../../../api/styles';
+import {appContext} from '../../../services/app-context';
+import {PluginApi} from '../../../api/plugin';
/**
* @fileoverview We should consider dropping support for this API:
@@ -77,10 +79,17 @@
* TODO(TS): move to util
*/
export class GrStylesApi implements StylesPluginApi {
+ private readonly reporting = appContext.reportingService;
+
+ constructor(readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'styles', 'constructor');
+ }
+
/**
* Creates a new GrStyleObject with specified style properties.
*/
css(ruleStr: string) {
+ this.reporting.trackApi(this.plugin, 'styles', 'css');
return new GrStyleObject(ruleStr);
}
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
index 894ec6c..c7be7f6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
@@ -18,14 +18,20 @@
import {GrCustomPluginHeader} from './gr-custom-plugin-header';
import {PluginApi} from '../../../api/plugin';
import {ThemePluginApi} from '../../../api/theme';
+import {appContext} from '../../../services/app-context';
/**
* Defines api for theme, can be used to set header logo and title.
*/
export class GrThemeApi implements ThemePluginApi {
- constructor(private readonly plugin: PluginApi) {}
+ private readonly reporting = appContext.reportingService;
+
+ constructor(private readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'theme', 'constructor');
+ }
setHeaderLogoAndTitle(logoUrl: string, title: string) {
+ this.reporting.trackApi(this.plugin, 'theme', 'setHeaderLogoAndTitle');
this.plugin.hook('header-title', {replace: true}).onAttached(element => {
const customHeader: GrCustomPluginHeader = document.createElement(
'gr-custom-plugin-header'
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
index 91ca402..59203d3 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
@@ -25,7 +25,7 @@
margin-bottom: var(--spacing-m);
}
.agreementsUrl {
- border: 1px solid #b0bdcc;
+ border: 1px solid var(--border-color);
margin-bottom: var(--spacing-xl);
margin-left: var(--spacing-xl);
margin-right: var(--spacing-xl);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
index 3bb1458..4e6dd1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
@@ -58,7 +58,6 @@
gr-button.remove:focus {
--gr-button: {
@apply --gr-remove-button-style;
- color: #333;
}
}
gr-button.remove {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
index d105c5d..e55c8f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
@@ -22,7 +22,7 @@
display: inline-block;
border-radius: 50%;
background-size: cover;
- background-color: var(--avatar-background-color, #f1f2f3);
+ background-color: var(--avatar-background-color, var(--gray-background));
}
</style>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 7a6ce2c..60b891e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -61,7 +61,7 @@
tooltip = '';
// Note: don't assign a value to this, since constructor is called
- // after created, the initial value maybe overriden by this
+ // after created, the initial value maybe overridden by this
@property({type: String})
_initialTabindex?: string;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index 76bbd67..55408c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -23,12 +23,12 @@
font-size: var(--font-size-normal);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-normal);
- /* Explicitly set the background color of the diff to be white. We
+ /* Explicitly set the background color of the diff. We
* cannot use the diff content type ab because of the skip chunk preceding
* it, diff processor assumes the chunk of type skip/ab can be collapsed
* and hides our diff behind context control buttons.
* */
- --dark-add-highlight-color: white;
+ --dark-add-highlight-color: var(--background-color-primary);
}
gr-button {
margin-left: var(--spacing-m);
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 2fbbd7c..119ed20 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -54,8 +54,8 @@
export type Stop = HTMLElement | AbortStop;
/**
- * Type guard and checker to check if a stop can be targetted.
- * Abort stops cannot be targetted.
+ * Type guard and checker to check if a stop can be targeted.
+ * Abort stops cannot be targeted.
*/
export function isTargetable(stop: Stop): stop is HTMLElement {
return !(stop instanceof AbortStop);
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index d2f003b..4c2a417 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -52,7 +52,7 @@
// TODO(TS): maybe default to [] as only used in dom-repeat
@property({type: Array})
- comamnds?: Command[];
+ commands?: Command[];
@property({type: Boolean})
_loggedIn = false;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index 3fce16e..888f34f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -32,7 +32,7 @@
import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
/**
- * Requred values are text and value. mobileText and triggerText will
+ * Required values are text and value. mobileText and triggerText will
* fall back to text if not provided.
*
* If bottomText is not provided, nothing will display on the second
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index c744eab..2780fbe 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -120,6 +120,8 @@
private readonly flagsService = appContext.flagsService;
+ private readonly reporting = appContext.reportingService;
+
/** @override */
ready() {
super.ready();
@@ -238,6 +240,10 @@
_toggleCommitCollapsed() {
this._commitCollapsed = !this._commitCollapsed;
+ this.reporting.reportInteraction('toggle show all button', {
+ sectionName: 'Commit message',
+ toState: !this._commitCollapsed ? 'Show all' : 'Show less',
+ });
if (this._commitCollapsed) {
window.scrollTo(0, 0);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index 374cc62..0f530bf 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -68,6 +68,11 @@
border-radius: 0 0 4px 4px;
border-color: var(--border-color);
box-shadow: var(--elevation-level-1);
+ /* slightly up to cover rounded corner of the commit msg */
+ margin-top: calc(-1 * var(--spacing-xs));
+ /* To make this bar pop over editor, since editor has relative position.
+ */
+ position: relative;
}
.show-all-container .show-all-button {
margin-right: auto;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
index a3d038d..857d079 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
@@ -41,10 +41,12 @@
private readonly reporting = appContext.reportingService;
constructor(private readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'annotation', 'constructor');
plugin.on(EventType.ANNOTATE_DIFF, this);
}
setLayer(annotationCallback: AnnotationCallback) {
+ this.reporting.trackApi(this.plugin, 'annotation', 'setLayer');
if (this.annotationCallback) {
console.warn('Overwriting an existing plugin annotation layer.');
}
@@ -55,6 +57,7 @@
setCoverageProvider(
coverageProvider: CoverageProvider
): GrAnnotationActionsInterface {
+ this.reporting.trackApi(this.plugin, 'annotation', 'setCoverageProvider');
if (this.coverageProvider) {
console.warn('Overwriting an existing coverage provider.');
}
@@ -74,6 +77,7 @@
checkboxLabel: string,
onAttached: (checkboxEl: Element | null) => void
) {
+ this.reporting.trackApi(this.plugin, 'annotation', 'enableToggleCheckbox');
this.plugin.hook('annotation-toggler').onAttached(element => {
if (!element.content) {
this.reporting.error(new Error('plugin endpoint without content.'));
@@ -104,6 +108,7 @@
}
notify(path: string, start: number, end: number, side: Side) {
+ this.reporting.trackApi(this.plugin, 'annotation', 'notify');
for (const annotationLayer of this.annotationLayers) {
// Notify only the annotation layer that is associated with the specified
// path.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
index a4c6974..15d4680 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
@@ -68,7 +68,10 @@
ActionType = ActionType;
+ private readonly reporting = appContext.reportingService;
+
constructor(public plugin: PluginApi, el?: GrChangeActionsElement) {
+ this.reporting.trackApi(this.plugin, 'actions', 'constructor');
this.setEl(el);
}
@@ -100,6 +103,7 @@
}
addPrimaryActionKey(key: PrimaryActionKey) {
+ this.reporting.trackApi(this.plugin, 'actions', 'addPrimaryActionKey');
const el = this.ensureEl();
if (el.primaryActionKeys.includes(key)) {
return;
@@ -109,63 +113,77 @@
}
removePrimaryActionKey(key: string) {
+ this.reporting.trackApi(this.plugin, 'actions', 'removePrimaryActionKey');
const el = this.ensureEl();
el.primaryActionKeys = el.primaryActionKeys.filter(k => k !== key);
}
hideQuickApproveAction() {
+ this.reporting.trackApi(this.plugin, 'actions', 'hideQuickApproveAction');
this.ensureEl().hideQuickApproveAction();
}
setActionOverflow(type: ActionType, key: string, overflow: boolean) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setActionOverflow');
// TODO(TS): remove return, unclear why it was written
return this.ensureEl().setActionOverflow(type, key, overflow);
}
setActionPriority(type: ActionType, key: string, priority: ActionPriority) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setActionPriority');
// TODO(TS): remove return, unclear why it was written
return this.ensureEl().setActionPriority(type, key, priority);
}
setActionHidden(type: ActionType, key: string, hidden: boolean) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setActionHidden');
// TODO(TS): remove return, unclear why it was written
return this.ensureEl().setActionHidden(type, key, hidden);
}
add(type: ActionType, label: string): string {
+ this.reporting.trackApi(this.plugin, 'actions', 'add');
return this.ensureEl().addActionButton(type, label);
}
remove(key: string) {
+ this.reporting.trackApi(this.plugin, 'actions', 'remove');
// TODO(TS): remove return, unclear why it was written
return this.ensureEl().removeActionButton(key);
}
addTapListener(key: string, handler: EventListenerOrEventListenerObject) {
+ this.reporting.trackApi(this.plugin, 'actions', 'addTapListener');
this.ensureEl().addEventListener(key + '-tap', handler);
}
removeTapListener(key: string, handler: EventListenerOrEventListenerObject) {
+ this.reporting.trackApi(this.plugin, 'actions', 'removeTapListener');
this.ensureEl().removeEventListener(key + '-tap', handler);
}
setLabel(key: string, text: string) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setLabel');
this.ensureEl().setActionButtonProp(key, 'label', text);
}
setTitle(key: string, text: string) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setTitle');
this.ensureEl().setActionButtonProp(key, 'title', text);
}
setEnabled(key: string, enabled: boolean) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setEnabled');
this.ensureEl().setActionButtonProp(key, 'enabled', enabled);
}
setIcon(key: string, icon: string) {
+ this.reporting.trackApi(this.plugin, 'actions', 'setIcon');
this.ensureEl().setActionButtonProp(key, 'icon', icon);
}
getActionDetails(action: string) {
+ this.reporting.trackApi(this.plugin, 'actions', 'getActionDetails');
const el = this.ensureEl();
return (
el.getActionDetails(action) ||
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
index effebe1..de57794 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
@@ -25,15 +25,20 @@
ReplyChangedCallback,
ValueChangedDetail,
} from '../../../api/change-reply';
+import {appContext} from '../../../services/app-context';
/**
* GrChangeReplyInterface, provides a set of handy methods on reply dialog.
*/
export class GrChangeReplyInterface implements ChangeReplyPluginApi {
+ private readonly reporting = appContext.reportingService;
+
constructor(
readonly plugin: PluginApi,
readonly sharedApiElement: JsApiService
- ) {}
+ ) {
+ this.reporting.trackApi(this.plugin, 'reply', 'constructor');
+ }
get _el(): GrReplyDialog {
return (this.sharedApiElement.getElement(
@@ -42,18 +47,22 @@
}
getLabelValue(label: string): string {
+ this.reporting.trackApi(this.plugin, 'reply', 'getLabelValue');
return this._el.getLabelValue(label);
}
setLabelValue(label: string, value: string) {
+ this.reporting.trackApi(this.plugin, 'reply', 'setLabelValue');
this._el.setLabelValue(label, value);
}
send(includeComments?: boolean) {
+ this.reporting.trackApi(this.plugin, 'reply', 'send');
this._el.send(includeComments);
}
addReplyTextChangedCallback(handler: ReplyChangedCallback) {
+ this.reporting.trackApi(this.plugin, 'reply', 'addReplyTextChangedCb');
const hookApi = this.plugin.hook('reply-text');
const registeredHandler = (e: Event) => {
const ce = e as CustomEvent<ValueChangedDetail>;
@@ -74,6 +83,7 @@
}
addLabelValuesChangedCallback(handler: LabelsChangedCallback) {
+ this.reporting.trackApi(this.plugin, 'reply', 'addLabelValuesChangedCb');
const hookApi = this.plugin.hook('reply-label-scores');
const registeredHandler = (e: Event) => {
const ce = e as CustomEvent<LabelsChangedDetail>;
@@ -95,6 +105,7 @@
}
showMessage(message: string) {
+ this.reporting.trackApi(this.plugin, 'reply', 'showMessage');
this._el.setPluginMessage(message);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index 27bc591..7f9218a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -250,7 +250,7 @@
* });
* });
*
- * // Listen on your-special-event from pluignB
+ * // Listen on your-special-event from pluginB
* Gerrit.install(pluginB => {
* Gerrit.on("your-special-event", ({plugin}) => {
* // do something, plugin is pluginA
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 8c0fce26..db34e5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -74,7 +74,7 @@
const UNKNOWN_PLUGIN_PREFIX = '__$$__';
// Current API version for Plugin,
-// plugins with incompatible version will not be laoded.
+// plugins with incompatible version will not be loaded.
const API_VERSION = '0.1';
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
index 6b62291..f5b1fca 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
@@ -225,7 +225,7 @@
assert.isTrue(alertStub.calledTwice);
});
- test('plugins installed failed becasue of wrong version', async () => {
+ test('plugins installed failed because of wrong version', async () => {
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
index cd35d4e..150c45a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
@@ -18,6 +18,7 @@
import {RequestPayload} from '../../../types/common';
import {appContext} from '../../../services/app-context';
import {ErrorCallback, RestPluginApi} from '../../../api/rest';
+import {PluginApi} from '../../../api/plugin';
async function getErrorMessage(response: Response): Promise<string> {
const text = await response.text();
@@ -36,33 +37,44 @@
export class GrPluginRestApi implements RestPluginApi {
private readonly restApi = appContext.restApiService;
- constructor(private readonly prefix = '') {}
+ private readonly reporting = appContext.reportingService;
+
+ constructor(readonly plugin: PluginApi, private readonly prefix = '') {
+ this.reporting.trackApi(this.plugin, 'rest', 'constructor');
+ }
getLoggedIn() {
+ this.reporting.trackApi(this.plugin, 'rest', 'getLoggedIn');
return this.restApi.getLoggedIn();
}
getVersion() {
+ this.reporting.trackApi(this.plugin, 'rest', 'getVersion');
return this.restApi.getVersion();
}
getConfig() {
+ this.reporting.trackApi(this.plugin, 'rest', 'getConfig');
return this.restApi.getConfig();
}
invalidateReposCache() {
+ this.reporting.trackApi(this.plugin, 'rest', 'invalidateReposCache');
this.restApi.invalidateReposCache();
}
getAccount() {
+ this.reporting.trackApi(this.plugin, 'rest', 'getAccount');
return this.restApi.getAccount();
}
getAccountCapabilities(capabilities: string[]) {
+ this.reporting.trackApi(this.plugin, 'rest', 'getAccountCapabilities');
return this.restApi.getAccountCapabilities(capabilities);
}
getRepos(filter: string, reposPerPage: number, offset?: number) {
+ this.reporting.trackApi(this.plugin, 'rest', 'getRepos');
return this.restApi.getRepos(filter, reposPerPage, offset);
}
@@ -100,6 +112,7 @@
errFn?: ErrorCallback,
contentType?: string
): Promise<Response | void> {
+ this.reporting.trackApi(this.plugin, 'rest', 'fetch');
return this.restApi.send(
method,
this.prefix + url,
@@ -119,6 +132,7 @@
errFn?: ErrorCallback,
contentType?: string
) {
+ this.reporting.trackApi(this.plugin, 'rest', 'send');
// Plugins typically don't want Gerrit to show error dialogs for failed
// requests. So we are defining a default errFn here, even if it is not
// explicitly set by the caller.
@@ -167,6 +181,7 @@
}
get(url: string) {
+ this.reporting.trackApi(this.plugin, 'rest', 'get');
return this.send(HttpMethod.GET, url);
}
@@ -176,6 +191,7 @@
errFn?: ErrorCallback,
contentType?: string
) {
+ this.reporting.trackApi(this.plugin, 'rest', 'post');
return this.send(HttpMethod.POST, url, payload, errFn, contentType);
}
@@ -185,10 +201,12 @@
errFn?: ErrorCallback,
contentType?: string
) {
+ this.reporting.trackApi(this.plugin, 'rest', 'put');
return this.send(HttpMethod.PUT, url, payload, errFn, contentType);
}
delete(url: string) {
+ this.reporting.trackApi(this.plugin, 'rest', 'delete');
return this.fetch(HttpMethod.DELETE, url).then(response => {
if (response.status !== 204) {
return response.text().then(text => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index e7843af..68fb96b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -54,6 +54,7 @@
import {ChangeReplyPluginApi} from '../../../api/change-reply';
import {RestPluginApi} from '../../../api/rest';
import {HookApi, RegisterOptions} from '../../../api/hook';
+import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
/**
* Plugin-provided custom components can affect content in extension
@@ -84,6 +85,8 @@
private readonly jsApi = appContext.jsApiService;
+ private readonly report = appContext.reportingService;
+
constructor(url?: string) {
this.domHooks = new GrDomHooksManager(this);
@@ -97,6 +100,7 @@
this._url = new URL(url);
this._name = getPluginNameFromUrl(this._url) ?? 'NULL';
+ this.report.trackApi(this, 'plugin', 'constructor');
}
getPluginName() {
@@ -104,6 +108,7 @@
}
registerStyleModule(endpoint: string, moduleName: string) {
+ this.report.trackApi(this, 'plugin', 'registerStyleModule');
getPluginEndpoints().registerModule(this, {
endpoint,
type: EndpointType.STYLE,
@@ -119,6 +124,7 @@
moduleName?: string,
options?: RegisterOptions
): HookApi {
+ this.report.trackApi(this, 'plugin', 'registerCustomComponent');
return this._registerCustomComponent(endpointName, moduleName, options);
}
@@ -133,6 +139,7 @@
moduleName?: string,
options?: RegisterOptions
): HookApi {
+ this.report.trackApi(this, 'plugin', 'registerDynamicCustomComponent');
const fullEndpointName = `${endpointName}-${this.getPluginName()}`;
return this._registerCustomComponent(
fullEndpointName,
@@ -169,19 +176,23 @@
* element for the first call.
*/
hook(endpointName: string, options?: RegisterOptions) {
+ this.report.trackApi(this, 'plugin', 'hook');
return this.registerCustomComponent(endpointName, undefined, options);
}
getServerInfo() {
+ this.report.trackApi(this, 'plugin', 'getServerInfo');
return appContext.restApiService.getConfig();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(eventName: EventType, callback: (...args: any[]) => any) {
+ this.report.trackApi(this, 'plugin', 'on');
this.jsApi.addEventCallback(eventName, callback);
}
url(path?: string) {
+ this.report.trackApi(this, 'plugin', 'url');
if (!this._url) throw new Error('plugin url not set');
const relPath = '/plugins/' + this._name + (path || '/');
const sameOriginPath = window.location.origin + `${getBaseUrl()}${relPath}`;
@@ -200,6 +211,7 @@
}
screenUrl(screenName?: string) {
+ this.report.trackApi(this, 'plugin', 'screenUrl');
const origin = location.origin;
const base = getBaseUrl();
const tokenPart = screenName ? '/' + screenName : '';
@@ -216,21 +228,25 @@
}
get(url: string, callback?: SendCallback) {
+ this.report.trackApi(this, 'plugin', 'get');
console.warn('.get() is deprecated! Use .restApi().get()');
return this._send(HttpMethod.GET, url, callback);
}
post(url: string, payload: RequestPayload, callback?: SendCallback) {
+ this.report.trackApi(this, 'plugin', 'post');
console.warn('.post() is deprecated! Use .restApi().post()');
return this._send(HttpMethod.POST, url, callback, payload);
}
put(url: string, payload: RequestPayload, callback?: SendCallback) {
+ this.report.trackApi(this, 'plugin', 'put');
console.warn('.put() is deprecated! Use .restApi().put()');
return this._send(HttpMethod.PUT, url, callback, payload);
}
delete(url: string, callback?: SendCallback) {
+ this.report.trackApi(this, 'plugin', 'delete');
console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
return this.restApi()
.delete(this.url(url))
@@ -286,19 +302,19 @@
}
styles(): StylesPluginApi {
- return new GrStylesApi();
+ return new GrStylesApi(this);
}
restApi(prefix?: string): RestPluginApi {
- return new GrPluginRestApi(prefix);
+ return new GrPluginRestApi(this, prefix);
}
- attributeHelper(element: HTMLElement) {
- return new GrAttributeHelper(element);
+ attributeHelper(element: HTMLElement): AttributeHelperPluginApi {
+ return new GrAttributeHelper(this, element);
}
eventHelper(element: HTMLElement): EventHelperPluginApi {
- return new GrEventHelper(element);
+ return new GrEventHelper(this, element);
}
popup(): Promise<PopupPluginApi>;
@@ -314,6 +330,7 @@
}
screen(screenName: string, moduleName?: string) {
+ this.report.trackApi(this, 'plugin', 'screen');
if (moduleName && typeof moduleName !== 'string') {
console.error(
'.screen(pattern, callback) deprecated, use ' +
@@ -328,6 +345,7 @@
}
_getScreenName(screenName: string) {
+ this.report.trackApi(this, 'plugin', '_getScreenName');
return `${this.getPluginName()}-screen-${screenName}`;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts
index d4b51a8..205f0fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api.ts
@@ -25,9 +25,12 @@
export class GrReportingJsApi implements ReportingPluginApi {
private readonly reporting = appContext.reportingService;
- constructor(private readonly plugin: PluginApi) {}
+ constructor(private readonly plugin: PluginApi) {
+ this.reporting.trackApi(this.plugin, 'reporting', 'constructor');
+ }
reportInteraction(eventName: string, details?: EventDetails) {
+ this.reporting.trackApi(this.plugin, 'reporting', 'reportInteraction');
this.reporting.reportInteraction(
`${this.plugin.getPluginName()}-${eventName}`,
details
@@ -35,6 +38,7 @@
}
reportLifeCycle(eventName: string, details?: EventDetails) {
+ this.reporting.trackApi(this.plugin, 'reporting', 'reportLifeCycle');
this.reporting.reportLifeCycle(
`${this.plugin.getPluginName()}-${eventName}`,
details
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
index a335db7..8581a0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
@@ -46,7 +46,6 @@
gr-button.remove:focus {
--gr-button: {
@apply --gr-remove-button-style;
- color: #333;
}
}
gr-button.remove {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 68f15dc..89abd57 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -457,7 +457,7 @@
* Send an XHR.
*
* @return Promise resolves to Response/ParsedJSON only if the request is successful
- * (i.e. no exception and response.ok is trsue). If response fails then
+ * (i.e. no exception and response.ok is true). If response fails then
* promise resolves either to void if errFn is set or rejects if errFn
* is not set */
send(req: SendRequest): Promise<Response | ParsedJSON | undefined> {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index b0b40dd..885db2a 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -112,7 +112,7 @@
@property({type: Boolean})
hideBorder = false;
- /** Text input should be rendered in monspace font. */
+ /** Text input should be rendered in monospace font. */
@property({type: Boolean})
monospace = false;
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
index 77d2d00..75ad608 100644
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
+++ b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
@@ -69,13 +69,13 @@
// Handler for mouseenter event
private mouseenterHandler?: (e: MouseEvent) => void;
- // Hanlder for scrolling on window
+ // Handler for scrolling on window
private readonly windowScrollHandler: () => void;
- // Hanlder for showing the tooltip, will be attached to certain events
+ // Handler for showing the tooltip, will be attached to certain events
private readonly showHandler: () => void;
- // Hanlder for hiding the tooltip, will be attached to certain events
+ // Handler for hiding the tooltip, will be attached to certain events
private readonly hideHandler: () => void;
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
diff --git a/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts b/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts
index 662d6bf..57e034f 100644
--- a/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts
+++ b/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts
@@ -29,7 +29,7 @@
// is used. To ensure that this import can't be avoided, the second parameter
// is added. Usage example:
// class Element extends IronFitMixin(PolymerElement, IronFitBehavior as IronFitBehavior)
-// The code 'IronFitBehavior as IronFitBehavior' required, becuase IronFitBehavior
+// The code 'IronFitBehavior as IronFitBehavior' required, because IronFitBehavior
// defined as an object, not as IronFitBehavior instance.
export const IronFitMixin = <T extends Constructor<PolymerElement>>(
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 63a05aa..ab85b87 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -544,7 +544,7 @@
}
/**
- * Shortcut manager, holds all hosts, bindings and listners.
+ * Shortcut manager, holds all hosts, bindings and listeners.
*/
export class ShortcutManager {
private readonly activeHosts = new Map<PolymerElement, Map<string, string>>();
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 7a6253b..f03c7e6 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -254,6 +254,14 @@
license: SharedLicenses.Polymer2017
},
{
+ name: "@types/resize-observer-browser",
+ license: {
+ name: 'DefinitelyTyped',
+ type: LicenseTypes.Mit,
+ packageLicenseFile: "LICENSE"
+ }
+ },
+ {
name: "@webcomponents/shadycss",
license: SharedLicenses.Polymer2017
},
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 3351386..5e15990 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -25,6 +25,7 @@
"@polymer/paper-tabs": "^3.1.0",
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/polymer": "^3.4.1",
+ "@types/resize-observer-browser": "^0.1.5",
"@webcomponents/shadycss": "^1.9.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
"ba-linkify": "file:../../lib/ba-linkify/src/",
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 09e0724..0369ccf 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -70,7 +70,7 @@
eventEmitter: () => new EventEmitter(),
authService: () => new Auth(appContext.eventEmitter),
restApiService: () => new GrRestApiInterface(appContext.authService),
- changeService: () => new ChangeService(appContext.restApiService),
+ changeService: () => new ChangeService(),
checksService: () => new ChecksService(),
jsApiService: () => new GrJsApiInterface(),
});
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index b2bdcfe..e7472de 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -46,10 +46,18 @@
// Must only be used by the change service or whatever is in control of this
// model.
export function updateState(change?: ParsedChangeInfo) {
- privateState$.next({
- ...privateState$.getValue(),
- change,
- });
+ const current = privateState$.getValue();
+ // We want to make it easy for subscribers to react to change changes, so we
+ // are explicitly emitting and additional `undefined` when the change number
+ // changes. So if you are subscribed to the latestPatchsetNumber for example,
+ // then you can rely on emissions even if the old and the new change have the
+ // same latestPatchsetNumber.
+ if (change !== undefined && current.change !== undefined) {
+ if (change._number !== current.change._number) {
+ privateState$.next({...current, change: undefined});
+ }
+ }
+ privateState$.next({...current, change});
}
/**
@@ -91,9 +99,6 @@
*
* Note that this selector can emit a patchNum without the change being
* available!
- *
- * TODO: It would be good to assert/enforce somehow that currentPatchNum$ cannot
- * emit 'PARENT'.
*/
export const currentPatchNum$: Observable<
PatchSetNum | undefined
diff --git a/polygerrit-ui/app/services/change/change-service.ts b/polygerrit-ui/app/services/change/change-service.ts
index 6a9a5e9..c292fb5 100644
--- a/polygerrit-ui/app/services/change/change-service.ts
+++ b/polygerrit-ui/app/services/change/change-service.ts
@@ -14,28 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import {routerChangeNum$} from '../router/router-model';
import {updateState} from './change-model';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-import {switchMap, tap} from 'rxjs/operators';
-import {of, from} from 'rxjs';
+import {ParsedChangeInfo} from '../../types/types';
export class ChangeService {
- private routerChangeNumEffect = routerChangeNum$.pipe(
- switchMap(changeNum => {
- if (!changeNum) return of(undefined);
- return from(this.restApiService.getChangeDetail(changeNum));
- }),
- tap(change => {
- updateState(change ?? undefined);
- })
- );
-
- constructor(private readonly restApiService: RestApiService) {
- this.routerChangeNumEffect.subscribe();
+ constructor() {
+ // TODO: In the future we will want to make restApiService.getChangeDetail()
+ // calls from a switchMap() here. For now just make sure to invalidate the
+ // change when no changeNum is set.
+ routerChangeNum$.subscribe(changeNum => {
+ if (!changeNum) updateState(undefined);
+ });
}
- // TODO: Remove.
- dontDoAnything() {}
+ /**
+ * This is a temporary indirection between change-view, which currently
+ * manages what the current change is, and the change-model, which will
+ * become the source of truth in the future. We will extract a substantial
+ * amount of code from change-view and move it into this change-service. This
+ * will take some time ...
+ */
+ updateChange(change: ParsedChangeInfo) {
+ updateState(change);
+ }
}
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 6705a85..1c5b862 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -26,6 +26,7 @@
RunStatus,
} from '../../api/checks';
import {distinctUntilChanged, map} from 'rxjs/operators';
+import {PatchSetNumber} from '../../types/common';
// This is a convenience type for working with results, because when working
// with a bunch of results you will typically also want to know about the run
@@ -34,28 +35,51 @@
interface ChecksProviderState {
pluginName: string;
+ loading: boolean;
config?: ChecksApiConfig;
runs: CheckRun[];
actions: Action[];
}
interface ChecksState {
- [name: string]: ChecksProviderState;
+ patchsetNumber?: PatchSetNumber;
+ providerNameToState: {
+ [name: string]: ChecksProviderState;
+ };
}
-const initialState: ChecksState = {};
+const initialState: ChecksState = {
+ providerNameToState: {},
+};
const privateState$ = new BehaviorSubject(initialState);
// Re-exporting as Observable so that you can only subscribe, but not emit.
export const checksState$: Observable<ChecksState> = privateState$;
-export const aPluginHasRegistered = checksState$.pipe(
+export const checksPatchsetNumber$ = checksState$.pipe(
+ map(state => state.patchsetNumber),
+ distinctUntilChanged()
+);
+
+export const checksProviderState$ = checksState$.pipe(
+ map(state => state.providerNameToState),
+ distinctUntilChanged()
+);
+
+export const aPluginHasRegistered$ = checksProviderState$.pipe(
map(state => Object.keys(state).length > 0),
distinctUntilChanged()
);
-export const allActions$ = checksState$.pipe(
+export const someProvidersAreLoading$ = checksProviderState$.pipe(
+ map(state => {
+ return Object.values(state).some(providerState => providerState.loading);
+ }),
+ distinctUntilChanged()
+);
+
+export const allActions$ = checksProviderState$.pipe(
map(state => {
return Object.values(state).reduce(
(allActions: Action[], providerState: ChecksProviderState) => [
@@ -67,7 +91,7 @@
})
);
-export const allRuns$ = checksState$.pipe(
+export const allRuns$ = checksProviderState$.pipe(
map(state => {
return Object.values(state).reduce(
(allRuns: CheckRun[], providerState: ChecksProviderState) => [
@@ -79,7 +103,19 @@
})
);
-export const allResults$ = checksState$.pipe(
+export const checkToPluginMap$ = checksProviderState$.pipe(
+ map(state => {
+ const map = new Map<string, string>();
+ for (const [pluginName, providerState] of Object.entries(state)) {
+ for (const run of providerState.runs) {
+ map.set(run.checkName, pluginName);
+ }
+ }
+ return map;
+ })
+);
+
+export const allResults$ = checksProviderState$.pipe(
map(state => {
return Object.values(state)
.reduce(
@@ -104,8 +140,10 @@
config?: ChecksApiConfig
) {
const nextState = {...privateState$.getValue()};
- nextState[pluginName] = {
+ nextState.providerNameToState = {...nextState.providerNameToState};
+ nextState.providerNameToState[pluginName] = {
pluginName,
+ loading: false,
config,
runs: [],
actions: [],
@@ -177,16 +215,34 @@
status: RunStatus.COMPLETED,
};
+export function updateStateSetLoading(pluginName: string) {
+ const nextState = {...privateState$.getValue()};
+ nextState.providerNameToState = {...nextState.providerNameToState};
+ nextState.providerNameToState[pluginName] = {
+ ...nextState.providerNameToState[pluginName],
+ loading: true,
+ };
+ privateState$.next(nextState);
+}
+
export function updateStateSetResults(
pluginName: string,
runs: CheckRun[],
actions: Action[] = []
) {
const nextState = {...privateState$.getValue()};
- nextState[pluginName] = {
- ...nextState[pluginName],
+ nextState.providerNameToState = {...nextState.providerNameToState};
+ nextState.providerNameToState[pluginName] = {
+ ...nextState.providerNameToState[pluginName],
+ loading: false,
runs: [...runs],
actions: [...actions],
};
privateState$.next(nextState);
}
+
+export function updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
+ const nextState = {...privateState$.getValue()};
+ nextState.patchsetNumber = patchsetNumber;
+ privateState$.next(nextState);
+}
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
index 2e63f98..fd1f810 100644
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ b/polygerrit-ui/app/services/checks/checks-service.ts
@@ -16,9 +16,9 @@
*/
import {
+ filter,
switchMap,
takeWhile,
- tap,
throttleTime,
withLatestFrom,
} from 'rxjs/operators';
@@ -29,8 +29,15 @@
FetchResponse,
ResponseCode,
} from '../../api/checks';
-import {change$, currentPatchNum$} from '../change/change-model';
-import {updateStateSetProvider, updateStateSetResults} from './checks-model';
+import {change$, changeNum$, latestPatchNum$} from '../change/change-model';
+import {
+ updateStateSetLoading,
+ checkToPluginMap$,
+ updateStateSetProvider,
+ updateStateSetResults,
+ checksPatchsetNumber$,
+ updateStateSetPatchset,
+} from './checks-model';
import {
BehaviorSubject,
combineLatest,
@@ -38,14 +45,34 @@
Observable,
of,
Subject,
+ timer,
} from 'rxjs';
+import {PatchSetNumber} from '../../types/common';
export class ChecksService {
private readonly providers: {[name: string]: ChecksProvider} = {};
private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
- private changeAndPatchNum$ = change$.pipe(withLatestFrom(currentPatchNum$));
+ private checkToPluginMap = new Map<string, string>();
+
+ private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
+
+ constructor() {
+ checkToPluginMap$.subscribe(map => {
+ this.checkToPluginMap = map;
+ });
+ latestPatchNum$.subscribe(num => {
+ updateStateSetPatchset(num);
+ });
+ document.addEventListener('visibilitychange', () => {
+ this.documentVisibilityChange$.next(undefined);
+ });
+ }
+
+ setPatchset(num: PatchSetNumber) {
+ updateStateSetPatchset(num);
+ }
reload(pluginName: string) {
this.reloadSubjects[pluginName].next();
@@ -55,6 +82,12 @@
Object.keys(this.providers).forEach(key => this.reload(key));
}
+ reloadForCheck(checkName?: string) {
+ if (!checkName) return;
+ const plugin = this.checkToPluginMap.get(checkName);
+ if (plugin) this.reload(plugin);
+ }
+
register(
pluginName: string,
provider: ChecksProvider,
@@ -63,38 +96,52 @@
this.providers[pluginName] = provider;
this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
updateStateSetProvider(pluginName, config);
- // Both, changed numbers and and announceUpdate request should trigger.
+ const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000;
+ // Various events should trigger fetching checks from the provider:
+ // 1. Change number and patchset number changes.
+ // 2. Specific reload requests.
+ // 3. Regular polling starting with an initial fetch right now.
+ // 4. A hidden Gerrit tab becoming visible.
combineLatest([
- this.changeAndPatchNum$,
+ changeNum$,
+ checksPatchsetNumber$,
this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
+ timer(0, pollIntervalMs),
+ this.documentVisibilityChange$,
])
.pipe(
takeWhile(_ => !!this.providers[pluginName]),
+ filter(_ => document.visibilityState !== 'hidden'),
+ withLatestFrom(change$),
switchMap(
- ([[change, patchNum], _]): Observable<FetchResponse> => {
- if (!change || !patchNum || typeof patchNum !== 'number') {
+ ([[changeNum, patchNum], change]): Observable<FetchResponse> => {
+ if (
+ !change ||
+ !changeNum ||
+ !patchNum ||
+ typeof patchNum !== 'number'
+ ) {
return of({
responseCode: ResponseCode.OK,
runs: [],
});
}
const data: ChangeData = {
- changeNumber: change._number,
+ changeNumber: changeNum,
patchsetNumber: patchNum,
repo: change.project,
};
+ updateStateSetLoading(pluginName);
return from(this.providers[pluginName].fetch(data));
}
- ),
- tap(response => {
- updateStateSetResults(
- pluginName,
- response.runs ?? [],
- response.actions
- );
- })
+ )
)
- .subscribe(() => {});
- this.reload(pluginName);
+ .subscribe(response => {
+ updateStateSetResults(
+ pluginName,
+ response.runs ?? [],
+ response.actions
+ );
+ });
}
}
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 176464f..ea532ea 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -24,7 +24,7 @@
return undefined;
}
-export function iconForCategory(category: Category) {
+export function iconForCategory(category: Category | 'SUCCESS') {
switch (category) {
case Category.ERROR:
return 'error';
@@ -32,6 +32,8 @@
return 'info-outline';
case Category.WARNING:
return 'warning';
+ case 'SUCCESS':
+ return 'check-circle-outline';
default:
assertNever(category, `Unsupported category: ${category}`);
}
@@ -74,8 +76,12 @@
}
export function iconForRun(run: CheckRun) {
- const category = worstCategory(run);
- return category ? iconForCategory(category) : iconForStatus(run.status);
+ if (run.status !== RunStatus.COMPLETED) {
+ return iconForStatus(run.status);
+ } else {
+ const category = worstCategory(run);
+ return category ? iconForCategory(category) : iconForStatus(run.status);
+ }
}
export function iconForStatus(status: RunStatus) {
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 4ca983a..1be1d63 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -17,6 +17,7 @@
import {NumericChangeId} from '../../types/common';
import {EventDetails} from '../../api/reporting';
+import {PluginApi} from '../../api/plugin';
export type EventValue = string | number | {error?: Error};
@@ -61,7 +62,7 @@
timeEnd(name: string, eventDetails?: EventDetails): void;
/**
* Reports just line timeEnd, but additionally reports an average given a
- * denominator and a separate reporiting name for the average.
+ * denominator and a separate reporting name for the average.
*
* @param name Timing name.
* @param averageName Average timing name.
@@ -74,7 +75,7 @@
denominator: number
): void;
/**
- * Get a timer object to for reporing a user timing. The start time will be
+ * Get a timer object for reporting a user timing. The start time will be
* the time that the object has been created, and the end time will be the
* time that the "end" method is called on the object.
*/
@@ -97,9 +98,10 @@
* Every execution is only reported once per session.
*/
reportExecution(id: string, details: EventDetails): void;
+ trackApi(plugin: PluginApi, object: string, method: string): void;
reportInteraction(eventName: string, details?: EventDetails): void;
/**
- * A draft interaction was started. Update the time-betweeen-draft-actions
+ * A draft interaction was started. Update the time-between-draft-actions
* timer.
*/
recordDraftInteraction(): 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 af06450..e57670d 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -20,6 +20,7 @@
import {hasOwnProperty} from '../../utils/common-util';
import {NumericChangeId} from '../../types/common';
import {EventDetails} from '../../api/reporting';
+import {PluginApi} from '../../api/plugin';
// Latency reporting constants.
@@ -651,7 +652,7 @@
if (baseTime !== 0) {
window.performance.measure(name, `${name}-start`);
} else {
- // Microsft Edge does not handle the 2nd param correctly
+ // Microsoft Edge does not handle the 2nd param correctly
// (if undefined).
window.performance.measure(name);
}
@@ -659,7 +660,7 @@
/**
* Reports just line timeEnd, but additionally reports an average given a
- * denominator and a separate reporiting name for the average.
+ * denominator and a separate reporting name for the average.
*
* @param name Timing name.
* @param averageName Average timing name.
@@ -703,7 +704,7 @@
}
/**
- * Get a timer object to for reporing a user timing. The start time will be
+ * Get a timer object to for reporting a user timing. The start time will be
* the time that the object has been created, and the end time will be the
* time that the "end" method is called on the object.
*/
@@ -800,12 +801,20 @@
id,
undefined,
details,
- false
+ true // skip console log
);
}
+ trackApi(plugin: PluginApi, object: string, method: string) {
+ this.reportExecution('plugin-api', {
+ plugin: plugin.getPluginName(),
+ object,
+ method,
+ });
+ }
+
/**
- * A draft interaction was started. Update the time-betweeen-draft-actions
+ * A draft interaction was started. Update the time-between-draft-actions
* timer.
*/
recordDraftInteraction() {
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 484ce45..7d66484 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -16,6 +16,7 @@
*/
import {ReportingService, Timer} from './gr-reporting';
import {EventDetails} from '../../api/reporting';
+import {PluginApi} from '../../api/plugin';
export class MockTimer implements Timer {
end(): this {
@@ -67,6 +68,9 @@
reportExecution: (id: string, details: EventDetails) => {
log(`reportExecution '${id}': ${JSON.stringify(details)}`);
},
+ trackApi: (plugin: PluginApi, object: string, method: string) => {
+ log(`trackApi '${plugin}', ${object}, ${method}`);
+ },
reportExtension: () => {},
reportInteraction: (eventName: string, details?: EventDetails) => {
log(`reportInteraction '${eventName}': ${JSON.stringify(details)}`);
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.ts b/polygerrit-ui/app/styles/gr-voting-styles.ts
index d4e6d52..c1989de 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.ts
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -27,7 +27,7 @@
<style>
:host {
--vote-chip-styles: {
- border: 1px solid rgba(0,0,0,.12);
+ border: 1px solid var(--border-color);
border-radius: 1em;
box-shadow: none;
box-sizing: border-box;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index c3b0681..18c12b0 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -76,8 +76,6 @@
--info-background: var(--blue-50);
--selected-foreground: var(--blue-700);
--selected-background: var(--blue-50);
- --info-deemphasized-foreground: var(--gray-300);
- --info-deemphasized-background: var(--gray-50);
--success-foreground: var(--green-700);
--success-background: var(--green-50);
--gray-foreground: var(--gray-700);
@@ -101,6 +99,7 @@
--tooltip-text-color: white;
--negative-red-text-color: #d93025;
--positive-green-text-color: #188038;
+ --indirect-ancestor-text-color: var(--green-700);
/* background colors */
/* primary background colors */
@@ -171,7 +170,7 @@
--line-height-mono: 1.286rem; /* 18px */
--line-height-small: 1.143rem; /* 16px */
--line-height-normal: 1.429rem; /* 20px */
- --line-height-h3: 1.714rem; /* 24px */
+ --line-height-h3: 1.715rem; /* 24px */
--line-height-h2: 2rem; /* 28px */
--line-height-h1: 2.286rem; /* 32px */
--font-weight-normal: 400; /* 400 is the same as 'normal' */
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 4057f7f..5455a24 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -44,9 +44,10 @@
--warning-background: var(--orange-900);
--info-foreground: var(--blue-200);
--info-background: var(--blue-900);
- --info-deemphasized-foreground: var(--gray-700);
- --info-deemphasized-background: var(--primary-text-color);
+ --selected-foreground: var(--blue-200);
+ --selected-background: var(--blue-900);
--success-foreground: var(--green-200);
+ --success-background: var(--green-900);
--gray-foreground: var(--gray-100);
--gray-background: var(--gray-900);
--tag-background: var(--cyan-900);
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 0d751ab..46d0173d 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -48,7 +48,7 @@
"include": [
// Items below must be in sync with the src_dirs list in the BUILD file
// Also items must be in sync with tsconfig_bazel.json, tsconfig_bazel_test.json
- // (include and exclude arrays are overriden when extends)
+ // (include and exclude arrays are overridden when extends)
"api/**/*",
"constants/**/*",
"elements/**/*",
diff --git a/polygerrit-ui/app/tsconfig_bazel.json b/polygerrit-ui/app/tsconfig_bazel.json
index 46bd926..dfd2078 100644
--- a/polygerrit-ui/app/tsconfig_bazel.json
+++ b/polygerrit-ui/app/tsconfig_bazel.json
@@ -9,7 +9,7 @@
"include": [
// Items below must be in sync with the src_dirs list in the BUILD file
// Also items must be in sync with tsconfig.json, tsconfig_bazel_test.json
- // (include and exclude arrays are overriden when extends)
+ // (include and exclude arrays are overridden when extends)
"api/**/*",
"constants/**/*",
"elements/**/*",
diff --git a/polygerrit-ui/app/tsconfig_bazel_test.json b/polygerrit-ui/app/tsconfig_bazel_test.json
index 1b6c226..9c2ff93 100644
--- a/polygerrit-ui/app/tsconfig_bazel_test.json
+++ b/polygerrit-ui/app/tsconfig_bazel_test.json
@@ -14,7 +14,7 @@
"include": [
// Items below must be in sync with the src_dirs list in the BUILD file
// Also items must be in sync with tsconfig.json, tsconfig_test.json
- // (include and exclude arrays are overriden when extends)
+ // (include and exclude arrays are overridden when extends)
"api/**/*",
"constants/**/*",
"elements/**/*",
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 95801e7..1bfabf8 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -76,6 +76,7 @@
export type ParsedJSON = BrandType<unknown, '_parsedJSON'>;
export type PatchSetNum = BrandType<'PARENT' | 'edit' | number, '_patchSet'>;
+export type PatchSetNumber = BrandType<number, '_patchSet'>;
export const EditPatchSetNum = 'edit' as PatchSetNum;
// TODO(TS): This is not correct, it is better to have a separate ApiPatchSetNum
@@ -1021,7 +1022,7 @@
*/
export interface PluginConfigInfo {
has_avatars: boolean;
- // The following 2 properies exists in Java class, but don't mention in docs
+ // The following 2 properties exists in Java class, but don't mention in docs
js_resource_paths: string[];
html_resource_paths: string[];
}
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 6b05fad..5965453 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -19,6 +19,7 @@
import {UIComment} from '../utils/comment-util';
import {FetchRequest} from './types';
import {MovedLinkClickedEventDetail} from '../api/diff';
+import {Category, RunStatus} from '../api/checks';
export interface TitleChangeEventDetail {
title: string;
@@ -152,6 +153,7 @@
export interface TabState {
commentTab?: CommentTabState;
+ checksTab?: ChecksTabState;
}
export enum CommentTabState {
@@ -160,6 +162,11 @@
SHOW_ALL = 'show all',
}
+export interface ChecksTabState {
+ statusOrCategory?: RunStatus | Category;
+ checkName?: string;
+}
+
export type SwitchTabEvent = CustomEvent<SwitchTabEventDetail>;
declare global {
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 1228863..a7f8b49 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -173,28 +173,44 @@
return states;
}
-export function isOwner(change?: ChangeInfo, account?: AccountInfo) {
+export function isOwner(change?: ChangeInfo, account?: AccountInfo): boolean {
if (!change || !account) return false;
return change.owner?._account_id === account._account_id;
}
-export function isReviewer(change?: ChangeInfo, account?: AccountInfo) {
+export function isReviewer(
+ change?: ChangeInfo,
+ account?: AccountInfo
+): boolean {
if (!change || !account) return false;
const reviewers = change.reviewers.REVIEWER ?? [];
return reviewers.some(r => r._account_id === account._account_id);
}
-export function isUploader(change?: ChangeInfo, account?: AccountInfo) {
+export function isCc(change?: ChangeInfo, account?: AccountInfo): boolean {
+ if (!change || !account) return false;
+ const ccs = change.reviewers.CC ?? [];
+ return ccs.some(r => r._account_id === account._account_id);
+}
+
+export function isUploader(
+ change?: ChangeInfo,
+ account?: AccountInfo
+): boolean {
if (!change || !account) return false;
const rev = getCurrentRevision(change);
return rev?.uploader?._account_id === account._account_id;
}
-export function isInvolved(change?: ChangeInfo, account?: AccountInfo) {
+export function isInvolved(
+ change?: ChangeInfo,
+ account?: AccountInfo
+): boolean {
const owner = isOwner(change, account);
const uploader = isUploader(change, account);
const reviewer = isReviewer(change, account);
- return owner || uploader || reviewer;
+ const cc = isCc(change, account);
+ return owner || uploader || reviewer || cc;
}
export function getCurrentRevision(change?: ChangeInfo) {
diff --git a/polygerrit-ui/app/utils/common-util_test.js b/polygerrit-ui/app/utils/common-util_test.js
index 917d652b..d6d66d7 100644
--- a/polygerrit-ui/app/utils/common-util_test.js
+++ b/polygerrit-ui/app/utils/common-util_test.js
@@ -29,7 +29,7 @@
assert.isTrue(hasOwnProperty(obj, 'name with spaces'));
assert.isFalse(hasOwnProperty(obj, 'def'));
});
- test('object prototype has overriden hasOwnProperty', () => {
+ test('object prototype has overridden hasOwnProperty', () => {
const F = function() {
this.abc = 23;
};
diff --git a/polygerrit-ui/app/utils/date-util.ts b/polygerrit-ui/app/utils/date-util.ts
index fad5041..3af8c59 100644
--- a/polygerrit-ui/app/utils/date-util.ts
+++ b/polygerrit-ui/app/utils/date-util.ts
@@ -37,9 +37,13 @@
// similar to fromNow from moment.js
export function fromNow(date: Date, noAgo = false) {
- const now = new Date();
+ return durationString(date, new Date(), noAgo);
+}
+
+// similar to fromNow from moment.js
+export function durationString(from: Date, to: Date, noAgo = false) {
const ago = noAgo ? '' : ' ago';
- const secondsAgo = Math.floor((now.valueOf() - date.valueOf()) / 1000);
+ const secondsAgo = Math.floor((to.valueOf() - from.valueOf()) / 1000);
if (secondsAgo <= 59) return 'just now';
if (secondsAgo <= 119) return `1 minute${ago}`;
const minutesAgo = Math.floor(secondsAgo / 60);
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 32014d7..7f9ef72 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -156,7 +156,7 @@
export function windowLocationReload() {
const e = new Error();
- console.info(`Calling window.location.realod(): ${e.stack}`);
+ console.info(`Calling window.location.reload(): ${e.stack}`);
window.location.reload();
}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 60ac4d8..4eed0a0 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -51,3 +51,11 @@
): ApprovalInfo | undefined {
return label.all?.filter(x => x._account_id === account._account_id)[0];
}
+
+export function labelCompare(labelName1: string, labelName2: string) {
+ if (labelName1 === CODE_REVIEW && labelName2 === CODE_REVIEW) return 0;
+ if (labelName1 === CODE_REVIEW) return -1;
+ if (labelName2 === CODE_REVIEW) return 1;
+
+ return labelName1.localeCompare(labelName2);
+}
diff --git a/polygerrit-ui/app/utils/label-util_test.js b/polygerrit-ui/app/utils/label-util_test.js
index 6a2f768..f9a30df 100644
--- a/polygerrit-ui/app/utils/label-util_test.js
+++ b/polygerrit-ui/app/utils/label-util_test.js
@@ -21,6 +21,7 @@
getVotingRangeOrDefault,
getMaxAccounts,
getApprovalInfo,
+ labelCompare,
} from './label-util.js';
const VALUES_1 = {
@@ -113,4 +114,11 @@
};
assert.isUndefined(getApprovalInfo(label, myAccountInfo));
});
+
+ test('labelCompare', () => {
+ let sorted = ['c', 'b', 'a'].sort(labelCompare);
+ assert.sameOrderedMembers(sorted, ['a', 'b', 'c']);
+ sorted = ['b', 'a', 'Code-Review'].sort(labelCompare);
+ assert.sameOrderedMembers(sorted, ['Code-Review', 'a', 'b']);
+ });
});
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index 40e3eef..af56798 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -3,11 +3,12 @@
ChangeInfo,
PatchSetNum,
EditPatchSetNum,
- BrandType,
ParentPatchSetNum,
+ PatchSetNumber,
} from '../types/common';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
+import {check} from './common-util';
/**
* @license
@@ -82,9 +83,7 @@
return patchset as PatchSetNum;
}
-export function isNumber(
- psn: PatchSetNum
-): psn is BrandType<number, '_patchSet'> {
+export function isNumber(psn: PatchSetNum): psn is PatchSetNumber {
return typeof psn === 'number';
}
@@ -250,14 +249,16 @@
export function computeLatestPatchNum(
allPatchSets?: PatchSet[]
-): PatchSetNum | undefined {
+): PatchSetNumber | undefined {
if (!allPatchSets || !allPatchSets.length) {
return undefined;
}
- if (allPatchSets[0].num === EditPatchSetNum) {
- return allPatchSets[1].num;
+ let latest = allPatchSets[0].num;
+ if (latest === EditPatchSetNum) {
+ latest = allPatchSets[1].num;
}
- return allPatchSets[0].num;
+ check(isNumber(latest), 'Latest patchset cannot be EDIT or PARENT.');
+ return latest;
}
export function computePredecessor(
diff --git a/polygerrit-ui/app/utils/url-util_test.js b/polygerrit-ui/app/utils/url-util_test.js
index 0658be3..b1b17f4 100644
--- a/polygerrit-ui/app/utils/url-util_test.js
+++ b/polygerrit-ui/app/utils/url-util_test.js
@@ -25,15 +25,15 @@
suite('url-util tests', () => {
suite('getBaseUrl tests', () => {
- let originialCanonicalPath;
+ let originalCanonicalPath;
suiteSetup(() => {
- originialCanonicalPath = window.CANONICAL_PATH;
+ originalCanonicalPath = window.CANONICAL_PATH;
window.CANONICAL_PATH = '/r';
});
suiteTeardown(() => {
- window.CANONICAL_PATH = originialCanonicalPath;
+ window.CANONICAL_PATH = originalCanonicalPath;
});
test('getBaseUrl', () => {
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index e544dbc..ec3b7a0 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -324,6 +324,11 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
+"@types/resize-observer-browser@^0.1.5":
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
+ integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==
+
"@webcomponents/shadycss@^1.9.1", "@webcomponents/shadycss@^1.9.2":
version "1.9.4"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.4.tgz#4f9d8ea1526bab084c60b53d4854dc39fdb2bb48"
diff --git a/proto/cache.proto b/proto/cache.proto
index 4fd037d..874e60a 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -524,13 +524,14 @@
}
// Serialized form of a list of com.google.gerrit.extensions.common.ContextLineInfo
-// Next ID: 2
+// Next ID: 3
message AllCommentContextProto {
message CommentContextProto {
int32 line_number = 1;
string context_line = 2;
}
repeated CommentContextProto context = 1;
+ string content_type = 2;
}
// Serialized key for
@@ -617,7 +618,7 @@
// Serialized form of
// com.google.gerrit.server.patch.filediff.FileDiffOutput
-// Next ID: 9
+// Next ID: 12
message FileDiffOutputProto {
// Next ID: 5
message Edit {
@@ -632,6 +633,11 @@
Edit edit = 1;
bool due_to_rebase = 2;
}
+ // Next ID: 3
+ message ComparisonType {
+ int32 parent_num = 1;
+ bool auto_merge = 2;
+ }
string old_path = 1;
string new_path = 2;
string change_type = 3;
@@ -640,4 +646,7 @@
int64 size = 6;
int64 size_delta = 7;
repeated TaggedEdit edits = 8;
+ bytes old_commit = 9;
+ bytes new_commit = 10;
+ ComparisonType comparison_type = 11;
}
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 15b1797..d96ffc2 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -60,7 +60,7 @@
if len(parts) == 3:
group_id, artifact_id, version = parts
elif len(parts) == 4:
- group_id, artifact_id, version, packaging = parts
+ group_id, artifact_id, version, classifier = parts
elif len(parts) == 5:
group_id, artifact_id, version, packaging, classifier = parts
else:
@@ -158,7 +158,10 @@
srcjar = None
if ctx.attr.src_sha1 or ctx.attr.attach_source:
srcjar = jar + "-src.jar"
- srcurl = url + "-sources.jar"
+ srcurl = url
+ if coordinates.classifier != None:
+ srcurl = url.replace("-" + coordinates.classifier, "")
+ srcurl += "-sources.jar"
srcjar_path = ctx.path("jar/" + srcjar)
args = [python, script, "-o", srcjar_path, "-u", srcurl]
if ctx.attr.src_sha1:
diff --git a/tools/remote-bazelrc b/tools/remote-bazelrc
index de7d0df..22ee330 100644
--- a/tools/remote-bazelrc
+++ b/tools/remote-bazelrc
@@ -42,9 +42,6 @@
# Set various strategies so that all actions execute remotely. Mixing remote
# and local execution will lead to errors unless the toolchain and remote
# machine exactly match the host machine.
-build:remote --spawn_strategy=remote,sandboxed
-build:remote --strategy=Javac=remote
-build:remote --strategy=Genrule=remote
build:remote --define=EXECUTOR=remote
# Enable the remote cache so action results can be shared across machines,
@@ -68,6 +65,3 @@
build:remote-cache --tls_enabled=true
build:remote-cache --remote_timeout=3600
build:remote-cache --auth_enabled=true
-build:remote-cache --spawn_strategy=standalone
-build:remote-cache --strategy=Javac=standalone
-build:remote-cache --strategy=Genrule=standalone