Merge "Prefactor to gr-related-changes-list"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index acf65a5..3109ec7 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1169,6 +1169,14 @@
Result of checking if one change or commit is a pure/clean revert of
another.
+cache `"soy_sauce_compiled_templates"`::
++
+Caches compiled soy templates. Stores at most only one key-value pair with
+a constant key value and the value is a compiled SoySauce templates. The value
+is reloaded automatically every few seconds if there are reads from the cache.
+If cache is not used for 1 minute, the item is removed (i.e. emails can be send
+with templates which are max 1 minute old).
+
cache `"sshkeys"`::
+
Caches unpacked versions of user SSH keys, so the internal SSH daemon
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 0318cd7..d8b6250 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -113,7 +113,7 @@
* `receivecommits/ps_revision_missing`: errors due to patch set revision missing
* `receivecommits/push_count`: number of pushes
** `kind`:
- The push kind (direct vs. magic).
+ The push kind (magic, direct or direct_submit).
** `project`:
The name of the project for which the push is done.
** `type`:
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index cc8d813..6e6b9d7 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -565,6 +565,11 @@
cherry-picked locally using the git cherry-pick command and then
pushed to Gerrit.
+[[pure-revert]]
+is:pure-revert::
++
+True if the change is a pure revert.
+
[[status]]
status:open, status:pending, status:new::
+
@@ -772,6 +777,22 @@
+
Matches changes with label voted with any score.
+`label:Code-Review=+1,count=2`::
++
+Matches changes with exactly two +1 votes to the code-review label. The {MAX,
+MIN, ANY} votes can also be used, for example `label:Code-Review=MAX,count=2` is
+equivalent to `label:Code-Review=2,count=2` (if 2 is the maximum positive vote
+for the code review label). The maximum supported value for `count` is 5.
+`count=0` is not allowed and the query request will fail with `400 Bad Request`.
+
+`label:Code-Review=+1,count>=2`::
++
+Matches changes having two or more +1 votes to the code-review label. Can also
+be used with the {MAX, MIN, ANY} label votes. All operators `>`, `>=`, `<`, `<=`
+are supported.
+Note that a query like `label:Code-Review=+1,count<x` will not match with
+changes having zero +1 votes to this label.
+
`label:Non-Author-Code-Review=need`::
+
Matches changes where the submit rules indicate that a label named
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index a3b5e8d..f7b343b 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -38,12 +38,14 @@
import com.github.rholder.retry.BlockStrategy;
import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.jimfs.Jimfs;
import com.google.common.primitives.Chars;
+import com.google.common.testing.FakeTicker;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -290,6 +292,7 @@
@Inject protected ChangeNotes.Factory notesFactory;
@Inject protected BatchAbandon batchAbandon;
@Inject protected TestSshKeys sshKeys;
+ @Inject protected TestTicker testTicker;
protected EventRecorder eventRecorder;
protected GerritServer server;
@@ -703,6 +706,8 @@
}
SystemReader.setInstance(oldSystemReader);
oldSystemReader = null;
+ // Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
+ testTicker.useDefaultTicker();
}
protected void closeSsh() {
@@ -1764,4 +1769,32 @@
moduleClass.getName());
}
}
+
+ /** {@link Ticker} implementation for mocking without restarting GerritServer */
+ public static class TestTicker extends Ticker {
+ Ticker actualTicker;
+
+ public TestTicker() {
+ useDefaultTicker();
+ }
+
+ /** Switches to system ticker */
+ public Ticker useDefaultTicker() {
+ this.actualTicker = Ticker.systemTicker();
+ return actualTicker;
+ }
+
+ /** Switches to {@link FakeTicker} */
+ public FakeTicker useFakeTicker() {
+ if (!(this.actualTicker instanceof FakeTicker)) {
+ this.actualTicker = new FakeTicker();
+ }
+ return (FakeTicker) actualTicker;
+ }
+
+ @Override
+ public long read() {
+ return actualTicker.read();
+ }
+ }
}
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index fa62cd9..fe6e160 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -124,6 +124,7 @@
"//lib/truth",
"//lib/truth:truth-java8-extension",
"//lib/greenmail",
+ "//lib:guava-testlib",
] + TEST_DEPS
java_library(
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 33abc68..402d21d 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -22,7 +22,9 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest.TestTicker;
import com.google.gerrit.acceptance.FakeGroupAuditService.FakeGroupAuditServiceModule;
import com.google.gerrit.acceptance.ReindexGroupsAtStartup.ReindexGroupsAtStartupModule;
import com.google.gerrit.acceptance.ReindexProjectsAtStartup.ReindexProjectsAtStartupModule;
@@ -75,6 +77,7 @@
import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.Singleton;
+import com.google.inject.multibindings.OptionalBinder;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.Field;
@@ -429,6 +432,23 @@
.to(GitObjectVisibilityChecker.class);
}
});
+ daemon.addAdditionalSysModuleForTesting(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ // GerritServer isn't restarted between tests. TestTicker allows to replace actual
+ // Ticker in tests without restarting server and transparently for other code.
+ // Alternative option with Provider<Ticker> is less convinient, because it affects how
+ // gerrit code should be written - i.e. Ticker must not be stored in fields and must
+ // always be obtained from the provider.
+ TestTicker testTicker = new TestTicker();
+ OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+ .setBinding()
+ .toInstance(testTicker);
+ bind(TestTicker.class).toInstance(testTicker);
+ }
+ });
if (desc.memory()) {
checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index 2263aba..349b67e 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -489,7 +489,7 @@
return Integer.parseInt(rest.substring(0, ie));
}
- static Integer parseRefSuffix(String name) {
+ public static Integer parseRefSuffix(String name) {
if (name == null) {
return null;
}
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
index 900b2e2..9e4416b 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -39,17 +39,17 @@
/**
* List leaf predicates that are fulfilled, for example the expression
*
- * <p><i>label:code-review=+2 and branch:refs/heads/master</i>
+ * <p><i>label:Code-Review=+2 and branch:refs/heads/master</i>
*
* <p>has two leaf predicates:
*
* <ul>
- * <li>label:code-review=+2
+ * <li>label:Code-Review=+2
* <li>branch:refs/heads/master
* </ul>
*
* This method will return the leaf predicates that were fulfilled, for example if only the first
- * predicate was fulfilled, the returned list will be equal to ["label:code-review=+2"].
+ * predicate was fulfilled, the returned list will be equal to ["label:Code-Review=+2"].
*/
public abstract ImmutableList<String> passingAtoms();
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index ba9f6d6..1d38877 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -52,9 +52,11 @@
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -465,10 +467,35 @@
}
}
+ /** returns all changes that contain draft comments of {@code accountId}. */
+ public Collection<Change.Id> getChangesWithDrafts(Account.Id accountId) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return getChangesWithDrafts(repo, accountId);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId) throws IOException {
return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId));
}
+ private Collection<Change.Id> getChangesWithDrafts(Repository repo, Account.Id accountId)
+ throws IOException {
+ Set<Change.Id> changes = new HashSet<>();
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
+ Integer accountIdFromRef = RefNames.parseRefSuffix(ref.getName());
+ if (accountIdFromRef != null && accountIdFromRef == accountId.get()) {
+ Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
+ if (changeId == null) {
+ continue;
+ }
+ changes.add(changeId);
+ }
+ }
+ return changes;
+ }
+
private static <T extends Comment> List<T> sort(List<T> comments) {
comments.sort(COMMENT_ORDER);
return comments;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 5dcbd01..7c61c92 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -289,6 +289,35 @@
}
}
+ public ImmutableSet<Change.Id> byAccountId(Account.Id accountId, String label) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
+ Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
+ // Skip all refs that don't correspond with accountId.
+ if (currentAccountId == null || !currentAccountId.equals(accountId)) {
+ continue;
+ }
+ // Skip all refs that don't contain the required label.
+ StarRef starRef = readLabels(repo, ref.getName());
+ if (!starRef.labels().contains(label)) {
+ continue;
+ }
+
+ // Skip invalid change ids.
+ Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
+ if (changeId == null) {
+ continue;
+ }
+ builder.add(changeId);
+ }
+ return builder.build();
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format("Get starred changes for account %d failed", accountId.get()), e);
+ }
+ }
+
public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
List<ChangeData> changeData =
queryProvider
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 85482e4..c8001bb 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -364,7 +364,7 @@
* <p>Should not be used in new code, as it doesn't result in a single atomic batch ref update for
* code and NoteDb meta refs.
*
- * @param updateRef whether to update the ref during {@code updateRepo}.
+ * @param updateRef whether to update the ref during {@link #updateRepo(RepoContext)}.
*/
@Deprecated
public ChangeInserter setUpdateRef(boolean updateRef) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 3a7f2b2..24882cb 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -16,6 +16,7 @@
import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.changes.ActionVisitor;
@@ -167,9 +168,8 @@
import com.google.gerrit.server.mail.send.FromAddressGenerator;
import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
-import com.google.gerrit.server.mail.send.MailSoySauceProvider;
+import com.google.gerrit.server.mail.send.MailSoySauceModule;
import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
-import com.google.gerrit.server.mail.send.MailTemplates;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.notedb.NoteDbModule;
@@ -187,6 +187,7 @@
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectNameLockManager;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.approval.ApprovalModule;
@@ -224,7 +225,7 @@
import com.google.inject.Inject;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.UniqueAnnotations;
-import com.google.template.soy.jbcsrc.api.SoySauce;
+import com.google.inject.multibindings.OptionalBinder;
import java.util.List;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.PostReceiveHook;
@@ -286,6 +287,7 @@
install(new FileInfoJsonModule());
install(ThreadLocalRequestContext.module());
install(new ApprovalModule());
+ install(new MailSoySauceModule());
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
@@ -327,7 +329,6 @@
bind(ApprovalsUtil.class);
- bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(MailSoySauceProvider.class);
bind(FromAddressGenerator.class).toProvider(FromAddressGeneratorProvider.class).in(SINGLETON);
bind(Boolean.class)
.annotatedWith(EnablePeerIPInReflogRecord.class)
@@ -337,6 +338,9 @@
bind(PatchSetInfoFactory.class);
bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
bind(AccountControl.Factory.class);
+ OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+ .setDefault()
+ .toInstance(Ticker.systemTicker());
bind(UiActions.class);
@@ -388,6 +392,8 @@
DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class);
DynamicSet.setOf(binder(), UserScopedEventListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
+ DynamicSet.bind(binder(), CommitValidationListener.class)
+ .to(SubmitRequirementExpressionsValidator.class);
DynamicSet.setOf(binder(), CommentValidator.class);
DynamicSet.setOf(binder(), ChangeMessageModifier.class);
DynamicSet.setOf(binder(), RefOperationValidationListener.class);
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 1486559..65f8f2d 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -45,6 +45,13 @@
GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS_BACKFILLING_ON_DASHBOARD =
"GerritBackendRequestFeature__enable_submit_requirements_backfilling_on_dashboard";
+ /**
+ * When set, we compute information from All-Users repository if able, instead of computing it
+ * from the change index.
+ */
+ public static final String GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY =
+ "GerritBackendRequestFeature__compute_from_all_users_repository";
+
/** Features, enabled by default in the current release. */
public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
ImmutableSet.of(UI_FEATURE_PATCHSET_COMMENTS);
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 3de8ff3..09f08bd 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -19,6 +19,7 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.server.CancellationMetrics;
@@ -180,6 +181,7 @@
private Optional<Long> timeout = Optional.empty();
private final long maxIntervalNanos;
+ private final Ticker ticker;
/**
* Create a new progress monitor for multiple sub-tasks.
@@ -190,10 +192,11 @@
@AssistedInject
private MultiProgressMonitor(
CancellationMetrics cancellationMetrics,
+ Ticker ticker,
@Assisted OutputStream out,
@Assisted TaskKind taskKind,
@Assisted String taskName) {
- this(cancellationMetrics, out, taskKind, taskName, 500, MILLISECONDS);
+ this(cancellationMetrics, ticker, out, taskKind, taskName, 500, MILLISECONDS);
}
/**
@@ -207,12 +210,14 @@
@AssistedInject
private MultiProgressMonitor(
CancellationMetrics cancellationMetrics,
+ Ticker ticker,
@Assisted OutputStream out,
@Assisted TaskKind taskKind,
@Assisted String taskName,
@Assisted long maxIntervalTime,
@Assisted TimeUnit maxIntervalUnit) {
this.cancellationMetrics = cancellationMetrics;
+ this.ticker = ticker;
this.out = out;
this.taskKind = taskKind;
this.taskName = taskName;
@@ -262,7 +267,7 @@
long cancellationTimeoutTime,
TimeUnit cancellationTimeoutUnit)
throws TimeoutException {
- long overallStart = System.nanoTime();
+ long overallStart = ticker.read();
long cancellationNanos =
cancellationTimeoutTime > 0
? NANOSECONDS.convert(cancellationTimeoutTime, cancellationTimeoutUnit)
@@ -278,16 +283,33 @@
synchronized (this) {
long left = maxIntervalNanos;
while (!done) {
- long start = System.nanoTime();
+ long start = ticker.read();
try {
- NANOSECONDS.timedWait(this, left);
+ // Conditions below gives better granularity for timeouts.
+ // Originally, code always used fixed interval:
+ // NANOSECONDS.timedWait(this, maxIntervalNanos);
+ // As a result, the actual check for timeouts happened only every maxIntervalNanos
+ // (default value 500ms); so even if timout was set to 1ms, the actual timeout was 500ms.
+ // This is not a big issue, however it made our tests for timeouts flaky. For example,
+ // some tests in the CancellationIT set timeout to 1ms and expect that server returns
+ // timeout. However, server often returned OK result, because a request takes less than
+ // 500ms.
+ if (deadlineExceeded || deadline == 0) {
+ // We want to set deadlineExceeded flag as earliest as possible. If it is already
+ // set - there is no reason to wait less than maxIntervalNanos
+ NANOSECONDS.timedWait(this, maxIntervalNanos);
+ } else if (start <= deadline) {
+ // if deadlineExceeded is not set, then we should wait until deadline, but no longer
+ // than maxIntervalNanos (because we want to report a progress every maxIntervalNanos).
+ NANOSECONDS.timedWait(this, Math.min(deadline - start + 1, maxIntervalNanos));
+ }
} catch (InterruptedException e) {
throw new UncheckedExecutionException(e);
}
// Send an update on every wakeup (manual or spurious), but only move
// the spinner every maxInterval.
- long now = System.nanoTime();
+ long now = ticker.read();
if (deadline > 0 && now > deadline) {
if (!deadlineExceeded) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 66e7d80..f4c7a92 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -747,14 +747,19 @@
return;
}
- if (!magicCommands.isEmpty()) {
- metrics.pushCount.increment("magic", project.getName(), getUpdateType(magicCommands));
- }
- if (!regularCommands.isEmpty()) {
- metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
- }
-
try {
+ if (!magicCommands.isEmpty()) {
+ parseMagicBranch(Iterables.getLast(magicCommands));
+ // Using the submit option submits the created change(s) immediately without checking labels
+ // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
+ // code review is being done.
+ String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
+ metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
+ }
+ if (!regularCommands.isEmpty()) {
+ metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
+ }
+
if (!regularCommands.isEmpty()) {
handleRegularCommands(regularCommands, progress);
return;
@@ -763,7 +768,6 @@
boolean first = true;
for (ReceiveCommand cmd : magicCommands) {
if (first) {
- parseMagicBranch(cmd);
first = false;
} else {
reject(cmd, "duplicate request");
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index e580f50..a2ef070 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
+import com.google.common.base.Ticker;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
@@ -60,6 +61,7 @@
import com.google.inject.Provides;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
+import com.google.inject.multibindings.OptionalBinder;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -112,6 +114,9 @@
@Override
protected void configure() {
factory(MultiProgressMonitor.Factory.class);
+ OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+ .setDefault()
+ .toInstance(Ticker.systemTicker());
bind(AccountIndexRewriter.class);
bind(AccountIndexCollection.class);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index b9569e4..2cdb7c8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -33,6 +33,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -412,6 +413,10 @@
integer(ChangeQueryBuilder.FIELD_REVERTOF)
.build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
+ public static final FieldDef<ChangeData, String> IS_PURE_REVERT =
+ fullText(ChangeQueryBuilder.FIELD_PURE_REVERT)
+ .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
+
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
@@ -612,47 +617,107 @@
private static Iterable<String> getLabels(ChangeData cd) {
Set<String> allApprovals = new HashSet<>();
Set<String> distinctApprovals = new HashSet<>();
+ Table<String, Short, Integer> voteCounts = HashBasedTable.create();
for (PatchSetApproval a : cd.currentApprovals()) {
if (a.value() != 0 && !a.isLegacySubmit()) {
- allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+ increment(voteCounts, a.label(), a.value());
Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
- allApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, a.accountId()));
- if (cd.change().getOwner().equals(a.accountId())) {
- allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
- allApprovals.addAll(
- getMaxMinAnyLabels(
- a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
- }
- if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
- allApprovals.add(
- formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
- allApprovals.addAll(
- getMaxMinAnyLabels(
- a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
- }
+
+ allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+ allApprovals.addAll(getMagicLabelFormats(a.label(), a.value(), labelType, a.accountId()));
+ allApprovals.addAll(getLabelOwnerFormats(a, cd, labelType));
+ allApprovals.addAll(getLabelNonUploaderFormats(a, cd, labelType));
distinctApprovals.add(formatLabel(a.label(), a.value()));
- distinctApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, null));
+ distinctApprovals.addAll(
+ getMagicLabelFormats(a.label(), a.value(), labelType, /* accountId= */ null));
}
}
allApprovals.addAll(distinctApprovals);
+ allApprovals.addAll(getCountLabelFormats(voteCounts, cd));
return allApprovals;
}
- private static List<String> getMaxMinAnyLabels(
+ private static void increment(Table<String, Short, Integer> table, String k1, short k2) {
+ if (!table.contains(k1, k2)) {
+ table.put(k1, k2, 1);
+ } else {
+ int val = table.get(k1, k2);
+ table.put(k1, k2, val + 1);
+ }
+ }
+
+ private static List<String> getCountLabelFormats(
+ Table<String, Short, Integer> voteCounts, ChangeData cd) {
+ List<String> allFormats = new ArrayList<>();
+ for (String label : voteCounts.rowMap().keySet()) {
+ Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label);
+ Map<Short, Integer> row = voteCounts.row(label);
+ for (short vote : row.keySet()) {
+ int count = row.get(vote);
+ allFormats.addAll(getCountLabelFormats(labelType, label, vote, count));
+ }
+ }
+ return allFormats;
+ }
+
+ private static List<String> getCountLabelFormats(
+ Optional<LabelType> labelType, String label, short vote, int count) {
+ List<String> formats =
+ getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count);
+ formats.add(formatLabel(label, vote, count));
+ return formats;
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
+ return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null);
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
+ String label,
+ short labelVal,
+ Optional<LabelType> labelType,
+ @Nullable Account.Id accountId,
+ @Nullable Integer count) {
List<String> labels = new ArrayList<>();
if (labelType.isPresent()) {
if (labelVal == labelType.get().getMaxPositive()) {
- labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count));
}
if (labelVal == labelType.get().getMaxNegative()) {
- labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count));
}
}
- labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count));
return labels;
}
+ private static List<String> getLabelOwnerFormats(
+ PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
+ List<String> allFormats = new ArrayList<>();
+ if (cd.change().getOwner().equals(a.accountId())) {
+ allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ allFormats.addAll(
+ getMagicLabelFormats(
+ a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ }
+ return allFormats;
+ }
+
+ private static List<String> getLabelNonUploaderFormats(
+ PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
+ List<String> allFormats = new ArrayList<>();
+ if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
+ allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+ allFormats.addAll(
+ getMagicLabelFormats(
+ a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+ }
+ return allFormats;
+ }
+
public static Set<String> getAuthorParts(ChangeData cd) {
return SchemaUtil.getPersonParts(cd.getAuthor());
}
@@ -727,25 +792,33 @@
decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
public static String formatLabel(String label, int value) {
- return formatLabel(label, value, null);
+ return formatLabel(label, value, /* accountId= */ null, /* count= */ null);
+ }
+
+ public static String formatLabel(String label, int value, @Nullable Integer count) {
+ return formatLabel(label, value, /* accountId= */ null, count);
}
public static String formatLabel(String label, int value, Account.Id accountId) {
+ return formatLabel(label, value, accountId, /* count= */ null);
+ }
+
+ public static String formatLabel(
+ String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ (value >= 0 ? "+" : "")
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
- public static String formatLabel(String label, String value) {
- return formatLabel(label, value, null);
- }
-
- public static String formatLabel(String label, String value, @Nullable Account.Id accountId) {
+ public static String formatLabel(
+ String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ "="
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
private static String formatAccount(Account.Id accountId) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 9339d62..ee93065 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -183,9 +183,18 @@
new Schema.Builder<ChangeData>().add(V69).add(ChangeField.ATTENTION_SET_USERS_COUNT).build();
/** Added new field {@link ChangeField#UPLOADER}. */
+ @Deprecated
static final Schema<ChangeData> V71 =
new Schema.Builder<ChangeData>().add(V70).add(ChangeField.UPLOADER).build();
+ /** Added new field {@link ChangeField#IS_PURE_REVERT}. */
+ @Deprecated
+ static final Schema<ChangeData> V72 =
+ new Schema.Builder<ChangeData>().add(V71).add(ChangeField.IS_PURE_REVERT).build();
+
+ /** Added new "count=$count" argument to the {@link ChangeField#LABEL} operator. */
+ static final Schema<ChangeData> V73 = schema(V72, false);
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
similarity index 94%
rename from java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
rename to java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index aade30f..ad1703d 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -19,7 +19,6 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import com.google.template.soy.SoyFileSet;
@@ -31,9 +30,13 @@
import java.nio.file.Files;
import java.nio.file.Path;
-/** Configures Soy Sauce object for rendering email templates. */
+/**
+ * Configures and loads Soy Sauce object for rendering email templates.
+ *
+ * <p>It reloads templates each time when {@link #load()} is called.
+ */
@Singleton
-public class MailSoySauceProvider implements Provider<SoySauce> {
+class MailSoySauceLoader {
// Note: will fail to construct the tofu object if this array is empty.
private static final String[] TEMPLATES = {
@@ -90,7 +93,7 @@
private final PluginSetContext<MailSoyTemplateProvider> templateProviders;
@Inject
- MailSoySauceProvider(
+ MailSoySauceLoader(
SitePaths site,
SoyAstCache cache,
PluginSetContext<MailSoyTemplateProvider> templateProviders) {
@@ -99,8 +102,7 @@
this.templateProviders = templateProviders;
}
- @Override
- public SoySauce get() throws ProvisionException {
+ public SoySauce load() {
SoyFileSet.Builder builder = SoyFileSet.builder();
builder.setSoyAstCache(cache);
for (String name : TEMPLATES) {
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
new file mode 100644
index 0000000..a3cf3e3
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
@@ -0,0 +1,89 @@
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.server.CacheRefreshExecutor;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.template.soy.jbcsrc.api.SoySauce;
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import javax.inject.Provider;
+
+/**
+ * Provides support for soy templates
+ *
+ * <p>Module loads templates with {@link MailSoySauceLoader} and caches compiled templates. The
+ * cache refreshes automatically, so Gerrit does not need to be restarted if templates are changed.
+ */
+public class MailSoySauceModule extends CacheModule {
+ static final String CACHE_NAME = "soy_sauce_compiled_templates";
+ private static final String SOY_LOADING_CACHE_KEY = "KEY";
+
+ @Override
+ protected void configure() {
+ // Cache stores only a single key-value pair (key is SOY_LOADING_CACHE_KEY). We are using
+ // cache only for it refresh/expire logic.
+ cache(CACHE_NAME, String.class, SoySauce.class)
+ // Cache refreshes a value only on the access (if refreshAfterWrite interval is
+ // passed). While the value is refreshed, cache returns old value.
+ // Adding expireAfterWrite interval prevents cache from returning very old template.
+ .refreshAfterWrite(Duration.ofSeconds(5))
+ .expireAfterWrite(Duration.ofMinutes(1))
+ .loader(SoySauceCacheLoader.class);
+ bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(SoySauceProvider.class);
+ }
+
+ @Singleton
+ static class SoySauceProvider implements Provider<SoySauce> {
+ private final LoadingCache<String, SoySauce> templateCache;
+
+ @Inject
+ SoySauceProvider(@Named(CACHE_NAME) LoadingCache<String, SoySauce> templateCache) {
+ this.templateCache = templateCache;
+ }
+
+ @Override
+ public SoySauce get() {
+ try {
+ return templateCache.get(SOY_LOADING_CACHE_KEY);
+ } catch (ExecutionException e) {
+ throw new ProvisionException("Can't get SoySauce from the cache", e);
+ }
+ }
+ }
+
+ @Singleton
+ static class SoySauceCacheLoader extends CacheLoader<String, SoySauce> {
+ private final ListeningExecutorService executor;
+ private final MailSoySauceLoader loader;
+
+ @Inject
+ SoySauceCacheLoader(
+ @CacheRefreshExecutor ListeningExecutorService executor, MailSoySauceLoader loader) {
+ this.executor = executor;
+ this.loader = loader;
+ }
+
+ @Override
+ public SoySauce load(String key) throws Exception {
+ checkArgument(
+ SOY_LOADING_CACHE_KEY.equals(key),
+ "Cache can have only one element with a key '%s'",
+ SOY_LOADING_CACHE_KEY);
+ return loader.load();
+ }
+
+ @Override
+ public ListenableFuture<SoySauce> reload(String key, SoySauce soySauce) {
+ return executor.submit(() -> loader.load());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 8f0b535..11ffcad 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -28,6 +28,7 @@
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
@@ -130,6 +131,13 @@
public static final String KEY_SR_SUBMITTABILITY_EXPRESSION = "submittableIf";
public static final String KEY_SR_OVERRIDE_EXPRESSION = "overrideIf";
public static final String KEY_SR_OVERRIDE_IN_CHILD_PROJECTS = "canOverrideInChildProjects";
+ public static final ImmutableSet<String> SR_KEYS =
+ ImmutableSet.of(
+ KEY_SR_DESCRIPTION,
+ KEY_SR_APPLICABILITY_EXPRESSION,
+ KEY_SR_SUBMITTABILITY_EXPRESSION,
+ KEY_SR_OVERRIDE_EXPRESSION,
+ KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);
public static final String KEY_MATCH = "match";
private static final String KEY_HTML = "html";
@@ -936,6 +944,8 @@
}
private void loadSubmitRequirementSections(Config rc) {
+ checkForUnsupportedSubmitRequirementParams(rc);
+
Map<String, String> lowerNames = new HashMap<>();
submitRequirementSections = new LinkedHashMap<>();
for (String name : rc.getSubsections(SUBMIT_REQUIREMENT)) {
@@ -951,18 +961,34 @@
String appExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION);
String blockExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION);
String overrideExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_EXPRESSION);
- boolean canInherit =
- rc.getBoolean(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, false);
+ boolean canInherit;
+ try {
+ canInherit =
+ rc.getBoolean(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, false);
+ } catch (IllegalArgumentException e) {
+ String canInheritValue =
+ rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);
+ error(
+ String.format(
+ "Invalid value %s.%s.%s for submit requirement '%s': %s",
+ SUBMIT_REQUIREMENT,
+ name,
+ KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ name,
+ canInheritValue));
+ continue;
+ }
if (blockExpr == null) {
error(
String.format(
- "Submit requirement '%s' does not define a submittability expression.", name));
+ "Setting a submittability expression for submit requirement '%s' is required:"
+ + " Missing %s.%s.%s",
+ name, SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION));
continue;
}
- // TODO(SR): add expressions validation. Expressions are stored as strings so we need to
- // validate their syntax.
+ // The expressions are validated in SubmitRequirementExpressionsValidator.
SubmitRequirement submitRequirement =
SubmitRequirement.builder()
@@ -978,6 +1004,43 @@
}
}
+ /**
+ * Report unsupported submit requirement parameters as errors.
+ *
+ * <p>Unsupported are submit requirements parameters that
+ *
+ * <ul>
+ * <li>are directly set in the {@code submit-requirement} section (as submit requirements are
+ * solely defined in subsections)
+ * <li>are unknown (maybe they were accidentally misspelled?)
+ * </ul>
+ */
+ private void checkForUnsupportedSubmitRequirementParams(Config rc) {
+ Set<String> directSubmitRequirementParams = rc.getNames(SUBMIT_REQUIREMENT);
+ if (!directSubmitRequirementParams.isEmpty()) {
+ error(
+ String.format(
+ "Submit requirements must be defined in %s.<name> subsections."
+ + " Setting parameters directly in the %s section is not allowed: %s",
+ SUBMIT_REQUIREMENT,
+ SUBMIT_REQUIREMENT,
+ directSubmitRequirementParams.stream().sorted().collect(toImmutableList())));
+ }
+
+ for (String subsection : rc.getSubsections(SUBMIT_REQUIREMENT)) {
+ ImmutableList<String> unknownSubmitRequirementParams =
+ rc.getNames(SUBMIT_REQUIREMENT, subsection).stream()
+ .filter(p -> !SR_KEYS.contains(p))
+ .collect(toImmutableList());
+ if (!unknownSubmitRequirementParams.isEmpty()) {
+ error(
+ String.format(
+ "Unsupported parameters for submit requirement '%s': %s",
+ subsection, unknownSubmitRequirementParams));
+ }
+ }
+ }
+
private void loadLabelSections(Config rc) {
Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
labelSections = new LinkedHashMap<>();
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java b/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
new file mode 100644
index 0000000..738e71b
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
@@ -0,0 +1,184 @@
+// 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.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.index.query.QueryParseException;
+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.git.validators.ValidationMessage;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffOperations;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * Validates the expressions of submit requirements in {@code project.config}.
+ *
+ * <p>Other validation of submit requirements is done in {@link ProjectConfig}, see {@code
+ * ProjectConfig#loadSubmitRequirementSections(Config)}.
+ *
+ * <p>The validation of the expressions cannot be in {@link ProjectConfig} as it requires injecting
+ * {@link SubmitRequirementsEvaluator} and we cannot do injections into {@link ProjectConfig} (since
+ * {@link ProjectConfig} is cached in the project cache).
+ */
+@Singleton
+public class SubmitRequirementExpressionsValidator implements CommitValidationListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final DiffOperations diffOperations;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final SubmitRequirementsEvaluator submitRequirementsEvaluator;
+
+ @Inject
+ SubmitRequirementExpressionsValidator(
+ DiffOperations diffOperations,
+ ProjectConfig.Factory projectConfigFactory,
+ SubmitRequirementsEvaluator submitRequirementsEvaluator) {
+ this.diffOperations = diffOperations;
+ this.projectConfigFactory = projectConfigFactory;
+ this.submitRequirementsEvaluator = submitRequirementsEvaluator;
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent event)
+ throws CommitValidationException {
+ try {
+ if (!event.refName.equals(RefNames.REFS_CONFIG)
+ || !isFileChanged(event, ProjectConfig.PROJECT_CONFIG)) {
+ // the project.config file in refs/meta/config was not modified, hence we do not need to
+ // validate the submit requirements in it
+ return ImmutableList.of();
+ }
+
+ ProjectConfig projectConfig = getProjectConfig(event);
+ ImmutableList<CommitValidationMessage> validationMessages =
+ validateSubmitRequirementExpressions(
+ projectConfig.getSubmitRequirementSections().values());
+ if (!validationMessages.isEmpty()) {
+ throw new CommitValidationException(
+ String.format(
+ "invalid submit requirement expressions in %s (revision = %s)",
+ ProjectConfig.PROJECT_CONFIG, projectConfig.getRevision()),
+ validationMessages);
+ }
+ return ImmutableList.of();
+ } catch (IOException | DiffNotAvailableException | ConfigInvalidException e) {
+ String errorMessage =
+ String.format(
+ "failed to validate submit requirement expressions in %s for revision %s in ref %s"
+ + " of project %s",
+ ProjectConfig.PROJECT_CONFIG,
+ event.commit.getName(),
+ RefNames.REFS_CONFIG,
+ event.project.getNameKey());
+ logger.atSevere().withCause(e).log(errorMessage);
+ throw new CommitValidationException(errorMessage, e);
+ }
+ }
+
+ /**
+ * Whether the given file was changed in the given revision.
+ *
+ * @param receiveEvent the receive event
+ * @param fileName the name of the file
+ */
+ private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
+ throws DiffNotAvailableException {
+ return diffOperations
+ .listModifiedFilesAgainstParent(
+ receiveEvent.project.getNameKey(), receiveEvent.commit, /* parentNum=*/ 0)
+ .keySet().stream()
+ .anyMatch(fileName::equals);
+ }
+
+ private ProjectConfig getProjectConfig(CommitReceivedEvent receiveEvent)
+ throws IOException, ConfigInvalidException {
+ ProjectConfig projectConfig = projectConfigFactory.create(receiveEvent.project.getNameKey());
+ projectConfig.load(receiveEvent.revWalk, receiveEvent.commit);
+ return projectConfig;
+ }
+
+ private ImmutableList<CommitValidationMessage> validateSubmitRequirementExpressions(
+ Collection<SubmitRequirement> submitRequirements) {
+ List<CommitValidationMessage> validationMessages = new ArrayList<>();
+ for (SubmitRequirement submitRequirement : submitRequirements) {
+ validateSubmitRequirementExpression(
+ validationMessages,
+ submitRequirement,
+ submitRequirement.submittabilityExpression(),
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION);
+ submitRequirement
+ .applicabilityExpression()
+ .ifPresent(
+ expression ->
+ validateSubmitRequirementExpression(
+ validationMessages,
+ submitRequirement,
+ expression,
+ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION));
+ submitRequirement
+ .overrideExpression()
+ .ifPresent(
+ expression ->
+ validateSubmitRequirementExpression(
+ validationMessages,
+ submitRequirement,
+ expression,
+ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION));
+ }
+ return ImmutableList.copyOf(validationMessages);
+ }
+
+ private void validateSubmitRequirementExpression(
+ List<CommitValidationMessage> validationMessages,
+ SubmitRequirement submitRequirement,
+ SubmitRequirementExpression expression,
+ String configKey) {
+ try {
+ submitRequirementsEvaluator.validateExpression(expression);
+ } catch (QueryParseException e) {
+ if (validationMessages.isEmpty()) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ "Invalid project configuration", ValidationMessage.Type.ERROR));
+ }
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ " %s: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: %s",
+ ProjectConfig.PROJECT_CONFIG,
+ expression.expressionString(),
+ submitRequirement.name(),
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirement.name(),
+ configKey,
+ e.getMessage()),
+ ValidationMessage.Type.ERROR));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 043b37d..7e50c6f 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.query.change;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
import com.google.common.base.CharMatcher;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
@@ -21,12 +23,15 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.HashtagsUtil;
import com.google.gerrit.server.index.change.ChangeField;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
/** Predicates that match against {@link ChangeData}. */
public class ChangePredicates {
@@ -76,8 +81,34 @@
* Returns a predicate that matches changes where the provided {@link
* com.google.gerrit.entities.Account.Id} has a pending draft comment.
*/
- public static Predicate<ChangeData> draftBy(Account.Id id) {
- return new ChangeIndexPredicate(ChangeField.DRAFTBY, id.toString());
+ public static Predicate<ChangeData> draftBy(
+ boolean computeFromAllUsersRepository, CommentsUtil commentsUtil, Account.Id id) {
+ if (!computeFromAllUsersRepository) {
+ return new ChangeIndexPredicate(ChangeField.DRAFTBY, id.toString());
+ }
+ Set<Predicate<ChangeData>> changeIdPredicates =
+ commentsUtil.getChangesWithDrafts(id).stream()
+ .map(ChangePredicates::idStr)
+ .collect(toImmutableSet());
+ return Predicate.or(changeIdPredicates);
+ }
+
+ /**
+ * Returns a predicate that matches changes where the provided {@link
+ * com.google.gerrit.entities.Account.Id} has starred changes with {@code label}.
+ */
+ public static Predicate<ChangeData> starBy(
+ boolean computeFromAllUsersRepository,
+ StarredChangesUtil starredChangesUtil,
+ Account.Id id,
+ String label) {
+ if (!computeFromAllUsersRepository) {
+ return new StarPredicate(id, label);
+ }
+ return Predicate.or(
+ starredChangesUtil.byAccountId(id, label).stream()
+ .map(ChangePredicates::idStr)
+ .collect(toImmutableSet()));
}
/**
@@ -311,4 +342,12 @@
public static Predicate<ChangeData> submitRuleStatus(String value) {
return new ChangeIndexPredicate(ChangeField.SUBMIT_RULE_RESULT, value);
}
+
+ /**
+ * Returns a predicate that matches with changes that are pure reverts if {@code value} is equal
+ * to "1", or non-pure reverts if {@code value} is "0".
+ */
+ public static Predicate<ChangeData> pureRevert(String value) {
+ return new ChangeIndexPredicate(ChangeField.IS_PURE_REVERT, value);
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index da36633..d435df1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -17,6 +17,7 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
import static com.google.gerrit.server.account.AccountResolver.isSelf;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -42,6 +43,7 @@
import com.google.gerrit.exceptions.NotSignedInException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaUtil;
@@ -70,6 +72,7 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.HasOperandAliasConfig;
import com.google.gerrit.server.config.OperatorAliasConfig;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndex;
@@ -81,6 +84,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ChildProjects;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.submit.SubmitDryRun;
import com.google.inject.Inject;
@@ -201,6 +205,7 @@
public static final String FIELD_WATCHEDBY = "watchedby";
public static final String FIELD_WIP = "wip";
public static final String FIELD_REVERTOF = "revertof";
+ public static final String FIELD_PURE_REVERT = "ispurerevert";
public static final String FIELD_CHERRYPICK = "cherrypick";
public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
@@ -210,6 +215,7 @@
public static final String ARG_ID_GROUP = "group";
public static final String ARG_ID_OWNER = "owner";
public static final String ARG_ID_NON_UPLOADER = "non_uploader";
+ public static final String ARG_COUNT = "count";
public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
@@ -253,6 +259,7 @@
final OperatorAliasConfig operatorAliasConfig;
final boolean indexMergeable;
final boolean conflictsPredicateEnabled;
+ final ExperimentFeatures experimentFeatures;
final HasOperandAliasConfig hasOperandAliasConfig;
final PluginSetContext<SubmitRule> submitRules;
@@ -288,6 +295,7 @@
GroupMembers groupMembers,
OperatorAliasConfig operatorAliasConfig,
@GerritServerConfig Config gerritConfig,
+ ExperimentFeatures experimentFeatures,
HasOperandAliasConfig hasOperandAliasConfig,
ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
PluginSetContext<SubmitRule> submitRules) {
@@ -320,6 +328,7 @@
operatorAliasConfig,
MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(),
gerritConfig.getBoolean("change", null, "conflictsPredicateEnabled", true),
+ experimentFeatures,
hasOperandAliasConfig,
changeIsVisbleToPredicateFactory,
submitRules);
@@ -354,6 +363,7 @@
OperatorAliasConfig operatorAliasConfig,
boolean indexMergeable,
boolean conflictsPredicateEnabled,
+ ExperimentFeatures experimentFeatures,
HasOperandAliasConfig hasOperandAliasConfig,
ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
PluginSetContext<SubmitRule> submitRules) {
@@ -386,6 +396,7 @@
this.operatorAliasConfig = operatorAliasConfig;
this.indexMergeable = indexMergeable;
this.conflictsPredicateEnabled = conflictsPredicateEnabled;
+ this.experimentFeatures = experimentFeatures;
this.hasOperandAliasConfig = hasOperandAliasConfig;
this.submitRules = submitRules;
}
@@ -420,6 +431,7 @@
operatorAliasConfig,
indexMergeable,
conflictsPredicateEnabled,
+ experimentFeatures,
hasOperandAliasConfig,
changeIsVisbleToPredicateFactory,
submitRules);
@@ -513,22 +525,14 @@
@Operator
public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
- if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
- throw new QueryParseException(
- String.format(
- "'%s' operator is not supported by change index version", OPERATOR_MERGED_BEFORE));
- }
+ checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_BEFORE);
return new BeforePredicate(
ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
}
@Operator
public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
- if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
- throw new QueryParseException(
- String.format(
- "'%s' operator is not supported by change index version", OPERATOR_MERGED_AFTER));
- }
+ checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_AFTER);
return new AfterPredicate(
ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
}
@@ -618,10 +622,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'has:attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
return new IsAttentionPredicate();
}
@@ -664,20 +665,13 @@
}
if ("uploader".equalsIgnoreCase(value)) {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException(
- "'is:uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "is:uploader");
return ChangePredicates.uploader(self());
}
if ("reviewer".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return Predicate.and(
- Predicate.not(new BooleanPredicate(ChangeField.WIP)),
- ReviewerPredicate.reviewer(self()));
- }
- return ReviewerPredicate.reviewer(self());
+ return Predicate.and(
+ Predicate.not(new BooleanPredicate(ChangeField.WIP)), ReviewerPredicate.reviewer(self()));
}
if ("cc".equalsIgnoreCase(value)) {
@@ -692,25 +686,16 @@
}
if ("merge".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.MERGE)) {
- return new BooleanPredicate(ChangeField.MERGE);
- }
- throw new QueryParseException("'is:merge' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.MERGE, "is:merge");
+ return new BooleanPredicate(ChangeField.MERGE);
}
if ("private".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.PRIVATE)) {
- return new BooleanPredicate(ChangeField.PRIVATE);
- }
- throw new QueryParseException(
- "'is:private' operator is not supported by change index version");
+ return new BooleanPredicate(ChangeField.PRIVATE);
}
if ("attention".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'is:attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
return new IsAttentionPredicate();
}
@@ -722,6 +707,11 @@
return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
}
+ if ("pure-revert".equalsIgnoreCase(value)) {
+ checkFieldAvailable(ChangeField.IS_PURE_REVERT, "is:pure-revert");
+ return ChangePredicates.pureRevert("1");
+ }
+
if ("submittable".equalsIgnoreCase(value)) {
// SubmittablePredicate will match if *any* of the submit records are OK,
// but we need to check that they're *all* OK, so check that none of the
@@ -739,26 +729,17 @@
}
if ("started".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.STARTED)) {
- return new BooleanPredicate(ChangeField.STARTED);
- }
- throw new QueryParseException(
- "'is:started' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.STARTED, "is:started");
+ return new BooleanPredicate(ChangeField.STARTED);
}
if ("wip".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return new BooleanPredicate(ChangeField.WIP);
- }
- throw new QueryParseException("'is:wip' operator is not supported by change index version");
+ return new BooleanPredicate(ChangeField.WIP);
}
if ("cherrypick".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.CHERRY_PICK)) {
- return new BooleanPredicate(ChangeField.CHERRY_PICK);
- }
- throw new QueryParseException(
- "'is:cherrypick' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.CHERRY_PICK, "is:cherrypick");
+ return new BooleanPredicate(ChangeField.CHERRY_PICK);
}
// for plugins the value will be operandName_pluginName
@@ -875,10 +856,7 @@
return ChangePredicates.hashtag(hashtag);
}
- if (!args.index.getSchema().hasField(ChangeField.FUZZY_HASHTAG)) {
- throw new QueryParseException(
- "'inhashtag' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
return ChangePredicates.fuzzyHashtag(hashtag);
}
@@ -934,10 +912,7 @@
@Operator
public Predicate<ChangeData> extension(String ext) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXTENSION)) {
- return new FileExtensionPredicate(ext);
- }
- throw new QueryParseException("'extension' operator is not supported by change index version");
+ return new FileExtensionPredicate(ext);
}
@Operator
@@ -947,19 +922,12 @@
@Operator
public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
- return new FileExtensionListPredicate(extList);
- }
- throw new QueryParseException(
- "'onlyextensions' operator is not supported by change index version");
+ return new FileExtensionListPredicate(extList);
}
@Operator
public Predicate<ChangeData> footer(String footer) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.FOOTER)) {
- return ChangePredicates.footer(footer);
- }
- throw new QueryParseException("'footer' operator is not supported by change index version");
+ return ChangePredicates.footer(footer);
}
@Operator
@@ -969,13 +937,10 @@
@Operator
public Predicate<ChangeData> directory(String directory) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
- if (directory.startsWith("^")) {
- return new RegexDirectoryPredicate(directory);
- }
- return ChangePredicates.directory(directory);
+ if (directory.startsWith("^")) {
+ return new RegexDirectoryPredicate(directory);
}
- throw new QueryParseException("'directory' operator is not supported by change index version");
+ return ChangePredicates.directory(directory);
}
@Operator
@@ -983,6 +948,8 @@
throws QueryParseException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
AccountGroup.UUID group = null;
+ Integer count = null;
+ PredicateArgs.Operator countOp = null;
// Parse for:
// label:Code-Review=1,user=jsmith or
@@ -993,6 +960,7 @@
// Special case: votes by owners can be tracked with ",owner":
// label:Code-Review+2,owner
// label:Code-Review+2,user=owner
+ // label:Code-Review+1,count=2
List<String> splitReviewer = Lists.newArrayList(Splitter.on(',').limit(2).split(name));
name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
@@ -1000,17 +968,40 @@
// process the user/group piece
PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
- for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
- if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
- if (pair.getValue().equals(ARG_ID_OWNER)) {
+ // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid
+ // unnecessary complexity.
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
+
+ for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
+ String key = pair.getKey();
+ String value = pair.getValue().value();
+ PredicateArgs.Operator operator = pair.getValue().operator();
+ if (key.equalsIgnoreCase(ARG_ID_USER)) {
+ if (value.equals(ARG_ID_OWNER)) {
accounts = Collections.singleton(OWNER_ACCOUNT_ID);
- } else if (pair.getValue().equals(ARG_ID_NON_UPLOADER)) {
+ } else if (value.equals(ARG_ID_NON_UPLOADER)) {
accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
} else {
- accounts = parseAccount(pair.getValue());
+ accounts = parseAccount(value);
}
- } else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
- group = parseGroup(pair.getValue()).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) {
+ group = parseGroup(value).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_COUNT)) {
+ if (!isInt(value)) {
+ throw new QueryParseException("Invalid count argument. Value should be an integer");
+ }
+ count = Integer.parseInt(value);
+ countOp = operator;
+ if (count == 0) {
+ throw new QueryParseException("Argument count=0 is not allowed.");
+ }
+ if (count > LabelPredicate.MAX_COUNT) {
+ throw new QueryParseException(
+ String.format(
+ "count=%d is not allowed. Maximum allowed value for count is %d.",
+ count, LabelPredicate.MAX_COUNT));
+ }
} else {
throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
}
@@ -1047,7 +1038,7 @@
// If the vote piece looks like Code-Review=NEED with a valid non-numeric
// submit record status, interpret as a submit record query.
int eq = name.indexOf('=');
- if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) {
+ if (eq > 0) {
String statusName = name.substring(eq + 1).toUpperCase();
if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
SubmitRecord.Label.Status status =
@@ -1059,7 +1050,18 @@
}
}
- return new LabelPredicate(args, name, accounts, group);
+ return new LabelPredicate(args, name, accounts, group, count, countOp);
+ }
+
+ /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
+ private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
+ throws QueryParseException {
+ Map<String, ValOp> keyValArgs = labelArgs.keyValue;
+ if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) {
+ throw new QueryParseException(
+ String.format(
+ "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2));
+ }
}
private static boolean isInt(String s) {
@@ -1089,15 +1091,29 @@
}
private Predicate<ChangeData> ignoredBySelf() throws QueryParseException {
- return new StarPredicate(self(), StarredChangesUtil.IGNORE_LABEL);
+ return ChangePredicates.starBy(
+ args.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ args.starredChangesUtil,
+ self(),
+ StarredChangesUtil.IGNORE_LABEL);
}
private Predicate<ChangeData> starredBySelf() throws QueryParseException {
- return new StarPredicate(self(), StarredChangesUtil.DEFAULT_LABEL);
+ return ChangePredicates.starBy(
+ args.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ args.starredChangesUtil,
+ self(),
+ StarredChangesUtil.DEFAULT_LABEL);
}
private Predicate<ChangeData> draftBySelf() throws QueryParseException {
- return ChangePredicates.draftBy(self());
+ return ChangePredicates.draftBy(
+ args.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ args.commentsUtil,
+ self());
}
@Operator
@@ -1175,9 +1191,7 @@
@Operator
public Predicate<ChangeData> uploader(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException("'uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "uploader");
return uploader(parseAccount(who, (AccountState s) -> true));
}
@@ -1192,10 +1206,7 @@
@Operator
public Predicate<ChangeData> attention(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
return attention(parseAccount(who, (AccountState s) -> true));
}
@@ -1240,9 +1251,7 @@
@Operator
public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException("'uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "uploaderin");
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
@@ -1287,10 +1296,7 @@
if (Objects.equals(byState, Predicate.<ChangeData>any())) {
return Predicate.any();
}
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
- }
- return byState;
+ return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
}
@Operator
@@ -1378,7 +1384,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1387,7 +1393,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1429,7 +1435,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1438,7 +1444,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1466,20 +1472,14 @@
@Operator
public Predicate<ChangeData> author(String who) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
- return getAuthorOrCommitterPredicate(
- who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
- }
- return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::author);
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
}
@Operator
public Predicate<ChangeData> committer(String who) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
- return getAuthorOrCommitterPredicate(
- who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
- }
- return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::committer);
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
}
@Operator
@@ -1502,41 +1502,31 @@
if (value == null || Ints.tryParse(value) == null) {
throw new QueryParseException("'revertof' must be an integer");
}
- if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
- return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
- }
- throw new QueryParseException("'revertof' operator is not supported by change index version");
+ return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
}
@Operator
public Predicate<ChangeData> submissionId(String value) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.SUBMISSIONID)) {
- return ChangePredicates.submissionId(value);
- }
- throw new QueryParseException(
- "'submissionid' operator is not supported by change index version");
+ return ChangePredicates.submissionId(value);
}
@Operator
public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
- && args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
- if (Ints.tryParse(value) != null) {
- return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
- }
- try {
- PatchSet.Id patchSetId = PatchSet.Id.parse(value);
- return ChangePredicates.cherryPickOf(patchSetId);
- } catch (IllegalArgumentException e) {
- throw new QueryParseException(
- "'"
- + value
- + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
- e);
- }
+ checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
+ checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
+ if (Ints.tryParse(value) != null) {
+ return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
}
- throw new QueryParseException(
- "'cherrypickof' operator is not supported by change index version");
+ try {
+ PatchSet.Id patchSetId = PatchSet.Id.parse(value);
+ return ChangePredicates.cherryPickOf(patchSetId);
+ } catch (IllegalArgumentException e) {
+ throw new QueryParseException(
+ "'"
+ + value
+ + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
+ e);
+ }
}
@Override
@@ -1595,6 +1585,14 @@
return Predicate.or(predicates);
}
+ protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator)
+ throws QueryParseException {
+ if (!args.index.getSchema().hasField(field)) {
+ throw new QueryParseException(
+ String.format("'%s' operator is not supported by change index version", operator));
+ }
+ }
+
private Predicate<ChangeData> getAuthorOrCommitterPredicate(
String who,
Function<String, Predicate<ChangeData>> exactPredicateFunc,
@@ -1709,11 +1707,9 @@
String who, ReviewerStateInternal state, boolean forDefaultField)
throws QueryParseException, IOException, ConfigInvalidException {
Predicate<ChangeData> reviewerByEmailPredicate = null;
- if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
- Address address = Address.tryParse(who);
- if (address != null) {
- reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
- }
+ Address address = Address.tryParse(who);
+ if (address != null) {
+ reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
}
Predicate<ChangeData> reviewerPredicate = null;
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 12efecb..b2bc6aa 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
@@ -34,17 +35,34 @@
protected final ProjectCache projectCache;
protected final PermissionBackend permissionBackend;
protected final IdentifiedUser.GenericFactory userFactory;
+ /** label name to be matched. */
protected final String label;
+
+ /** Expected vote value for the label. */
protected final int expVal;
+
+ /**
+ * Number of times the value {@link #expVal} for label {@link #label} should occur. If null, match
+ * with any count greater or equal to 1.
+ */
+ @Nullable protected final Integer count;
+
+ /** Account ID that has voted on the label. */
protected final Account.Id account;
+
protected final AccountGroup.UUID group;
public EqualsLabelPredicate(
- LabelPredicate.Args args, String label, int expVal, Account.Id account) {
- super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+ LabelPredicate.Args args,
+ String label,
+ int expVal,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account, count));
this.permissionBackend = args.permissionBackend;
this.projectCache = args.projectCache;
this.userFactory = args.userFactory;
+ this.count = count;
this.group = args.group;
this.label = label;
this.expVal = expVal;
@@ -60,6 +78,14 @@
return false;
}
+ if (Integer.valueOf(0).equals(count)) {
+ // We don't match against count=0 so that the computation is identical to the stored values
+ // in the index. We do that since computing count=0 requires looping on all {label_type,
+ // vote_value} for the change and storing a {count=0} format for it in the change index which
+ // is computationally expensive.
+ return false;
+ }
+
Optional<ProjectState> project = projectCache.get(c.getDest().project());
if (!project.isPresent()) {
// The project has disappeared.
@@ -73,12 +99,13 @@
}
boolean hasVote = false;
+ int matchingVotes = 0;
object.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
for (PatchSetApproval p : object.currentApprovals()) {
if (labelType.matches(p)) {
hasVote = true;
if (match(object, p.value(), p.accountId())) {
- return true;
+ matchingVotes += 1;
}
}
}
@@ -87,7 +114,7 @@
return true;
}
- return false;
+ return count == null ? matchingVotes >= 1 : matchingVotes == count;
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 5f017fb..2e09075 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.query.change;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.index.query.OrPredicate;
@@ -29,9 +29,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.stream.IntStream;
public class LabelPredicate extends OrPredicate<ChangeData> {
protected static final int MAX_LABEL_VALUE = 4;
+ protected static final int MAX_COUNT = 5; // inclusive
protected static class Args {
protected final ProjectCache projectCache;
@@ -40,6 +42,8 @@
protected final String value;
protected final Set<Account.Id> accounts;
protected final AccountGroup.UUID group;
+ protected final Integer count;
+ protected final PredicateArgs.Operator countOp;
protected Args(
ProjectCache projectCache,
@@ -47,13 +51,17 @@
IdentifiedUser.GenericFactory userFactory,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
this.projectCache = projectCache;
this.permissionBackend = permissionBackend;
this.userFactory = userFactory;
this.value = value;
this.accounts = accounts;
this.group = group;
+ this.count = count;
+ this.countOp = countOp;
}
}
@@ -75,19 +83,35 @@
ChangeQueryBuilder.Arguments a,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
super(
predicates(
- new Args(a.projectCache, a.permissionBackend, a.userFactory, value, accounts, group)));
+ new Args(
+ a.projectCache,
+ a.permissionBackend,
+ a.userFactory,
+ value,
+ accounts,
+ group,
+ count,
+ countOp)));
this.value = value;
}
protected static List<Predicate<ChangeData>> predicates(Args args) {
String v = args.value;
-
+ List<Integer> counts = getCounts(args.count, args.countOp);
try {
MagicLabelVote mlv = MagicLabelVote.parseWithEquals(v);
- return ImmutableList.of(magicLabelPredicate(args, mlv));
+ List<Predicate<ChangeData>> result = Lists.newArrayListWithCapacity(counts.size());
+ if (counts.isEmpty()) {
+ result.add(magicLabelPredicate(args, mlv, /* count= */ null));
+ } else {
+ counts.forEach(count -> result.add(magicLabelPredicate(args, mlv, count)));
+ }
+ return result;
} catch (IllegalArgumentException e) {
// Try next format.
}
@@ -123,16 +147,24 @@
int min = range.min;
int max = range.max;
- List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(max - min + 1);
+ List<Predicate<ChangeData>> r =
+ Lists.newArrayListWithCapacity((counts.isEmpty() ? 1 : counts.size()) * (max - min + 1));
for (int i = min; i <= max; i++) {
- r.add(onePredicate(args, prefix, i));
+ if (counts.isEmpty()) {
+ r.add(onePredicate(args, prefix, i, /* count= */ null));
+ } else {
+ for (int count : counts) {
+ r.add(onePredicate(args, prefix, i, count));
+ }
+ }
}
return r;
}
- protected static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> onePredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (expVal != 0) {
- return equalsLabelPredicate(args, label, expVal);
+ return equalsLabelPredicate(args, label, expVal, count);
}
return noLabelQuery(args, label);
}
@@ -140,34 +172,66 @@
protected static Predicate<ChangeData> noLabelQuery(Args args, String label) {
List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
- r.add(equalsLabelPredicate(args, label, i));
- r.add(equalsLabelPredicate(args, label, -i));
+ r.add(equalsLabelPredicate(args, label, i, /* count= */ null));
+ r.add(equalsLabelPredicate(args, label, -i, /* count= */ null));
}
return not(or(r));
}
- protected static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> equalsLabelPredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new EqualsLabelPredicate(args, label, expVal, null);
+ return new EqualsLabelPredicate(args, label, expVal, null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new EqualsLabelPredicate(args, label, expVal, a));
+ r.add(new EqualsLabelPredicate(args, label, expVal, a, count));
}
return or(r);
}
- protected static Predicate<ChangeData> magicLabelPredicate(Args args, MagicLabelVote mlv) {
+ protected static Predicate<ChangeData> magicLabelPredicate(
+ Args args, MagicLabelVote mlv, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new MagicLabelPredicate(args, mlv, /* account= */ null);
+ return new MagicLabelPredicate(args, mlv, /* account= */ null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new MagicLabelPredicate(args, mlv, a));
+ r.add(new MagicLabelPredicate(args, mlv, a, count));
}
return or(r);
}
+ private static List<Integer> getCounts(
+ @Nullable Integer count, @Nullable PredicateArgs.Operator countOp) {
+ List<Integer> result = new ArrayList<>();
+ if (count == null) {
+ return result;
+ }
+ switch (countOp) {
+ case EQUAL:
+ case GREATER_EQUAL:
+ case LESS_EQUAL:
+ result.add(count);
+ break;
+ default:
+ break;
+ }
+ switch (countOp) {
+ case GREATER:
+ case GREATER_EQUAL:
+ IntStream.range(count + 1, MAX_COUNT + 1).forEach(result::add);
+ break;
+ case LESS:
+ case LESS_EQUAL:
+ IntStream.range(0, count).forEach(result::add);
+ break;
+ default:
+ break;
+ }
+ return result;
+ }
+
@Override
public String toString() {
return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 3917c79..5a81ca1 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
@@ -30,13 +31,21 @@
protected final LabelPredicate.Args args;
private final MagicLabelVote magicLabelVote;
private final Account.Id account;
+ @Nullable private final Integer count;
public MagicLabelPredicate(
- LabelPredicate.Args args, MagicLabelVote magicLabelVote, Account.Id account) {
- super(ChangeField.LABEL, magicLabelVote.formatLabel());
+ LabelPredicate.Args args,
+ MagicLabelVote magicLabelVote,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(
+ ChangeField.LABEL,
+ ChangeField.formatLabel(
+ magicLabelVote.label(), magicLabelVote.value().name(), account, count));
this.account = account;
this.args = args;
this.magicLabelVote = magicLabelVote;
+ this.count = count;
}
@Override
@@ -87,7 +96,7 @@
}
private EqualsLabelPredicate numericPredicate(String label, short value) {
- return new EqualsLabelPredicate(args, label, value, account);
+ return new EqualsLabelPredicate(args, label, value, account, count);
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/PredicateArgs.java b/java/com/google/gerrit/server/query/change/PredicateArgs.java
index d82b9bc..9f0dffb 100644
--- a/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.query.change;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.gerrit.index.query.QueryParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* This class is used to extract comma separated values in a predicate.
@@ -30,8 +33,35 @@
* appear in the map and others in the positional list (e.g. "vote=approved,jb_2.3).
*/
public class PredicateArgs {
+ private static final Pattern SPLIT_PATTERN = Pattern.compile("(>|>=|=|<|<=)([^=].*)$");
+
public List<String> positional;
- public Map<String, String> keyValue;
+ public Map<String, ValOp> keyValue;
+
+ enum Operator {
+ EQUAL("="),
+ GREATER_EQUAL(">="),
+ GREATER(">"),
+ LESS_EQUAL("<="),
+ LESS("<");
+
+ final String op;
+
+ Operator(String op) {
+ this.op = op;
+ }
+ };
+
+ @AutoValue
+ public abstract static class ValOp {
+ abstract String value();
+
+ abstract Operator operator();
+
+ static ValOp create(String value, Operator operator) {
+ return new AutoValue_PredicateArgs_ValOp(value, operator);
+ }
+ }
/**
* Parses query arguments into {@link #keyValue} and/or {@link #positional}..
@@ -46,19 +76,39 @@
keyValue = new HashMap<>();
for (String arg : Splitter.on(',').split(args)) {
- List<String> splitKeyValue = Splitter.on('=').splitToList(arg);
+ Matcher m = SPLIT_PATTERN.matcher(arg);
- if (splitKeyValue.size() == 1) {
- positional.add(splitKeyValue.get(0));
- } else if (splitKeyValue.size() == 2) {
- if (!keyValue.containsKey(splitKeyValue.get(0))) {
- keyValue.put(splitKeyValue.get(0), splitKeyValue.get(1));
+ if (!m.find()) {
+ positional.add(arg);
+ } else if (m.groupCount() == 2) {
+ String key = arg.substring(0, m.start());
+ String op = m.group(1);
+ String val = m.group(2);
+ if (!keyValue.containsKey(key)) {
+ keyValue.put(key, ValOp.create(val, getOperator(op)));
} else {
- throw new QueryParseException("Duplicate key " + splitKeyValue.get(0));
+ throw new QueryParseException("Duplicate key " + key);
}
} else {
- throw new QueryParseException("invalid arg " + arg);
+ throw new QueryParseException("Invalid arg " + arg);
}
}
}
+
+ private Operator getOperator(String operator) {
+ switch (operator) {
+ case "<":
+ return Operator.LESS;
+ case "<=":
+ return Operator.LESS_EQUAL;
+ case "=":
+ return Operator.EQUAL;
+ case ">=":
+ return Operator.GREATER_EQUAL;
+ case ">":
+ return Operator.GREATER;
+ default:
+ throw new IllegalArgumentException("Invalid Operator " + operator);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index e63714f..cd0fee3 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.inject.Inject;
@@ -32,4 +33,10 @@
SubmitRequirementChangeQueryBuilder(Arguments args) {
super(def, args);
}
+
+ @Override
+ protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator) {
+ // Submit requirements don't rely on the index, so they can be used regardless of index schema
+ // version.
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index 1485a6e56..ad3c56b 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.account;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
@@ -39,6 +40,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangePredicates;
@@ -75,6 +77,7 @@
private final Provider<CommentJson> commentJsonProvider;
private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
+ private final ExperimentFeatures experimentFeatures;
@Inject
DeleteDraftComments(
@@ -86,7 +89,8 @@
ChangeJson.Factory changeJsonFactory,
Provider<CommentJson> commentJsonProvider,
CommentsUtil commentsUtil,
- PatchSetUtil psUtil) {
+ PatchSetUtil psUtil,
+ ExperimentFeatures experimentFeatures) {
this.userProvider = userProvider;
this.batchUpdateFactory = batchUpdateFactory;
this.queryBuilderProvider = queryBuilderProvider;
@@ -96,6 +100,7 @@
this.commentJsonProvider = commentJsonProvider;
this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
+ this.experimentFeatures = experimentFeatures;
}
@Override
@@ -146,7 +151,12 @@
private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input)
throws BadRequestException {
- Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(accountId);
+ Predicate<ChangeData> hasDraft =
+ ChangePredicates.draftBy(
+ experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ commentsUtil,
+ accountId);
if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(input.query)).isEmpty()) {
return hasDraft;
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 0d7210f..74407c0 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -225,7 +225,6 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
-import org.junit.Ignore;
import org.junit.Test;
@NoHttpd
@@ -4059,7 +4058,7 @@
assertThat(testLabel.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
assertThat(testLabel.appliedBy).isNull();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
// Code review record is satisfied after voting +2
change = gApi.changes().id(changeId).get();
assertThat(change.submitRecords).hasSize(2);
@@ -4166,9 +4165,9 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=MAX"))
+ SubmitRequirementExpression.create("label:Code-Review=MAX"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4178,13 +4177,13 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
}
@Test
@@ -4239,9 +4238,9 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("-label:code-review=MIN"))
+ SubmitRequirementExpression.create("-label:Code-Review=MIN"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4252,27 +4251,27 @@
assertThat(change.submitRequirements).hasSize(2);
// Requirement is satisfied because there are no votes
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
// Legacy requirement (coming from the label function definition) is not satisfied. We return
// both legacy and non-legacy requirements in this case since their statuses are not identical.
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
- voteLabel(changeId, "code-review", -1);
+ voteLabel(changeId, "Code-Review", -1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
// Requirement is still satisfied because -1 is not the max negative value
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
- voteLabel(changeId, "code-review", -2);
+ voteLabel(changeId, "Code-Review", -2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
// Requirement is now unsatisfied because -2 is the max negative value
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
}
@Test
@@ -4335,9 +4334,9 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=ANY"))
+ SubmitRequirementExpression.create("label:Code-Review=ANY"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4347,14 +4346,14 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
// Legacy and non-legacy requirements have mismatching status. Both are returned from the API.
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@@ -4368,15 +4367,15 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("verified")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:verified=+1"))
+ .setName("Verified")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4386,18 +4385,18 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
- change.submitRequirements, "verified", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
- change.submitRequirements, "verified", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false);
}
@Test
@@ -4409,9 +4408,9 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setApplicabilityExpression(SubmitRequirementExpression.of("project:foo"))
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4421,7 +4420,7 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@@ -4445,8 +4444,8 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4456,36 +4455,38 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
voteLabel(changeId, "build-cop-override", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.OVERRIDDEN, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@Test
- @Ignore("Test is flaky")
+ @GerritConfig(
+ name = "experiments.enabled",
+ value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_overriddenInChildProject() throws Exception {
configSubmitRequirement(
allProjects,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+1"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(true)
.build());
- // Override submit requirement in child project (requires code-review=+2 instead of +1)
+ // Override submit requirement in child project (requires Code-Review=+2 instead of +1)
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4495,19 +4496,19 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
}
@Test
@@ -4518,8 +4519,8 @@
configSubmitRequirement(
allProjects,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+1"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4529,13 +4530,13 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
// Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
@@ -4550,19 +4551,19 @@
configSubmitRequirement(
allProjects,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+1"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
- // Override submit requirement in child project (requires code-review=+2 instead of +1).
+ // Override submit requirement in child project (requires Code-Review=+2 instead of +1).
// Will have no effect since parent does not allow override.
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4572,14 +4573,14 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
// +1 was enough to fulfill the requirement: override in child project was ignored
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
// Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
@@ -4600,9 +4601,9 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=+2"))
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4610,12 +4611,12 @@
createChange(repo, "master", "Add a file", "foo", "content", "topic");
String changeId = r.getChangeId();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
@@ -4629,7 +4630,7 @@
assertThat(result.submittabilityExpressionResult().status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
assertThat(result.submittabilityExpressionResult().expression().expressionString())
- .isEqualTo("label:code-review=+2");
+ .isEqualTo("label:Code-Review=+2");
}
}
@@ -4645,8 +4646,8 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4656,14 +4657,14 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
gApi.changes().id(changeId).current().submit();
@@ -4671,16 +4672,16 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("verified")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:verified=+1"))
+ .setName("Verified")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1"))
.setAllowOverrideInChildProjects(false)
.build());
- // The new "verified" submit requirement is not returned, since this change is closed
+ // The new "Verified" submit requirement is not returned, since this change is closed
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
}
@Test
@@ -4869,15 +4870,15 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
// Query the change. ChangeInfo is back-filled from the change index.
List<ChangeInfo> changeInfos =
@@ -4889,7 +4890,7 @@
assertThat(changeInfos).hasSize(1);
assertSubmitRequirementStatus(
changeInfos.get(0).submitRequirements,
- "code-review",
+ "Code-Review",
Status.SATISFIED,
/* isLegacy= */ false);
}
@@ -4906,15 +4907,15 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
gApi.changes().id(changeId).current().submit();
// Query the change. ChangeInfo is back-filled from the change index.
@@ -4927,7 +4928,7 @@
assertThat(changeInfos).hasSize(1);
assertSubmitRequirementStatus(
changeInfos.get(0).submitRequirements,
- "code-review",
+ "Code-Review",
Status.SATISFIED,
/* isLegacy= */ false);
}
@@ -4937,8 +4938,8 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4948,11 +4949,11 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).isEmpty();
- voteLabel(changeId, "code-review", -1);
+ voteLabel(changeId, "Code-Review", -1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).isEmpty();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).isEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index ed5e559..c868d0b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -35,6 +35,7 @@
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.message.BasicHeader;
@@ -194,6 +195,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void abortIfServerDeadlineExceeded() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
@@ -203,6 +205,7 @@
@GerritConfig(name = "deadline.foo.timeout", value = "1ms")
@GerritConfig(name = "deadline.bar.timeout", value = "100ms")
public void stricterDeadlineTakesPrecedence() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -213,6 +216,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestType", value = "REST")
public void abortIfServerDeadlineExceeded_requestType() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -223,6 +227,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
public void abortIfServerDeadlineExceeded_requestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -235,6 +240,7 @@
name = "deadline.default.excludedRequestUriPattern",
value = "/projects/non-matching")
public void abortIfServerDeadlineExceeded_excludedRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -249,6 +255,7 @@
value = "/projects/non-matching")
public void abortIfServerDeadlineExceeded_requestUriPatternAndExcludedRequestUriPattern()
throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -259,6 +266,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.projectPattern", value = ".*new.*")
public void abortIfServerDeadlineExceeded_projectPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -269,6 +277,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.account", value = "1000000")
public void abortIfServerDeadlineExceeded_account() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -279,6 +288,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestType", value = "SSH")
public void nonMatchingServerDeadlineIsIgnored_requestType() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -287,6 +297,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestUriPattern", value = "/changes/.*")
public void nonMatchingServerDeadlineIsIgnored_requestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -295,6 +306,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*")
public void nonMatchingServerDeadlineIsIgnored_excludedRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -305,6 +317,7 @@
@GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*new")
public void nonMatchingServerDeadlineIsIgnored_requestUriPatternAndExcludedRequestUriPattern()
throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -313,6 +326,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.projectPattern", value = ".*foo.*")
public void nonMatchingServerDeadlineIsIgnored_projectPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -321,6 +335,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.account", value = "999")
public void nonMatchingServerDeadlineIsIgnored_account() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -329,6 +344,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.isAdvisory", value = "true")
public void advisoryServerDeadlineIsIgnored() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -338,6 +354,7 @@
@GerritConfig(name = "deadline.test.isAdvisory", value = "true")
@GerritConfig(name = "deadline.default.timeout", value = "2ms")
public void nonAdvisoryDeadlineIsAppliedIfStricterAdvisoryDeadlineExists() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(4));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -347,6 +364,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1")
public void invalidServerDeadlineIsIgnored_missingTimeUnit() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -354,6 +372,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1x")
public void invalidServerDeadlineIsIgnored_invalidTimeUnit() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -369,6 +388,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestType", value = "INVALID")
public void invalidServerDeadlineIsIgnored_invalidRequestType() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -377,6 +397,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestUriPattern", value = "][")
public void invalidServerDeadlineIsIgnored_invalidRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -385,6 +406,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "][")
public void invalidServerDeadlineIsIgnored_invalidExcludedRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -393,6 +415,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.projectPattern", value = "][")
public void invalidServerDeadlineIsIgnored_invalidProjectPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -401,6 +424,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.account", value = "invalid")
public void invalidServerDeadlineIsIgnored_invalidAccount() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -416,6 +440,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "0ms")
@GerritConfig(name = "deadline.default.requestType", value = "REST")
public void deadlineConfigWithZeroTimeoutIsIgnored() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -449,6 +474,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientProvidedDeadlineOverridesServerDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response =
adminRestSession.putWithHeaders(
"/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "2ms"));
@@ -460,6 +486,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientCanDisableDeadlineBySettingZeroAsDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response =
adminRestSession.putWithHeaders(
"/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "0"));
@@ -574,6 +601,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void abortPushIfServerDeadlineExceeded() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertErrorStatus("Server Deadline Exceeded (default.timeout=1ms)");
@@ -582,6 +610,7 @@
@Test
@GerritConfig(name = "receive.timeout", value = "1ms")
public void abortPushIfTimeoutExceeded() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
@@ -591,6 +620,7 @@
@GerritConfig(name = "receive.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.timeout", value = "10s")
public void receiveTimeoutTakesPrecedence() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
@@ -649,6 +679,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientProvidedDeadlineOnPushOverridesServerDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
List<String> pushOptions = new ArrayList<>();
pushOptions.add("deadline=2ms");
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
@@ -660,6 +691,7 @@
@Test
@GerritConfig(name = "receive.timeout", value = "1ms")
public void clientProvidedDeadlineOnPushDoesntOverrideServerTimeout() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
List<String> pushOptions = new ArrayList<>();
pushOptions.add("deadline=10m");
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
@@ -671,6 +703,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientCanDisableDeadlineOnPushBySettingZeroAsDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
List<String> pushOptions = new ArrayList<>();
pushOptions.add("deadline=0");
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 6e19c39..b76d5cb 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -25,6 +25,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
@@ -32,6 +33,7 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
@@ -138,13 +140,13 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "project:" + project.get(),
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
assertThat(result.submittabilityExpressionResult().failingAtoms())
- .containsExactly("label:\"code-review=+2\"");
+ .containsExactly("label:\"Code-Review=+2\"");
}
@Test
@@ -160,7 +162,7 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "project:" + project.get(),
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "label:\"build-cop-override=+1\"");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
@@ -175,7 +177,7 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "invalid_field:invalid_value",
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "label:\"build-cop-override=+1\"");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
@@ -206,7 +208,7 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "project:" + project.get(),
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "invalid_field:invalid_value");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
@@ -215,6 +217,33 @@
.isEqualTo("Unsupported operator invalid_field:invalid_value");
}
+ @Test
+ public void byPureRevert() throws Exception {
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result pushResult =
+ createChange(testRepo, "refs/heads/master", "Fix a bug", "file.txt", "content", "topic");
+ changeData = pushResult.getChange();
+ changeId = pushResult.getChangeId();
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "is:pure-revert",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).revert().get();
+ String revertId = Integer.toString(changeInfo._number);
+ ChangeData revertChangeData =
+ changeQueryProvider.get().byLegacyChangeId(Change.Id.parse(revertId)).get(0);
+ result = evaluator.evaluateRequirement(sr, revertChangeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
index 4675bc0..d8aa789 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
@@ -50,7 +50,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\""));
+ /* value= */ "label:\"Code-Review=+2\""));
PushResult r = pushRefsMetaConfig();
assertOkStatus(r);
@@ -77,7 +77,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\"");
+ /* value= */ "label:\"Code-Review=+2\"");
projectConfig.setString(
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
@@ -95,6 +95,78 @@
}
@Test
+ public void parametersDirectlyInSubmitRequirementsSectionAreRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ updateProjectConfig(
+ projectConfig -> {
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ null,
+ /* name= */ ProjectConfig.KEY_SR_DESCRIPTION,
+ /* value= */ "foo bar description");
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ null,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ "label:\"Code-Review=+2\"");
+ });
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Submit requirements must be defined in submit-requirement.<name>"
+ + " subsections. Setting parameters directly in the submit-requirement section is"
+ + " not allowed: [%s, %s]",
+ ProjectConfig.KEY_SR_DESCRIPTION, ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION));
+ }
+
+ @Test
+ public void unsupportedParameterDirectlyInSubmitRequirementsSectionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ null,
+ /* name= */ "unknown",
+ /* value= */ "value"));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ "project.config: Submit requirements must be defined in submit-requirement.<name>"
+ + " subsections. Setting parameters directly in the submit-requirement section is"
+ + " not allowed: [unknown]");
+ }
+
+ @Test
+ public void unsupportedParameterForSubmitRequirementIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ "unknown",
+ /* value= */ "value"));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Unsupported parameters for submit requirement '%s': [unknown]",
+ submitRequirementName));
+ }
+
+ @Test
public void conflictingSubmitRequirementsAreRejected() throws Exception {
fetchRefsMetaConfig();
@@ -105,12 +177,12 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\"");
+ /* value= */ "label:\"Code-Review=+2\"");
projectConfig.setString(
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName.toLowerCase(Locale.US),
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\"");
+ /* value= */ "label:\"Code-Review=+2\"");
});
PushResult r = pushRefsMetaConfig();
@@ -132,7 +204,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\""));
+ /* value= */ "label:\"Code-Review=+2\""));
PushResult r = pushRefsMetaConfig();
assertOkStatus(r);
@@ -142,7 +214,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName.toLowerCase(Locale.US),
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\""));
+ /* value= */ "label:\"Code-Review=+2\""));
r = pushRefsMetaConfig();
assertErrorStatus(
r,
@@ -170,8 +242,139 @@
r,
"Invalid project configuration",
String.format(
- "project.config: Submit requirement '%s' does not define a submittability expression.",
- submitRequirementName));
+ "project.config: Setting a submittability expression for submit requirement '%s' is"
+ + " required: Missing %s.%s.%s",
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidSubmittabilityExpressionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidExpression = "invalid_field:invalid_value";
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ invalidExpression));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: Unsupported operator %s",
+ invalidExpression,
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ invalidExpression));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidApplicabilityExpressionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidExpression = "invalid_field:invalid_value";
+ updateProjectConfig(
+ projectConfig -> {
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ "label:\"Code-Review=+2\"");
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
+ /* value= */ invalidExpression);
+ });
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: Unsupported operator %s",
+ invalidExpression,
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
+ invalidExpression));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidOverrideExpressionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidExpression = "invalid_field:invalid_value";
+ updateProjectConfig(
+ projectConfig -> {
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ "label:\"Code-Review=+2\"");
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
+ /* value= */ invalidExpression);
+ });
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: Unsupported operator %s",
+ invalidExpression,
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
+ invalidExpression));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidAllowOverrideInChildProjectsIsRejected()
+ throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidValue = "invalid";
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ /* value= */ invalidValue));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Invalid value %s.%s.%s for submit requirement '%s': %s",
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ submitRequirementName,
+ invalidValue));
}
private void fetchRefsMetaConfig() throws Exception {
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 0527a91..689698e 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -54,6 +54,7 @@
"//java/com/google/gerrit/proto/testing",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/account/externalids/testing",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/cache/testing",
"//java/com/google/gerrit/server/cancellation",
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
index a1dee1a..6993dfe 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
@@ -26,7 +26,7 @@
public class SubmitRequirementSerializerTest {
private static final SubmitRequirement submitReq =
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setDescription(Optional.of("require code review +2"))
.setApplicabilityExpression(SubmitRequirementExpression.of("branch(refs/heads/master)"))
.setSubmittabilityExpression(SubmitRequirementExpression.create("label(code-review, 2+)"))
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 66a98e8..67b0342 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -56,6 +56,7 @@
new Config(),
null,
null,
+ null,
null));
}
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
similarity index 89%
rename from javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java
rename to javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
index 2ec5e4d..fbeabe1 100644
--- a/javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
@@ -25,7 +25,7 @@
import org.junit.Before;
import org.junit.Test;
-public class MailSoySauceProviderTest {
+public class MailSoySauceLoaderTest {
private SitePaths sitePaths;
private DynamicSet<MailSoyTemplateProvider> set;
@@ -38,11 +38,11 @@
@Test
public void soyCompilation() {
- MailSoySauceProvider provider =
- new MailSoySauceProvider(
+ MailSoySauceLoader loader =
+ new MailSoySauceLoader(
sitePaths,
new SoyAstCache(),
new PluginSetContext<>(set, PluginMetrics.DISABLED_INSTANCE));
- assertThat(provider.get()).isNotNull(); // should not throw
+ assertThat(loader.load()).isNotNull(); // should not throw
}
}
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
new file mode 100644
index 0000000..bb443f8
--- /dev/null
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
@@ -0,0 +1,60 @@
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.CacheRefreshExecutor;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+import com.google.template.soy.jbcsrc.api.SoySauce;
+import java.nio.file.Paths;
+import javax.inject.Provider;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class MailSoySauceModuleTest {
+ @Test
+ public void soySauceProviderReturnsCachedValue() throws Exception {
+ SitePaths sitePaths = new SitePaths(Paths.get("."));
+ Injector injector =
+ Guice.createInjector(
+ new MailSoySauceModule(),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ bind(ListeningExecutorService.class)
+ .annotatedWith(CacheRefreshExecutor.class)
+ .toInstance(newDirectExecutorService());
+ bind(SitePaths.class).toInstance(sitePaths);
+ bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(new Config());
+ bind(MetricMaker.class).to(DisabledMetricMaker.class);
+ install(new DefaultMemoryCacheModule());
+ }
+ });
+ Provider<SoySauce> soySauceProvider =
+ injector.getProvider(Key.get(SoySauce.class, MailTemplates.class));
+ LoadingCache<String, SoySauce> cache =
+ injector.getInstance(
+ Key.get(
+ new TypeLiteral<LoadingCache<String, SoySauce>>() {},
+ Names.named(MailSoySauceModule.CACHE_NAME)));
+ assertThat(cache.stats().loadCount()).isEqualTo(0);
+ // Theoretically, this can be flaky, if the delay before the second get takes several seconds.
+ // We assume that tests is fast enough.
+ assertThat(soySauceProvider.get()).isNotNull();
+ assertThat(soySauceProvider.get()).isNotNull();
+ assertThat(cache.stats().loadCount()).isEqualTo(1);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 61002f9..0c26f1a 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -696,7 +696,7 @@
.setApplicabilityExpression(
SubmitRequirementExpression.of("project:foo"))
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=+2"))
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build())
.applicabilityExpressionResult(
@@ -708,10 +708,10 @@
ImmutableList.of())))
.submittabilityExpressionResult(
SubmitRequirementExpressionResult.create(
- SubmitRequirementExpression.create("label:code-review=+2"),
+ SubmitRequirementExpression.create("label:Code-Review=+2"),
SubmitRequirementExpressionResult.Status.FAIL,
ImmutableList.of(),
- ImmutableList.of("label:code-review=+2")))
+ ImmutableList.of("label:Code-Review=+2")))
.build()))
.build(),
newProtoBuilder()
@@ -726,7 +726,7 @@
SubmitRequirementProto.newBuilder()
.setName("Code-Review")
.setApplicabilityExpression("project:foo")
- .setSubmittabilityExpression("label:code-review=+2")
+ .setSubmittabilityExpression("label:Code-Review=+2")
.setAllowOverrideInChildProjects(false)
.build())
.setApplicabilityExpressionResult(
@@ -737,9 +737,9 @@
.build())
.setSubmittabilityExpressionResult(
SubmitRequirementExpressionResultProto.newBuilder()
- .setExpression("label:code-review=+2")
+ .setExpression("label:Code-Review=+2")
.setStatus("FAIL")
- .addFailingAtoms("label:code-review=+2")
+ .addFailingAtoms("label:Code-Review=+2")
.build())
.build())
.build());
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 9df59c2..aed1648 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -217,7 +217,7 @@
"[submit-requirement \"Code-review\"]\n"
+ " description = At least one Code Review +2\n"
+ " applicableIf =branch(refs/heads/master)\n"
- + " submittableIf = label(code-review, +2)\n"
+ + " submittableIf = label(Code-Review, +2)\n"
+ "[submit-requirement \"api-review\"]\n"
+ " description = Additional review required for API modifications\n"
+ " applicableIf =commit_filepath_contains(\\\"/api/.*\\\")\n"
@@ -237,7 +237,7 @@
.setApplicabilityExpression(
SubmitRequirementExpression.of("branch(refs/heads/master)"))
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label(code-review, +2)"))
+ SubmitRequirementExpression.create("label(Code-Review, +2)"))
.setOverrideExpression(Optional.empty())
.setAllowOverrideInChildProjects(false)
.build(),
@@ -262,19 +262,19 @@
.add("groups", group(developers))
.add(
"project.config",
- "[submit-requirement \"code-review\"]\n"
- + " submittableIf = label(code-review, +2)\n")
+ "[submit-requirement \"Code-Review\"]\n"
+ + " submittableIf = label(Code-Review, +2)\n")
.create();
ProjectConfig cfg = read(rev);
Map<String, SubmitRequirement> submitRequirements = cfg.getSubmitRequirementSections();
assertThat(submitRequirements)
.containsExactly(
- "code-review",
+ "Code-Review",
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label(code-review, +2)"))
+ SubmitRequirementExpression.create("label(Code-Review, +2)"))
.setAllowOverrideInChildProjects(false)
.build());
}
@@ -320,8 +320,8 @@
.add("groups", group(developers))
.add(
"project.config",
- "[submit-requirement \"code-review\"]\n"
- + " applicableIf =label(code-review, +2)\n")
+ "[submit-requirement \"Code-Review\"]\n"
+ + " applicableIf =label(Code-Review, +2)\n")
.create();
ProjectConfig cfg = read(rev);
@@ -330,8 +330,9 @@
assertThat(cfg.getValidationErrors()).hasSize(1);
assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
.isEqualTo(
- "project.config: Submit requirement 'code-review' does not define a submittability"
- + " expression.");
+ "project.config: Setting a submittability expression for submit requirement"
+ + " 'Code-Review' is required: Missing"
+ + " submit-requirement.Code-Review.submittableIf");
}
@Test
@@ -952,10 +953,10 @@
tr.commit()
.add(
"project.config",
- "[submit-requirement \"code-review\"]\n"
+ "[submit-requirement \"Code-Review\"]\n"
+ " description = At least one Code Review +2\n"
+ " applicableIf =branch(refs/heads/master)\n"
- + " submittableIf = label(code-review, +2)\n"
+ + " submittableIf = label(Code-Review, +2)\n"
+ "[notify \"name\"]\n"
+ " email = example@example.com\n")
.create();
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 332548a..5253a5b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -26,6 +26,7 @@
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -40,6 +41,9 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -48,6 +52,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
@@ -111,6 +116,7 @@
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -140,7 +146,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -1037,6 +1042,7 @@
ChangeInserter ins3 = newChange(repo);
ChangeInserter ins4 = newChange(repo);
ChangeInserter ins5 = newChange(repo);
+ ChangeInserter ins6 = newChange(repo);
Change reviewMinus2Change = insert(repo, ins);
gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject());
@@ -1049,7 +1055,13 @@
Change reviewPlus1Change = insert(repo, ins4);
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- Change reviewPlus2Change = insert(repo, ins5);
+ Change reviewTwoPlus1Change = insert(repo, ins5);
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(createAccount("user1")));
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(userId));
+
+ Change reviewPlus2Change = insert(repo, ins6);
gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1060,8 +1072,10 @@
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 1));
- Map<Integer, Change> changes = new LinkedHashMap<>(5);
+ Multimap<Integer, Change> changes =
+ Multimaps.newListMultimap(Maps.newLinkedHashMap(), () -> Lists.newArrayList());
changes.put(2, reviewPlus2Change);
+ changes.put(1, reviewTwoPlus1Change);
changes.put(1, reviewPlus1Change);
changes.put(0, noLabelChange);
changes.put(-1, reviewMinus1Change);
@@ -1073,9 +1087,9 @@
assertQuery("label:Code-Review=-1", reviewMinus1Change);
assertQuery("label:Code-Review-1", reviewMinus1Change);
assertQuery("label:Code-Review=0", noLabelChange);
- assertQuery("label:Code-Review=+1", reviewPlus1Change);
- assertQuery("label:Code-Review=1", reviewPlus1Change);
- assertQuery("label:Code-Review+1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review+1", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2", reviewPlus2Change);
assertQuery("label:Code-Review=2", reviewPlus2Change);
assertQuery("label:Code-Review+2", reviewPlus2Change);
@@ -1083,6 +1097,7 @@
assertQuery(
"label:Code-Review=ANY",
reviewPlus2Change,
+ reviewTwoPlus1Change,
reviewPlus1Change,
reviewMinus1Change,
reviewMinus2Change);
@@ -1111,14 +1126,70 @@
assertQuery("label:Code-Review<-2");
assertQuery("label:Code-Review=+1,anotheruser");
- assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=owner", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,owner", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=owner", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,owner", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2,owner", reviewPlus2Change);
assertQuery("label:Code-Review=-2,owner", reviewMinus2Change);
+
+ // count=0 is not allowed
+ Exception thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=+2,count=0"));
+ assertThat(thrown).hasMessageThat().isEqualTo("Argument count=0 is not allowed.");
+ assertQuery("label:Code-Review=1,count=1", reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>1", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=3");
+ thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=1,count=7"));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("count=7 is not allowed. Maximum allowed value for count is 5.");
+
+ // Less than operator does not match with changes having count=0 for a specific vote value (i.e.
+ // no votes for that specific value). We do that deliberately since the computation of count=0
+ // for label values is expensive when the change is re-indexed. This is because the operation
+ // requires generating all formats for all {label-type, vote}=0 values that are non-voted for
+ // the change and storing them with the 'count=0' format.
+ assertQuery("label:Code-Review=1,count<5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=1", // reviewTwoPlus1Change is not matched since its count=2
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=1 label:Code-Review=1,count>=1", reviewPlus1Change);
+
+ assertQuery("label:Code-Review=MAX,count=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MAX,count=2");
+ assertQuery("label:Code-Review=MIN,count=1", reviewMinus2Change);
+ assertQuery("label:Code-Review=MIN,count>1");
+ assertQuery("label:Code-Review=MAX,count<2", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1");
+ assertQuery("label:Code-Review=MAX,count<2 label:Code-Review=MAX,count>=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1 label:Code-Review=MIN,count>=1");
+ assertQuery("label:Code-Review>=+1,count=2", reviewTwoPlus1Change);
+
+ // "count" and "user" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class,
+ () -> assertQuery("label:Code-Review=+1,user=non_uploader,count=2"));
+
+ // "count" and "group" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class, () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
}
@Test
@@ -1223,16 +1294,15 @@
assertQuery("label:Code-Review=+1,non_uploader", reviewPlus1Change);
}
- private Change[] codeReviewInRange(Map<Integer, Change> changes, int start, int end) {
- int size = 0;
- Change[] range = new Change[end - start + 1];
- for (int i : changes.keySet()) {
+ private Change[] codeReviewInRange(Multimap<Integer, Change> changes, int start, int end) {
+ List<Change> range = new ArrayList<>();
+ for (Map.Entry<Integer, Change> entry : changes.entries()) {
+ int i = entry.getKey();
if (i >= start && i <= end) {
- range[size] = changes.get(i);
- size++;
+ range.add(entry.getValue());
}
}
- return range;
+ return range.toArray(new Change[0]);
}
private String createGroup(String name, String owner) throws Exception {
@@ -2358,7 +2428,21 @@
}
@Test
- public void byHasDraft() throws Exception {
+ public void byHasDraft_draftsComputedFromIndex() throws Exception {
+ byHasDraft();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byHasDraft_draftsComputedFromAllUsersRepository() throws Exception {
+ byHasDraft();
+ }
+
+ private void byHasDraft() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
@@ -2386,7 +2470,11 @@
assertQuery("has:draft");
}
- @Test
+ /**
+ * This test does not have a test about drafts computed from All-Users Repository because zombie
+ * drafts can't be filtered when computing from All-Users repository. TODO(paiking): During
+ * rollout, we should find a way to fix zombie drafts.
+ */
public void byHasDraftExcludesZombieDrafts() throws Exception {
Project.NameKey project = Project.nameKey("repo");
TestRepository<Repo> repo = createProject(project.get());
@@ -2424,8 +2512,62 @@
assertQuery("has:draft");
}
+ public void byHasDraftWithManyDrafts_draftsComputedFromIndex() throws Exception {
+ byHasDraftWithManyDrafts();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byHasDraftWithManyDrafts_draftsComputedFromAllUsersRepository() throws Exception {
+ byHasDraftWithManyDrafts();
+ }
+
+ private void byHasDraftWithManyDrafts() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change[] changesWithDrafts = new Change[30];
+
+ // unrelated change not shown in the result.
+ insert(repo, newChange(repo));
+
+ for (int i = 0; i < changesWithDrafts.length; i++) {
+ // put the changes in reverse order since this is the order we receive them from the index.
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo));
+ DraftInput in = new DraftInput();
+ in.line = 1;
+ in.message = "nit: trailing whitespace";
+ in.path = Patch.COMMIT_MSG;
+ gApi.changes()
+ .id(changesWithDrafts[changesWithDrafts.length - 1 - i].getId().get())
+ .current()
+ .createDraft(in);
+ }
+ assertQuery("has:draft", changesWithDrafts);
+
+ Account.Id user2 =
+ accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+ requestContext.setContext(newRequestContext(user2));
+ assertQuery("has:draft");
+ }
+
@Test
- public void byStarredBy() throws Exception {
+ public void byStarredBy_starsComputedFromIndex() throws Exception {
+ byStarredBy();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ @Test
+ public void byStarredBy_starsComputedFromAllUsersRepository() throws Exception {
+ byStarredBy();
+ }
+
+ private void byStarredBy() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
@@ -2446,7 +2588,21 @@
}
@Test
- public void byStar() throws Exception {
+ public void byStar_starsComputedFromIndex() throws Exception {
+ byStar();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ @Test
+ public void byStar_starsComputedFromAllUsersRepository() throws Exception {
+ byStar();
+ }
+
+ private void byStar() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
@@ -2472,7 +2628,21 @@
}
@Test
- public void byIgnore() throws Exception {
+ public void byIgnore_starsComputedFromIndex() throws Exception {
+ byIgnore();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byIgnore_starsComputedFromAllUsersRepository() throws Exception {
+ byIgnore();
+ }
+
+ private void byIgnore() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
@@ -2492,6 +2662,42 @@
assertQuery("-star:ignore", change2, change1);
}
+ public void byStarWithManyStars_starsComputedFromIndex() throws Exception {
+ byStarWithManyStars();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byStarWithManyStars_starsComputedFromAllUsersRepository() throws Exception {
+ byStarWithManyStars();
+ }
+
+ private void byStarWithManyStars() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change[] changesWithDrafts = new Change[30];
+ for (int i = 0; i < changesWithDrafts.length; i++) {
+ // put the changes in reverse order since this is the order we receive them from the index.
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo));
+
+ // star the change
+ gApi.accounts()
+ .self()
+ .starChange(changesWithDrafts[changesWithDrafts.length - 1 - i].getId().toString());
+
+ // ignore the change
+ gApi.changes()
+ .id(changesWithDrafts[changesWithDrafts.length - 1 - i].getId().toString())
+ .ignore(true);
+ }
+
+ // all changes are both starred and ignored.
+ assertQuery("is:ignored", changesWithDrafts);
+ assertQuery("is:starred", changesWithDrafts);
+ }
+
@Test
public void byFrom() throws Exception {
TestRepository<Repo> repo = createProject("repo");
@@ -3754,6 +3960,33 @@
}
@Test
+ public void isPureRevert() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT)).isTrue();
+ TestRepository<Repo> repo = createProject("repo");
+ // Create two commits and revert second commit (initial commit can't be reverted)
+ Change initial = insert(repo, newChange(repo));
+ gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(initial.getChangeId()).current().submit();
+
+ ChangeInfo changeToRevert =
+ gApi.changes().create(new ChangeInput("repo", "master", "commit to revert")).get();
+ gApi.changes().id(changeToRevert.id).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToRevert.id).current().submit();
+
+ ChangeInfo changeThatReverts = gApi.changes().id(changeToRevert.id).revert().get();
+ Change.Id changeThatRevertsId = Change.id(changeThatReverts._number);
+ assertQueryByIds("is:pure-revert", changeThatRevertsId);
+
+ // Update the change that reverts such that it's not a pure revert
+ gApi.changes()
+ .id(changeThatReverts.id)
+ .edit()
+ .modifyFile("some-file.txt", RawInputUtil.create("newcontent".getBytes(UTF_8)));
+ gApi.changes().id(changeThatReverts.id).edit().publish();
+ assertQueryByIds("is:pure-revert");
+ }
+
+ @Test
public void selfFailsForAnonymousUser() throws Exception {
for (String query : ImmutableList.of("assignee:self", "has:star", "is:starred", "star:star")) {
assertQuery(query);
diff --git a/lib/BUILD b/lib/BUILD
index f924e4ca..b2810cf 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -128,6 +128,15 @@
)
java_library(
+ name = "guava-testlib",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = [
+ "@guava-testlib//jar",
+ ],
+)
+
+java_library(
name = "caffeine",
data = ["//lib:LICENSE-Apache2.0"],
visibility = [
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 591e76e..90d38b0 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -23,6 +23,7 @@
flogger-log4j-backend
flogger-system-backend
guava
+guava-testlib
guice-assistedinject
guice-library
guice-servlet
diff --git a/plugins/package.json b/plugins/package.json
index 4e3c376..e5d245c 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -6,7 +6,7 @@
"@polymer/decorators": "^3.0.0",
"@polymer/polymer": "^3.4.1",
"@gerritcodereview/typescript-api": "3.4.4",
- "lit": "2.0.0-rc.3"
+ "lit": "^2.0.2"
},
"license": "Apache-2.0",
"private": true
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 3ff1cc4..4cbe489 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.4.4.tgz#9f09687038088dd7edd3b4e30d249502eb21bfbc"
integrity sha512-MAiQwntcQ59b92yYDsVIXj3oBbAB4C7HELkLFFbYs4ZjzC43XqqtR9VF0dh5OUC8wzFZttgUiOmGehk9edpPuw==
-"@lit/reactive-element@^1.0.0-rc.2":
- version "1.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz#f24dba16ea571a08dca70f1783bd2ca5ec8de3ee"
- integrity sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ==
+"@lit/reactive-element@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294"
+ integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ==
"@polymer/decorators@^3.0.0":
version "3.0.0"
@@ -26,43 +26,36 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
-"@types/trusted-types@^1.0.1":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
- integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+"@types/trusted-types@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
+ integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@webcomponents/shadycss@^1.9.1":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.0.tgz#73e289996c002d8be694cd3be0e83c46ad25e7e0"
integrity sha512-L5O/+UPum8erOleNjKq6k58GVl3fNsEQdSOyh0EUhNmi7tHUyRuCJy1uqJiWydWcLARE5IPsMoPYMZmUGrz1JA==
-lit-element@^3.0.0-rc.2:
- version "3.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.0-rc.2.tgz#883d0b6fd7b846226d360699d1b713da5fc7e1b7"
- integrity sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==
+lit-element@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4"
+ integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-html "^2.0.0-rc.3"
+ "@lit/reactive-element" "^1.0.0"
+ lit-html "^2.0.0"
-lit-html@^2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.3.tgz#1c216e548630e18d3093d97f4e29563abce659af"
- integrity sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==
+lit-html@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95"
+ integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w==
dependencies:
- "@types/trusted-types" "^1.0.1"
+ "@types/trusted-types" "^2.0.2"
-lit-html@^2.0.0-rc.4:
- version "2.0.0-rc.4"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.4.tgz#1015fa8f1f7c8c5b79999ed0bc11c3b79ff1aab5"
- integrity sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==
+lit@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
+ integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
dependencies:
- "@types/trusted-types" "^1.0.1"
-
-lit@2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.0-rc.3.tgz#8b6a85268aba287c11125dfe57e88e0bc09beaff"
- integrity sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==
- dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-element "^3.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-element "^3.0.0"
+ lit-html "^2.0.0"
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 3a647d4..896f9ac 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -125,7 +125,6 @@
"elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts",
"elements/diff/gr-diff-view/gr-diff-view_html.ts",
"elements/diff/gr-diff/gr-diff_html.ts",
- "elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts",
"elements/gr-app-element_html.ts",
"elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
"elements/shared/gr-account-list/gr-account-list_html.ts",
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index f86e825..e2b1502 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -1065,7 +1065,7 @@
url: string;
/** URL to the icon of the link. */
image_url?: string;
- /* The links target. */
+ /* Value of the "target" attribute for anchor elements. */
target?: string;
}
@@ -1081,6 +1081,7 @@
applicability_expression_result?: SubmitRequirementExpressionInfo;
submittability_expression_result: SubmitRequirementExpressionInfo;
override_expression_result?: SubmitRequirementExpressionInfo;
+ is_legacy?: boolean;
}
/**
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 645e770..6ff2894 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -256,7 +256,6 @@
export function createDefaultPreferences() {
return {
changes_per_page: 25,
- default_diff_view: DiffViewMode.SIDE_BY_SIDE,
diff_view: DiffViewMode.SIDE_BY_SIDE,
size_bar_in_change_table: true,
} as PreferencesInfo;
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index a10bdda..0029f5c 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -98,4 +98,6 @@
TOGGLE_SHOW_ALL_BUTTON = 'toggle show all button',
SHOW_TAB = 'show-tab',
ATTENTION_SET_CHIP = 'attention-set-chip',
+ SAVE_COMMENT = 'save-comment',
+ COMMENT_SAVED = 'comment-saved',
}
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index 0c34a84..b7ea237 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -18,6 +18,7 @@
import '@polymer/paper-toggle-button/paper-toggle-button';
import '../../../styles/gr-form-styles';
import '../../../styles/gr-menu-page-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
index 3559194..a8405df 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
@@ -70,6 +70,9 @@
display: block;
}
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 32812dd..7092c9b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -36,6 +36,7 @@
PluginConfigOptionsChangedEventDetail,
PluginOption,
} from './gr-repo-plugin-config-types';
+import {paperStyles} from '../../../styles/gr-paper-styles';
const PLUGIN_CONFIG_CHANGED_EVENT_NAME = 'plugin-config-changed';
@@ -71,6 +72,7 @@
return [
sharedStyles,
formStyles,
+ paperStyles,
subpageStyles,
css`
.inherited {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
new file mode 100644
index 0000000..4432cc82
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright (C) 2015 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 '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {ChangeInfo, SubmitRequirementStatus} from '../../../api/rest-api';
+import {changeIsMerged} from '../../../utils/change-util';
+import {getRequirements} from '../../../utils/label-util';
+
+@customElement('gr-change-list-column-requirements')
+export class GrChangeListColumRequirements extends LitElement {
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ static override get styles() {
+ return [
+ css`
+ iron-icon {
+ width: var(--line-height-normal, 20px);
+ height: var(--line-height-normal, 20px);
+ vertical-align: top;
+ }
+ span {
+ line-height: var(--line-height-normal);
+ }
+ .check {
+ color: var(--success-foreground);
+ }
+ iron-icon.close {
+ color: var(--error-foreground);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ if (changeIsMerged(this.change)) {
+ return this.renderState('check', 'Merged');
+ }
+
+ const submitRequirements = getRequirements(this.change);
+ if (!submitRequirements.length) return html`n/a`;
+ const numOfRequirements = submitRequirements.length;
+ const numOfSatisfiedRequirements = submitRequirements.filter(
+ req => req.status === SubmitRequirementStatus.SATISFIED
+ ).length;
+
+ if (numOfSatisfiedRequirements === numOfRequirements) {
+ return this.renderState('check', 'Ready');
+ }
+ return this.renderState(
+ 'close',
+ `${numOfSatisfiedRequirements} of ${numOfRequirements} granted`
+ );
+ }
+
+ renderState(icon: string, message: string) {
+ return html`<span class="${icon}"
+ ><gr-submit-requirement-dashboard-hovercard .change=${this.change}>
+ </gr-submit-requirement-dashboard-hovercard>
+ <iron-icon class="${icon}" icon="gr-icons:${icon}" role="img"></iron-icon
+ >${message}</span
+ >`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-change-list-column-requirements': GrChangeListColumRequirements;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index c476d2d..43f6730 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -26,6 +26,8 @@
import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../gr-change-list-column/gr-change-list-column';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-list-item_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index a0aa962..f481fd9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -46,7 +46,8 @@
width: 100%;
}
.comments,
- .reviewers {
+ .reviewers,
+ .requirements {
white-space: nowrap;
}
.reviewers {
@@ -290,6 +291,13 @@
</template>
</gr-tooltip-content>
</td>
+ <td
+ class="cell requirements"
+ hidden$="[[_computeIsColumnHidden('Requirements', visibleChangeTableColumns)]]"
+ >
+ <gr-change-list-column-requirements change="[[change]]">
+ </gr-change-list-column-requirements>
+ </td>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
<td
title$="[[_computeLabelTitle(change, labelName)]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index 34cb6eb..35be3de 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -369,6 +369,7 @@
'Branch',
'Updated',
'Size',
+ 'Requirements',
];
await flush();
@@ -392,6 +393,7 @@
'Branch',
'Updated',
'Size',
+ 'Requirements',
];
await flush();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index a79dd8e..68566f0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -16,9 +16,10 @@
*/
import '../../../styles/gr-change-list-styles';
+import '../../../styles/gr-font-styles';
+import '../../../styles/shared-styles';
import '../../shared/gr-cursor-manager/gr-cursor-manager';
import '../gr-change-list-item/gr-change-list-item';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -50,6 +51,8 @@
import {fireEvent, fireReload} from '../../../utils/event-util';
import {ScrollMode} from '../../../constants/constants';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {PRIORITY_REQUIREMENTS_ORDER} from '../../../utils/label-util';
const NUMBER_FIXED_COLUMNS = 3;
const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
@@ -67,6 +70,7 @@
'Branch',
'Updated',
'Size',
+ 'Requirements',
];
export interface ChangeListSection {
@@ -262,6 +266,8 @@
if (!config || !config.change) return true;
if (column === 'Assignee') return !!config.change.enable_assignee;
if (column === 'Comments') return experiments.includes('comments-column');
+ if (column === 'Requirements')
+ return experiments.includes(KnownExperimentId.SUBMIT_REQUIREMENTS_UI);
return true;
}
@@ -316,6 +322,13 @@
labels = labels.concat(currentLabels.filter(nonExistingLabel));
}
}
+ if (
+ this.flagsService.enabledExperiments.includes(
+ KnownExperimentId.SUBMIT_REQUIREMENTS_UI
+ )
+ ) {
+ labels = labels.filter(l => PRIORITY_REQUIREMENTS_ORDER.includes(l));
+ }
return labels.sort();
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
index c31da77..77320b9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
@@ -20,6 +20,9 @@
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="gr-change-list-styles">
#changeList {
border-collapse: collapse;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index de1a0a2..472435c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -282,6 +282,7 @@
'Branch',
'Updated',
'Size',
+ 'Requirements',
],
};
element._config = {};
@@ -319,6 +320,7 @@
'Branch',
'Updated',
'Size',
+ 'Requirements',
],
};
element._config = {};
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 97101f6..25dab25 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
@@ -256,7 +256,6 @@
id="assigneeValue"
placeholder="Set assignee..."
max-count="1"
- skip-suggest-on-empty=""
accounts="{{_assignee}}"
readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
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 ad8f72f..e4b466b 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
@@ -232,6 +232,9 @@
height: var(--line-height-small);
vertical-align: top;
}
+ .checksChip a iron-icon.launch {
+ color: var(--link-color);
+ }
.checksChip.error {
color: var(--error-foreground);
border-color: var(--error-foreground);
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 b635d3f..9c6a079 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
@@ -16,6 +16,7 @@
*/
import '@polymer/paper-tabs/paper-tabs';
import '../../../styles/gr-a11y-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import '../../diff/gr-comment-api/gr-comment-api';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -61,12 +62,12 @@
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
-import {DiffViewMode} from '../../../api/diff';
import {
ChangeStatus,
DefaultBase,
PrimaryTab,
SecondaryTab,
+ DiffViewMode,
} from '../../../constants/constants';
import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages';
@@ -131,7 +132,11 @@
ChangeComments,
GrCommentApi,
} from '../../diff/gr-comment-api/gr-comment-api';
-import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
+import {
+ assertIsDefined,
+ hasOwnProperty,
+ query,
+} from '../../../utils/common-util';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {
CommentThread,
@@ -196,6 +201,7 @@
hasAttention,
} from '../../../utils/attention-set-util';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {preferenceDiffViewMode$} from '../../../services/user/user-model';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
@@ -233,7 +239,6 @@
downloadOverlay: GrOverlay;
downloadDialog: GrDownloadDialog;
replyOverlay: GrOverlay;
- replyDialog: GrReplyDialog;
mainContent: HTMLDivElement;
changeStar: GrChangeStar;
actions: GrChangeActions;
@@ -518,7 +523,7 @@
_activeTabs: string[] = [PrimaryTab.FILES, SecondaryTab.CHANGE_LOG];
@property({type: Boolean})
- unresolvedOnly = false;
+ unresolvedOnly = true;
@property({type: Boolean})
_showAllRobotComments = false;
@@ -543,6 +548,10 @@
@property({type: String})
scrollCommentId?: UrlEncodedCommentId;
+ /** Just reflects the `opened` prop of the overlay. */
+ @property({type: Boolean})
+ replyOverlayOpened = false;
+
@property({
type: Array,
computed: '_computeResolveWeblinks(_change, _commitInfo, _serverConfig)',
@@ -551,12 +560,12 @@
restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
+
private readonly commentsService = appContext.commentsService;
private readonly shortcuts = appContext.shortcutsService;
- private replyDialogResizeObserver?: ResizeObserver;
-
override keyboardShortcuts(): ShortcutListener[] {
return [
listen(Shortcut.SEND_REPLY, _ => {}), // docOnly
@@ -611,6 +620,14 @@
private lastStarredTimestamp?: number;
+ private diffViewMode?: DiffViewMode;
+
+ /**
+ * If the user comes back to the change page we want to remember the scroll
+ * position when we re-render the page as is.
+ */
+ private scrollPosition?: number;
+
override ready() {
super.ready();
aPluginHasRegistered$.pipe(takeUntil(this.disconnected$)).subscribe(b => {
@@ -622,6 +639,11 @@
drafts$.pipe(takeUntil(this.disconnected$)).subscribe(drafts => {
this._diffDrafts = {...drafts};
});
+ preferenceDiffViewMode$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffViewMode => {
+ this.diffViewMode = diffViewMode;
+ });
changeComments$
.pipe(takeUntil(this.disconnected$))
.subscribe(changeComments => {
@@ -666,14 +688,8 @@
this._account = acct;
});
}
- this._setDiffViewMode();
});
- this.replyDialogResizeObserver = new ResizeObserver(() =>
- this.$.replyOverlay.center()
- );
- this.replyDialogResizeObserver.observe(this.$.replyDialog);
-
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
@@ -700,6 +716,7 @@
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ document.addEventListener('scroll', this.handleScroll);
this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
this._setActivePrimaryTab(e)
@@ -718,6 +735,7 @@
'visibilitychange',
this.handleVisibilityChange
);
+ document.removeEventListener('scroll', this.handleScroll);
this.replyRefitTask?.cancel();
this.scrollTask?.cancel();
@@ -735,23 +753,14 @@
return this.shadowRoot!.querySelector<GrThreadList>('gr-thread-list');
}
- _setDiffViewMode(opt_reset?: boolean) {
- if (!opt_reset && this.viewState.diffViewMode) {
- return;
- }
-
- return this._getPreferences()
- .then(prefs => {
- if (!this.viewState.diffMode && prefs) {
- this.set('viewState.diffMode', prefs.default_diff_view);
- }
- })
- .then(() => {
- if (!this.viewState.diffMode) {
- this.set('viewState.diffMode', 'SIDE_BY_SIDE');
- }
- });
- }
+ private readonly handleScroll = () => {
+ if (!this.isViewCurrent) return;
+ this.scrollTask = debounce(
+ this.scrollTask,
+ () => (this.scrollPosition = document.documentElement.scrollTop),
+ 150
+ );
+ };
_onOpenFixPreview(e: OpenFixPreviewEvent) {
this.$.applyFixDialog.open(e);
@@ -762,10 +771,12 @@
}
_handleToggleDiffMode() {
- if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
+ if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
+ this.userService.updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
+ this.userService.updatePreferences({
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ });
}
}
@@ -872,7 +883,7 @@
const hasUnresolvedThreads =
(this._commentThreads ?? []).filter(thread => isUnresolved(thread))
.length > 0;
- if (hasUnresolvedThreads) this.unresolvedOnly = true;
+ if (!hasUnresolvedThreads) this.unresolvedOnly = false;
}
this.reporting.reportInteraction(Interaction.SHOW_TAB, {
@@ -1062,7 +1073,7 @@
_handleReplyTap(e: MouseEvent) {
e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
}
onReplyOverlayCanceled() {
@@ -1106,8 +1117,7 @@
.split('\n')
.map(line => '> ' + line)
.join('\n') + '\n\n';
- this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
+ this._openReplyDialog(FocusTarget.BODY, quoteStr);
}
_handleHideBackgroundContent() {
@@ -1144,9 +1154,9 @@
}
_handleShowReplyDialog(e: CustomEvent<{value: {ccsOnly: boolean}}>) {
- let target = this.$.replyDialog.FocusTarget.REVIEWERS;
+ let target = FocusTarget.REVIEWERS;
if (e.detail.value && e.detail.value.ccsOnly) {
- target = this.$.replyDialog.FocusTarget.CCS;
+ target = FocusTarget.CCS;
}
this._openReplyDialog(target);
}
@@ -1224,27 +1234,47 @@
basePatchNum: value.basePatchNum,
};
- this.$.fileList.collapseAllDiffs();
this._patchRange = patchRange;
this.scrollCommentId = value.commentId;
const patchKnown =
!patchRange.patchNum ||
(this._allPatchSets ?? []).some(ps => ps.num === patchRange.patchNum);
+ // _allPatchsets does not know value.patchNum so force a reload.
+ const forceReload = value.forceReload || !patchKnown;
- // If the change has already been loaded and the parameter change is only
- // in the patch range, then don't do a full reload.
- if (this._changeNum !== undefined && patchChanged && patchKnown) {
+ // If changeNum is defined that means the change has already been
+ // rendered once before so a full reload is not required.
+ if (this._changeNum !== undefined && !forceReload) {
if (!patchRange.patchNum) {
- patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
+ this._patchRange = {
+ ...this._patchRange,
+ patchNum: computeLatestPatchNum(this._allPatchSets),
+ };
rightPatchNumChanged = true;
}
- this._reloadPatchNumDependentResources(rightPatchNumChanged).then(() => {
- this._sendShowChangeEvent();
- });
+ if (patchChanged) {
+ // We need to collapse all diffs when params change so that a non
+ // existing diff is not requested. See Issue 125270 for more details.
+ this.$.fileList.collapseAllDiffs();
+ this._reloadPatchNumDependentResources(rightPatchNumChanged).then(
+ () => {
+ this._sendShowChangeEvent();
+ }
+ );
+ }
+
+ // If there is no change in patchset or changeNum, such as when user goes
+ // to the diff view and then comes back to change page then there is no
+ // need to reload anything and we render the change view component as is.
+ document.documentElement.scrollTop = this.scrollPosition ?? 0;
return;
}
+ // We need to collapse all diffs when params change so that a non existing
+ // diff is not requested. See Issue 125270 for more details.
+ this.$.fileList.collapseAllDiffs();
+
this._initialLoadComplete = false;
this._changeNum = value.changeNum;
this.loadData(true).then(() => {
@@ -1260,8 +1290,8 @@
_initActiveTabs(params?: AppElementChangeViewParams) {
let primaryTab = PrimaryTab.FILES;
- if (params && params.queryMap && params.queryMap.has('tab')) {
- primaryTab = params.queryMap.get('tab') as PrimaryTab;
+ if (params?.tab) {
+ primaryTab = params?.tab as PrimaryTab;
} else if (params && 'commentId' in params) {
primaryTab = PrimaryTab.COMMENT_THREADS;
}
@@ -1397,7 +1427,7 @@
}
if (this.viewState.showReplyDialog) {
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
this.set('viewState.showReplyDialog', false);
}
});
@@ -1416,9 +1446,6 @@
!!this.viewState.changeNum &&
this.viewState.changeNum !== this._changeNum
) {
- // Reset the diff mode to null when navigating from one change to
- // another, so that the user's preference is restored.
- this._setDiffViewMode(true);
this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN);
}
this.set('viewState.changeNum', this._changeNum);
@@ -1506,7 +1533,7 @@
fireEvent(this, 'show-auth-required');
return;
}
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
});
}
@@ -1691,12 +1718,17 @@
});
}
- _openReplyDialog(section?: FocusTarget) {
+ _openReplyDialog(focusTarget?: FocusTarget, quote?: string) {
if (!this._change) return;
- this.$.replyOverlay.open().finally(() => {
+ const overlay = this.$.replyOverlay;
+ overlay.open().finally(async () => {
// the following code should be executed no matter open succeed or not
+ const dialog = query<GrReplyDialog>(this, '#replyDialog');
+ assertIsDefined(dialog, 'reply dialog');
this._resetReplyOverlayFocusStops();
- this.$.replyDialog.open(section);
+ dialog.open(focusTarget, quote);
+ const observer = new ResizeObserver(() => overlay.center());
+ observer.observe(dialog);
});
fireDialogChange(this, {opened: true});
this._changeViewAriaHidden = true;
@@ -2027,8 +2059,8 @@
*
* @param isLocationChange Reloads the related changes
* when true and ends reporting events that started on location change.
- * @param clearPatchset Reloads the related changes
- * ignoring any patchset choice made.
+ * @param clearPatchset Reloads the change ignoring any patchset
+ * choice made.
* @return A promise that resolves when the core data has loaded.
* Some non-core data loading may still be in-flight when the core data
* promise resolves.
@@ -2036,7 +2068,14 @@
loadData(isLocationChange?: boolean, clearPatchset?: boolean): Promise<void> {
if (this.isChangeObsolete()) return Promise.resolve();
if (clearPatchset && this._change) {
- GerritNav.navigateToChange(this._change);
+ GerritNav.navigateToChange(
+ this._change,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ true
+ );
return Promise.resolve();
}
this._loading = true;
@@ -2480,18 +2519,34 @@
) {
patchNum = this._patchRange.patchNum;
}
- GerritNav.navigateToChange(this._change, patchNum, undefined, true);
+ GerritNav.navigateToChange(
+ this._change,
+ patchNum,
+ undefined,
+ true,
+ undefined,
+ true
+ );
}
_handleStopEditTap() {
assertIsDefined(this._change, '_change');
if (!this._patchRange)
throw new Error('missing required _patchRange property');
- GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
+ GerritNav.navigateToChange(
+ this._change,
+ this._patchRange.patchNum,
+ undefined,
+ undefined,
+ undefined,
+ true
+ );
}
_resetReplyOverlayFocusStops() {
- this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+ const dialog = query<GrReplyDialog>(this, '#replyDialog');
+ if (!dialog) return;
+ this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
}
_handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
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 0b77bc7..155d817 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
@@ -20,6 +20,9 @@
<style include="gr-a11y-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
.container:not(.loading) {
background-color: var(--background-color-tertiary);
@@ -533,7 +536,6 @@
server-config="[[_serverConfig]]"
shown-file-count="[[_shownFileCount]]"
diff-prefs="[[_diffPrefs]]"
- diff-view-mode="{{viewState.diffMode}}"
patch-num="{{_patchRange.patchNum}}"
base-patch-num="{{_patchRange.basePatchNum}}"
files-expanded="[[_filesExpanded]]"
@@ -696,24 +698,27 @@
no-cancel-on-esc-key=""
scroll-action="lock"
with-backdrop=""
+ opened="{{replyOverlayOpened}}"
on-iron-overlay-canceled="onReplyOverlayCanceled"
>
- <gr-reply-dialog
- id="replyDialog"
- change="{{_change}}"
- patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
- permitted-labels="[[_change.permitted_labels]]"
- draft-comment-threads="[[_draftCommentThreads]]"
- project-config="[[_projectConfig]]"
- server-config="[[_serverConfig]]"
- can-be-started="[[_canStartReview]]"
- on-send="_handleReplySent"
- on-cancel="_handleReplyCancel"
- on-autogrow="_handleReplyAutogrow"
- on-send-disabled-changed="_resetReplyOverlayFocusStops"
- hidden$="[[!_loggedIn]]"
- >
- </gr-reply-dialog>
+ <template is="dom-if" if="[[replyOverlayOpened]]">
+ <gr-reply-dialog
+ id="replyDialog"
+ change="{{_change}}"
+ patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+ permitted-labels="[[_change.permitted_labels]]"
+ draft-comment-threads="[[_draftCommentThreads]]"
+ project-config="[[_projectConfig]]"
+ server-config="[[_serverConfig]]"
+ can-be-started="[[_canStartReview]]"
+ on-send="_handleReplySent"
+ on-cancel="_handleReplyCancel"
+ on-autogrow="_handleReplyAutogrow"
+ on-send-disabled-changed="_resetReplyOverlayFocusStops"
+ hidden$="[[!_loggedIn]]"
+ >
+ </gr-reply-dialog>
+ </template>
</gr-overlay>
<gr-comment-api id="commentAPI"></gr-comment-api>
`;
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 6cbe59c..591aa41 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
@@ -26,6 +26,8 @@
HttpMethod,
MessageTag,
PrimaryTab,
+ createDefaultPreferences,
+ createDefaultDiffPrefs,
} from '../../../constants/constants';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
@@ -35,7 +37,14 @@
import {EventType, PluginApi} from '../../../api/plugin';
import 'lodash/lodash';
-import {mockPromise, stubRestApi} from '../../../test/test-utils';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+ stubUsers,
+ waitQueryAndAssert,
+ waitUntil,
+} from '../../../test/test-utils';
import {
createAppElementChangeViewParams,
createApproval,
@@ -94,6 +103,9 @@
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
import {appContext} from '../../../services/app-context';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
+import {_testOnly_setState} from '../../../services/user/user-model';
+import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
+import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
const pluginApi = _testOnly_initGerritPluginApi();
const fixture = fixtureFromElement('gr-change-view');
@@ -545,13 +557,12 @@
test('param change should switch primary tab correctly', async () => {
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
- const queryMap = new Map<string, string>();
- queryMap.set('tab', PrimaryTab.FINDINGS);
// view is required
+ element._changeNum = undefined;
element.params = {
...createAppElementChangeViewParams(),
...element.params,
- queryMap,
+ tab: PrimaryTab.FINDINGS,
};
await flush();
assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
@@ -559,13 +570,11 @@
test('invalid param change should not switch primary tab', async () => {
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
- const queryMap = new Map<string, string>();
- queryMap.set('tab', 'random');
// view is required
element.params = {
...createAppElementChangeViewParams(),
...element.params,
- queryMap,
+ tab: 'random',
};
await flush();
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
@@ -678,9 +687,7 @@
element.$.replyOverlay.close();
assert.isFalse(element.$.replyOverlay.opened);
assert(
- openSpy.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY
- ),
+ openSpy.lastCall.calledWithExactly(FocusTarget.ANY),
'_openReplyDialog should have been passed ANY'
);
assert.equal(openSpy.callCount, 1);
@@ -702,7 +709,8 @@
},
};
const handlerSpy = sinon.spy(element, '_handleHideBackgroundContent');
- element.$.replyDialog.dispatchEvent(
+ const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+ overlay.dispatchEvent(
new CustomEvent('fullscreen-overlay-opened', {
composed: true,
bubbles: true,
@@ -729,7 +737,8 @@
},
};
const handlerSpy = sinon.spy(element, '_handleShowBackgroundContent');
- element.$.replyDialog.dispatchEvent(
+ const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+ overlay.dispatchEvent(
new CustomEvent('fullscreen-overlay-closed', {
composed: true,
bubbles: true,
@@ -803,20 +812,36 @@
assert.isTrue(stub.called);
});
- test('m should toggle diff mode', () => {
- const setModeStub = sinon.stub(
- element.$.fileListHeader,
- 'setDiffViewMode'
+ test('m should toggle diff mode', async () => {
+ const updatePreferencesStub = stubUsers('updatePreferences');
+ await flush();
+
+ const prefs = {
+ ...createDefaultPreferences(),
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ };
+ _testOnly_setState({
+ preferences: prefs,
+ diffPreferences: createDefaultDiffPrefs(),
+ });
+ element._handleToggleDiffMode();
+ assert.isTrue(
+ updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
);
- flush();
- element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
+ const newPrefs = {
+ ...createDefaultPreferences(),
+ diff_view: DiffViewMode.UNIFIED,
+ };
+ _testOnly_setState({
+ preferences: newPrefs,
+ diffPreferences: createDefaultDiffPrefs(),
+ });
+ await flush();
element._handleToggleDiffMode();
- assert.isTrue(setModeStub.calledWith(DiffViewMode.UNIFIED));
-
- element.viewState.diffMode = DiffViewMode.UNIFIED;
- element._handleToggleDiffMode();
- assert.isTrue(setModeStub.calledWith(DiffViewMode.SIDE_BY_SIDE));
+ assert.isTrue(
+ updatePreferencesStub.calledWith({diff_view: DiffViewMode.SIDE_BY_SIDE})
+ );
});
});
@@ -1278,52 +1303,6 @@
assert.equal(element._numFilesShown, 200);
});
- test('_setDiffViewMode is called with reset when new change is loaded', () => {
- const setDiffViewModeStub = sinon.stub(element, '_setDiffViewMode');
- element.viewState = {changeNum: 1 as NumericChangeId};
- element._changeNum = 2 as NumericChangeId;
- element._resetFileListViewState();
- assert.isTrue(setDiffViewModeStub.calledWithExactly(true));
- });
-
- test('diffViewMode is propagated from file list header', () => {
- element.viewState = {diffMode: DiffViewMode.UNIFIED};
- element.$.fileListHeader.diffViewMode = DiffViewMode.SIDE_BY_SIDE;
- assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
- });
-
- test('diffMode defaults to side by side without preferences', async () => {
- stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
- // No user prefs or diff view mode set.
-
- await element._setDiffViewMode()!;
- assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
- });
-
- test('diffMode defaults to preference when not already set', async () => {
- stubRestApi('getPreferences').returns(
- Promise.resolve({
- ...createPreferences(),
- default_diff_view: DiffViewMode.UNIFIED,
- })
- );
-
- await element._setDiffViewMode()!;
- assert.equal(element.viewState.diffMode, DiffViewMode.UNIFIED);
- });
-
- test('existing diffMode overrides preference', async () => {
- element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
- stubRestApi('getPreferences').returns(
- Promise.resolve({
- ...createPreferences(),
- default_diff_view: DiffViewMode.UNIFIED,
- })
- );
- await element._setDiffViewMode()!;
- assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
- });
-
test('don’t reload entire page when patchRange changes', async () => {
const reloadStub = sinon
.stub(element, 'loadData')
@@ -1333,12 +1312,12 @@
.callsFake(() => Promise.resolve([undefined, undefined, undefined]));
flush();
const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
-
const value: AppElementChangeViewParams = {
...createAppElementChangeViewParams(),
view: GerritView.CHANGE,
patchNum: 1 as RevisionPatchSetNum,
};
+ element._changeNum = undefined;
element.params = value;
await flush();
assert.isTrue(reloadStub.calledOnce);
@@ -1396,7 +1375,7 @@
assert.isTrue(reloadPortedCommentsStub.calledOnce);
});
- test('reload entire page when patchRange doesnt change', async () => {
+ test('do not reload entire page when patchRange doesnt change', async () => {
const reloadStub = sinon
.stub(element, 'loadData')
.callsFake(() => Promise.resolve());
@@ -1404,13 +1383,15 @@
const value: AppElementChangeViewParams =
createAppElementChangeViewParams();
element.params = value;
+ // change already loaded
+ assert.isOk(element._changeNum);
await flush();
- assert.isTrue(reloadStub.calledOnce);
+ assert.isFalse(reloadStub.calledOnce);
element._initialLoadComplete = true;
element.params = {...value};
await flush();
- assert.isTrue(reloadStub.calledTwice);
- assert.isTrue(collapseStub.calledTwice);
+ assert.isFalse(reloadStub.calledTwice);
+ assert.isFalse(collapseStub.calledTwice);
});
test('do not handle new change numbers', async () => {
@@ -1635,9 +1616,7 @@
const openStub = sinon.stub(element, '_openReplyDialog');
tap(element.$.replyBtn);
assert(
- openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY
- ),
+ openStub.lastCall.calledWithExactly(FocusTarget.ANY),
'_openReplyDialog should have been passed ANY'
);
assert.equal(openStub.callCount, 1);
@@ -1656,18 +1635,12 @@
bubbles: true,
})
);
- assert(
- openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.BODY
- ),
- '_openReplyDialog should have been passed BODY'
- );
- assert.equal(openStub.callCount, 1);
+ assert.isTrue(openStub.calledOnce);
+ assert.equal(openStub.lastCall.args[0], FocusTarget.BODY);
}
);
test('reply dialog focus can be controlled', () => {
- const FocusTarget = element.$.replyDialog.FocusTarget;
const openStub = sinon.stub(element, '_openReplyDialog');
const e = new CustomEvent('show-reply-dialog', {
@@ -1743,7 +1716,6 @@
suite('reply dialog tests', () => {
setup(() => {
- sinon.stub(element.$.replyDialog, '_draftChanged');
element._change = {
...createChangeViewChange(),
revisions: createRevisions(1),
@@ -1774,52 +1746,18 @@
assert.isTrue(openReplyDialogStub.calledOnce);
});
- test('reply from comment adds quote text', () => {
+ test('reply from comment adds quote text', async () => {
const e = new CustomEvent('', {
detail: {message: {message: 'quote text'}},
});
element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from comment replaces quote text', () => {
- element.$.replyDialog.draft = '> old quote text\n\n some draft text';
- element.$.replyDialog.quote = '> old quote text\n\n';
- const e = new CustomEvent('', {
- detail: {message: {message: 'quote text'}},
- });
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from same comment preserves quote text', () => {
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = new CustomEvent('', {
- detail: {message: {message: 'quote text'}},
- });
- element._handleMessageReply(e);
- assert.equal(
- element.$.replyDialog.draft,
- '> quote text\n\n some draft text'
+ const dialog = await waitQueryAndAssert<GrReplyDialog>(
+ element,
+ '#replyDialog'
);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from top of page contains previous draft', () => {
- const div = document.createElement('div');
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {
- target: div,
- preventDefault: sinon.spy(),
- } as unknown as MouseEvent;
- element._handleReplyTap(e);
- assert.equal(
- element.$.replyDialog.draft,
- '> quote text\n\n some draft text'
- );
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ const openSpy = sinon.spy(dialog, 'open');
+ await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]);
+ assert.equal(openSpy.lastCall.args[1], '> quote text\n\n');
});
});
@@ -2124,7 +2062,7 @@
test('no edit exists in revisions, non-latest patchset', async () => {
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 4);
+ assert.equal(args.length, 6);
assert.equal(args[1], 1 as PatchSetNum); // patchNum
assert.equal(args[3], true); // opt_isEdit
promise.resolve();
@@ -2141,7 +2079,7 @@
test('no edit exists in revisions, latest patchset', async () => {
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 4);
+ assert.equal(args.length, 6);
// No patch should be specified when patchNum == latest.
assert.isNotOk(args[1]); // patchNum
assert.equal(args[3], true); // opt_isEdit
@@ -2165,7 +2103,7 @@
navigateToChangeStub.restore();
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 2);
+ assert.equal(args.length, 6);
assert.equal(args[1], 1 as PatchSetNum); // patchNum
promise.resolve();
});
@@ -2283,6 +2221,8 @@
appContext.reportingService,
'changeFullyLoaded'
);
+ // reset so reload is triggered
+ element._changeNum = undefined;
element.params = {
...createAppElementChangeViewParams(),
changeNum: TEST_NUMERIC_CHANGE_ID,
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index ae3eee5..1b44e35 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -41,7 +41,6 @@
import {DiffPreferencesInfo} from '../../../types/diff';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {GrDiffModeSelector} from '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
-import {DiffViewMode} from '../../../constants/constants';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fireEvent} from '../../../utils/event-util';
import {
@@ -122,9 +121,6 @@
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
- @property({type: String, notify: true})
- diffViewMode?: DiffViewMode;
-
@property({type: String})
patchNum?: PatchSetNum;
@@ -144,10 +140,6 @@
private readonly shortcuts = appContext.shortcutsService;
- setDiffViewMode(mode: DiffViewMode) {
- this.$.modeSelect.setMode(mode);
- }
-
_expandAllDiffs() {
fireEvent(this, 'expand-diffs');
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index 73d0819..5a85531 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -169,7 +169,6 @@
<span class="fileViewActionsLabel">Diff view:</span>
<gr-diff-mode-selector
id="modeSelect"
- mode="{{diffViewMode}}"
save-on-change="[[loggedIn]]"
></gr-diff-mode-selector>
<span
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 cc76145..95984b8 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
@@ -83,11 +83,15 @@
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
-import {preferences$} from '../../../services/user/user-model';
+import {
+ diffPreferences$,
+ sizeBarInChangeTable$,
+} from '../../../services/user/user-model';
import {changeComments$} from '../../../services/comments/comments-model';
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {diffViewMode$} from '../../../services/browser/browser-model';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -316,6 +320,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
+
disconnected$ = new Subject();
/** Called in disconnectedCallback. */
@@ -377,6 +383,20 @@
.subscribe(changeComments => {
this.changeComments = changeComments;
});
+ diffViewMode$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffView => (this.diffViewMode = diffView));
+ diffPreferences$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ });
+ sizeBarInChangeTable$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(sizeBarInChangeTable => {
+ this._showSizeBars = sizeBarInChangeTable;
+ });
+
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
@@ -421,7 +441,9 @@
});
this.cleanups.push(
addGlobalShortcut({key: Key.ESC}, _ => this._handleEscKey()),
- addShortcut(this, {key: Key.ENTER}, _ => this.handleOpenFile())
+ addShortcut(this, {key: Key.ENTER}, _ => this.handleOpenFile(), {
+ shouldSuppress: true,
+ })
);
}
@@ -472,16 +494,6 @@
})
);
- promises.push(
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- })
- );
-
- preferences$.pipe(takeUntil(this.disconnected$)).subscribe(prefs => {
- this._showSizeBars = !!prefs?.size_bar_in_change_table;
- });
-
return Promise.all(promises).then(() => {
this._loading = false;
this._detectChromiteButler();
@@ -1638,9 +1650,7 @@
}
_handleReloadingDiffPreference() {
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ this.userService.getDiffPreferences();
}
/**
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 f7be36b..e8371e3 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
@@ -643,7 +643,6 @@
prefs="[[diffPrefs]]"
project-name="[[change.project]]"
no-render-on-prefs-change=""
- view-mode="[[diffViewMode]]"
></gr-diff-host>
</template>
</div>
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 7409be7..0db7690 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
@@ -820,10 +820,8 @@
MockInteractions.tap(row);
flush();
- const diffDisplay = element.diffs[0];
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
+ element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
element.set('diffViewMode', 'UNIFIED_DIFF');
- assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
assert.isTrue(element._updateDiffPreferences.called);
});
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 a496be5..962ccef 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
@@ -33,9 +33,11 @@
LabelValuesMap,
} from '../gr-label-score-row/gr-label-score-row';
import {appContext} from '../../../services/app-context';
-import {labelCompare} from '../../../utils/label-util';
+import {getTriggerVotes, labelCompare} from '../../../utils/label-util';
import {Execution} from '../../../constants/reporting';
import {ChangeStatus} from '../../../constants/constants';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {fontStyles} from '../../../styles/gr-font-styles';
@customElement('gr-label-scores')
export class GrLabelScores extends LitElement {
@@ -50,8 +52,11 @@
private readonly reporting = appContext.reportingService;
+ private readonly flagsService = appContext.flagsService;
+
static override get styles() {
return [
+ fontStyles,
css`
.scoresTable {
display: table;
@@ -72,26 +77,74 @@
gr-label-score-row.no-access {
display: none;
}
+ .heading-3 {
+ padding-left: var(--spacing-xl);
+ margin-bottom: var(--spacing-m);
+ margin-top: var(--spacing-l);
+ }
+ .heading-3:first-of-type {
+ margin-top: 0;
+ }
`,
];
}
override render() {
+ if (this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
+ return this.renderNewSubmitRequirements();
+ } else {
+ return this.renderOldSubmitRequirements();
+ }
+ }
+
+ private renderOldSubmitRequirements() {
const labels = this._computeLabels();
+ return html`${this.renderLabels(labels)}${this.renderErrorMessages()}`;
+ }
+
+ private renderNewSubmitRequirements() {
+ return html`${this.renderSubmitReqsLabels()}${this.renderTriggerVotes()}
+ ${this.renderErrorMessages()}`;
+ }
+
+ private renderSubmitReqsLabels() {
+ const triggerVotes = getTriggerVotes(this.change);
+ const labels = this._computeLabels().filter(
+ label => !triggerVotes.includes(label.name)
+ );
+ if (!labels.length) return;
+ return html`<h3 class="heading-3">Submit requirements votes</h3>
+ ${this.renderLabels(labels)}`;
+ }
+
+ private renderTriggerVotes() {
+ const triggerVotes = getTriggerVotes(this.change);
+ const labels = this._computeLabels().filter(label =>
+ triggerVotes.includes(label.name)
+ );
+ if (!labels.length) return;
+ return html`<h3 class="heading-3">Trigger Votes</h3>
+ ${this.renderLabels(labels)}`;
+ }
+
+ private renderLabels(labels: Label[]) {
const labelValues = this._computeColumns();
return html`<div class="scoresTable">
- ${labels.map(
- label => html`<gr-label-score-row
- class="${this.computeLabelAccessClass(label.name)}"
- .label="${label}"
- .name="${label.name}"
- .labels="${this.change?.labels}"
- .permittedLabels="${this.permittedLabels}"
- .labelValues="${labelValues}"
- ></gr-label-score-row>`
- )}
- </div>
- <div
+ ${labels.map(
+ label => html`<gr-label-score-row
+ class="${this.computeLabelAccessClass(label.name)}"
+ .label="${label}"
+ .name="${label.name}"
+ .labels="${this.change?.labels}"
+ .permittedLabels="${this.permittedLabels}"
+ .labelValues="${labelValues}"
+ ></gr-label-score-row>`
+ )}
+ </div>`;
+ }
+
+ private renderErrorMessages() {
+ return html`<div
class="mergedMessage"
?hidden=${this.change?.status !== ChangeStatus.MERGED}
>
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 e16c073..c3acfb0 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
@@ -18,6 +18,7 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
import '../gr-message/gr-message';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-messages-list_html';
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
index 56fae87..087ee19 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
@@ -51,6 +51,9 @@
border-bottom: 1px solid var(--border-color);
}
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<div class="header">
<div id="showAllActivityToggleContainer" class="container">
<template
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index c67fd0c..0e98ddc 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -724,18 +724,13 @@
css`
.title {
color: var(--deemphasized-text-color);
- padding-left: var(--metadata-horizontal-padding);
- }
- h4 {
display: flex;
align-self: flex-end;
+ margin-left: 20px;
}
gr-button {
display: flex;
}
- h4 {
- margin-left: 20px;
- }
gr-button iron-icon {
color: inherit;
--iron-icon-height: 18px;
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 b8932e5..5109b72 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
@@ -240,9 +240,6 @@
@property({type: String, observer: '_draftChanged'})
draft = '';
- @property({type: String})
- quote = '';
-
@property({type: Object})
filterReviewerSuggestion: (input: Suggestion) => boolean;
@@ -427,7 +424,13 @@
super.disconnectedCallback();
}
- open(focusTarget?: FocusTarget) {
+ /**
+ * Note that this method is not actually *opening* the dialog. Opening and
+ * showing the dialog is dealt with by the overlay. This method is used by the
+ * change view for initializing the dialog after opening the overlay. Maybe it
+ * should be called `onOpened()` or `initialize()`?
+ */
+ open(focusTarget?: FocusTarget, quote?: string) {
assertIsDefined(this.change, 'change');
this.knownLatestState = LatestPatchState.CHECKING;
this.changeService.fetchChangeUpdates(this.change).then(result => {
@@ -437,10 +440,9 @@
});
this._focusOn(focusTarget);
- if (this.quote && this.quote.length) {
- // If a reply quote has been provided, use it and clear the property.
- this.draft = this.quote;
- this.quote = '';
+ if (quote?.length) {
+ // If a reply quote has been provided, use it.
+ this.draft = quote;
} else {
// Otherwise, check for an unsaved draft in localstorage.
this.draft = this._loadStoredDraft();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 5a11f47..cf31a4f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -18,9 +18,11 @@
import '../../../test/common-test-setup-karma';
import './gr-reply-dialog';
import {
+ addListenerForTest,
mockPromise,
queryAll,
queryAndAssert,
+ stubRestApi,
stubStorage,
} from '../../../test/test-utils';
import {
@@ -29,8 +31,6 @@
SpecialFilePath,
} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
-import {addListenerForTest} from '../../../test/test-utils';
-import {stubRestApi} from '../../../test/test-utils';
import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {StandardLabels} from '../../../utils/label-util';
import {
@@ -44,7 +44,7 @@
pressAndReleaseKeyOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
-import {GrReplyDialog} from './gr-reply-dialog';
+import {FocusTarget, GrReplyDialog} from './gr-reply-dialog';
import {
AccountId,
AccountInfo,
@@ -1317,11 +1317,9 @@
const storedDraft = 'hello world';
const quote = '> foo bar';
getDraftCommentStub.returns({message: storedDraft});
- element.quote = quote;
- element.open();
+ element.open(FocusTarget.ANY, quote);
assert.isFalse(getDraftCommentStub.called);
assert.equal(element.draft, quote);
- assert.isNotOk(element.quote);
});
test('updates stored draft on edits', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard.ts
new file mode 100644
index 0000000..ebe3ce3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard.ts
@@ -0,0 +1,57 @@
+/**
+ * @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 '../gr-submit-requirements/gr-submit-requirements';
+import {customElement, property} from 'lit/decorators';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {ParsedChangeInfo} from '../../../types/types';
+
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardMixin(LitElement);
+
+@customElement('gr-submit-requirement-dashboard-hovercard')
+export class GrSubmitRequirementDashboardHovercard extends base {
+ @property({type: Object})
+ change?: ParsedChangeInfo;
+
+ static override get styles() {
+ return [
+ base.styles || [],
+ css`
+ #container {
+ padding: var(--spacing-xl);
+ padding-left: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`<div id="container" role="tooltip" tabindex="-1">
+ <gr-submit-requirements
+ .change=${this.change}
+ suppress-title
+ ></gr-submit-requirements>
+ </div>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-submit-requirement-dashboard-hovercard': GrSubmitRequirementDashboardHovercard;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index c493be4..6406e3c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -29,10 +29,11 @@
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
-import {unique} from '../../../utils/common-util';
import {
extractAssociatedLabels,
getAllUniqueApprovals,
+ getRequirements,
+ getTriggerVotes,
hasNeutralStatus,
hasVotes,
iconForStatus,
@@ -49,6 +50,9 @@
import {Category} from '../../../api/checks';
import '../../shared/gr-vote-chip/gr-vote-chip';
+/**
+ * @attr {Boolean} suppress-title - hide titles, currently for hovercard view
+ */
@customElement('gr-submit-requirements')
export class GrSubmitRequirements extends LitElement {
@property({type: Object})
@@ -67,6 +71,9 @@
return [
fontStyles,
css`
+ :host([suppress-title]) .metadata-title {
+ display: none;
+ }
.metadata-title {
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
@@ -108,6 +115,7 @@
}
td {
padding: var(--spacing-s);
+ white-space: nowrap;
}
.votes-cell {
display: flex;
@@ -133,8 +141,8 @@
override render() {
const submit_requirements = orderSubmitRequirements(
- this.change?.submit_requirements ?? []
- ).filter(req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE);
+ getRequirements(this.change)
+ );
return html` <h3
class="metadata-title heading-3"
@@ -151,25 +159,8 @@
</tr>
</thead>
<tbody>
- ${submit_requirements.map(
- requirement => html`<tr
- id="requirement-${charsOnly(requirement.name)}"
- >
- <td>${this.renderStatus(requirement.status)}</td>
- <td class="name">
- <gr-limited-text
- class="name"
- limit="25"
- .text="${requirement.name}"
- ></gr-limited-text>
- </td>
- <td>
- <div class="votes-cell">
- ${this.renderVotes(requirement)}
- ${this.renderChecks(requirement)}
- </div>
- </td>
- </tr>`
+ ${submit_requirements.map(requirement =>
+ this.renderRequirement(requirement)
)}
</tbody>
</table>
@@ -184,7 +175,38 @@
></gr-submit-requirement-hovercard>
`
)}
- ${this.renderTriggerVotes(submit_requirements)}`;
+ ${this.renderTriggerVotes()}`;
+ }
+
+ renderRequirement(requirement: SubmitRequirementResultInfo) {
+ return html`
+ <tr id="requirement-${charsOnly(requirement.name)}">
+ <td>${this.renderStatus(requirement.status)}</td>
+ <td class="name">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ .text="${requirement.name}"
+ ></gr-limited-text>
+ </td>
+ <td>
+ <gr-endpoint-decorator
+ class="votes-cell"
+ name="${`submit-requirement-${charsOnly(requirement.name)}`}"
+ >
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="requirement"
+ .value=${requirement}
+ ></gr-endpoint-param>
+ ${this.renderVotes(requirement)}${this.renderChecks(requirement)}
+ </gr-endpoint-decorator>
+ </td>
+ </tr>
+ `;
}
renderStatus(status: SubmitRequirementStatus) {
@@ -259,15 +281,11 @@
return;
}
- renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
+ renderTriggerVotes() {
const labels = this.change?.labels ?? {};
- const allLabels = Object.keys(labels);
- const labelAssociatedWithSubmitReqs = submitReqs
- .flatMap(req => extractAssociatedLabels(req))
- .filter(unique);
- const triggerVotes = allLabels
- .filter(label => !labelAssociatedWithSubmitReqs.includes(label))
- .filter(label => hasVotes(labels[label]));
+ const triggerVotes = getTriggerVotes(this.change).filter(label =>
+ hasVotes(labels[label])
+ );
if (!triggerVotes.length) return;
return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
<section class="trigger-votes">
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 b8f2630..6b4006c 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -256,11 +256,9 @@
edit?: boolean;
host?: string;
messageHash?: string;
- queryMap?: Map<string, string> | URLSearchParams;
commentId?: UrlEncodedCommentId;
-
- // TODO(TS): querystring isn't set anywhere, try to remove
- querystring?: string;
+ forceReload?: boolean;
+ tab?: string;
}
export interface GenerateUrlRepoViewParameters {
@@ -612,7 +610,8 @@
patchNum?: PatchSetNum,
basePatchNum?: BasePatchSetNum,
isEdit?: boolean,
- messageHash?: string
+ messageHash?: string,
+ forceReload?: boolean
) {
if (basePatchNum === ParentPatchSetNum) {
basePatchNum = undefined;
@@ -628,6 +627,7 @@
edit: isEdit,
host: change.internalHost || undefined,
messageHash,
+ forceReload,
});
},
@@ -649,17 +649,28 @@
* @param redirect redirect to a change - if true, the current
* location (i.e. page which makes redirect) is not added to a history.
* I.e. back/forward buttons skip current location
- *
+ * @param forceReload Some views are smart about how to handle the reload
+ * of the view. In certain cases we want to force the view to reload
+ * and re-render everything.
*/
+ // TODO(dhruvsri): move the arguments into one options object
navigateToChange(
change: Pick<ChangeInfo, '_number' | 'project' | 'internalHost'>,
patchNum?: PatchSetNum,
basePatchNum?: BasePatchSetNum,
isEdit?: boolean,
- redirect?: boolean
+ redirect?: boolean,
+ forceReload?: boolean
) {
this._navigate(
- this.getUrlForChange(change, patchNum, basePatchNum, isEdit),
+ this.getUrlForChange(
+ change,
+ patchNum,
+ basePatchNum,
+ isEdit,
+ undefined,
+ forceReload
+ ),
redirect
);
},
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 f0cff85..65ac9df 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -530,17 +530,22 @@
range = '/' + range;
}
let suffix = `${range}`;
- if (params.querystring) {
- suffix += '?' + params.querystring;
- } else if (params.edit) {
- suffix += ',edit';
+ let queryString = '';
+ if (params.forceReload) {
+ queryString = 'forceReload=true';
}
- if (params.messageHash) {
- suffix += params.messageHash;
+ if (params.edit) {
+ suffix += ',edit';
}
if (params.commentId) {
suffix = suffix + `/comments/${params.commentId}`;
}
+ if (queryString) {
+ suffix += '?' + queryString;
+ }
+ if (params.messageHash) {
+ suffix += params.messageHash;
+ }
if (params.project) {
const encodedProject = encodeURL(params.project, true);
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
@@ -1563,9 +1568,20 @@
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]),
view: GerritView.CHANGE,
- queryMap: ctx.queryMap,
};
+ if (ctx.queryMap.has('forceReload')) {
+ params.forceReload = true;
+ history.replaceState(
+ null,
+ '',
+ location.href.replace(/[?&]forceReload=true/, '')
+ );
+ }
+
+ const tab = ctx.queryMap.get('tab');
+ if (tab) params.tab = tab;
+
this.reporting.setRepoName(params.project);
this.reporting.setChangeId(changeNum);
this._redirectOrNavigate(params);
@@ -1661,13 +1677,24 @@
// Parameter order is based on the regex group number matched.
const project = ctx.params[0] as RepoName;
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- this._redirectOrNavigate({
+ const params: GenerateUrlChangeViewParameters = {
project,
changeNum,
patchNum: convertToPatchSetNum(ctx.params[3]),
view: GerritView.CHANGE,
edit: true,
- });
+ tab: ctx.queryMap.get('tab') ?? '',
+ };
+ if (ctx.queryMap.has('forceReload')) {
+ params.forceReload = true;
+ history.replaceState(
+ null,
+ '',
+ location.href.replace(/[?&]forceReload=true/, '')
+ );
+ }
+ this._redirectOrNavigate(params);
+
this.reporting.setRepoName(project);
this.reporting.setChangeId(changeNum);
}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index b91bf0c..7f1a40b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -312,28 +312,14 @@
changeNum: '1234',
project: 'test',
};
- const paramsWithQuery = {
- view: GerritView.CHANGE,
- changeNum: '1234',
- project: 'test',
- querystring: 'revert&foo=bar',
- };
assert.equal(element._generateUrl(params), '/c/test/+/1234');
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234?revert&foo=bar');
params.patchNum = 10;
assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
- paramsWithQuery.patchNum = 10;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/10?revert&foo=bar');
params.basePatchNum = 5;
assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
- paramsWithQuery.basePatchNum = 5;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/5..10?revert&foo=bar');
params.messageHash = '#123';
assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
@@ -1382,7 +1368,6 @@
changeNum: 1234,
basePatchNum: 4,
patchNum: 7,
- queryMap: new Map(),
});
assert.isFalse(redirectStub.called);
assert.isTrue(normalizeRangeStub.called);
@@ -1549,6 +1534,7 @@
null,
3, // 3 Patch num
],
+ queryMap: new Map(),
};
const appParams = {
project: 'foo/bar',
@@ -1556,6 +1542,7 @@
view: GerritView.CHANGE,
patchNum: 3,
edit: true,
+ tab: '',
};
element._handleChangeEditRoute(ctx);
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.js b/polygerrit-ui/app/elements/custom-dark-theme_test.ts
similarity index 72%
rename from polygerrit-ui/app/elements/custom-dark-theme_test.js
rename to polygerrit-ui/app/elements/custom-dark-theme_test.ts
index 768a461..71e0740 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.ts
@@ -14,17 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import '../test/common-test-setup-karma.js';
-import {getComputedStyleValue} from '../utils/dom-util.js';
-import './gr-app.js';
-import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {removeTheme} from '../styles/themes/dark-theme.js';
+import '../test/common-test-setup-karma';
+import {getComputedStyleValue} from '../utils/dom-util';
+import './gr-app';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {GrApp} from './gr-app';
const basicFixture = fixtureFromElement('gr-app');
suite('gr-app custom dark theme tests', () => {
- let element;
+ let element: GrApp;
setup(async () => {
window.localStorage.setItem('dark-theme', 'true');
@@ -36,7 +35,6 @@
teardown(() => {
window.localStorage.removeItem('dark-theme');
- removeTheme();
// The app sends requests to server. This can lead to
// unexpected gr-alert elements in document.body
document.body.querySelectorAll('gr-alert').forEach(grAlert => {
@@ -45,19 +43,17 @@
});
test('should tried to load dark theme', () => {
- assert.isTrue(
- !!document.head.querySelector('#dark-theme')
- );
+ assert.isTrue(!!document.head.querySelector('#dark-theme'));
});
test('applies the right theme', () => {
assert.equal(
- getComputedStyleValue('--header-background-color', element)
- .toLowerCase(),
- '#3c4043');
+ getComputedStyleValue('--header-background-color', element).toLowerCase(),
+ '#3c4043'
+ );
assert.equal(
- getComputedStyleValue('--footer-background-color', element)
- .toLowerCase(),
- '#3c4043');
+ getComputedStyleValue('--footer-background-color', element).toLowerCase(),
+ '#3c4043'
+ );
});
});
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.js b/polygerrit-ui/app/elements/custom-light-theme_test.ts
similarity index 69%
rename from polygerrit-ui/app/elements/custom-light-theme_test.js
rename to polygerrit-ui/app/elements/custom-light-theme_test.ts
index c6e9642..80a7cab 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.ts
@@ -14,21 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import '../test/common-test-setup-karma.js';
-import {getComputedStyleValue} from '../utils/dom-util.js';
-import './gr-app.js';
-import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {stubRestApi} from '../test/test-utils.js';
+import '../test/common-test-setup-karma';
+import {getComputedStyleValue} from '../utils/dom-util';
+import './gr-app';
+import '../styles/themes/app-theme';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {stubRestApi} from '../test/test-utils';
+import {GrApp} from './gr-app';
+import {
+ createAccountDetailWithId,
+ createServerInfo,
+} from '../test/test-data-generators';
const basicFixture = fixtureFromElement('gr-app');
suite('gr-app custom light theme tests', () => {
- let element;
+ let element: GrApp;
setup(async () => {
window.localStorage.removeItem('dark-theme');
- stubRestApi('getConfig').returns(Promise.resolve({test: 'config'}));
- stubRestApi('getAccount').returns(Promise.resolve({}));
+ stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
+ stubRestApi('getAccount').returns(
+ Promise.resolve(createAccountDetailWithId())
+ );
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -52,12 +59,12 @@
test('applies the right theme', () => {
assert.equal(
- getComputedStyleValue('--header-background-color', element)
- .toLowerCase(),
- '#f1f3f4');
+ getComputedStyleValue('--header-background-color', element).toLowerCase(),
+ '#f1f3f4'
+ );
assert.equal(
- getComputedStyleValue('--footer-background-color', element)
- .toLowerCase(),
- 'transparent');
+ getComputedStyleValue('--footer-background-color', element).toLowerCase(),
+ 'transparent'
+ );
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index f5072b9..a6176e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -41,7 +41,6 @@
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrButton} from '../../shared/gr-button/gr-button';
import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
-import {KnownExperimentId} from '../../../services/flags/flags';
export interface GrApplyFixDialog {
$: {
@@ -109,10 +108,7 @@
constructor() {
super();
this.restApiService.getPreferences().then(prefs => {
- if (
- !prefs?.disable_token_highlighting &&
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
- ) {
+ if (!prefs?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
}
});
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index b99a17b..7e7e507 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -70,11 +70,11 @@
* elements of that which uses the gr-comment-api.
*/
constructor(
- comments: PathToCommentsInfoMap | undefined,
- robotComments: {[path: string]: RobotCommentInfo[]} | undefined,
- drafts: {[path: string]: DraftInfo[]} | undefined,
- portedComments: PathToCommentsInfoMap | undefined,
- portedDrafts: PathToCommentsInfoMap | undefined
+ comments?: PathToCommentsInfoMap,
+ robotComments?: {[path: string]: RobotCommentInfo[]},
+ drafts?: {[path: string]: DraftInfo[]},
+ portedComments?: PathToCommentsInfoMap,
+ portedDrafts?: PathToCommentsInfoMap
) {
this._comments = addPath(comments);
this._robotComments = addPath(robotComments);
@@ -580,7 +580,10 @@
/**
* Computes a number of unresolved comment threads in a given file and path.
*/
- computeUnresolvedNum(file: PatchSetFile | PatchNumOnly) {
+ computeUnresolvedNum(
+ file: PatchSetFile | PatchNumOnly,
+ ignorePatchsetLevelComments?: boolean
+ ) {
let comments: Comment[] = [];
let drafts: Comment[] = [];
@@ -595,7 +598,11 @@
comments = comments.concat(drafts);
const threads = createCommentThreads(comments);
- const unresolvedThreads = threads.filter(isUnresolved);
+ let unresolvedThreads = threads.filter(isUnresolved);
+ if (ignorePatchsetLevelComments)
+ unresolvedThreads = unresolvedThreads.filter(
+ thread => !isPatchsetLevel(thread)
+ );
return unresolvedThreads.length;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index de7d007..54b2450f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -198,7 +198,6 @@
element,
} = this.findTokenAncestor(e?.target);
if (!newHighlight || newHighlight === this.currentHighlight) return;
- if (this.countOccurrences(newHighlight) <= 1) return;
this.hoveredElement = element;
this.updateTokenTask = debounce(
this.updateTokenTask,
@@ -247,13 +246,6 @@
return this.findTokenAncestor(el.parentElement);
}
- countOccurrences(token: string | undefined) {
- if (!token) return 0;
- const linesLeft = this.tokenToLinesLeft.get(token);
- const linesRight = this.tokenToLinesRight.get(token);
- return (linesLeft?.size ?? 0) + (linesRight?.size ?? 0);
- }
-
private updateTokenHighlight(
newHighlight: string | undefined,
newLineNumber: number,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 2993d35..a0670b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -290,6 +290,34 @@
assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
});
+ test('triggers listener on token with single occurrence', async () => {
+ const clock = sinon.useFakeTimers();
+ const line1 = createLine('a tokenWithSingleOccurence');
+ const line2 = createLine('can be highlighted', 2);
+ annotate(line1);
+ annotate(line2, Side.RIGHT, 2);
+ const tokenNode = queryAndAssert(line1, '.tk-tokenWithSingleOccurence');
+ assert.isTrue(tokenNode.classList.contains('token'));
+ dispatchMouseEvent(
+ 'mouseover',
+ MockInteractions.middleOfNode(tokenNode),
+ tokenNode
+ );
+ assert.equal(tokenHighlightingCalls.length, 0);
+ clock.tick(HOVER_DELAY_MS);
+ assert.equal(tokenHighlightingCalls.length, 1);
+ assert.deepEqual(tokenHighlightingCalls[0].details, {
+ token: 'tokenWithSingleOccurence',
+ side: Side.RIGHT,
+ element: tokenNode,
+ range: {start_line: 1, start_column: 3, end_line: 1, end_column: 26},
+ });
+
+ MockInteractions.click(container);
+ assert.equal(tokenHighlightingCalls.length, 2);
+ assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
+ });
+
test('clicking clears highlight', async () => {
const clock = sinon.useFakeTimers();
const line1 = createLine('two words');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index b1bad1c..025e477 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -88,10 +88,11 @@
import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
import {Timing} from '../../../constants/reporting';
import {changeComments$} from '../../../services/comments/comments-model';
-import {takeUntil} from 'rxjs/operators';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {Subject} from 'rxjs';
import {RenderPreferences} from '../../../api/diff';
+import {diffViewMode$} from '../../../services/browser/browser-model';
+import {takeUntil} from 'rxjs/operators';
const EMPTY_BLAME = 'No blame information for this diff.';
@@ -205,12 +206,12 @@
@property({type: Boolean})
lineWrapping = false;
- @property({type: String})
- viewMode = DiffViewMode.SIDE_BY_SIDE;
-
@property({type: Object})
lineOfInterest?: LineOfInterest;
+ @property({type: String})
+ viewMode = DiffViewMode.SIDE_BY_SIDE;
+
@property({type: Boolean})
showLoadFailure?: boolean;
@@ -312,6 +313,9 @@
override connectedCallback() {
super.connectedCallback();
+ diffViewMode$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffView => (this.viewMode = diffView));
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
@@ -332,9 +336,7 @@
const preferencesPromise = appContext.restApiService.getPreferences();
await getPluginLoader().awaitPluginsLoaded();
const prefs = await preferencesPromise;
- const enableTokenHighlight =
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !prefs?.disable_token_highlighting;
+ const enableTokenHighlight = !prefs?.disable_token_highlighting;
assertIsDefined(this.path, 'path');
this._layers = this.getLayers(this.path, enableTokenHighlight);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 8901636..6facdca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -1304,7 +1304,7 @@
test('gr-diff-host provides syntax highlighting layer', async () => {
stubRestApi('getDiff').returns(Promise.resolve({content: []}));
await element.reload();
- assert.equal(element.$.diff.layers[0], element.syntaxLayer);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
});
test('rendering normal-sized diff does not disable syntax', () => {
@@ -1358,7 +1358,7 @@
test('gr-diff-host provides syntax highlighting layer', async () => {
stubRestApi('getDiff').returns(Promise.resolve({content: []}));
await element.reload();
- assert.equal(element.$.diff.layers[0], element.syntaxLayer);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
});
test('syntax layer should be disabled', () => {
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 b47c51c..3d43ef3 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
@@ -26,6 +26,9 @@
import {FixIronA11yAnnouncer} from '../../../types/types';
import {appContext} from '../../../services/app-context';
import {fireIronAnnounce} from '../../../utils/event-util';
+import {diffViewMode$} from '../../../services/browser/browser-model';
+import {Subject} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
@customElement('gr-diff-mode-selector')
export class GrDiffModeSelector extends PolymerElement {
@@ -34,7 +37,7 @@
}
@property({type: String, notify: true})
- mode?: DiffViewMode;
+ mode: DiffViewMode = DiffViewMode.SIDE_BY_SIDE;
/**
* If set to true, the user's preference will be updated every time a
@@ -48,11 +51,24 @@
private readonly userService = appContext.userService;
+ disconnected$ = new Subject();
+
+ constructor() {
+ super();
+ }
+
override connectedCallback() {
super.connectedCallback();
(
IronA11yAnnouncer as unknown as FixIronA11yAnnouncer
).requestAvailability();
+ diffViewMode$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffView => (this.mode = diffView));
+ }
+
+ override disconnectedCallback() {
+ this.disconnected$.next();
}
/**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 8b06c75..fe5f389 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -20,6 +20,7 @@
import {GrDiffModeSelector} from './gr-diff-mode-selector';
import {DiffViewMode} from '../../../constants/constants';
import {stubUsers} from '../../../test/test-utils';
+import {_testOnly_setState} from '../../../services/browser/browser-model';
const basicFixture = fixtureFromElement('gr-diff-mode-selector');
@@ -47,8 +48,10 @@
});
test('setMode', () => {
+ _testOnly_setState({screenWidth: 0});
const saveStub = stubUsers('updatePreferences');
+ flush();
// Setting the mode initially does not save prefs.
element.saveOnChange = true;
element.setMode(DiffViewMode.SIDE_BY_SIDE);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
index 5a8c55d..9f38655b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
@@ -87,18 +87,16 @@
});
}
- _handleSaveDiffPreferences() {
+ async _handleSaveDiffPreferences() {
this.diffPrefs = this._editableDiffPrefs;
- this.$.diffPreferences.save().then(() => {
- this.dispatchEvent(
- new CustomEvent('reload-diff-preference', {
- composed: true,
- bubbles: false,
- })
- );
-
- this.$.diffPrefsOverlay.close();
- });
+ await this.$.diffPreferences.save();
+ this.dispatchEvent(
+ new CustomEvent('reload-diff-preference', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ this.$.diffPrefsOverlay.close();
}
}
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 0813900..e83f948 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
@@ -98,7 +98,7 @@
getPatchRangeForCommentUrl,
isInBaseOfPatchRange,
} from '../../../utils/comment-util';
-import {AppElementParams} from '../../gr-app-types';
+import {AppElementParams, AppElementDiffViewParam} from '../../gr-app-types';
import {EventType, OpenFixPreviewEvent} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
@@ -109,8 +109,11 @@
import {changeComments$} from '../../../services/comments/comments-model';
import {takeUntil} from 'rxjs/operators';
import {Subject} from 'rxjs';
-import {preferences$} from '../../../services/user/user-model';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {
+ preferences$,
+ diffPreferences$,
+} from '../../../services/user/user-model';
const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const LOADING_BLAME = 'Loading blame...';
@@ -165,7 +168,7 @@
@property({type: Object, observer: '_paramsChanged'})
params?: AppElementParams;
- @property({type: Object, notify: true, observer: '_changeViewStateChanged'})
+ @property({type: Object, notify: true})
changeViewState: Partial<ChangeViewState> = {};
@property({type: Object})
@@ -222,12 +225,6 @@
@property({type: Object})
_userPrefs?: PreferencesInfo;
- @property({
- type: String,
- computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
- })
- _diffMode?: string;
-
@property({type: Boolean})
_isImageDiff?: boolean;
@@ -349,6 +346,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
+
private readonly commentsService = appContext.commentsService;
private readonly shortcuts = appContext.shortcutsService;
@@ -369,10 +368,6 @@
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
- // TODO(brohlfs): This just ensures that the userService is instantiated at
- // all. We need the service to manage the model, but we are not making any
- // direct calls. Will need to find a better solution to this problem ...
- assertIsDefined(appContext.userService);
changeComments$
.pipe(takeUntil(this.disconnected$))
@@ -383,6 +378,11 @@
preferences$.pipe(takeUntil(this.disconnected$)).subscribe(preferences => {
this._userPrefs = preferences;
});
+ diffPreferences$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffPreferences => {
+ this._prefs = diffPreferences;
+ });
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.cursor.replaceDiffs([this.$.diffHost]);
this._onRenderHandler = (_: Event) => {
@@ -502,12 +502,6 @@
});
}
- _getDiffPreferences() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- this._prefs = prefs;
- });
- }
-
_getPreferences() {
return this.restApiService.getPreferences();
}
@@ -716,10 +710,13 @@
}
_handleToggleDiffMode() {
- if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
+ if (!this._userPrefs) return;
+ if (this._userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
+ this.userService.updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
+ this.userService.updatePreferences({
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ });
}
}
@@ -1017,6 +1014,14 @@
);
}
+ private isSameDiffLoaded(value: AppElementDiffViewParam) {
+ return (
+ this._patchRange?.basePatchNum === value.basePatchNum &&
+ this._patchRange?.patchNum === value.patchNum &&
+ this._path === value.path
+ );
+ }
+
_paramsChanged(value: AppElementParams) {
if (value.view !== GerritView.DIFF) {
return;
@@ -1028,6 +1033,10 @@
if (this._changeNum !== undefined && changeChanged) {
fireEvent(this, EventType.RECREATE_DIFF_VIEW);
return;
+ } else if (this._changeNum !== undefined && this.isSameDiffLoaded(value)) {
+ // changeNum has not changed, so check if there are changes in patchRange
+ // path. If no changes then we can simply render the view as is.
+ return;
}
this._files = {sortedFileList: [], changeFilesByPath: {}};
@@ -1056,8 +1065,6 @@
const promises: Promise<unknown>[] = [];
- promises.push(this._getDiffPreferences());
-
if (!this._change) promises.push(this._getChangeDetail(this._changeNum));
if (!this._changeComments) this._loadComments(value.patchNum);
@@ -1126,17 +1133,6 @@
});
}
- _changeViewStateChanged(changeViewState: Partial<ChangeViewState>) {
- if (changeViewState.diffMode === null) {
- // If screen size is small, always default to unified view.
- this.restApiService.getPreferences().then(prefs => {
- if (prefs) {
- this.set('changeViewState.diffMode', prefs.default_diff_view);
- }
- });
- }
- }
-
@observe('_path', '_prefs', '_reviewedFiles', '_patchRange')
_setReviewedObserver(
path?: string,
@@ -1351,29 +1347,6 @@
this.$.diffPreferencesDialog.open();
}
- /**
- * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
- * the current state.
- *
- * The expected behavior is to use the mode specified in the user's
- * preferences unless they have manually chosen the alternative view or they
- * are on a mobile device. If the user navigates up to the change view, it
- * should clear this choice and revert to the preference the next time a
- * diff is viewed.
- *
- * Use side-by-side if the user is not logged in.
- */
- _getDiffViewMode() {
- if (this.changeViewState.diffMode) {
- return this.changeViewState.diffMode;
- } else if (this._userPrefs) {
- this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
- return this._userPrefs.default_diff_view;
- } else {
- return 'SIDE_BY_SIDE';
- }
- }
-
_computeModeSelectHideClass(diff?: DiffInfo) {
return !diff || diff.binary ? 'hide' : '';
}
@@ -1744,7 +1717,7 @@
}
_handleReloadingDiffPreference() {
- this._getDiffPreferences();
+ this.userService.getDiffPreferences();
}
_computeCanEdit(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 308c353..16adb45 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -339,7 +339,6 @@
<gr-diff-mode-selector
id="modeSelect"
save-on-change="[[_loggedIn]]"
- mode="{{changeViewState.diffMode}}"
show-tooltip-below=""
></gr-diff-mode-selector>
</div>
@@ -409,7 +408,6 @@
path="[[_path]]"
prefs="[[_prefs]]"
project-name="[[_change.project]]"
- view-mode="[[_diffMode]]"
is-blame-loaded="{{_isBlameLoaded}}"
on-comment-anchor-tap="_onLineSelected"
on-line-selected="_onLineSelected"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index e4a8aa4..46ec5d0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -18,8 +18,8 @@
import '../../../test/common-test-setup-karma.js';
import './gr-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {ChangeStatus} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {ChangeStatus, DiffViewMode} from '../../../constants/constants.js';
+import {stubRestApi, stubUsers} from '../../../test/test-utils.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
@@ -30,11 +30,10 @@
import {EditPatchSetNum} from '../../../types/common.js';
import {CursorMoveResult} from '../../../api/core.js';
import {EventType} from '../../../types/events.js';
+import {_testOnly_resetState, _testOnly_setState} from '../../../services/browser/browser-model.js';
const basicFixture = fixtureFromElement('gr-diff-view');
-const blankFixture = fixtureFromElement('div');
-
suite('gr-diff-view tests', () => {
suite('basic tests', () => {
let element;
@@ -70,6 +69,8 @@
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getPortedComments').returns(Promise.resolve({}));
+ _testOnly_resetState();
+
element = basicFixture.instantiate();
element._changeNum = '42';
element._path = 'some/path.txt';
@@ -330,6 +331,7 @@
assert.isFalse(getDiffChangeDetailStub.called);
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
+ sinon.stub(element, '_pathChanged');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
element._change = undefined;
@@ -432,6 +434,7 @@
test('keyboard shortcuts', () => {
element._changeNum = '42';
+ _testOnly_setState({screenWidth: 0});
element._patchRange = {
basePatchNum: PARENT,
patchNum: 10,
@@ -510,11 +513,11 @@
'_computeContainerClass');
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', true));
+ false, DiffViewMode.SIDE_BY_SIDE, true));
MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'Escape');
assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', false));
+ false, DiffViewMode.SIDE_BY_SIDE, false));
// Note that stubbing _setReviewed means that the value of the
// `element.$.reviewed` checkbox is not flipped.
@@ -991,7 +994,9 @@
});
suite('diff prefs hidden', () => {
- test('whenlogged out', () => {
+ test('when no prefs or logged out', () => {
+ element._prefs = undefined;
+ element.disableDiffPrefs = false;
element._loggedIn = false;
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
@@ -1308,47 +1313,23 @@
test('diff mode selector correctly toggles the diff', () => {
const select = element.$.modeSelect;
const diffDisplay = element.$.diffHost;
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
+ element._userPrefs = {diff_view: DiffViewMode.SIDE_BY_SIDE};
+ _testOnly_setState({screenWidth: 0});
+ const userStub = stubUsers('updatePreferences');
+
+ flush();
// The mode selected in the view state reflects the selected option.
- assert.equal(element._getDiffViewMode(), select.mode);
+ // assert.equal(element._userPrefs.diff_view, select.mode);
// The mode selected in the view state reflects the view rednered in the
// diff.
assert.equal(select.mode, diffDisplay.viewMode);
// We will simulate a user change of the selected mode.
- const newMode = 'UNIFIED_DIFF';
-
- // Set the mode, and simulate the change event.
- element.set('changeViewState.diffMode', newMode);
-
- // Make sure the handler was called and the state is still coherent.
- assert.equal(element._getDiffViewMode(), newMode);
- assert.equal(element._getDiffViewMode(), select.mode);
- assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
- });
-
- test('diff mode selector initializes from preferences', () => {
- let resolvePrefs;
- const prefsPromise = new Promise(resolve => {
- resolvePrefs = resolve;
- });
- stubRestApi('getPreferences')
- .callsFake(() => prefsPromise);
-
- // Attach a new gr-diff-view so we can intercept the preferences fetch.
- const view = document.createElement('gr-diff-view');
- blankFixture.instantiate().appendChild(view);
- flush();
-
- // At this point the diff mode doesn't yet have the user's preference.
- assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // Receive the overriding preference.
- resolvePrefs({default_diff_view: 'UNIFIED'});
- flush();
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ element._handleToggleDiffMode();
+ assert.isTrue(userStub.calledWithExactly({
+ diff_view: DiffViewMode.UNIFIED}));
});
test('diff mode selector should be hidden for binary', async () => {
@@ -1509,32 +1490,22 @@
assert.isTrue(getUrlStub.lastCall.args[6]);
});
- test('_getDiffViewMode', () => {
- // No user prefs or change view state set.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // User prefs but no change view state set.
- element.changeViewState.diffMode = undefined;
- element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
-
- // User prefs and change view state set.
- element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
test('_handleToggleDiffMode', () => {
+ const userStub = stubUsers('updatePreferences');
const e = new CustomEvent('keydown', {
detail: {keyboardEvent: new KeyboardEvent('keydown'), key: 'x'},
});
- // Initial state.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ element._userPrefs = {diff_view: DiffViewMode.SIDE_BY_SIDE};
element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+ assert.deepEqual(userStub.lastCall.args[0], {
+ diff_view: DiffViewMode.UNIFIED});
+
+ element._userPrefs = {diff_view: DiffViewMode.UNIFIED};
element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ assert.deepEqual(userStub.lastCall.args[0], {
+ diff_view: DiffViewMode.SIDE_BY_SIDE});
});
suite('_initPatchRange', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 7efd2f8..6003a2f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -950,7 +950,7 @@
// Safari is not binding newly created comment-thread
// with the slot somehow, replace itself will rebind it
// @see Issue 11182
- if (lastEl && lastEl.replaceWith) {
+ if (isSafari() && lastEl && lastEl.replaceWith) {
lastEl.replaceWith(lastEl);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 0d6cadc..501f688 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -14,14 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-a11y-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
import '../../shared/gr-select/gr-select';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-patch-range-select_html';
-import {pluralize} from '../../../utils/string-util';
+import {convertToString, pluralize} from '../../../utils/string-util';
import {appContext} from '../../../services/app-context';
import {
computeLatestPatchNum,
@@ -33,7 +28,6 @@
PatchSet,
convertToPatchSetNum,
} from '../../../utils/patch-set-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {hasOwnProperty} from '../../../utils/common-util';
import {
@@ -44,7 +38,6 @@
Timestamp,
} from '../../../types/common';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {
DropdownItem,
@@ -52,6 +45,11 @@
GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GeneratedWebLink} from '../../core/gr-navigation/gr-navigation';
+import {EditRevisionInfo} from '../../../types/types';
+import {a11yStyles} from '../../../styles/gr-a11y-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -68,10 +66,13 @@
meta_b: GeneratedWebLink[];
}
-export interface GrPatchRangeSelect {
- $: {
- patchNumDropdown: GrDropdownList;
- };
+declare global {
+ interface HTMLElementEventMap {
+ 'value-change': DropDownValueChangeEvent;
+ }
+ interface HTMLElementTagNameMap {
+ 'gr-patch-range-select': GrPatchRangeSelect;
+ }
}
/**
@@ -83,30 +84,13 @@
* @property {string} basePatchNum
*/
@customElement('gr-patch-range-select')
-export class GrPatchRangeSelect extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrPatchRangeSelect extends LitElement {
+ @query('#patchNumDropdown')
+ patchNumDropdown?: GrDropdownList;
@property({type: Array})
availablePatches?: PatchSet[];
- @property({
- type: Object,
- computed:
- '_computeBaseDropdownContent(availablePatches, patchNum,' +
- '_sortedRevisions, changeComments, revisionInfo)',
- })
- _baseDropdownContent?: DropdownItem[];
-
- @property({
- type: Object,
- computed:
- '_computePatchDropdownContent(availablePatches,' +
- 'basePatchNum, _sortedRevisions, changeComments)',
- })
- _patchDropdownContent?: DropdownItem[];
-
@property({type: String})
changeNum?: string;
@@ -129,13 +113,106 @@
revisionInfo?: RevisionInfoClass;
@property({type: Array})
- _sortedRevisions?: RevisionInfo[];
+ @state()
+ protected sortedRevisions?: RevisionInfo[];
private readonly reporting: ReportingService = appContext.reportingService;
- constructor() {
- super();
- this.reporting = appContext.reportingService;
+ static override get styles() {
+ return [
+ a11yStyles,
+ sharedStyles,
+ css`
+ :host {
+ align-items: center;
+ display: flex;
+ }
+ select {
+ max-width: 15em;
+ }
+ .arrow {
+ color: var(--deemphasized-text-color);
+ margin: 0 var(--spacing-m);
+ }
+ gr-dropdown-list {
+ --trigger-style-text-color: var(--deemphasized-text-color);
+ --trigger-style-font-family: var(--font-family);
+ }
+ @media screen and (max-width: 50em) {
+ .filesWeblinks {
+ display: none;
+ }
+ gr-dropdown-list {
+ --native-select-style: {
+ max-width: 5.25em;
+ }
+ }
+ }
+ `,
+ ];
+ }
+
+ private renderWeblinks(fileLink?: GeneratedWebLink[]) {
+ if (!fileLink) return;
+
+ return html`<span class="filesWeblinks">
+ ${fileLink.map(
+ weblink => html`
+ <a target="_blank" rel="noopener" href="${weblink.url}">
+ ${weblink.name}
+ </a>
+ `
+ )}</span
+ > `;
+ }
+
+ override render() {
+ return html`
+ <h3 class="assistive-tech-only">Patchset Range Selection</h3>
+ <span class="patchRange" aria-label="patch range starts with">
+ <gr-dropdown-list
+ id="basePatchDropdown"
+ .value="${convertToString(this.basePatchNum)}"
+ .items="${this._computeBaseDropdownContent(
+ this.availablePatches,
+ this.patchNum,
+ this.sortedRevisions,
+ this.changeComments,
+ this.revisionInfo
+ )}"
+ @value-change=${this._handlePatchChange}
+ >
+ </gr-dropdown-list>
+ </span>
+ ${this.renderWeblinks(this.filesWeblinks?.meta_a)}
+ <span aria-hidden="true" class="arrow">→</span>
+ <span class="patchRange" aria-label="patch range ends with">
+ <gr-dropdown-list
+ id="patchNumDropdown"
+ .value="${convertToString(this.patchNum)}"
+ .items="${this._computePatchDropdownContent(
+ this.availablePatches,
+ this.basePatchNum,
+ this.sortedRevisions,
+ this.changeComments
+ )}"
+ @value-change=${this._handlePatchChange}
+ >
+ </gr-dropdown-list>
+ ${this.renderWeblinks(this.filesWeblinks?.meta_b)}
+ </span>
+ `;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('revisions')) {
+ this._updateSortedRevisions(this.revisions);
+ }
+ }
+
+ _updateSortedRevisions(revisions?: RevisionInfo[]) {
+ if (!revisions) return;
+ this.sortedRevisions = sortRevisions(Object.values(revisions));
}
_getShaForPatch(patch: PatchSet) {
@@ -145,19 +222,19 @@
_computeBaseDropdownContent(
availablePatches?: PatchSet[],
patchNum?: PatchSetNum,
- _sortedRevisions?: RevisionInfo[],
+ sortedRevisions?: (RevisionInfo | EditRevisionInfo)[],
changeComments?: ChangeComments,
revisionInfo?: RevisionInfoClass
- ): DropdownItem[] | undefined {
+ ): DropdownItem[] {
// Polymer 2: check for undefined
if (
availablePatches === undefined ||
patchNum === undefined ||
- _sortedRevisions === undefined ||
+ sortedRevisions === undefined ||
changeComments === undefined ||
revisionInfo === undefined
) {
- return undefined;
+ return [];
}
const parentCounts = revisionInfo.getParentCountMap();
@@ -173,7 +250,7 @@
const entry: DropdownItem = this._createDropdownEntry(
basePatchNum,
'Patchset ',
- _sortedRevisions,
+ sortedRevisions,
changeComments,
this._getShaForPatch(basePatch)
);
@@ -182,7 +259,7 @@
disabled: this._computeLeftDisabled(
basePatch.num,
patchNum,
- _sortedRevisions
+ sortedRevisions
),
});
}
@@ -208,7 +285,7 @@
_computeMobileText(
patchNum: PatchSetNum,
changeComments: ChangeComments,
- revisions: RevisionInfo[]
+ revisions: (RevisionInfo | EditRevisionInfo)[]
) {
return (
`${patchNum}` +
@@ -220,17 +297,17 @@
_computePatchDropdownContent(
availablePatches?: PatchSet[],
basePatchNum?: BasePatchSetNum,
- _sortedRevisions?: RevisionInfo[],
+ sortedRevisions?: (RevisionInfo | EditRevisionInfo)[],
changeComments?: ChangeComments
- ): DropdownItem[] | undefined {
+ ): DropdownItem[] {
// Polymer 2: check for undefined
if (
availablePatches === undefined ||
basePatchNum === undefined ||
- _sortedRevisions === undefined ||
+ sortedRevisions === undefined ||
changeComments === undefined
) {
- return undefined;
+ return [];
}
const dropdownContent: DropdownItem[] = [];
@@ -239,7 +316,7 @@
const entry = this._createDropdownEntry(
patchNum,
patchNum === 'edit' ? '' : 'Patchset ',
- _sortedRevisions,
+ sortedRevisions,
changeComments,
this._getShaForPatch(patch)
);
@@ -248,7 +325,7 @@
disabled: this._computeRightDisabled(
basePatchNum,
patchNum,
- _sortedRevisions
+ sortedRevisions
),
});
}
@@ -271,7 +348,7 @@
_createDropdownEntry(
patchNum: PatchSetNum,
prefix: string,
- sortedRevisions: RevisionInfo[],
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[],
changeComments: ChangeComments,
sha: string
) {
@@ -296,15 +373,6 @@
return entry;
}
- @observe('revisions.*')
- _updateSortedRevisions(
- revisionsRecord: PolymerDeepPropertyChange<RevisionInfo[], RevisionInfo[]>
- ) {
- const revisions = revisionsRecord.base;
- if (!revisions) return;
- this._sortedRevisions = sortRevisions(Object.values(revisions));
- }
-
/**
* The basePatchNum should always be <= patchNum -- because sortedRevisions
* is sorted in reverse order (higher patchset nums first), invalid base
@@ -316,7 +384,7 @@
_computeLeftDisabled(
basePatchNum: PatchSetNum,
patchNum: PatchSetNum,
- sortedRevisions: RevisionInfo[]
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[]
): boolean {
return (
findSortedIndex(basePatchNum, sortedRevisions) <=
@@ -341,7 +409,7 @@
_computeRightDisabled(
basePatchNum: PatchSetNum,
patchNum: PatchSetNum,
- sortedRevisions: RevisionInfo[]
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[]
): boolean {
if (basePatchNum === ParentPatchSetNum) {
return false;
@@ -381,7 +449,10 @@
);
const commentThreadString = pluralize(commentThreadCount, 'comment');
- const unresolvedCount = changeComments.computeUnresolvedNum({patchNum});
+ const unresolvedCount = changeComments.computeUnresolvedNum(
+ {patchNum},
+ true
+ );
const unresolvedString =
unresolvedCount === 0 ? '' : `${unresolvedCount} unresolved`;
@@ -398,7 +469,7 @@
}
_computePatchSetDescription(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum,
addFrontSpace?: boolean
) {
@@ -410,7 +481,7 @@
}
_computePatchSetDate(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum
): Timestamp | undefined {
const rev = getRevisionByPatchNum(revisions, patchNum);
@@ -426,10 +497,10 @@
patchNum: this.patchNum,
basePatchNum: this.basePatchNum,
};
- const target = (dom(e) as EventApi).localTarget;
+ const target = e.target;
const patchSetValue = convertToPatchSetNum(e.detail.value)!;
const latestPatchNum = computeLatestPatchNum(this.availablePatches);
- if (target === this.$.patchNumDropdown) {
+ if (target === this.patchNumDropdown) {
if (detail.patchNum === e.detail.value) return;
this.reporting.reportInteraction('right-patchset-changed', {
previous: detail.patchNum,
@@ -457,9 +528,3 @@
);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-patch-range-select': GrPatchRangeSelect;
- }
-}
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
deleted file mode 100644
index 26944a4..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ /dev/null
@@ -1,82 +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 include="gr-a11y-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- align-items: center;
- display: flex;
- }
- select {
- max-width: 15em;
- }
- .arrow {
- color: var(--deemphasized-text-color);
- margin: 0 var(--spacing-m);
- }
- gr-dropdown-list {
- --trigger-style-text-color: var(--deemphasized-text-color);
- --trigger-style-font-family: var(--font-family);
- }
- @media screen and (max-width: 50em) {
- .filesWeblinks {
- display: none;
- }
- gr-dropdown-list {
- --native-select-style: {
- max-width: 5.25em;
- }
- }
- }
- </style>
- <h3 class="assistive-tech-only">Patchset Range Selection</h3>
- <span class="patchRange" aria-label="patch range starts with">
- <gr-dropdown-list
- id="basePatchDropdown"
- value="[[basePatchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_baseDropdownContent]]"
- >
- </gr-dropdown-list>
- </span>
- <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
- <a target="_blank" rel="noopener" href$="[[weblink.url]]"
- >[[weblink.name]]</a
- >
- </template>
- </span>
- <span aria-hidden="true" class="arrow">→</span>
- <span class="patchRange" aria-label="patch range ends with">
- <gr-dropdown-list
- id="patchNumDropdown"
- value="[[patchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_patchDropdownContent]]"
- >
- </gr-dropdown-list>
- <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
- <a target="_blank" href$="[[weblink.url]]">[[weblink.name]]</a>
- </template>
- </span>
- </span>
-`;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
deleted file mode 100644
index 0fe1fe2..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 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 '../gr-comment-api/gr-comment-api.js';
-import '../../shared/revision-info/revision-info.js';
-import './gr-patch-range-select.js';
-import '../../../test/mocks/comment-api.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-import {EditPatchSetNum} from '../../../types/common.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-
-const commentApiMockElement = createCommentApiMockWithTemplateElement(
- 'gr-patch-range-select-comment-api-mock', html`
- <gr-patch-range-select id="patchRange" auto
- change-comments="[[_changeComments]]"></gr-patch-range-select>
- <gr-comment-api id="commentAPI"></gr-comment-api>
-`);
-
-const basicFixture = fixtureFromElement(commentApiMockElement.is);
-
-suite('gr-patch-range-select tests', () => {
- let element;
-
- let commentApiWrapper;
-
- function getInfo(revisions) {
- const revisionObj = {};
- for (let i = 0; i < revisions.length; i++) {
- revisionObj[i] = revisions[i];
- }
- return new RevisionInfo({revisions: revisionObj});
- }
-
- setup(() => {
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.patchRange;
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- element.changeComments = new ChangeComments();
- });
-
- test('enabled/disabled options', () => {
- const patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 3,
- };
- const sortedRevisions = [
- {_number: 3},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2},
- {_number: 1},
- ];
- for (const patchNum of ['1', '2', '3']) {
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
- patchNum, sortedRevisions));
- }
- for (const basePatchNum of ['1', '2']) {
- assert.isFalse(element._computeLeftDisabled(basePatchNum,
- patchRange.patchNum, sortedRevisions));
- }
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
-
- patchRange.basePatchNum = EditPatchSetNum;
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
- sortedRevisions));
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
- EditPatchSetNum, sortedRevisions));
- });
-
- test('_computeBaseDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const revisions = [
- {
- commit: {parents: []},
- _number: 2,
- description: 'description',
- },
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(revisions);
- const patchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
- const expectedResult = [
- {
- disabled: true,
- triggerText: 'Patchset edit',
- text: 'Patchset edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: true,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: true,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- {
- text: 'Base',
- value: 'PARENT',
- },
- ];
- assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
- patchNum, sortedRevisions, element.changeComments,
- element.revisionInfo),
- expectedResult);
- });
-
- test('_computeBaseDropdownContent called when patchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flush();
-
- sinon.stub(element, '_computeBaseDropdownContent');
-
- // Should be recomputed for each available patch
- element.set('patchNum', 1);
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computeBaseDropdownContent called when changeComments update',
- async () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- await flush();
-
- // Should be recomputed for each available patch
- sinon.stub(element, '_computeBaseDropdownContent');
- assert.equal(element._computeBaseDropdownContent.callCount, 0);
- element.changeComments = new ChangeComments();
- await flush();
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent called when basePatchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flush();
-
- // Should be recomputed for each available patch
- sinon.stub(element, '_computePatchDropdownContent');
- element.set('basePatchNum', 1);
- assert.equal(element._computePatchDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const basePatchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
-
- const expectedResult = [
- {
- disabled: false,
- triggerText: 'edit',
- text: 'edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: false,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: false,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- ];
-
- assert.deepEqual(element._computePatchDropdownContent(availablePatches,
- basePatchNum, sortedRevisions, element.changeComments),
- expectedResult);
- });
-
- test('filesWeblinks', () => {
- element.filesWeblinks = {
- meta_a: [
- {
- name: 'foo',
- url: 'f.oo',
- },
- ],
- meta_b: [
- {
- name: 'bar',
- url: 'ba.r',
- },
- ],
- };
- flush();
- const domApi = dom(element.root);
- assert.equal(
- domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
- assert.equal(
- domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
- });
-
- test('_computePatchSetCommentsString', () => {
- // Test string with unresolved comments.
- const comments = {
- foo: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- bar: [
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-12 20:48:40.000000000',
- },
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-13 20:48:40.000000000',
- },
- ],
- abc: [],
- // Patchset level comment does not contribute to the count
- [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- },
- };
- element.changeComments = new ChangeComments(comments, {}, {}, 123);
-
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (3 comments, 1 unresolved)');
-
- // Test string with no unresolved comments.
- delete element.changeComments._comments['foo'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (2 comments)');
-
- // Test string with no comments.
- delete element.changeComments._comments['bar'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), '');
- });
-
- test('patch-range-change fires', () => {
- const handler = sinon.stub();
- element.basePatchNum = 1;
- element.patchNum = 3;
- element.addEventListener('patch-range-change', handler);
-
- element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
- assert.isTrue(handler.calledOnce);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 2, patchNum: 3});
-
- // BasePatchNum should not have changed, due to one-way data binding.
- element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 1, patchNum: 'edit'});
- });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
new file mode 100644
index 0000000..a47b685
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -0,0 +1,491 @@
+/**
+ * @license
+ * Copyright (C) 2015 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';
+import '../gr-comment-api/gr-comment-api';
+import '../../shared/revision-info/revision-info';
+import './gr-patch-range-select';
+import {GrPatchRangeSelect} from './gr-patch-range-select';
+import '../../../test/mocks/comment-api';
+import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
+import {ChangeComments} from '../gr-comment-api/gr-comment-api';
+import {stubRestApi} from '../../../test/test-utils';
+import {
+ BasePatchSetNum,
+ EditPatchSetNum,
+ PatchSetNum,
+ RevisionInfo,
+ Timestamp,
+ UrlEncodedCommentId,
+ PathToCommentsInfoMap,
+} from '../../../types/common';
+import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
+import {SpecialFilePath} from '../../../constants/constants';
+import {
+ createEditRevision,
+ createRevision,
+} from '../../../test/test-data-generators';
+import {PatchSet} from '../../../utils/patch-set-util';
+import {
+ DropdownItem,
+ GrDropdownList,
+} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {queryAndAssert} from '../../../test/test-utils';
+
+const basicFixture = fixtureFromElement('gr-patch-range-select');
+
+type RevIdToRevisionInfo = {
+ [revisionId: string]: RevisionInfo | EditRevisionInfo;
+};
+
+suite('gr-patch-range-select tests', () => {
+ let element: GrPatchRangeSelect;
+
+ function getInfo(revisions: RevisionInfo[]) {
+ const revisionObj: Partial<RevIdToRevisionInfo> = {};
+ for (let i = 0; i < revisions.length; i++) {
+ revisionObj[i] = revisions[i];
+ }
+ return new RevisionInfoClass({revisions: revisionObj} as ParsedChangeInfo);
+ }
+
+ setup(async () => {
+ stubRestApi('getDiffComments').returns(Promise.resolve({}));
+ stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+ stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ element = basicFixture.instantiate();
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ element.changeComments = new ChangeComments();
+ await element.updateComplete;
+ });
+
+ test('enabled/disabled options', () => {
+ const patchRange = {
+ basePatchNum: 'PARENT' as PatchSetNum,
+ patchNum: 3 as PatchSetNum,
+ };
+ const sortedRevisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2) as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+ for (const patchNum of [1, 2, 3]) {
+ assert.isFalse(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ patchNum as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ }
+ for (const basePatchNum of [1, 2]) {
+ assert.isFalse(
+ element._computeLeftDisabled(
+ basePatchNum as PatchSetNum,
+ patchRange.patchNum,
+ sortedRevisions
+ )
+ );
+ }
+ assert.isTrue(
+ element._computeLeftDisabled(3 as PatchSetNum, patchRange.patchNum, [])
+ );
+
+ patchRange.basePatchNum = EditPatchSetNum;
+ assert.isTrue(
+ element._computeLeftDisabled(
+ 3 as PatchSetNum,
+ patchRange.patchNum,
+ sortedRevisions
+ )
+ );
+ assert.isTrue(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ 1 as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ assert.isTrue(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ 2 as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ assert.isFalse(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ 3 as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ assert.isTrue(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ EditPatchSetNum,
+ sortedRevisions
+ )
+ );
+ });
+
+ test('_computeBaseDropdownContent', () => {
+ const availablePatches = [
+ {num: 'edit', sha: '1'} as PatchSet,
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ const revisions: RevisionInfo[] = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(revisions);
+ const sortedRevisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2) as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+ const expectedResult: DropdownItem[] = [
+ {
+ disabled: true,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2',
+ bottomText: '',
+ value: 2,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ text: 'Base',
+ value: 'PARENT',
+ } as DropdownItem,
+ ];
+ assert.deepEqual(
+ element._computeBaseDropdownContent(
+ availablePatches,
+ 1 as PatchSetNum,
+ sortedRevisions,
+ element.changeComments,
+ element.revisionInfo
+ ),
+ expectedResult
+ );
+ });
+
+ test('_computeBaseDropdownContent called when patchNum updates', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'} as PatchSet,
+ {num: 2, sha: '2'} as PatchSet,
+ {num: 3, sha: '3'} as PatchSet,
+ {num: 'edit', sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ const baseDropDownStub = sinon.stub(element, '_computeBaseDropdownContent');
+
+ // Should be recomputed for each available patch
+ element.patchNum = 1 as PatchSetNum;
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('_computeBaseDropdownContent called when changeComments update', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ // Should be recomputed for each available patch
+ const baseDropDownStub = sinon.stub(element, '_computeBaseDropdownContent');
+ assert.equal(baseDropDownStub.callCount, 0);
+ element.changeComments = new ChangeComments();
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('_computePatchDropdownContent called when basePatchNum updates', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'} as PatchSet,
+ {num: 2, sha: '2'} as PatchSet,
+ {num: 3, sha: '3'} as PatchSet,
+ {num: 'edit', sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ // Should be recomputed for each available patch
+ const baseDropDownStub = sinon.stub(
+ element,
+ '_computePatchDropdownContent'
+ );
+ element.basePatchNum = 1 as BasePatchSetNum;
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('_computePatchDropdownContent', () => {
+ const availablePatches: PatchSet[] = [
+ {num: 'edit', sha: '1'} as PatchSet,
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ const basePatchNum = 1;
+ const sortedRevisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2, 'description') as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+
+ const expectedResult: DropdownItem[] = [
+ {
+ disabled: false,
+ triggerText: 'edit',
+ text: 'edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: false,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ ];
+
+ assert.deepEqual(
+ element._computePatchDropdownContent(
+ availablePatches,
+ basePatchNum as BasePatchSetNum,
+ sortedRevisions,
+ element.changeComments
+ ),
+ expectedResult
+ );
+ });
+
+ test('filesWeblinks', async () => {
+ element.filesWeblinks = {
+ meta_a: [
+ {
+ name: 'foo',
+ url: 'f.oo',
+ },
+ ],
+ meta_b: [
+ {
+ name: 'bar',
+ url: 'ba.r',
+ },
+ ],
+ };
+ await element.updateComplete;
+ assert.equal(
+ queryAndAssert(element, 'a[href="f.oo"]').textContent!.trim(),
+ 'foo'
+ );
+ assert.equal(
+ queryAndAssert(element, 'a[href="ba.r"]').textContent!.trim(),
+ 'bar'
+ );
+ });
+
+ test('_computePatchSetCommentsString', () => {
+ // Test string with unresolved comments.
+ const comments: PathToCommentsInfoMap = {
+ foo: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ bar: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ updated: '2017-10-12 20:48:40.000000000' as Timestamp,
+ },
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ updated: '2017-10-13 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ abc: [],
+ // Patchset level comment does not contribute to the count
+ [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ };
+ element.changeComments = new ChangeComments(comments);
+
+ assert.equal(
+ element._computePatchSetCommentsString(
+ element.changeComments,
+ 1 as PatchSetNum
+ ),
+ ' (3 comments, 1 unresolved)'
+ );
+
+ // Test string with no unresolved comments.
+ delete comments['foo'];
+ element.changeComments = new ChangeComments(comments);
+ assert.equal(
+ element._computePatchSetCommentsString(
+ element.changeComments,
+ 1 as PatchSetNum
+ ),
+ ' (2 comments)'
+ );
+
+ // Test string with no comments.
+ delete comments['bar'];
+ element.changeComments = new ChangeComments(comments);
+ assert.equal(
+ element._computePatchSetCommentsString(
+ element.changeComments,
+ 1 as PatchSetNum
+ ),
+ ''
+ );
+ });
+
+ test('patch-range-change fires', () => {
+ const handler = sinon.stub();
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 3 as PatchSetNum;
+ element.addEventListener('patch-range-change', handler);
+
+ queryAndAssert<GrDropdownList>(
+ element,
+ '#basePatchDropdown'
+ )._handleValueChange('2', [{text: '', value: '2'}]);
+ assert.isTrue(handler.calledOnce);
+ assert.deepEqual(handler.lastCall.args[0].detail, {
+ basePatchNum: 2,
+ patchNum: 3,
+ });
+
+ // BasePatchNum should not have changed, due to one-way data binding.
+ queryAndAssert<GrDropdownList>(
+ element,
+ '#patchNumDropdown'
+ )._handleValueChange('edit', [{text: '', value: 'edit'}]);
+ assert.deepEqual(handler.lastCall.args[0].detail, {
+ basePatchNum: 1,
+ patchNum: 'edit',
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
index dcf7236..8ce8ce2 100644
--- a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
@@ -55,7 +55,7 @@
override render() {
const icon = this.icon ?? '';
return html` <div class="row">
- <iron-icon class="icon" .icon=${icon}></iron-icon>
+ <iron-icon class="icon" .icon=${icon} aria-hidden="true"></iron-icon>
<slot></slot>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index 3adb0f3..173a27e 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -14,28 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-list-view/gr-list-view';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-documentation-search_html';
import {getBaseUrl} from '../../../utils/url-util';
-import {customElement, property} from '@polymer/decorators';
import {DocResult} from '../../../types/common';
import {fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {ListViewParams} from '../../gr-app-types';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {LitElement, PropertyValues, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
@customElement('gr-documentation-search')
-export class GrDocumentationSearch extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrDocumentationSearch extends LitElement {
/**
* URL params passed from the router.
*/
- @property({type: Object, observer: '_paramsChanged'})
+ @property({type: Object})
params?: ListViewParams;
@property({type: Array})
@@ -54,7 +49,57 @@
fireTitleChange(this, 'Documentation Search');
}
- _paramsChanged(params: ListViewParams) {
+ static override get styles() {
+ return [sharedStyles, tableStyles];
+ }
+
+ override render() {
+ return html` <gr-list-view
+ .filter="${this._filter}"
+ .offset="${0}"
+ .loading="${this._loading}"
+ .path="/Documentation"
+ >
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="name topHeader">Name</th>
+ <th class="name topHeader"></th>
+ <th class="name topHeader"></th>
+ </tr>
+ <tr
+ id="loading"
+ class="loadingMsg ${this.computeLoadingClass(this._loading)}"
+ >
+ <td>Loading...</td>
+ </tr>
+ </tbody>
+ <tbody class="${this.computeLoadingClass(this._loading)}">
+ ${this._documentationSearches?.map(
+ search => html`
+ <tr class="table">
+ <td class="name">
+ <a href="${this._computeSearchUrl(search.url)}"
+ >${search.title}</a
+ >
+ </td>
+ <td></td>
+ <td></td>
+ </tr>
+ `
+ )}
+ </tbody>
+ </table>
+ </gr-list-view>`;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this._paramsChanged(this.params);
+ }
+ }
+
+ _paramsChanged(params?: ListViewParams) {
this._loading = true;
this._filter = params?.filter ?? '';
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
deleted file mode 100644
index 95ce1ec..0000000
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
+++ /dev/null
@@ -1,56 +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 include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- filter="[[_filter]]"
- offset="0"
- loading="[[_loading]]"
- path="/Documentation"
- >
- <table id="list" class="genericList">
- <tbody>
- <tr class="headerRow">
- <th class="name topHeader">Name</th>
- <th class="name topHeader"></th>
- <th class="name topHeader"></th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_documentationSearches]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a>
- </td>
- <td></td>
- <td></td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
-`;
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
index bf6a0d5..47c83da 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
@@ -20,7 +20,7 @@
import {GrDocumentationSearch} from './gr-documentation-search';
import {page} from '../../../utils/page-wrapper-utils';
import 'lodash/lodash';
-import {stubRestApi} from '../../../test/test-utils';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
import {DocResult} from '../../../types/common';
import {ListViewParams} from '../../gr-app-types';
@@ -40,10 +40,11 @@
let value: ListViewParams;
- setup(() => {
+ setup(async () => {
sinon.stub(page, 'show');
element = basicFixture.instantiate();
counter = 0;
+ await flush();
});
suite('list with searches for documentation', () => {
@@ -87,13 +88,19 @@
test('correct contents are displayed', async () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#loading')).display,
+ 'block'
+ );
element._loading = false;
await flush();
assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#loading')).display,
+ 'none'
+ );
});
});
});
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 24ebd67..a295588 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
@@ -259,7 +259,14 @@
_viewEditInChangeView() {
if (this._change)
- GerritNav.navigateToChange(this._change, undefined, undefined, true);
+ GerritNav.navigateToChange(
+ this._change,
+ undefined,
+ undefined,
+ true,
+ undefined,
+ true
+ );
}
_getFileData(
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index d88eeaf..7f7749a 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -212,6 +212,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly browserService = appContext.browserService;
+
override keyboardShortcuts(): ShortcutListener[] {
return [
listen(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, _ =>
@@ -252,6 +254,8 @@
this.handleRecreateView(GerritView.DIFF)
);
document.addEventListener(EventType.GR_RPC_LOG, e => this._handleRpcLog(e));
+ const resizeObserver = this.browserService.observeWidth();
+ resizeObserver.observe(this);
}
override ready() {
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 6c8bdb9..39070bd 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -124,8 +124,9 @@
edit?: boolean;
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
- queryMap?: Map<string, string> | URLSearchParams;
commentId?: UrlEncodedCommentId;
+ forceReload?: boolean;
+ tab?: string;
}
export interface AppElementJustRegisteredParams {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 256e956..25e9de8 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -20,11 +20,8 @@
import '../../../styles/gr-form-styles';
import '../../../styles/gr-menu-page-styles';
import '../../../styles/gr-page-nav-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
-import {
- applyTheme as applyDarkTheme,
- removeTheme as removeDarkTheme,
-} from '../../../styles/themes/dark-theme';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../gr-change-table-editor/gr-change-table-editor';
import '../../shared/gr-button/gr-button';
@@ -74,6 +71,7 @@
TimeFormat,
} from '../../../constants/constants';
import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
+import {windowLocationReload} from '../../../utils/dom-util';
const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
'changes_per_page',
@@ -243,7 +241,6 @@
this.$.groupList.loadData(),
this.$.identities.loadData(),
this.$.editPrefs.loadData(),
- this.$.diffPrefs.loadData(),
];
// TODO(dhruvsri): move this to the service
@@ -537,13 +534,14 @@
_handleToggleDark() {
if (this._isDark) {
window.localStorage.removeItem('dark-theme');
- removeDarkTheme();
} else {
window.localStorage.setItem('dark-theme', 'true');
- applyDarkTheme();
}
- this._isDark = !!window.localStorage.getItem('dark-theme');
- fireAlert(this, `Theme changed to ${this._isDark ? 'dark' : 'light'}.`);
+ this.reloadPage();
+ }
+
+ reloadPage() {
+ windowLocationReload();
}
_showHttpAuth(config?: ServerInfo) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 78c4a62..c1ebcac 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -20,6 +20,9 @@
<style include="gr-font-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
color: var(--primary-text-color);
@@ -100,6 +103,7 @@
</gr-page-nav>
<div class="main gr-form-styles">
<h1 class="heading-1">User Settings</h1>
+ <h2 id="Theme">Theme</h2>
<section class="darkToggle">
<div class="toggle">
<paper-toggle-button
@@ -108,13 +112,10 @@
on-change="_handleToggleDark"
on-click="_onTapDarkToggle"
></paper-toggle-button>
- <div id="darkThemeToggleLabel">Dark theme (alpha)</div>
+ <div id="darkThemeToggleLabel">
+ Dark theme (the toggle reloads the page)
+ </div>
</div>
- <p>
- Gerrit's dark theme is in early alpha, and almost definitely will not
- play nicely with themes set by specific Gerrit hosts. Filing feedback
- via the link in the app footer is strongly encouraged!
- </p>
</section>
<h2 id="Profile" class$="[[_computeHeaderClass(_accountInfoChanged)]]">
Profile
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 1165f1e..61876fe 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -16,7 +16,6 @@
*/
import '../../../test/common-test-setup-karma';
-import {getComputedStyleValue} from '../../../utils/dom-util';
import './gr-settings-view';
import {GrSettingsView} from './gr-settings-view';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
@@ -130,23 +129,24 @@
await element._testOnly_loadingPromise;
});
- test('theme changing', () => {
+ test('theme changing', async () => {
+ const reloadStub = sinon.stub(element, 'reloadPage');
+
window.localStorage.removeItem('dark-theme');
assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
const themeToggle = queryAndAssert(
element,
'.darkToggle paper-toggle-button'
);
- /* const themeToggle = element.shadowRoot
- .querySelector('.darkToggle paper-toggle-button'); */
MockInteractions.tap(themeToggle);
assert.isTrue(window.localStorage.getItem('dark-theme') === 'true');
- assert.equal(
- getComputedStyleValue('--primary-text-color', document.body),
- '#e8eaed'
- );
+ assert.isTrue(reloadStub.calledOnce);
+
+ element._isDark = true;
+ await flush();
MockInteractions.tap(themeToggle);
assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
+ assert.isTrue(reloadStub.calledTwice);
});
test('calls the title-change event', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
index df5f441..acb8348 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
@@ -62,9 +62,6 @@
@property({type: String})
placeholder = '';
- @property({type: Number})
- suggestFrom = 0;
-
@property({type: Object, notify: true})
querySuggestions: AutocompleteQuery = () => Promise.resolve([]);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
index c6c2b7f..d84ef62 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
@@ -28,7 +28,6 @@
id="input"
borderless="[[borderless]]"
placeholder="[[placeholder]]"
- threshold="[[suggestFrom]]"
query="[[querySuggestions]]"
allow-non-suggested-values="[[allowAnyInput]]"
on-commit="_handleInputCommit"
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 dabf761..0283ca4 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
@@ -261,7 +261,7 @@
${!this.hideStatus && account.status
? html`<iron-icon
class="status"
- icon="gr-icons:calendar"
+ icon="gr-icons:unavailable"
></iron-icon>`
: ''}
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index d97e38e..5449981 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -178,12 +178,6 @@
@property({type: Object})
_querySuggestions: (input: string) => Promise<SuggestionItem[]>;
- /**
- * Set to true to disable suggestions on empty input.
- */
- @property({type: Boolean})
- skipSuggestOnEmpty = false;
-
reporting: ReportingService;
private pendingRemoval: Set<AccountInput> = new Set();
@@ -206,17 +200,10 @@
}
_getSuggestions(input: string) {
- if (this.skipSuggestOnEmpty && !input) {
- return Promise.resolve([]);
- }
const provider = this.suggestionsProvider;
- if (!provider) {
- return Promise.resolve([]);
- }
+ if (!provider) return Promise.resolve([]);
return provider.getSuggestions(input).then(suggestions => {
- if (!suggestions) {
- return [];
- }
+ if (!suggestions) return [];
if (this.filter) {
suggestions = suggestions.filter(this.filter);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index b667aba..23e5a72 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -398,53 +398,6 @@
assert.equal(makeSuggestionItemSpy.getCalls().length, 2);
});
- test('suggestion on empty', async () => {
- element.skipSuggestOnEmpty = false;
- const suggestions: Suggestion[] = [
- {
- email: 'abc@example.com' as EmailAddress,
- text: 'abcd',
- } as AccountInfo,
- {
- email: 'qwe@example.com' as EmailAddress,
- text: 'qwer',
- } as AccountInfo,
- ];
- const getSuggestionsStub = sinon
- .stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(suggestions));
-
- const makeSuggestionItemSpy = sinon.spy(
- suggestionsProvider,
- 'makeSuggestionItem'
- );
-
- const input = element.$.entry.$.input;
-
- input.text = '';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- await flush();
- assert.isTrue(getSuggestionsStub.calledOnce);
- assert.equal(getSuggestionsStub.lastCall.args[0], '');
- assert.equal(makeSuggestionItemSpy.getCalls().length, 2);
- });
-
- test('skip suggestion on empty', async () => {
- element.skipSuggestOnEmpty = true;
- const getSuggestionsStub = sinon
- .stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve([]));
-
- const input = element.$.entry.$.input;
-
- input.text = '';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- await flush();
- assert.isTrue(getSuggestionsStub.notCalled);
- });
-
suite('allowAnyInput', () => {
setup(() => {
element.allowAnyInput = true;
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 8dc23e2..ea5b5bb 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -88,7 +88,9 @@
background-color: var(--background-color);
color: var(--text-color);
display: flex;
- font-family: inherit;
+ font-family: var(--font-family, inherit);
+ /** Without this '.keyboard-focus' buttons will get bolded. */
+ font-weight: var(--font-weight-normal, inherit);
justify-content: center;
margin: var(--margin, 0);
min-width: var(--border, 0);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 6b2e5c4..7ecced0 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -41,7 +41,7 @@
SpecialFilePath,
} from '../../../constants/constants';
import {computeDisplayPath} from '../../../utils/path-list-util';
-import {computed, customElement, observe, property} from '@polymer/decorators';
+import {customElement, observe, property} from '@polymer/decorators';
import {
AccountDetailInfo,
CommentRange,
@@ -55,7 +55,6 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {FILE, LineNumber} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {DiffLayer, RenderPreferences} from '../../../api/diff';
import {
@@ -201,13 +200,14 @@
@property({type: Array})
layers: DiffLayer[] = [];
+ @property({type: Object, computed: 'computeDiff(comments, path)'})
+ _diff?: DiffInfo;
+
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
private readonly reporting = appContext.reportingService;
- private readonly flagsService = appContext.flagsService;
-
private readonly commentsService = appContext.commentsService;
readonly storage = appContext.storageService;
@@ -261,15 +261,19 @@
this._setInitialExpandedState();
}
- @computed('comments', 'path')
- get _diff() {
- if (this.comments === undefined || this.path === undefined) return;
- if (!this.comments[0]?.context_lines?.length) return;
+ computeDiff(comments?: UIComment[], path?: string) {
+ if (comments === undefined || path === undefined) return undefined;
+ if (!comments[0]?.context_lines?.length) return undefined;
const diff = computeDiffFromContext(
- this.comments[0].context_lines,
- this.path,
- this.comments[0].source_content_type
+ comments[0].context_lines,
+ path,
+ comments[0].source_content_type
);
+ // Do we really have to re-compute (and re-render) the diff?
+ if (this._diff && JSON.stringify(this._diff) === JSON.stringify(diff)) {
+ return this._diff;
+ }
+
if (!anyLineTooLong(diff)) {
this.syntaxLayer.init(diff);
waitForEventOnce(this, 'render').then(() => {
@@ -370,10 +374,7 @@
}
_initLayers(disableTokenHighlighting: boolean) {
- if (
- this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !disableTokenHighlighting
- ) {
+ if (!disableTokenHighlighting) {
this.layers.push(new TokenHighlightLayer(this));
}
this.layers.push(this.syntaxLayer);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 154a045..4f6702d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -38,11 +38,11 @@
import {GrOverlay} from '../gr-overlay/gr-overlay';
import {
AccountDetailInfo,
- NumericChangeId,
+ BasePatchSetNum,
ConfigInfo,
+ NumericChangeId,
PatchSetNum,
RepoName,
- BasePatchSetNum,
} from '../../../types/common';
import {GrButton} from '../gr-button/gr-button';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
@@ -60,6 +60,7 @@
import {debounce, DelayedTask} from '../../../utils/async-util';
import {StorageLocation} from '../../../services/storage/gr-storage';
import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
+import {Interaction} from '../../../constants/reporting';
const STORAGE_DEBOUNCE_INTERVAL = 400;
const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -489,6 +490,8 @@
return this._discardDraft();
}
+ const details = this.commentDetailsForReporting();
+ this.reporting.reportInteraction(Interaction.SAVE_COMMENT, details);
this._xhrPromise = this._saveDraft(comment)
.then(response => {
this.disabled = false;
@@ -508,6 +511,8 @@
}
if (!resComment.patch_set) resComment.patch_set = this.patchNum;
this.comment = resComment;
+ const details = this.commentDetailsForReporting();
+ this.reporting.reportInteraction(Interaction.COMMENT_SAVED, details);
this._fireSave();
return obj;
});
@@ -520,6 +525,17 @@
return this._xhrPromise;
}
+ private commentDetailsForReporting() {
+ return {
+ id: this.comment?.id,
+ message_length: this.comment?.message?.length,
+ in_reply_to: this.comment?.in_reply_to,
+ unresolved: this.comment?.unresolved,
+ path_length: this.comment?.path?.length,
+ line: this.comment?.range?.start_line ?? this.comment?.line,
+ };
+ }
+
_eraseDraftCommentFromStorage() {
// Prevents a race condition in which removing the draft comment occurs
// prior to it being saved.
@@ -765,7 +781,7 @@
const timer = this.reporting.getTimer(timingLabel);
this.set('comment.__editing', false);
return this.save().then(() => {
- timer.end();
+ timer.end({id: this.comment?.id});
});
}
@@ -849,7 +865,7 @@
if (!response.ok) {
this.discarding = false;
}
- timer.end();
+ timer.end({id: this.comment?.id});
this._fireDiscard();
return response;
})
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index e560773..6ddaccf 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -24,6 +24,9 @@
import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
import {GrSelect} from '../gr-select/gr-select';
import {appContext} from '../../../services/app-context';
+import {diffPreferences$} from '../../../services/user/user-model';
+import {takeUntil} from 'rxjs/operators';
+import {Subject} from 'rxjs';
export interface GrDiffPreferences {
$: {
@@ -39,7 +42,7 @@
contextSelect: GrSelect;
ignoreWhiteSpace: HTMLInputElement;
};
- save(): Promise<void>;
+ save(): void;
}
@customElement('gr-diff-preferences')
@@ -54,12 +57,22 @@
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
- private readonly restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
- loadData() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ private readonly disconnected$ = new Subject();
+
+ override connectedCallback() {
+ super.connectedCallback();
+ diffPreferences$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ });
+ }
+
+ override disconnectedCallback() {
+ this.disconnected$.next();
+ super.disconnectedCallback();
}
_handleDiffPrefsChanged() {
@@ -125,12 +138,10 @@
this._handleDiffPrefsChanged();
}
- save() {
- if (!this.diffPrefs)
- return Promise.reject(new Error('Missing diff preferences'));
- return this.restApiService.saveDiffPreferences(this.diffPrefs).then(_ => {
- this.hasUnsavedChanges = false;
- });
+ async save() {
+ if (!this.diffPrefs) return;
+ await this.userService.updateDiffPreference(this.diffPrefs);
+ this.hasUnsavedChanges = false;
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
index 6c1404e..41ac3e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
@@ -51,7 +51,6 @@
element = basicFixture.instantiate();
- await element.loadData();
await flush();
});
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 8322682..cac3d59 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
@@ -17,6 +17,7 @@
import '@polymer/paper-tabs/paper-tab';
import '@polymer/paper-tabs/paper-tabs';
import '../gr-shell-command/gr-shell-command';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-download-commands_html';
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
index 5a75c13..f9c08ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
paper-tabs {
height: 3rem;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 6a34fbb..e2ebb14 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -226,7 +226,7 @@
return html`
<div class="status">
<span class="title">
- <iron-icon icon="gr-icons:calendar"></iron-icon>
+ <iron-icon icon="gr-icons:unavailable"></iron-icon>
Status:
</span>
<span class="value">${this.account.status}</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 1a6239f..2533e00 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -162,6 +162,8 @@
<g id="description"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=settings_backup_restore and 0.65 scale and 4 translate https://fonts.google.com/icons?selected=Material+Icons&icon.query=done-->
<g id="overridden"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M12 15 zM2 4v6h6V8H5.09C6.47 5.61 9.04 4 12 4c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8H2c0 5.52 4.48 10 10.01 10C17.53 22 22 17.52 22 12S17.53 2 12.01 2C8.73 2 5.83 3.58 4 6.01V4H2z"/><path xmlns="http://www.w3.org/2000/svg" d="M9.85 14.53 7.12 11.8l-.91.91L9.85 16.35 17.65 8.55l-.91-.91L9.85 14.53z"/></g>
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:event_busy -->
+ <g id="unavailable"><path d="M0 0h24v24H0z" fill="none"/><path d="M9.31 17l2.44-2.44L14.19 17l1.06-1.06-2.44-2.44 2.44-2.44L14.19 10l-2.44 2.44L9.31 10l-1.06 1.06 2.44 2.44-2.44 2.44L9.31 17zM19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index 63576c2..c2b0269 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -149,7 +149,6 @@
createDefaultDiffPrefs,
createDefaultEditPrefs,
createDefaultPreferences,
- DiffViewMode,
HttpMethod,
ReviewerState,
} from '../../../constants/constants';
@@ -159,8 +158,6 @@
import {FlagsService, KnownExperimentId} from '../../../services/flags/flags';
const MAX_PROJECT_RESULTS = 25;
-// This value is somewhat arbitrary and not based on research or calculations.
-const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
const Requests = {
SEND_DIFF_DRAFT: 'sendDiffDraft',
@@ -977,13 +974,6 @@
return res;
}
const prefInfo = res as unknown as PreferencesInfo;
- if (this._isNarrowScreen()) {
- // Note that this can be problematic, because the diff will stay
- // unified even after increasing the window width.
- prefInfo.default_diff_view = DiffViewMode.UNIFIED;
- } else {
- prefInfo.default_diff_view = prefInfo.diff_view;
- }
return prefInfo;
});
}
@@ -1019,10 +1009,6 @@
});
}
- _isNarrowScreen() {
- return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
- }
-
getChanges(
changesPerPage?: number,
query?: string,
@@ -1145,7 +1131,8 @@
_getChangesOptionsHex() {
if (
window.DEFAULT_DETAIL_HEXES &&
- window.DEFAULT_DETAIL_HEXES.dashboardPage
+ window.DEFAULT_DETAIL_HEXES.dashboardPage &&
+ !this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)
) {
return window.DEFAULT_DETAIL_HEXES.dashboardPage;
}
@@ -1153,6 +1140,9 @@
ListChangesOption.LABELS,
ListChangesOption.DETAILED_ACCOUNTS,
];
+ if (this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
+ options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
+ }
return listChangesOptionsToHex(...options);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
index a60a1ef..6a02985 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
@@ -333,26 +333,23 @@
stub.lastCall.args[0].errFn({});
});
- const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
+ const preferenceSetup = function(testJSON, loggedIn) {
sinon.stub(element, 'getLoggedIn')
.callsFake(() => Promise.resolve(loggedIn));
- sinon.stub(element, '_isNarrowScreen').callsFake(() => smallScreen);
sinon.stub(
element._restApiHelper,
'fetchCacheURL')
.callsFake(() => Promise.resolve(testJSON));
};
- test('getPreferences returns correctly on small screens logged in',
+ test('getPreferences returns correctly logged in',
() => {
const testJSON = {diff_view: 'SIDE_BY_SIDE'};
const loggedIn = true;
- const smallScreen = true;
- preferenceSetup(testJSON, loggedIn, smallScreen);
+ preferenceSetup(testJSON, loggedIn);
return element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
});
});
@@ -361,12 +358,10 @@
() => {
const testJSON = {diff_view: 'UNIFIED_DIFF'};
const loggedIn = true;
- const smallScreen = false;
- preferenceSetup(testJSON, loggedIn, smallScreen);
+ preferenceSetup(testJSON, loggedIn);
return element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
assert.equal(obj.diff_view, 'UNIFIED_DIFF');
});
});
@@ -375,12 +370,10 @@
() => {
const testJSON = {diff_view: 'UNIFIED_DIFF'};
const loggedIn = false;
- const smallScreen = false;
- preferenceSetup(testJSON, loggedIn, smallScreen);
+ preferenceSetup(testJSON, loggedIn);
return element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
});
});
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
index 793e5d6..fd4da4f 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -159,7 +159,12 @@
}
private addTargetEventListeners() {
- this._target?.addEventListener('mouseenter', this.debounceShow);
+ // We intentionally listen on 'mousemove' instead of 'mouseenter', because
+ // otherwise the target appearing under the mouse cursor would also
+ // trigger the hovercard, which can annoying for the user, for example
+ // when added reviewer chips appear in the reply dialog via keyboard
+ // interaction.
+ this._target?.addEventListener('mousemove', this.debounceShow);
this._target?.addEventListener('focus', this.debounceShow);
this._target?.addEventListener('mouseleave', this.debounceHide);
this._target?.addEventListener('blur', this.debounceHide);
@@ -167,7 +172,7 @@
}
private removeTargetEventListeners() {
- this._target?.removeEventListener('mouseenter', this.debounceShow);
+ this._target?.removeEventListener('mousemove', this.debounceShow);
this._target?.removeEventListener('focus', this.debounceShow);
this._target?.removeEventListener('mouseleave', this.debounceHide);
this._target?.removeEventListener('blur', this.debounceHide);
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
index bd12789..e6b63e6 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -47,7 +47,7 @@
let element: HovercardMixinTest;
let button: HTMLElement;
- let testPromise: MockPromise;
+ let testPromise: MockPromise<void>;
setup(() => {
testPromise = mockPromise();
@@ -130,12 +130,12 @@
test('card is scheduled to show on enter and hides on leave', async () => {
const button = document.querySelector('button');
const enterPromise = mockPromise();
- button!.addEventListener('mouseenter', () => enterPromise.resolve());
+ button!.addEventListener('mousemove', () => enterPromise.resolve());
const leavePromise = mockPromise();
button!.addEventListener('mouseleave', () => leavePromise.resolve());
assert.isFalse(element._isShowing);
- button!.dispatchEvent(new CustomEvent('mouseenter'));
+ button!.dispatchEvent(new CustomEvent('mousemove'));
await enterPromise;
await flush();
@@ -158,12 +158,12 @@
const button = document.querySelector('button');
const enterPromise = mockPromise();
const clickPromise = mockPromise();
- button!.addEventListener('mouseenter', () => enterPromise.resolve());
+ button!.addEventListener('mousemove', () => enterPromise.resolve());
button!.addEventListener('click', () => clickPromise.resolve());
assert.isFalse(element._isShowing);
- button!.dispatchEvent(new CustomEvent('mouseenter'));
+ button!.dispatchEvent(new CustomEvent('mousemove'));
await enterPromise;
await flush();
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 3a6f7c5..b9c4f49 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -28,6 +28,7 @@
import {UserService} from './user/user-service';
import {CommentsService} from './comments/comments-service';
import {ShortcutsService} from './shortcuts/shortcuts-service';
+import {BrowserService} from './browser/browser-service';
type ServiceName = keyof AppContext;
type ServiceCreator<T> = () => T;
@@ -84,5 +85,6 @@
configService: () => new ConfigService(),
userService: () => new UserService(appContext.restApiService),
shortcutsService: () => new ShortcutsService(appContext.reportingService),
+ browserService: () => new BrowserService(),
});
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index e5828d6..47da722 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -27,6 +27,7 @@
import {UserService} from './user/user-service';
import {CommentsService} from './comments/comments-service';
import {ShortcutsService} from './shortcuts/shortcuts-service';
+import {BrowserService} from './browser/browser-service';
export interface AppContext {
flagsService: FlagsService;
@@ -41,6 +42,7 @@
storageService: StorageService;
configService: ConfigService;
userService: UserService;
+ browserService: BrowserService;
shortcutsService: ShortcutsService;
}
diff --git a/polygerrit-ui/app/services/browser/browser-model.ts b/polygerrit-ui/app/services/browser/browser-model.ts
new file mode 100644
index 0000000..db790f6
--- /dev/null
+++ b/polygerrit-ui/app/services/browser/browser-model.ts
@@ -0,0 +1,74 @@
+/**
+ * @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 {BehaviorSubject, Observable, combineLatest} from 'rxjs';
+import {distinctUntilChanged, map} from 'rxjs/operators';
+import {preferenceDiffViewMode$} from '../user/user-model';
+import {DiffViewMode} from '../../api/diff';
+
+// This value is somewhat arbitrary and not based on research or calculations.
+const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
+
+interface BrowserState {
+ /**
+ * We maintain the screen width in the state so that the app can react to
+ * changes in the width such as automatically changing to unified diff view
+ */
+ screenWidth?: number;
+}
+
+const initialState: BrowserState = {};
+
+// Mutable for testing
+let privateState$ = new BehaviorSubject(initialState);
+
+export function _testOnly_resetState() {
+ privateState$ = new BehaviorSubject(initialState);
+}
+
+export function _testOnly_setState(state: BrowserState) {
+ privateState$.next(state);
+}
+
+export function _testOnly_getState() {
+ return privateState$.getValue();
+}
+
+export const viewState$: Observable<BrowserState> = privateState$;
+
+export function updateStateScreenWidth(screenWidth: number) {
+ privateState$.next({...privateState$.getValue(), screenWidth});
+}
+
+export const isScreenTooSmall$ = viewState$.pipe(
+ map(
+ state =>
+ !!state.screenWidth &&
+ state.screenWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX
+ ),
+ distinctUntilChanged()
+);
+
+export const diffViewMode$: Observable<DiffViewMode> = combineLatest([
+ isScreenTooSmall$,
+ preferenceDiffViewMode$,
+]).pipe(
+ map(([isScreenTooSmall, preferenceDiffViewMode]) => {
+ if (isScreenTooSmall) return DiffViewMode.UNIFIED;
+ else return preferenceDiffViewMode;
+ }, distinctUntilChanged())
+);
diff --git a/polygerrit-ui/app/services/browser/browser-service.ts b/polygerrit-ui/app/services/browser/browser-service.ts
new file mode 100644
index 0000000..d98f8f7
--- /dev/null
+++ b/polygerrit-ui/app/services/browser/browser-service.ts
@@ -0,0 +1,29 @@
+import {updateStateScreenWidth} from './browser-model';
+
+/**
+ * @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 class BrowserService {
+ /* Observer the screen width so that the app can react to changes to it */
+ observeWidth() {
+ return new ResizeObserver(entries => {
+ entries.forEach(entry => {
+ updateStateScreenWidth(entry.contentRect.width);
+ });
+ });
+ }
+}
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 2839874..863f95f 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -25,7 +25,6 @@
*/
export enum KnownExperimentId {
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
- TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
}
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index 19f12c5..a26fa08 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -28,6 +28,7 @@
Key,
Modifier,
Binding,
+ shouldSuppress,
} from '../../utils/dom-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
@@ -154,34 +155,8 @@
shouldSuppress(e: KeyboardEvent) {
if (this.shortcutsDisabled) return true;
+ if (shouldSuppress(e)) return true;
- // Note that when you listen on document, then `e.currentTarget` will be the
- // document and `e.target` will be `<gr-app>` due to shadow dom, but by
- // using the composedPath() you can actually find the true origin of the
- // event.
- const rootTarget = e.composedPath()[0];
- if (!isElementTarget(rootTarget)) return false;
- const tagName = rootTarget.tagName;
- const type = rootTarget.getAttribute('type');
-
- if (
- // Suppress shortcuts on <input> and <textarea>, but not on
- // checkboxes, because we want to enable workflows like 'click
- // mark-reviewed and then press ] to go to the next file'.
- (tagName === 'INPUT' && type !== 'checkbox') ||
- tagName === 'TEXTAREA' ||
- // Suppress shortcuts if the key is 'enter'
- // and target is an anchor or button or paper-tab.
- (e.keyCode === 13 &&
- (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
- ) {
- return true;
- }
- const path: EventTarget[] = e.composedPath() ?? [];
- for (const el of path) {
- if (!isElementTarget(el)) continue;
- if (el.tagName === 'GR-OVERLAY') return true;
- }
// eg: {key: "k:keydown", ..., from: "gr-diff-view"}
let key = `${e.key}:${e.type}`;
if (this.isInSpecificComboKeyMode(ComboKey.G)) key = 'g+' + key;
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index 72ce3e1..000887c 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -17,7 +17,11 @@
import {AccountDetailInfo, PreferencesInfo} from '../../types/common';
import {BehaviorSubject, Observable} from 'rxjs';
import {map, distinctUntilChanged} from 'rxjs/operators';
-import {createDefaultPreferences} from '../../constants/constants';
+import {
+ createDefaultPreferences,
+ createDefaultDiffPrefs,
+} from '../../constants/constants';
+import {DiffPreferencesInfo, DiffViewMode} from '../../api/diff';
interface UserState {
/**
@@ -25,13 +29,28 @@
*/
account?: AccountDetailInfo;
preferences: PreferencesInfo;
+ diffPreferences: DiffPreferencesInfo;
}
const initialState: UserState = {
preferences: createDefaultPreferences(),
+ diffPreferences: createDefaultDiffPrefs(),
};
-const privateState$ = new BehaviorSubject(initialState);
+// Mutable for testing
+let privateState$ = new BehaviorSubject(initialState);
+
+export function _testOnly_resetState() {
+ privateState$ = new BehaviorSubject(initialState);
+}
+
+export function _testOnly_setState(state: UserState) {
+ privateState$.next(state);
+}
+
+export function _testOnly_getState() {
+ return privateState$.getValue();
+}
// Re-exporting as Observable so that you can only subscribe, but not emit.
export const userState$: Observable<UserState> = privateState$;
@@ -46,6 +65,11 @@
privateState$.next({...current, preferences});
}
+export function updateDiffPreferences(diffPreferences: DiffPreferencesInfo) {
+ const current = privateState$.getValue();
+ privateState$.next({...current, diffPreferences});
+}
+
export const account$ = userState$.pipe(
map(userState => userState.account),
distinctUntilChanged()
@@ -56,11 +80,26 @@
distinctUntilChanged()
);
+export const diffPreferences$ = userState$.pipe(
+ map(userState => userState.diffPreferences),
+ distinctUntilChanged()
+);
+
+export const preferenceDiffViewMode$ = preferences$.pipe(
+ map(preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE),
+ distinctUntilChanged()
+);
+
export const myTopMenuItems$ = preferences$.pipe(
map(preferences => preferences?.my ?? []),
distinctUntilChanged()
);
+export const sizeBarInChangeTable$ = preferences$.pipe(
+ map(prefs => !!prefs?.size_bar_in_change_table),
+ distinctUntilChanged()
+);
+
export const disableShortcuts$ = preferences$.pipe(
map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
distinctUntilChanged()
diff --git a/polygerrit-ui/app/services/user/user-service.ts b/polygerrit-ui/app/services/user/user-service.ts
index 125d20c..d08da8b 100644
--- a/polygerrit-ui/app/services/user/user-service.ts
+++ b/polygerrit-ui/app/services/user/user-service.ts
@@ -14,16 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- AccountDetailInfo,
- PreferencesInfo,
- PreferencesInput,
-} from '../../types/common';
+import {AccountDetailInfo, PreferencesInfo} from '../../types/common';
import {from, of} from 'rxjs';
-import {account$, updateAccount, updatePreferences} from './user-model';
+import {
+ account$,
+ updateAccount,
+ updatePreferences,
+ updateDiffPreferences,
+} from './user-model';
import {switchMap} from 'rxjs/operators';
-import {createDefaultPreferences} from '../../constants/constants';
+import {
+ createDefaultPreferences,
+ createDefaultDiffPrefs,
+} from '../../constants/constants';
import {RestApiService} from '../gr-rest-api/gr-rest-api';
+import {DiffPreferencesInfo} from '../../types/diff';
export class UserService {
constructor(readonly restApiService: RestApiService) {
@@ -42,9 +47,19 @@
.subscribe((preferences?: PreferencesInfo) => {
updatePreferences(preferences ?? createDefaultPreferences());
});
+ account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(createDefaultDiffPrefs());
+ return from(this.restApiService.getDiffPreferences());
+ })
+ )
+ .subscribe((diffPrefs?: DiffPreferencesInfo) => {
+ updateDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
+ });
}
- updatePreferences(prefs: PreferencesInput) {
+ updatePreferences(prefs: Partial<PreferencesInfo>) {
this.restApiService
.savePreferences(prefs)
.then((newPrefs: PreferencesInfo | undefined) => {
@@ -52,4 +67,23 @@
updatePreferences(newPrefs);
});
}
+
+ updateDiffPreference(diffPrefs: DiffPreferencesInfo) {
+ return this.restApiService
+ .saveDiffPreferences(diffPrefs)
+ .then((response: Response) => {
+ this.restApiService.getResponseObject(response).then(obj => {
+ const newPrefs = obj as unknown as DiffPreferencesInfo;
+ if (!newPrefs) return;
+ updateDiffPreferences(newPrefs);
+ });
+ });
+ }
+
+ getDiffPreferences() {
+ return this.restApiService.getDiffPreferences().then(prefs => {
+ if (!prefs) return;
+ updateDiffPreferences(prefs);
+ });
+ }
}
diff --git a/polygerrit-ui/app/styles/gr-paper-styles.ts b/polygerrit-ui/app/styles/gr-paper-styles.ts
new file mode 100644
index 0000000..1ef7124
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-paper-styles.ts
@@ -0,0 +1,60 @@
+/**
+ * @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} from 'lit';
+
+export const paperStyles = css`
+ paper-toggle-button {
+ --paper-toggle-button-checked-bar-color: var(--link-color);
+ --paper-toggle-button-checked-button-color: var(--link-color);
+ }
+ paper-tabs {
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ --paper-font-common-base: {
+ 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 */
+ opacity: 1;
+ color: var(--deemphasized-text-color);
+ }
+ }
+ paper-tab:focus {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-paper-styles">
+ <template>
+ <style>
+ ${paperStyles.cssText}
+ </style>
+ </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 98f6eb2..e99cf27 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -189,36 +189,6 @@
.separator.transparent {
border-color: transparent;
}
- paper-toggle-button {
- --paper-toggle-button-checked-bar-color: var(--link-color);
- --paper-toggle-button-checked-button-color: var(--link-color);
- }
- paper-tabs {
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- --paper-font-common-base: {
- 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 */
- opacity: 1;
- 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/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index a24a666..05c6b7d 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -249,10 +249,3 @@
export function applyTheme() {
document.head.appendChild(getStyleEl());
}
-
-export function removeTheme() {
- const darkThemeEls = document.head.querySelectorAll('#dark-theme');
- if (darkThemeEls.length) {
- darkThemeEls.forEach(darkThemeEl => darkThemeEl.remove());
- }
-}
diff --git a/polygerrit-ui/app/styles/themes/dark-theme_test.ts b/polygerrit-ui/app/styles/themes/dark-theme_test.ts
deleted file mode 100644
index 16e609e..0000000
--- a/polygerrit-ui/app/styles/themes/dark-theme_test.ts
+++ /dev/null
@@ -1,28 +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 '../../test/common-test-setup-karma';
-import {applyTheme, removeTheme} from './dark-theme';
-
-suite('dark-theme test', () => {
- test('apply and remove theme', () => {
- applyTheme();
- assert.equal(document.head.querySelectorAll('#dark-theme').length, 1);
- removeTheme();
- assert.isEmpty(document.head.querySelectorAll('#dark-theme'));
- });
-});
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 949c268..bd5504a 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -30,6 +30,7 @@
registerTestCleanup,
addIronOverlayBackdropStyleEl,
removeIronOverlayBackdropStyleEl,
+ removeThemeStyles,
} from './test-utils';
import {safeTypesBridge} from '../utils/safe-types-util';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
@@ -197,6 +198,7 @@
cleanupTestUtils();
checkGlobalSpace();
removeIronOverlayBackdropStyleEl();
+ removeThemeStyles();
cancelAllTasks();
cleanUpStorage();
// Reset state
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 351cc13..dd56ce2 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -230,7 +230,10 @@
};
}
-export function createRevision(patchSetNum = 1): RevisionInfo {
+export function createRevision(
+ patchSetNum = 1,
+ description = ''
+): RevisionInfo {
return {
_number: patchSetNum as PatchSetNum,
commit: createCommit(),
@@ -238,13 +241,14 @@
kind: RevisionKind.REWORK,
ref: 'refs/changes/5/6/1' as GitRef,
uploader: createAccountWithId(),
+ description,
};
}
-export function createEditRevision(): EditRevisionInfo {
+export function createEditRevision(basePatchNum = 1): EditRevisionInfo {
return {
_number: EditPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
+ basePatchNum: basePatchNum as BasePatchSetNum,
commit: createCommit(),
};
}
@@ -270,7 +274,7 @@
[revisionId: string]: RevisionInfo;
} {
const revisions: {[revisionId: string]: RevisionInfo} = {};
- const revisionDate = TEST_CHANGE_CREATED;
+ let revisionDate = TEST_CHANGE_CREATED;
const revisionIdStart = 1; // The same as getCurrentRevision
for (let i = 0; i < count; i++) {
const revisionId = (i + revisionIdStart).toString(16);
@@ -281,6 +285,7 @@
};
revisions[revisionId] = revision;
// advance 1 day
+ revisionDate = new Date(revisionDate);
revisionDate.setDate(revisionDate.getDate() + 1);
}
return revisions;
@@ -294,12 +299,13 @@
export function createChangeMessages(count: number): ChangeMessageInfo[] {
const messageIdStart = 1000;
const messages: ChangeMessageInfo[] = [];
- const messageDate = TEST_CHANGE_CREATED;
+ let messageDate = TEST_CHANGE_CREATED;
for (let i = 0; i < count; i++) {
messages.push({
...createChangeMessageInfo((i + messageIdStart).toString(16)),
date: dateToTimestamp(messageDate),
});
+ messageDate = new Date(messageDate);
messageDate.setDate(messageDate.getDate() + 1);
}
return messages;
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 39c30ad..4a513f8 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -25,20 +25,22 @@
import {ReportingService} from '../services/gr-reporting/gr-reporting';
import {CommentsService} from '../services/comments/comments-service';
import {UserService} from '../services/user/user-service';
+import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
+import {queryAndAssert, query} from '../utils/common-util';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
-export interface MockPromise extends Promise<unknown> {
- resolve: (value?: unknown) => void;
+export interface MockPromise<T> extends Promise<T> {
+ resolve: (value?: T) => void;
}
-export const mockPromise = () => {
- let res: (value?: unknown) => void;
- const promise: MockPromise = new Promise(resolve => {
+export function mockPromise<T = unknown>(): MockPromise<T> {
+ let res: (value?: T) => void;
+ const promise: MockPromise<T> = new Promise<T | undefined>(resolve => {
res = resolve;
- }) as MockPromise;
+ }) as MockPromise<T>;
promise.resolve = res!;
return promise;
-};
+}
export function isHidden(el: Element | undefined | null) {
if (!el) return true;
@@ -116,6 +118,10 @@
return sinon.stub(appContext.userService, method);
}
+export function stubShortcuts<K extends keyof ShortcutsService>(method: K) {
+ return sinon.stub(appContext.shortcutsService, method);
+}
+
export function stubStorage<K extends keyof StorageService>(method: K) {
return sinon.stub(appContext.storageService, method);
}
@@ -155,22 +161,40 @@
el.parentNode?.removeChild(el);
}
+export function removeThemeStyles() {
+ // Do not remove the light theme, because it is only added once statically,
+ // not once per gr-app instantiation.
+ // document.head.querySelector('#light-theme')?.remove();
+ document.head.querySelector('#dark-theme')?.remove();
+}
+
+export async function waitQueryAndAssert<E extends Element = Element>(
+ el: Element | null | undefined,
+ selector: string
+): Promise<E> {
+ await waitUntil(
+ () => !!query<E>(el, selector),
+ `The element '${selector}' did not appear in the DOM within 1000 ms.`
+ );
+ return queryAndAssert<E>(el, selector);
+}
+
export function waitUntil(
predicate: () => boolean,
- maxMillis = 100
+ message = 'The waitUntil() predicate is still false after 1000 ms.'
): Promise<void> {
const start = Date.now();
- let sleep = 1;
+ let sleep = 0;
return new Promise((resolve, reject) => {
const waiter = () => {
if (predicate()) {
return resolve();
}
- if (Date.now() - start >= maxMillis) {
- return reject(new Error('Took to long to waitUntil'));
+ if (Date.now() - start >= 1000) {
+ return reject(new Error(message));
}
setTimeout(waiter, sleep);
- sleep *= 2;
+ sleep = sleep === 0 ? 1 : sleep * 4;
};
waiter();
});
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 1617aa3..3ac7c7b 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1147,8 +1147,6 @@
work_in_progress_by_default?: boolean;
// The email_format doesn't mentioned in doc, but exists in Java class GeneralPreferencesInfo
email_format?: EmailFormat;
- // The following property doesn't exist in RestAPI, it is added by GrRestApiInterface
- default_diff_view?: DiffViewMode;
}
/**
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 223f290..8146eb3 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -98,7 +98,7 @@
export interface DiffPreferencesInfo extends DiffPreferenceInfoApi {
expand_all_comments?: boolean;
- cursor_blink_rate: number;
+ cursor_blink_rate?: number;
manual_review?: boolean;
retain_header?: boolean;
skip_deleted?: boolean;
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 2d4d412..b90b12f 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -189,7 +189,6 @@
showDownloadDialog: boolean;
diffMode: DiffViewMode | null;
numFilesShown: number | null;
- diffViewMode?: boolean;
}
export interface ChangeListViewState {
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index b6dece0..e2fa8fe 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -398,10 +398,16 @@
export function addShortcut(
element: HTMLElement,
shortcut: Binding,
- listener: (e: KeyboardEvent) => void
+ listener: (e: KeyboardEvent) => void,
+ options: {
+ shouldSuppress: boolean;
+ } = {
+ shouldSuppress: false,
+ }
) {
const wrappedListener = (e: KeyboardEvent) => {
if (e.repeat) return;
+ if (options.shouldSuppress && shouldSuppress(e)) return;
if (eventMatchesShortcut(e, shortcut)) {
listener(e);
}
@@ -417,3 +423,43 @@
export function shiftPressed(e: KeyboardEvent) {
return e.shiftKey;
}
+
+/**
+ * When you listen on keyboard events, then within Gerrit's web app you may want
+ * to avoid firing in certain common scenarios such as key strokes from <input>
+ * elements. But this can also be undesirable, for example Ctrl-Enter from
+ * <input> should trigger a save event.
+ *
+ * The shortcuts-service has a stateful method `shouldSuppress()` with
+ * reporting functionality, which delegates to here.
+ */
+export function shouldSuppress(e: KeyboardEvent): boolean {
+ // Note that when you listen on document, then `e.currentTarget` will be the
+ // document and `e.target` will be `<gr-app>` due to shadow dom, but by
+ // using the composedPath() you can actually find the true origin of the
+ // event.
+ const rootTarget = e.composedPath()[0];
+ if (!isElementTarget(rootTarget)) return false;
+ const tagName = rootTarget.tagName;
+ const type = rootTarget.getAttribute('type');
+
+ if (
+ // Suppress shortcuts on <input> and <textarea>, but not on
+ // checkboxes, because we want to enable workflows like 'click
+ // mark-reviewed and then press ] to go to the next file'.
+ (tagName === 'INPUT' && type !== 'checkbox') ||
+ tagName === 'TEXTAREA' ||
+ // Suppress shortcuts if the key is 'enter'
+ // and target is an anchor or button or paper-tab.
+ (e.keyCode === 13 &&
+ (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
+ ) {
+ return true;
+ }
+ const path: EventTarget[] = e.composedPath() ?? [];
+ for (const el of path) {
+ if (!isElementTarget(el)) continue;
+ if (el.tagName === 'GR-OVERLAY') return true;
+ }
+ return false;
+}
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 2993c0e..e139805 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -22,12 +22,35 @@
getEventPath,
Modifier,
querySelectorAll,
+ shouldSuppress,
strToClassName,
} from './dom-util';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {queryAndAssert} from '../test/test-utils';
+import {mockPromise, queryAndAssert} from '../test/test-utils';
+
+/**
+ * You might think that instead of passing in the callback with assertions as a
+ * parameter that you could as well just `await keyEventOn()` and *then* run
+ * your assertions. But at that point the event is not "hot" anymore, so most
+ * likely you want to assert stuff about the event within the callback
+ * parameter.
+ */
+function keyEventOn(
+ el: HTMLElement,
+ callback: (e: KeyboardEvent) => void,
+ keyCode = 75,
+ key = 'k'
+): Promise<KeyboardEvent> {
+ const promise = mockPromise<KeyboardEvent>();
+ el.addEventListener('keydown', (e: KeyboardEvent) => {
+ callback(e);
+ promise.resolve(e);
+ });
+ MockInteractions.keyDownOn(el, keyCode, null, key);
+ return promise;
+}
class TestEle extends PolymerElement {
static get is() {
@@ -266,4 +289,53 @@
assert.isTrue(eventMatchesShortcut(e, sShift));
});
});
+
+ suite('shouldSuppress', () => {
+ test('do not suppress shortcut event from <div>', async () => {
+ await keyEventOn(document.createElement('div'), e => {
+ assert.isFalse(shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from <input>', async () => {
+ await keyEventOn(document.createElement('input'), e => {
+ assert.isTrue(shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from <textarea>', async () => {
+ await keyEventOn(document.createElement('textarea'), e => {
+ assert.isTrue(shouldSuppress(e));
+ });
+ });
+
+ test('do not suppress shortcut event from checkbox <input>', async () => {
+ const inputEl = document.createElement('input');
+ inputEl.setAttribute('type', 'checkbox');
+ await keyEventOn(inputEl, e => {
+ assert.isFalse(shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from children of <gr-overlay>', async () => {
+ const overlay = document.createElement('gr-overlay');
+ const div = document.createElement('div');
+ overlay.appendChild(div);
+ await keyEventOn(div, e => {
+ assert.isTrue(shouldSuppress(e));
+ });
+ });
+
+ test('suppress "enter" shortcut event from <a>', async () => {
+ await keyEventOn(document.createElement('a'), e => {
+ assert.isFalse(shouldSuppress(e));
+ });
+ await keyEventOn(
+ document.createElement('a'),
+ e => assert.isTrue(shouldSuppress(e)),
+ 13,
+ 'enter'
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index c50bbe2..1a48a7b 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import {
+ ChangeInfo,
isQuickLabelInfo,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
@@ -28,6 +29,7 @@
LabelNameToInfoMap,
VotingRangeInfo,
} from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
import {assertNever, unique} from './common-util';
// Name of the standard Code-Review label.
@@ -204,19 +206,27 @@
return;
}
-export function extractAssociatedLabels(
- requirement: SubmitRequirementResultInfo
-): string[] {
+function extractLabelsFrom(expression: string) {
const pattern = new RegExp('label[0-9]*:([\\w-]+)', 'g');
const labels = [];
let match;
- while (
- (match = pattern.exec(
- requirement.submittability_expression_result.expression
- )) !== null
- ) {
+ while ((match = pattern.exec(expression)) !== null) {
labels.push(match[1]);
}
+ return labels;
+}
+
+export function extractAssociatedLabels(
+ requirement: SubmitRequirementResultInfo
+): string[] {
+ let labels = extractLabelsFrom(
+ requirement.submittability_expression_result.expression
+ );
+ if (requirement.override_expression_result) {
+ labels = labels.concat(
+ extractLabelsFrom(requirement.override_expression_result.expression)
+ );
+ }
return labels.filter(unique);
}
@@ -235,8 +245,30 @@
}
}
+/**
+ * Show only applicable.
+ * If there are only legacy requirements, show all legacy requirements.
+ * If there is at least one non-legacy requirement, filter legacy requirements.
+ */
+export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
+ let submit_requirements = (change?.submit_requirements ?? []).filter(
+ req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
+ );
+
+ const hasNonLegacyRequirements = submit_requirements.some(
+ req => req.is_legacy === false
+ );
+ if (hasNonLegacyRequirements) {
+ submit_requirements = submit_requirements.filter(
+ req => req.is_legacy === false
+ );
+ }
+
+ return submit_requirements;
+}
+
// TODO(milutin): This may be temporary for demo purposes
-const PRIORITY_REQUIREMENTS_ORDER: string[] = [
+export const PRIORITY_REQUIREMENTS_ORDER: string[] = [
StandardLabels.CODE_REVIEW,
StandardLabels.CODE_OWNERS,
StandardLabels.PRESUBMIT_VERIFIED,
@@ -255,3 +287,14 @@
);
return priorityRequirementList.concat(nonPriorityRequirements);
}
+
+export function getTriggerVotes(change?: ParsedChangeInfo | ChangeInfo) {
+ const allLabels = Object.keys(change?.labels ?? {});
+ const submitReqs = getRequirements(change);
+ const labelAssociatedWithSubmitReqs = submitReqs
+ .flatMap(req => extractAssociatedLabels(req))
+ .filter(unique);
+ return allLabels.filter(
+ label => !labelAssociatedWithSubmitReqs.includes(label)
+ );
+}
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 9360688..142c607 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -24,6 +24,8 @@
getRepresentativeValue,
getVotingRange,
getVotingRangeOrDefault,
+ getRequirements,
+ getTriggerVotes,
hasNeutralStatus,
labelCompare,
LabelStatus,
@@ -38,9 +40,15 @@
} from '../types/common';
import {
createAccountWithEmail,
+ createChange,
createSubmitRequirementExpressionInfo,
createSubmitRequirementResultInfo,
+ createDetailedLabelInfo,
} from '../test/test-data-generators';
+import {
+ SubmitRequirementResultInfo,
+ SubmitRequirementStatus,
+} from '../api/rest-api';
const VALUES_0 = {
'0': 'neutral',
@@ -256,5 +264,93 @@
const labels = extractAssociatedLabels(submitRequirement);
assert.deepEqual(labels, ['Verified', 'Code-Review']);
});
+ test('overridden label', () => {
+ const submitRequirement = {
+ ...createSubmitRequirementExpressionInfoWith(
+ 'label:Verified=MAX -label:Verified=MIN'
+ ),
+ override_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Build-cop-override',
+ },
+ };
+ const labels = extractAssociatedLabels(submitRequirement);
+ assert.deepEqual(labels, ['Verified', 'Build-cop-override']);
+ });
+ });
+
+ suite('getRequirements()', () => {
+ function createChangeInfoWith(
+ submit_requirements: SubmitRequirementResultInfo[]
+ ) {
+ return {
+ ...createChange(),
+ submit_requirements,
+ };
+ }
+ test('only legacy', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const change = createChangeInfoWith([requirement]);
+ assert.deepEqual(getRequirements(change), [requirement]);
+ });
+ test('legacy and non-legacy - filter legacy', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const requirement2 = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: false,
+ };
+ const change = createChangeInfoWith([requirement, requirement2]);
+ assert.deepEqual(getRequirements(change), [requirement2]);
+ });
+ test('filter not applicable', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const requirement2 = {
+ ...createSubmitRequirementResultInfo(),
+ status: SubmitRequirementStatus.NOT_APPLICABLE,
+ };
+ const change = createChangeInfoWith([requirement, requirement2]);
+ assert.deepEqual(getRequirements(change), [requirement]);
+ });
+ });
+
+ suite('getTriggerVotes()', () => {
+ test('no requirements', () => {
+ const triggerVote = 'Trigger-Vote';
+ const change = {
+ ...createChange(),
+ labels: {
+ [triggerVote]: createDetailedLabelInfo(),
+ },
+ };
+ assert.deepEqual(getTriggerVotes(change), [triggerVote]);
+ });
+ test('no trigger votes, all labels associated with sub requirement', () => {
+ const triggerVote = 'Trigger-Vote';
+ const change = {
+ ...createChange(),
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: `label:${triggerVote}=MAX`,
+ },
+ },
+ ],
+ labels: {
+ [triggerVote]: createDetailedLabelInfo(),
+ },
+ };
+ assert.deepEqual(getTriggerVotes(change), []);
+ });
});
});
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index ce5e5a4..ee4ed8b 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -95,7 +95,7 @@
* @return The correspondent revision obj from {revisions}
*/
export function getRevisionByPatchNum(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum
) {
for (const rev of revisions) {
@@ -309,10 +309,11 @@
*/
export function findSortedIndex(
patchNum: PatchSetNum,
- revisions: RevisionInfo[]
+ revisions: (RevisionInfo | EditRevisionInfo)[]
) {
revisions = revisions || [];
- const findNum = (rev: RevisionInfo) => `${rev._number}` === `${patchNum}`;
+ const findNum = (rev: RevisionInfo | EditRevisionInfo) =>
+ `${rev._number}` === `${patchNum}`;
return revisions.findIndex(findNum);
}
diff --git a/polygerrit-ui/app/utils/string-util.ts b/polygerrit-ui/app/utils/string-util.ts
index 43c0765..0b217ec 100644
--- a/polygerrit-ui/app/utils/string-util.ts
+++ b/polygerrit-ui/app/utils/string-util.ts
@@ -38,3 +38,12 @@
if (n % 10 === 3 && n % 100 !== 13) return `${n}rd`;
return `${n}th`;
}
+
+/**
+ * This converts any inputed value into string.
+ *
+ * This is so typescript checker doesn't fail.
+ */
+export function convertToString(key?: unknown) {
+ return key !== undefined ? String(key) : '';
+}
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 793703e..25561bb 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -14,7 +14,7 @@
"@polymer/test-fixture": "^4.0.2",
"accessibility-developer-tools": "^2.12.0",
"chai": "^4.3.4",
- "karma": "^6.3.4",
+ "karma": "^6.3.6",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 7c7ef45..e9c54e9 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -1094,7 +1094,7 @@
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.4.tgz#de48cf01c79c9f1560bcfd8ae43217ab028657f8"
integrity sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==
-"@types/cookie@^0.4.0":
+"@types/cookie@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
@@ -1109,7 +1109,7 @@
"@types/keygrip" "*"
"@types/node" "*"
-"@types/cors@^2.8.8":
+"@types/cors@^2.8.12":
version "2.8.12"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
@@ -1498,10 +1498,10 @@
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base64-arraybuffer@0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
- integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
+base64-arraybuffer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c"
+ integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==
base64id@2.0.0, base64id@~2.0.0:
version "2.0.0"
@@ -1939,7 +1939,7 @@
dependencies:
ms "^2.1.1"
-debug@^4.1.0, debug@^4.1.1, debug@~4.3.1:
+debug@^4.1.0, debug@^4.1.1, debug@~4.3.1, debug@~4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -2085,25 +2085,28 @@
dependencies:
once "^1.4.0"
-engine.io-parser@~4.0.0:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
- integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
+engine.io-parser@~5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.1.tgz#6695fc0f1e6d76ad4a48300ff80db5f6b3654939"
+ integrity sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA==
dependencies:
- base64-arraybuffer "0.1.4"
+ base64-arraybuffer "~1.0.1"
-engine.io@~4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b"
- integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==
+engine.io@~6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.0.tgz#2b993fcd73e6b3a6abb52b40b803651cd5747cf0"
+ integrity sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg==
dependencies:
+ "@types/cookie" "^0.4.1"
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "2.0.0"
cookie "~0.4.1"
cors "~2.8.5"
debug "~4.3.1"
- engine.io-parser "~4.0.0"
- ws "~7.4.2"
+ engine.io-parser "~5.0.0"
+ ws "~8.2.3"
ent@~2.2.0:
version "2.2.0"
@@ -2833,10 +2836,10 @@
dependencies:
minimist "^1.2.3"
-karma@^6.3.4:
- version "6.3.4"
- resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.4.tgz#359899d3aab3d6b918ea0f57046fd2a6b68565e6"
- integrity sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==
+karma@^6.3.6:
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.6.tgz#6f64cdd558c7d0c9da6fcdece156089582694611"
+ integrity sha512-xsiu3D6AjCv6Uq0YKXJgC6TvXX2WloQ5+XtHXmC1lwiLVG617DDV3W2DdM4BxCMKHlmz6l3qESZHFQGHAKvrew==
dependencies:
body-parser "^1.19.0"
braces "^3.0.2"
@@ -2856,10 +2859,10 @@
qjobs "^1.2.0"
range-parser "^1.2.1"
rimraf "^3.0.2"
- socket.io "^3.1.0"
+ socket.io "^4.2.0"
source-map "^0.6.1"
tmp "^0.2.1"
- ua-parser-js "^0.7.28"
+ ua-parser-js "^0.7.30"
yargs "^16.1.1"
keygrip@~1.1.0:
@@ -3741,12 +3744,12 @@
nise "^5.0.1"
supports-color "^7.1.0"
-socket.io-adapter@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
- integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
+socket.io-adapter@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289"
+ integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==
-socket.io-parser@~4.0.3:
+socket.io-parser@~4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
@@ -3755,20 +3758,17 @@
component-emitter "~1.3.0"
debug "~4.3.1"
-socket.io@^3.1.0:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
- integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
+socket.io@^4.2.0:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.1.tgz#c0aa14f3f916a8ab713e83a5bd20c16600245763"
+ integrity sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA==
dependencies:
- "@types/cookie" "^0.4.0"
- "@types/cors" "^2.8.8"
- "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "~2.0.0"
- debug "~4.3.1"
- engine.io "~4.1.0"
- socket.io-adapter "~2.1.0"
- socket.io-parser "~4.0.3"
+ debug "~4.3.2"
+ engine.io "~6.0.0"
+ socket.io-adapter "~2.3.2"
+ socket.io-parser "~4.0.4"
source-map-support@^0.5.19, source-map-support@~0.5.12:
version "0.5.19"
@@ -4056,10 +4056,10 @@
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
-ua-parser-js@^0.7.28:
- version "0.7.28"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
- integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+ua-parser-js@^0.7.30:
+ version "0.7.30"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.30.tgz#4cf5170e8b55ac553fe8b38df3a82f0669671f0b"
+ integrity sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
@@ -4218,10 +4218,10 @@
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-ws@~7.4.2:
- version "7.4.6"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
- integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+ws@~8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
y18n@^5.0.5:
version "5.0.8"
diff --git a/tools/BUILD b/tools/BUILD
index 47a2a2e..5d8491a 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -244,7 +244,7 @@
"-Xep:InvalidInlineTag:ERROR",
"-Xep:InvalidJavaTimeConstant:ERROR",
"-Xep:InvalidLink:ERROR",
- # "-Xep:InvalidParam:WARN",
+ "-Xep:InvalidParam:ERROR",
"-Xep:InvalidPatternSyntax:ERROR",
"-Xep:InvalidThrows:ERROR",
"-Xep:InvalidThrowsLink:ERROR",
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index b1ebb5c..51be39f 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -4,6 +4,8 @@
GUAVA_BIN_SHA1 = "00d0c3ce2311c9e36e73228da25a6e99b2ab826f"
+GUAVA_TESTLIB_BIN_SHA1 = "798c3827308605cd69697d8f1596a1735d3ef6e2"
+
GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
TESTCONTAINERS_VERSION = "1.15.3"
@@ -147,6 +149,12 @@
sha1 = GUAVA_BIN_SHA1,
)
+ maven_jar(
+ name = "guava-testlib",
+ artifact = "com.google.guava:guava-testlib:" + GUAVA_VERSION,
+ sha1 = GUAVA_TESTLIB_BIN_SHA1,
+ )
+
GUICE_VERS = "5.0.1"
maven_jar(