Merge "Add SHRINK_ONLY responsive mode."
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index e34071f..c45de05 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -104,7 +104,7 @@
Now that you have a simple version of Gerrit running, use the installation to
explore the user interface and learn about Gerrit. For more detailed
installation instructions, see
-link:[Standalone Daemon Installation Guide](install.html).
+link:install.html[Standalone Daemon Installation Guide].
GERRIT
------
diff --git a/Documentation/repository-maintenance.txt b/Documentation/repository-maintenance.txt
index 1672436..4bf84b5 100644
--- a/Documentation/repository-maintenance.txt
+++ b/Documentation/repository-maintenance.txt
@@ -28,7 +28,7 @@
Unlike a typical server database, access to Git repositories is not
marshalled through a single process or a set of inter communicating
-processes. Unfortuntatlely the design of the on-disk layout of a Git
+processes. Unfortunately the design of the on-disk layout of a Git
repository does not allow for 100% race free operations when accessed by
multiple actors concurrently. These design shortcomings are more likely
to impact the operations of busy repositories since racy conditions are
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 39db61d..7e6ab58 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -46,6 +46,7 @@
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
+import static javax.servlet.http.HttpServletResponse.SC_REQUEST_TIMEOUT;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
@@ -112,6 +113,7 @@
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -225,6 +227,7 @@
public static final String XD_METHOD = "$m";
public static final int SC_UNPROCESSABLE_ENTITY = 422;
public static final int SC_TOO_MANY_REQUESTS = 429;
+ public static final int SC_CLIENT_CLOSED_REQUEST = 499;
private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
private static final String PLAIN_TEXT = "text/plain";
@@ -709,6 +712,25 @@
messageOr(e, "Quota limit reached"),
e.caching(),
e);
+ } catch (RequestCancelledException e) {
+ cause = Optional.of(e);
+ switch (e.getCancellationReason()) {
+ case CLIENT_CLOSED_REQUEST:
+ statusCode = SC_CLIENT_CLOSED_REQUEST;
+ break;
+ case CLIENT_PROVIDED_DEADLINE_EXCEEDED:
+ case SERVER_DEADLINE_EXCEEDED:
+ statusCode = SC_REQUEST_TIMEOUT;
+ break;
+ }
+
+ StringBuilder msg = new StringBuilder(e.formatCancellationReason());
+ if (e.getCancellationMessage().isPresent()) {
+ msg.append("\n\n");
+ msg.append(e.getCancellationMessage().get());
+ }
+
+ responseBytes = replyError(req, res, statusCode, msg.toString(), e);
} catch (Exception e) {
cause = Optional.of(e);
statusCode = SC_INTERNAL_SERVER_ERROR;
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 344549e..b18f499 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -438,7 +438,7 @@
// unignore the test in PortedCommentsIT.
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchset.commitId(), /* parentNum= */ null);
+ change.getProject(), patchset.commitId(), /* parentNum= */ 0);
return modifiedFiles.isEmpty()
? null
: modifiedFiles.values().iterator().next().oldCommitId();
diff --git a/java/com/google/gerrit/server/cancellation/RequestCancelledException.java b/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
new file mode 100644
index 0000000..3c668fb
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/RequestCancelledException.java
@@ -0,0 +1,68 @@
+// 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.cancellation;
+
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+import org.apache.commons.lang.WordUtils;
+
+/** Exception to signal that the current request is cancelled and should be aborted. */
+public class RequestCancelledException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ private final RequestStateProvider.Reason cancellationReason;
+ private final Optional<String> cancellationMessage;
+
+ /**
+ * Create a {@code RequestCancelledException}.
+ *
+ * @param cancellationReason the reason why the request is cancelled
+ * @param cancellationMessage an optional message providing details about the cancellation
+ */
+ public RequestCancelledException(
+ RequestStateProvider.Reason cancellationReason, @Nullable String cancellationMessage) {
+ super(createMessage(cancellationReason, cancellationMessage));
+ this.cancellationReason = cancellationReason;
+ this.cancellationMessage = Optional.ofNullable(cancellationMessage);
+ }
+
+ private static String createMessage(
+ RequestStateProvider.Reason cancellationReason, @Nullable String message) {
+ StringBuilder messageBuilder = new StringBuilder();
+ messageBuilder.append(String.format("Request cancelled: %s", cancellationReason.name()));
+ if (message != null) {
+ messageBuilder.append(String.format(" (%s)", message));
+ }
+ return messageBuilder.toString();
+ }
+
+ /** Returns the reason why the request is cancelled. */
+ public RequestStateProvider.Reason getCancellationReason() {
+ return cancellationReason;
+ }
+
+ /** Returns the cancellation reason as a user-readable string. */
+ public String formatCancellationReason() {
+ return WordUtils.capitalizeFully(cancellationReason.name().replaceAll("_", " "));
+ }
+
+ /**
+ * Returns a message providing details about the cancellation, or {@link Optional#empty()} if none
+ * is available.
+ */
+ public Optional<String> getCancellationMessage() {
+ return cancellationMessage;
+ }
+}
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateProvider.java b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
new file mode 100644
index 0000000..e1716eb
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/RequestStateProvider.java
@@ -0,0 +1,59 @@
+// 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.cancellation;
+
+import com.google.gerrit.common.Nullable;
+
+/** Interface that provides information about the state of the current request. */
+public interface RequestStateProvider {
+ /**
+ * Checks whether the current request is cancelled.
+ *
+ * <p>Invoked by Gerrit to check whether the current request is cancelled and should be aborted.
+ *
+ * <p>If the current request is cancelled {@link OnCancelled#onCancel(Reason, String)} is invoked
+ * on the provided callback.
+ *
+ * @param onCancelled callback that should be invoked if the request is cancelled
+ */
+ void checkIfCancelled(OnCancelled onCancelled);
+
+ /** Callback interface to be invoked if a request is cancelled. */
+ interface OnCancelled {
+ /**
+ * Callback that is invoked if the request is cancelled.
+ *
+ * @param reason the reason for the cancellation of the request
+ * @param message an optional message providing details about the cancellation
+ */
+ void onCancel(Reason reason, @Nullable String message);
+ }
+
+ /** Reason why a request is cancelled. */
+ enum Reason {
+ /** The client got disconnected or has cancelled the request. */
+ CLIENT_CLOSED_REQUEST,
+
+ /** The deadline that the client provided for the request exceeded. */
+ CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+
+ /**
+ * A server-side deadline for the request exceeded.
+ *
+ * <p>Server-side deadlines are usually configurable, but may also be hard-coded.
+ */
+ SERVER_DEADLINE_EXCEEDED;
+ }
+}
diff --git a/java/com/google/gerrit/server/change/FileInfoJson.java b/java/com/google/gerrit/server/change/FileInfoJson.java
index ad6f9c7..ab557dc 100644
--- a/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -46,7 +46,8 @@
*
* @param change a Gerrit change.
* @param objectId a commit SHA-1 identifying a patchset commit.
- * @param parentNum an integer identifying the parent number used for comparison.
+ * @param parentNum 1-based integer identifying the parent number used for comparison. If zero,
+ * the only parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @return a mapping of the file paths to their related diff information.
*/
default Map<String, FileInfo> getFileInfoMap(Change change, ObjectId objectId, int parentNum)
@@ -74,7 +75,8 @@
*
* @param project a project identifying a repository.
* @param objectId a commit SHA-1 identifying a patchset commit.
- * @param parentNum an integer identifying the parent number used for comparison.
+ * @param parentNum 1-based integer identifying the parent number used for comparison. If zero,
+ * the only parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @return a mapping of the file paths to their related diff information.
*/
Map<String, FileInfo> getFileInfoMap(Project.NameKey project, ObjectId objectId, int parentNum)
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
index 1ca2c93..7277404 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
@@ -47,8 +47,11 @@
throws ResourceConflictException, PatchListNotAvailableException {
try {
if (base == null) {
+ // Setting parentNum=0 requests the default parent, which is the only parent for
+ // single-parent commits, or the auto-merge otherwise
return asFileInfo(
- diffs.listModifiedFilesAgainstParent(change.getProject(), objectId, null));
+ diffs.listModifiedFilesAgainstParent(
+ change.getProject(), objectId, /* parentNum= */ 0));
}
return asFileInfo(diffs.listModifiedFiles(change.getProject(), base.commitId(), objectId));
} catch (DiffNotAvailableException e) {
@@ -63,7 +66,7 @@
throws ResourceConflictException, PatchListNotAvailableException {
try {
Map<String, FileDiffOutput> modifiedFiles =
- diffs.listModifiedFilesAgainstParent(project, objectId, parent + 1);
+ diffs.listModifiedFilesAgainstParent(project, objectId, parent);
return asFileInfo(modifiedFiles);
} catch (DiffNotAvailableException e) {
convertException(e);
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
index 55d162a..0570296 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
@@ -60,10 +60,10 @@
Project.NameKey project, ObjectId objectId, int parentNum)
throws ResourceConflictException, PatchListNotAvailableException {
PatchListKey key =
- parentNum == -1
+ parentNum == 0
? PatchListKey.againstDefaultBase(objectId, Whitespace.IGNORE_NONE)
: PatchListKey.againstParentNum(
- parentNum + 1, objectId, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
+ parentNum, objectId, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
return toFileInfoMap(project, key);
}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 3a4dcff..3f988a3 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -399,7 +399,7 @@
try {
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parent= */ null);
+ change.getProject(), patchSet.commitId(), /* parent= */ 0);
for (FileDiffOutput diff : modifiedFiles.values()) {
if (patchSetAttribute.files == null) {
@@ -456,7 +456,7 @@
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parent= */ null);
+ change.getProject(), patchSet.commitId(), /* parent= */ 0);
for (FileDiffOutput fileDiff : modifiedFiles.values()) {
p.sizeDeletions += fileDiff.deletions();
p.sizeInsertions += fileDiff.insertions();
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 454df66..d074f1e 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -114,6 +114,7 @@
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.change.AttentionSetUnchangedOp;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.NotifyResolver;
@@ -640,8 +641,17 @@
Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
commands =
commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
- processCommandsUnsafe(commands, progress);
- rejectRemaining(commands, INTERNAL_SERVER_ERROR);
+
+ try {
+ processCommandsUnsafe(commands, progress);
+ rejectRemaining(commands, INTERNAL_SERVER_ERROR);
+ } catch (RequestCancelledException e) {
+ StringBuilder msg = new StringBuilder(e.formatCancellationReason());
+ if (e.getCancellationMessage().isPresent()) {
+ msg.append(String.format(" (%s)", e.getCancellationMessage().get()));
+ }
+ rejectRemaining(commands, msg.toString());
+ }
// This sends error messages before the 'done' string of the progress monitor is sent.
// Currently, the test framework relies on this ordering to understand if pushes completed
diff --git a/java/com/google/gerrit/server/patch/DiffOperations.java b/java/com/google/gerrit/server/patch/DiffOperations.java
index 7213581..d2da736 100644
--- a/java/com/google/gerrit/server/patch/DiffOperations.java
+++ b/java/com/google/gerrit/server/patch/DiffOperations.java
@@ -46,8 +46,8 @@
*
* @param project a project name representing a git repository.
* @param newCommit 20 bytes SHA-1 of the new commit used in the diff.
- * @param parentNum integer specifying which parent to use as base. If null, the only parent will
- * be used or the auto-merge if {@code newCommit} is a merge commit.
+ * @param parentNum 1-based integer specifying which parent to use as base. If zero, the only
+ * parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @return map of file paths to the file diffs. The map key is the new file path for all {@link
* ChangeType} file diffs except {@link ChangeType#DELETED} entries where the map key contains
* the old file path. The map entries are not sorted by key.
@@ -56,8 +56,7 @@
* an internal error occurred in Git while evaluating the diff.
*/
Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
- Project.NameKey project, ObjectId newCommit, @Nullable Integer parentNum)
- throws DiffNotAvailableException;
+ Project.NameKey project, ObjectId newCommit, int parentNum) throws DiffNotAvailableException;
/**
* Returns the list of added, deleted or modified files between two commits (patchsets). The
@@ -85,8 +84,8 @@
*
* @param project a project name representing a git repository.
* @param newCommit 20 bytes SHA-1 of the new commit used in the diff.
- * @param parentNum integer specifying which parent to use as base. If null, the only parent will
- * be used or the auto-merge if {@code newCommit} is a merge commit.
+ * @param parentNum 1-based integer specifying which parent to use as base. If zero, the only
+ * parent will be used or the auto-merge if {@code newCommit} is a merge commit.
* @param fileName the file name for which the diff should be evaluated.
* @param whitespace preference controlling whitespace effect in diff computation.
* @return the diff for the single file between the two commits.
@@ -96,7 +95,7 @@
FileDiffOutput getModifiedFileAgainstParent(
Project.NameKey project,
ObjectId newCommit,
- @Nullable Integer parentNum,
+ int parentNum,
String fileName,
@Nullable DiffPreferencesInfo.Whitespace whitespace)
throws DiffNotAvailableException;
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index f500796..3423b32 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -91,8 +91,7 @@
@Override
public Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
- Project.NameKey project, ObjectId newCommit, @Nullable Integer parent)
- throws DiffNotAvailableException {
+ Project.NameKey project, ObjectId newCommit, int parent) throws DiffNotAvailableException {
try {
DiffParameters diffParams = computeDiffParameters(project, newCommit, parent);
return getModifiedFiles(diffParams);
@@ -120,7 +119,7 @@
public FileDiffOutput getModifiedFileAgainstParent(
Project.NameKey project,
ObjectId newCommit,
- @Nullable Integer parent,
+ int parent,
String fileName,
@Nullable DiffPreferencesInfo.Whitespace whitespace)
throws DiffNotAvailableException {
@@ -376,7 +375,7 @@
Project.NameKey project, ObjectId newCommit, Integer parent) throws IOException {
DiffParameters.Builder result =
DiffParameters.builder().project(project).newCommit(newCommit).parent(parent);
- if (parent != null) {
+ if (parent > 0) {
result.baseCommit(baseCommitUtil.getBaseCommit(project, newCommit, parent));
result.comparisonType(ComparisonType.againstParent(parent));
return result.build();
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 0c648b5..fbb6559 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -172,7 +172,7 @@
this.fileName = fileName;
this.psa = patchSetA;
- this.parentNum = -1;
+ this.parentNum = 0;
this.psb = patchSetB;
this.diffPrefs = diffPrefs;
this.currentUser = currentUser;
@@ -223,7 +223,7 @@
this.runNewDiffCache = cfg.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
changeId = patchSetB.changeId();
- checkArgument(parentNum >= 0, "parentNum must be >= 0");
+ checkArgument(parentNum > 0, "parentNum must be > 0");
}
@Override
@@ -326,11 +326,7 @@
FileDiffOutput fileDiffOutput =
aId == null
? diffOperations.getModifiedFileAgainstParent(
- notes.getProjectName(),
- bId,
- parentNum == -1 ? null : parentNum + 1,
- fileName,
- diffPrefs.ignoreWhitespace)
+ notes.getProjectName(), bId, parentNum, fileName, diffPrefs.ignoreWhitespace)
: diffOperations.getModifiedFile(
notes.getProjectName(), aId, bId, fileName, diffPrefs.ignoreWhitespace);
return newBuilder().toPatchScriptNew(git, fileDiffOutput);
@@ -395,7 +391,7 @@
if (psa == null) {
return Optional.empty();
}
- checkState(parentNum < 0, "expected no parentNum when psa is present");
+ checkState(parentNum == 0, "expected no parentNum when psa is present");
checkArgument(psa.get() != 0, "edit not supported for left side");
return Optional.of(getCommitId(psa));
}
@@ -409,10 +405,10 @@
}
private PatchListKey keyFor(ObjectId aId, ObjectId bId, Whitespace whitespace) {
- if (parentNum < 0) {
+ if (parentNum == 0) {
return PatchListKey.againstCommit(aId, bId, whitespace);
}
- return PatchListKey.againstParentNum(parentNum + 1, bId, whitespace);
+ return PatchListKey.againstParentNum(parentNum, bId, whitespace);
}
private PatchList listFor(PatchListKey key) throws PatchListNotAvailableException {
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index 1efe378..320e57d 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -183,7 +183,7 @@
r =
Response.ok(
fileInfoJson.getFileInfoMap(
- resource.getChange(), resource.getPatchSet().commitId(), parentNum - 1));
+ resource.getChange(), resource.getPatchSet().commitId(), parentNum));
} else {
r = Response.ok(fileInfoJson.getFileInfoMap(resource.getChange(), resource.getPatchSet()));
}
@@ -280,11 +280,11 @@
Map<String, FileDiffOutput> oldList =
diffOperations.listModifiedFilesAgainstParent(
- project, patchSet.commitId(), /* parentNum= */ null);
+ project, patchSet.commitId(), /* parentNum= */ 0);
Map<String, FileDiffOutput> curList =
diffOperations.listModifiedFilesAgainstParent(
- project, resource.getPatchSet().commitId(), /* parentNum= */ null);
+ project, resource.getPatchSet().commitId(), /* parentNum= */ 0);
int sz = paths.size();
List<String> pathList = Lists.newArrayListWithCapacity(sz);
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 2169d57..dd951a8 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -74,6 +74,7 @@
@Option(name = "--base", metaVar = "REVISION")
String base;
+ /** 1-based index of the parent's position in the commit object. */
@Option(name = "--parent", metaVar = "parent-number")
int parentNum;
@@ -143,7 +144,7 @@
} else if (parentNum > 0) {
psf =
patchScriptFactoryFactory.create(
- notes, fileName, parentNum - 1, pId, prefs, currentUser.get());
+ notes, fileName, parentNum, pId, prefs, currentUser.get());
} else {
psf = patchScriptFactoryFactory.create(notes, fileName, null, pId, prefs, currentUser.get());
}
diff --git a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
index 7bee2f2..6d054bd 100644
--- a/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/FilesInCommitCollection.java
@@ -77,6 +77,10 @@
}
public static final class ListFiles implements RestReadView<CommitResource> {
+ /**
+ * The 1-based parent number. If zero, the default base commit will be used, which is the only
+ * parent for commits having one parent or the auto-merge commit otherwise.
+ */
@Option(name = "--parent", metaVar = "parent-number")
int parentNum;
@@ -97,8 +101,7 @@
throws ResourceConflictException, PatchListNotAvailableException {
RevCommit commit = resource.getCommit();
return Response.ok(
- fileInfoJson.getFileInfoMap(
- resource.getProjectState().getNameKey(), commit, parentNum - 1));
+ fileInfoJson.getFileInfoMap(resource.getProjectState().getNameKey(), commit, parentNum));
}
}
}
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index c94b25c..93c6c2c 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -19,6 +19,7 @@
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.PerformanceLogContext;
import com.google.gerrit.server.logging.PerformanceLogger;
@@ -61,6 +62,12 @@
RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
requestListeners.runEach(l -> l.onRequest(requestInfo));
SshCommand.this.run();
+ } catch (RequestCancelledException e) {
+ StringBuilder msg = new StringBuilder(e.formatCancellationReason());
+ if (e.getCancellationMessage().isPresent()) {
+ msg.append(String.format(" (%s)", e.getCancellationMessage().get()));
+ }
+ stderr.println(msg.toString());
} finally {
stdout.flush();
stderr.flush();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index d8dab33..a01b340 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -153,7 +153,8 @@
PushOneCommit.Result result = push.to("refs/heads/master");
Map<String, FileDiffOutput> modifiedFiles =
- diffOperations.listModifiedFilesAgainstParent(project, result.getCommit(), null);
+ diffOperations.listModifiedFilesAgainstParent(
+ project, result.getCommit(), /* parentNum= */ 0);
assertThat(modifiedFiles.keySet()).containsExactly("/COMMIT_MSG", "f.txt");
assertThat(
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
new file mode 100644
index 0000000..29d54cc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -0,0 +1,206 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_CLIENT_CLOSED_REQUEST;
+import static org.apache.http.HttpStatus.SC_REQUEST_TIMEOUT;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateProvider;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import java.util.List;
+import org.junit.Test;
+
+public class CancellationIT extends AbstractDaemonTest {
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ @Test
+ public void handleClientDisconnected() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate a request cancellation by throwing RequestCancelledException. In contrast to
+ // an actual request cancellation this allows us to verify the HTTP status code that is
+ // set when a request is cancelled.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ RestResponse response = adminRestSession.put("/projects/" + name("new"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_CLIENT_CLOSED_REQUEST);
+ assertThat(response.getEntityContent()).isEqualTo("Client Closed Request");
+ }
+ }
+
+ @Test
+ public void handleClientDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ RestResponse response = adminRestSession.put("/projects/" + name("new"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+ assertThat(response.getEntityContent()).isEqualTo("Client Provided Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleServerDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ RestResponse response = adminRestSession.put("/projects/" + name("new"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+ assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleRequestCancellationWithMessage() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m");
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ RestResponse response = adminRestSession.put("/projects/" + name("new"));
+ assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+ assertThat(response.getEntityContent())
+ .isEqualTo("Server Deadline Exceeded\n\ndeadline = 10m");
+ }
+ }
+
+ @Test
+ public void handleClientDisconnectedForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate a request cancellation by throwing RequestCancelledException. In contrast to
+ // an actual request cancellation this allows us verify the error message that is sent
+ // to the client.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Client Closed Request");
+ }
+ }
+
+ @Test
+ public void handleClientDeadlineExceededForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Client Provided Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleServerDeadlineExceededForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Server Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleRequestCancellationWithMessageForPush() throws Exception {
+ CommitValidationListener commitValidationListener =
+ new CommitValidationListener() {
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ // Simulate an exceeded deadline by throwing RequestCancelledException.
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m");
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(commitValidationListener)) {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertErrorStatus("Server Deadline Exceeded (deadline = 10m)");
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
new file mode 100644
index 0000000..2cb9637
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateProvider;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@UseSsh
+public class SshCancellationIT extends AbstractDaemonTest {
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ @Test
+ public void handleClientDisconnected() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Client Closed Request");
+ }
+ }
+
+ @Test
+ public void handleClientDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Client Provided Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleServerDeadlineExceeded() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
+ /* cancellationMessage= */ null);
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Server Deadline Exceeded");
+ }
+ }
+
+ @Test
+ public void handleRequestCancellationWithMessage() throws Exception {
+ ProjectCreationValidationListener projectCreationListener =
+ new ProjectCreationValidationListener() {
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED, "deadline = 10m");
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(projectCreationListener)) {
+ adminSshSession.exec("gerrit create-project " + name("new"));
+ adminSshSession.assertFailure("Server Deadline Exceeded (deadline = 10m)");
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 5bf5154..aa313e3 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -72,7 +72,7 @@
FileDiffOutput diffOutput =
diffOperations.getModifiedFileAgainstParent(
- testProjectName, newCommitId, /* parentNum=*/ null, fileName2, /* whitespace=*/ null);
+ testProjectName, newCommitId, /* parentNum=*/ 0, fileName2, /* whitespace=*/ null);
assertThat(diffOutput.oldCommitId()).isEqualTo(oldCommitId);
assertThat(diffOutput.newCommitId()).isEqualTo(newCommitId);
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 a0b1794..fce7db9 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
@@ -1486,6 +1486,7 @@
}
_handleDeleteConfirm() {
+ this._hideAllDialogs();
this._fireAction(
'/',
assertUIActionInfo(this.actions[ChangeActions.DELETE]),
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 9dcb67e..5206fdb 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
@@ -952,7 +952,7 @@
return;
}
e.preventDefault();
- this.fileCursor.next();
+ this.fileCursor.next({circular: true});
this.selectedIndex = this.fileCursor.index;
}
}
@@ -972,7 +972,7 @@
return;
}
e.preventDefault();
- this.fileCursor.previous();
+ this.fileCursor.previous({circular: true});
this.selectedIndex = this.fileCursor.index;
}
}
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 0655721..79bc9f6 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
@@ -533,7 +533,7 @@
assert.equal(element.fileCursor.index, 2);
// up should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'up');
assert.equal(element.fileCursor.index, 2);
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
@@ -548,8 +548,8 @@
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.fileCursor.index, 0);
- assert.equal(element.selectedIndex, 0);
+ assert.equal(element.fileCursor.index, 1);
+ assert.equal(element.selectedIndex, 1);
const createCommentInPlaceStub = sinon.stub(element.diffCursor,
'createCommentInPlace');
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index bd4cb76..8201dbc 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -487,8 +487,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
></gr-account-label>
</template>
@@ -558,8 +558,8 @@
account="[[_owner]]"
force-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -573,8 +573,8 @@
account="[[_uploader]]"
force-attention="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -593,8 +593,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -614,8 +614,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hide-hovercard=""
- selection-chip-style
+ hideHovercard
+ selectionChipStyle
on-click="_handleAttentionClick"
>
</gr-account-label>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 73f3dd3..93a432b 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -109,7 +109,7 @@
<gr-account-label
account="[[item]]"
on-click="handleAccountClicked"
- selection-chip-style
+ selectionChipStyle
selected="[[isSelected(item, selectedAuthors)]]"
> </gr-account-label>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index cc66734..66214b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -15,30 +15,25 @@
* limitations under the License.
*/
import '@polymer/iron-icon/iron-icon';
-import '../../../styles/shared-styles';
import '../gr-avatar/gr-avatar';
import '../gr-hovercard-account/gr-hovercard-account';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-account-label_html';
import {appContext} from '../../../services/app-context';
import {getDisplayName} from '../../../utils/display-name-util';
import {isSelf, isServiceUser} from '../../../utils/account-util';
-import {customElement, property} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {ChangeInfo, AccountInfo, ServerInfo} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
import {fireEvent} from '../../../utils/event-util';
import {isInvolved} from '../../../utils/change-util';
import {ShowAlertEventDetail} from '../../../types/events';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property, state} from 'lit-element';
+import {classMap} from 'lit-html/directives/class-map';
@customElement('gr-account-label')
-export class GrAccountLabel extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrAccountLabel extends GrLitElement {
@property({type: Object})
- account!: AccountInfo;
+ account?: AccountInfo;
@property({type: Object})
_selfAccount?: AccountInfo;
@@ -49,7 +44,7 @@
* related features like adding the user as a reviewer.
*/
@property({type: Object})
- change!: ChangeInfo;
+ change?: ChangeInfo;
@property({type: String})
voteableText?: string;
@@ -83,44 +78,190 @@
@property({
type: Boolean,
- reflectToAttribute: true,
- computed:
- '_computeCancelLeftPadding(hideAvatar, ' +
- 'highlightAttention, account, change, forceAttention)',
+ reflect: true,
})
cancelLeftPadding = false;
@property({type: Boolean})
hideStatus = false;
- @property({type: Object})
+ @state()
_config?: ServerInfo;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
selectionChipStyle = false;
@property({
type: Boolean,
- reflectToAttribute: true,
- observer: 'selectedChanged',
+ reflect: true,
})
selected = false;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
deselected = false;
reporting: ReportingService;
private readonly restApiService = appContext.restApiService;
+ static get styles() {
+ return [
+ css`
+ :host {
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+ border-radius: var(--label-border-radius);
+ box-sizing: border-box;
+ white-space: nowrap;
+ padding: 0 var(--account-label-padding-horizontal, 0);
+ }
+ /* If the first element is the avatar, then we cancel the left padding,
+ so we can fit nicely into the gr-account-chip rounding. The obvious
+ alternative of 'chip has padding' and 'avatar gets negative margin'
+ does not work, because we need 'overflow:hidden' on the label. */
+ :host([cancelLeftPadding]) {
+ padding-left: 0;
+ }
+ :host::after {
+ content: var(--account-label-suffix);
+ }
+ :host([deselected][selectionChipStyle]) {
+ background-color: var(--background-color-primary);
+ border: 1px solid var(--comment-separator-color);
+ border-radius: 8px;
+ color: var(--deemphasized-text-color);
+ }
+ :host([selected][selectionChipStyle]) {
+ background-color: var(--chip-selected-background-color);
+ border: 1px solid var(--chip-selected-background-color);
+ border-radius: 8px;
+ color: var(--chip-selected-text-color);
+ }
+ :host([selected]) iron-icon.attention {
+ color: var(--chip-selected-text-color);
+ }
+ gr-avatar {
+ height: calc(var(--line-height-normal) - 2px);
+ width: calc(var(--line-height-normal) - 2px);
+ vertical-align: top;
+ position: relative;
+ top: 1px;
+ }
+ #attentionButton {
+ /* This negates the 4px horizontal padding, which we appreciate as a
+ larger click target, but which we don't want to consume space. :-) */
+ margin: 0 -4px 0 -4px;
+ vertical-align: top;
+ }
+ iron-icon.attention {
+ color: var(--deemphasized-text-color);
+ width: 12px;
+ height: 12px;
+ vertical-align: top;
+ }
+ iron-icon.status {
+ color: var(--deemphasized-text-color);
+ width: 14px;
+ height: 14px;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ }
+ .name {
+ display: inline-block;
+ text-decoration: inherit;
+ vertical-align: top;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: var(--account-max-length, 180px);
+ }
+ .hasAttention .name {
+ font-weight: var(--font-weight-bold);
+ }
+ `,
+ ];
+ }
+
+ render() {
+ const {account, change, highlightAttention, forceAttention} = this;
+ if (!account) return;
+ const hasAttention =
+ forceAttention ||
+ this._hasUnforcedAttention(highlightAttention, account, change);
+ this.deselected = !this.selected;
+ this.cancelLeftPadding = !this.hideAvatar && !hasAttention;
+ return html`<span>
+ ${!this.hideHovercard
+ ? html`<gr-hovercard-account
+ for="hovercardTarget"
+ .account="${account}"
+ .change="${change}"
+ ?highlight-attention=${highlightAttention}
+ .voteable-text=${this.voteableText}
+ ></gr-hovercard-account>`
+ : ''}
+ ${hasAttention
+ ? html`<gr-button
+ id="attentionButton"
+ link=""
+ aria-label="Remove user from attention set"
+ @click=${this._handleRemoveAttentionClick}
+ ?disabled=${!this._computeAttentionButtonEnabled(
+ highlightAttention,
+ account,
+ change,
+ this.selected,
+ this._selfAccount
+ )}
+ ?has-tooltip=${this._computeAttentionButtonEnabled(
+ highlightAttention,
+ account,
+ change,
+ false,
+ this._selfAccount
+ )}
+ title="${this._computeAttentionIconTitle(
+ highlightAttention,
+ account,
+ change,
+ forceAttention,
+ this.selected,
+ this._selfAccount
+ )}"
+ ><iron-icon
+ class="attention"
+ icon="gr-icons:attention"
+ ></iron-icon>
+ </gr-button>`
+ : ''}
+ </span>
+ <span
+ id="hovercardTarget"
+ class="${classMap({
+ hasAttention: !!hasAttention,
+ })}"
+ >
+ ${!this.hideAvatar
+ ? html`<gr-avatar .account="${account}" imageSize="32"></gr-avatar>`
+ : ''}
+ <span class="text" part="gr-account-label-text">
+ <span class="name"
+ >${this._computeName(account, this.firstName, this._config)}</span
+ >
+ ${!this.hideStatus && account.status
+ ? html`<iron-icon
+ class="status"
+ icon="gr-icons:calendar"
+ ></iron-icon>`
+ : ''}
+ </span>
+ </span>`;
+ }
+
constructor() {
super();
this.reporting = appContext.reportingService;
- }
-
- /** @override */
- ready() {
- super.ready();
this.restApiService.getConfig().then(config => {
this._config = config;
});
@@ -129,72 +270,42 @@
});
this.addEventListener('attention-set-updated', () => {
// For re-evaluation of everything that depends on 'change'.
- this.change = {...this.change};
+ if (this.change) this.change = {...this.change};
});
}
- selectedChanged(selected?: boolean) {
- this.deselected = !selected;
- }
-
_isAttentionSetEnabled(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo
+ change?: ChangeInfo
) {
return highlight && !!change && !!account && !isServiceUser(account);
}
- _computeCancelLeftPadding(
- hideAvatar: boolean,
- highlight: boolean,
- account: AccountInfo,
- change: ChangeInfo,
- force: boolean
- ) {
- return (
- !hideAvatar && !this._hasAttention(highlight, account, change, force)
- );
- }
-
- _hasAttention(
- highlight: boolean,
- account: AccountInfo,
- change: ChangeInfo,
- force: boolean
- ) {
- return force || this._hasUnforcedAttention(highlight, account, change);
- }
-
_hasUnforcedAttention(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo
+ change?: ChangeInfo
) {
return (
this._isAttentionSetEnabled(highlight, account, change) &&
+ change &&
change.attention_set &&
!!account._account_id &&
hasOwnProperty(change.attention_set, account._account_id)
);
}
- _computeHasAttentionClass(
- highlight: boolean,
- account: AccountInfo,
- change: ChangeInfo,
- force: boolean
+ _computeName(
+ account?: AccountInfo,
+ firstName?: boolean,
+ config?: ServerInfo
) {
- return this._hasAttention(highlight, account, change, force)
- ? 'hasAttention'
- : '';
- }
-
- _computeName(account: AccountInfo, firstName: boolean, config?: ServerInfo) {
return getDisplayName(config, account, firstName);
}
_handleRemoveAttentionClick(e: MouseEvent) {
+ if (!this.account || !this.change) return;
if (this.selected) return;
e.preventDefault();
e.stopPropagation();
@@ -236,6 +347,7 @@
}
_reportingDetails() {
+ if (!this.account) return;
const targetId = this.account._account_id;
const ownerId =
(this.change && this.change.owner && this.change.owner._account_id) || -1;
@@ -259,7 +371,7 @@
_computeAttentionButtonEnabled(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo,
+ change: ChangeInfo | undefined,
selected: boolean,
selfAccount?: AccountInfo
) {
@@ -273,7 +385,7 @@
_computeAttentionIconTitle(
highlight: boolean,
account: AccountInfo,
- change: ChangeInfo,
+ change: ChangeInfo | undefined,
force: boolean,
selected: boolean,
selfAccount?: AccountInfo
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
deleted file mode 100644
index 352763b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- :host {
- display: inline-block;
- vertical-align: top;
- position: relative;
- border-radius: var(--label-border-radius);
- box-sizing: border-box;
- white-space: nowrap;
- padding: 0 var(--account-label-padding-horizontal, 0);
- }
- /* If the first element is the avatar, then we cancel the left padding, so
- we can fit nicely into the gr-account-chip rounding.
- The obvious alternative of 'chip has padding' and 'avatar gets negative
- margin' does not work, because we need 'overflow:hidden' on the label. */
- :host([cancel-left-padding]) {
- padding-left: 0;
- }
- :host::after {
- content: var(--account-label-suffix);
- }
- :host([deselected][selection-chip-style]) {
- background-color: var(--background-color-primary);
- border: 1px solid var(--comment-separator-color);
- border-radius: 8px;
- color: var(--deemphasized-text-color);
- }
- :host([selected][selection-chip-style]) {
- background-color: var(--chip-selected-background-color);
- border: 1px solid var(--chip-selected-background-color);
- border-radius: 8px;
- color: var(--chip-selected-text-color);
- }
- :host([selected]) iron-icon.attention {
- color: var(--chip-selected-text-color);
- }
- gr-avatar {
- height: calc(var(--line-height-normal) - 2px);
- width: calc(var(--line-height-normal) - 2px);
- vertical-align: top;
- position: relative;
- top: 1px;
- }
- #attentionButton {
- /* This negates the 4px horizontal padding, which we appreciate as a
- larger click target, but which we don't want to consume space. :-) */
- margin: 0 -4px 0 -4px;
- vertical-align: top;
- }
- iron-icon.attention {
- color: var(--deemphasized-text-color);
- width: 12px;
- height: 12px;
- vertical-align: top;
- }
- iron-icon.status {
- color: var(--deemphasized-text-color);
- width: 14px;
- height: 14px;
- vertical-align: top;
- position: relative;
- top: 2px;
- }
- .name {
- display: inline-block;
- text-decoration: inherit;
- vertical-align: top;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: var(--account-max-length, 180px);
- }
- .hasAttention .name {
- font-weight: var(--font-weight-bold);
- }
- </style>
- <span>
- <template is="dom-if" if="[[!hideHovercard]]">
- <gr-hovercard-account
- for="hovercardTarget"
- account="[[account]]"
- change="[[change]]"
- highlight-attention="[[highlightAttention]]"
- voteable-text="[[voteableText]]"
- >
- </gr-hovercard-account>
- </template>
- <template
- is="dom-if"
- if="[[_hasAttention(highlightAttention, account, change, forceAttention)]]"
- >
- <gr-button
- id="attentionButton"
- link=""
- aria-label="Remove user from attention set"
- on-click="_handleRemoveAttentionClick"
- disabled="[[!_computeAttentionButtonEnabled(highlightAttention, account, change, selected, _selfAccount)]]"
- has-tooltip="[[_computeAttentionButtonEnabled(highlightAttention, account, change, false, _selfAccount)]]"
- title="[[_computeAttentionIconTitle(highlightAttention, account, change, forceAttention, selected, _selfAccount)]]"
- ><iron-icon class="attention" icon="gr-icons:attention"></iron-icon>
- </gr-button>
- </template>
- </span>
- <span
- id="hovercardTarget"
- class$="[[_computeHasAttentionClass(highlightAttention, account, change, forceAttention)]]"
- >
- <template is="dom-if" if="[[!hideAvatar]]">
- <gr-avatar account="[[account]]" imageSize="32"></gr-avatar>
- </template>
- <span class="text" part="gr-account-label-text">
- <span class="name">[[_computeName(account, firstName, _config)]]</span>
- <template is="dom-if" if="[[!hideStatus]]">
- <template is="dom-if" if="[[account.status]]">
- <iron-icon class="status" icon="gr-icons:calendar"></iron-icon>
- </template>
- </template>
- </span>
- </span>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
index 317806c..a610ffa 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
@@ -89,12 +89,12 @@
<gr-account-label
.account="${this.account}"
.change="${this.change}"
- ?force-attention=${this.forceAttention}
- ?highlight-attention=${this.highlightAttention}
- ?hide-avatar=${this.hideAvatar}
- ?hide-status=${this.hideStatus}
- ?first-name=${this.firstName}
- .voteable-text=${this.voteableText}
+ ?forceAttention=${this.forceAttention}
+ ?highlightAttention=${this.highlightAttention}
+ ?hideAvatar=${this.hideAvatar}
+ ?hideStatus=${this.hideStatus}
+ ?firstName=${this.firstName}
+ .voteableText=${this.voteableText}
part="gr-account-link-text => gr-account-label-text"
>
</gr-account-label>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index a848b2f..3e0b9a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -262,7 +262,7 @@
<gr-account-label
account="[[_getAuthor(comment, _selfAccount)]]"
class$="[[_computeAccountLabelClass(draft)]]"
- hide-status=""
+ hideStatus
>
</gr-account-label>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index bbf3442..a218959 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -367,23 +367,20 @@
getTimerStub = stubReporting('getTimer').returns(mockTimer);
});
- test('create', () => {
+ test('create', async () => {
element.patchNum = 1 as PatchSetNum;
element.comment = {};
- return element._handleSave(mockEvent)!.then(() => {
- assert.equal(
- (queryAndAssert(
- element,
- 'gr-account-label'
- ).shadowRoot?.querySelector(
- 'span.name'
- ) as HTMLSpanElement).innerText.trim(),
- 'Dhruv Srivastava'
- );
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
- });
+ await element._handleSave(mockEvent);
+ await flush();
+ const grAccountLabel = queryAndAssert(element, 'gr-account-label');
+ const spanName = queryAndAssert<HTMLSpanElement>(
+ grAccountLabel,
+ 'span.name'
+ );
+ assert.equal(spanName.innerText.trim(), 'Dhruv Srivastava');
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
});
test('update', () => {
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 017ba50..9dce127 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
@@ -87,7 +87,7 @@
}
/**
- * Move the cursor forward. Clipped to the ends of the stop list.
+ * Move the cursor forward. Clipped to the end of the stop list.
*
* @param options.filter Skips any stops for which filter returns false.
* @param options.getTargetHeight Optional function to calculate the
@@ -95,22 +95,36 @@
* sometimes different, used by the diff cursor.
* @param options.clipToTop When none of the next indices match, move
* back to first instead of to last.
+ * @param options.circular When on last element, you get to first element.
* @return If a move was performed or why not.
- * @private
*/
next(
options: {
filter?: (stop: HTMLElement) => boolean;
getTargetHeight?: (target: HTMLElement) => number;
clipToTop?: boolean;
+ circular?: boolean;
} = {}
): CursorMoveResult {
return this._moveCursor(1, options);
}
+ /**
+ * Move the cursor backward. Clipped to the beginning of stop list.
+ *
+ * @param options.filter Skips any stops for which filter returns false.
+ * @param options.getTargetHeight Optional function to calculate the
+ * height of the target's 'section'. The height of the target itself is
+ * sometimes different, used by the diff cursor.
+ * @param options.clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @param options.circular When on first element, you get to last element.
+ * @return If a move was performed or why not.
+ */
previous(
options: {
filter?: (stop: HTMLElement) => boolean;
+ circular?: boolean;
} = {}
): CursorMoveResult {
return this._moveCursor(-1, options);
@@ -276,34 +290,18 @@
}
}
- /**
- * Move the cursor forward or backward by delta. Clipped to the beginning or
- * end of stop list.
- *
- * @param delta either -1 or 1.
- * @param options.abort Will abort moving the cursor when encountering a
- * stop for which this condition is met. Will abort even if the stop
- * would have been filtered
- * @param options.filter Will keep going and skip any stops for which this
- * condition is not met.
- * @param options.getTargetHeight Optional function to calculate the
- * height of the target's 'section'. The height of the target itself is
- * sometimes different, used by the diff cursor.
- * @param options.clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @return If a move was performed or why not.
- * @private
- */
_moveCursor(
delta: number,
{
filter,
getTargetHeight,
clipToTop,
+ circular,
}: {
filter?: (stop: HTMLElement) => boolean;
getTargetHeight?: (target: HTMLElement) => number;
clipToTop?: boolean;
+ circular?: boolean;
} = {}
): CursorMoveResult {
if (!this.stops.length) {
@@ -326,7 +324,10 @@
(delta > 0 && newIndex >= this.stops.length) ||
(delta < 0 && newIndex < 0)
) {
- newIndex = delta < 0 || clipToTop ? 0 : this.stops.length - 1;
+ newIndex =
+ (delta < 0 && !circular) || (delta > 0 && circular) || clipToTop
+ ? 0
+ : this.stops.length - 1;
newStop = this.stops[newIndex];
clipped = true;
break;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index ba7e4f8..d0bd420 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -255,6 +255,25 @@
assert.isTrue(cursor.target.focus.called);
});
+ suite('circular options', () => {
+ const options = {circular: true};
+ setup(() => {
+ cursor.stops = [...list.querySelectorAll('li')];
+ });
+
+ test('previous() on first element goes to last element', () => {
+ cursor.setCursor(list.children[0]);
+ cursor.previous(options);
+ assert.equal(cursor.index, list.children.length - 1);
+ });
+
+ test('next() on last element goes to first element', () => {
+ cursor.setCursor(list.children[list.children.length - 1]);
+ cursor.next(options);
+ assert.equal(cursor.index, 0);
+ });
+ });
+
suite('_scrollToTarget', () => {
let scrollStub;
setup(() => {
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 3d5a208..5f53819 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
@@ -815,8 +815,11 @@
(tagName === 'INPUT' && type !== 'checkbox') ||
tagName === 'TEXTAREA' ||
// Suppress shortcuts if the key is 'enter'
- // and target is an anchor or button.
- (e.keyCode === 13 && (tagName === 'A' || tagName === 'BUTTON'))
+ // and target is an anchor or button or paper-tab.
+ (e.keyCode === 13 &&
+ (tagName === 'A' ||
+ tagName === 'BUTTON' ||
+ tagName === 'PAPER-TAB'))
) {
return true;
}
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 287cf68..a8274cc 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -239,9 +239,13 @@
font-family: var(--header-font-family);
-webkit-font-smoothing: initial;
}
+ --paper-tab-content: {
+ margin-bottom: var(--spacing-s);
+ }
--paper-tab-content-focused: {
/* paper-tabs uses 700 here, which can look awkward */
font-weight: var(--font-weight-h3);
+ background: var(--gray-background-focus);
}
--paper-tab-content-unselected: {
/* paper-tabs uses 0.8 here, but we want to control the color directly */
@@ -249,6 +253,10 @@
color: var(--deemphasized-text-color);
}
}
+ paper-tab:focus {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
iron-autogrow-textarea {
/** This is needed for firefox */
--iron-autogrow-textarea_-_white-space: pre-wrap;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 1996800..134003b 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -128,20 +128,20 @@
--error-foreground: var(--red-700);
--error-background: var(--red-50);
- --error-background-hover: linear-gradient(var(--red-700-04), var(--red-700-04)), var(--red-50);
- --error-background-focus: linear-gradient(var(--red-700-12), var(--red-700-12)), var(--red-50);
+ --error-background-hover: linear-gradient(var(--red-700-04), var(--red-700-04), var(--red-50));
+ --error-background-focus: linear-gradient(var(--red-700-12), var(--red-700-12), var(--red-50));
--error-ripple: var(--red-700-10);
--warning-foreground: var(--orange-700);
--warning-background: var(--orange-50);
- --warning-background-hover: linear-gradient(var(--orange-700-04), var(--orange-700-04)), var(--orange-50);
- --warning-background-focus: linear-gradient(var(--orange-700-12), var(--orange-700-12)), var(--orange-50);
+ --warning-background-hover: linear-gradient(var(--orange-700-04), var(--orange-700-04), var(--orange-50));
+ --warning-background-focus: linear-gradient(var(--orange-700-12), var(--orange-700-12), var(--orange-50));
--warning-ripple: var(--orange-700-10);
--info-foreground: var(--blue-700);
--info-background: var(--blue-50);
- --info-background-hover: linear-gradient(var(--blue-700-04), var(--blue-700-04)), var(--blue-50);
- --info-background-focus: linear-gradient(var(--blue-700-12), var(--blue-700-12)), var(--blue-50);
+ --info-background-hover: linear-gradient(var(--blue-700-04), var(--blue-700-04), var(--blue-50));
+ --info-background-focus: linear-gradient(var(--blue-700-12), var(--blue-700-12), var(--blue-50));
--info-ripple: var(--blue-700-10);
--primary-button-text-color: white;
@@ -154,14 +154,14 @@
--success-foreground: var(--green-700);
--success-background: var(--green-50);
- --success-background-hover: linear-gradient(var(--green-700-04), var(--green-700-04)), var(--green-50);
- --success-background-focus: linear-gradient(var(--green-700-12), var(--green-700-12)), var(--green-50);
+ --success-background-hover: linear-gradient(var(--green-700-04), var(--green-700-04), var(--green-50));
+ --success-background-focus: linear-gradient(var(--green-700-12), var(--green-700-12), var(--green-50));
--success-ripple: var(--green-700-10);
--gray-foreground: var(--gray-700);
--gray-background: var(--gray-100);
- --gray-background-hover: linear-gradient(var(--gray-700-04), var(--gray-700-04)), var(--gray-100);
- --gray-background-focus: linear-gradient(var(--gray-700-12), var(--gray-700-12)), var(--gray-100);
+ --gray-background-hover: linear-gradient(var(--gray-700-04), var(--gray-700-04), var(--gray-100));
+ --gray-background-focus: linear-gradient(var(--gray-700-12), var(--gray-700-12), var(--gray-100));
--gray-ripple: var(--gray-700-10);
--disabled-foreground: var(--gray-800-38);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 926b02d..69256b2 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -38,20 +38,20 @@
--error-foreground: var(--red-200);
--error-background: var(--red-tonal);
- --error-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--red-tonal);
- --error-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--red-tonal);
+ --error-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--red-tonal));
+ --error-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--red-tonal));
--error-ripple: var(--white-10);
--warning-foreground: var(--orange-200);
--warning-background: var(--orange-tonal);
- --warning-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--orange-tonal);
- --warning-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--orange-tonal);
+ --warning-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--orange-tonal));
+ --warning-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--orange-tonal));
--warning-ripple: var(--white-10);
--info-foreground: var(--blue-200);
--info-background: var(--blue-tonal);
- --info-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--blue-tonal);
- --info-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--blue-tonal);
+ --info-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--blue-tonal));
+ --info-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--blue-tonal));
--info-ripple: var(--white-10);
--primary-button-text-color: black;
@@ -64,14 +64,14 @@
--success-foreground: var(--green-200);
--success-background: var(--green-tonal);
- --success-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--green-tonal);
- --success-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--green-tonal);
+ --success-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--green-tonal));
+ --success-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--green-tonal));
--success-ripple: var(--white-10);
--gray-foreground: var(--gray-300);
--gray-background: var(--gray-tonal);
- --gray-background-hover: linear-gradient(var(--white-04), var(--white-04)), var(--gray-tonal);
- --gray-background-focus: linear-gradient(var(--white-12), var(--white-12)), var(--gray-tonal);
+ --gray-background-hover: linear-gradient(var(--white-04), var(--white-04), var(--gray-tonal));
+ --gray-background-focus: linear-gradient(var(--white-12), var(--white-12), var(--gray-tonal));
--gray-ripple: var(--white-10);
--disabled-foreground: var(--gray-200-38);