Merge "Return X-Gerrit-UpdatedRef in the response headers of WRITE requests"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1d787cc..82e15d9 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -914,6 +914,12 @@
+
If direct updates are made to `All-Users`, this cache should be flushed.
+cache `"approvals"`::
++
+Cache entries contain approvals for a given patch set. This includes
+approvals granted on this patch set as well as approvals copied from
+earlier patch sets.
+
cache `"adv_bases"`::
+
Used only for push over smart HTTP when branch level access controls
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 9d3446e..f5346c1 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -268,6 +268,46 @@
If true, any score for the label is copied forward when a new patch
set is uploaded. Defaults to false.
+[[label_copyCondition]]
+=== `label.Label-Name.copyCondition`
+
+If set, Gerrit matches patch set approvals against the provided query
+string. If the query matches, the approval is copied from one patch set
+to the next. The query language is the same as for
+link:user-search.html[other queries].
+
+This logic is triggered whenever a new patch set is uploaded.
+
+Gerrit currently supports the following predicates:
+
+==== change-kind:{REWORK,TRIVIAL_REBASE,MERGE_FIRST_PARENT_UPDATE,NO_CODE_CHANGE,NO_CHANGE}
+
+Matches if the diff between two patch sets was of a certain change kind.
+
+==== is:{MIN,MAX,ANY}
+
+Matches votes that are equal to the minimal or maximal voting range. Or any votes.
+
+==== approverin:`groupUUID`
+
+Matches votes granted by a user who is a member of `groupUUID`.
+
+==== uploaderin:`groupUUID`
+
+Matches votes where the new patch set was uploaded by a member of `groupUUID`.
+
+==== has:unchanged-files
+
+Matches when the new patch-set includes the same files as the old patch-set.
+
+Only 'unchanged-files' is supported for 'has'.
+
+==== Example
+
+----
+copyCondition = is:MIN OR -change-kind:REWORK OR uploaderin:dead...beef
+----
+
[[label_copyMinScore]]
=== `label.Label-Name.copyMinScore`
diff --git a/Documentation/intro-gerrit-walkthrough-github.txt b/Documentation/intro-gerrit-walkthrough-github.txt
index 8f3ff88..173f709 100644
--- a/Documentation/intro-gerrit-walkthrough-github.txt
+++ b/Documentation/intro-gerrit-walkthrough-github.txt
@@ -25,7 +25,7 @@
Here’s how getting code reviewed and submitted with Gerrit is different from
doing the same with GitHub:
-* You need the add a commit-msg hook script when you clone a repo for the first
+* You need to add a commit-msg hook script when you clone a repo for the first
time using a snippet you can find e.g. https://gerrit-review.googlesource.com/admin/repos/gerrit[here,role=external,window=_blank];
* Your review will be on a single commit instead of a branch. You use
`git commit --amend` to modify a code change.
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index 29bb409..e34071f 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -19,8 +19,7 @@
. A Unix-based server, including any Linux flavor, MacOS, or Berkeley Software
Distribution (BSD).
-. Java SE Runtime Environment version 1.8. Gerrit is not compatible with Java
- 9 or newer yet.
+. Java SE Runtime Environment version 11 and up.
== Download Gerrit
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index df83f1a..eb38434 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3964,6 +3964,8 @@
|`copy_any_score`|`false` if not set|
Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the
label.
+|`copy_condition`|optional|
+See link:config-labels.html#label_copyCondition[copyCondition].
|`copy_min_score`|`false` if not set|
Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the
label.
@@ -4034,6 +4036,10 @@
|`copy_any_score`|optional|
Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the
label.
+|`copy_condition`|optional|
+See link:config-labels.html#label_copyCondition[copyCondition].
+|`unset_copy_condition`|optional|
+If true, clears the value stored in `copy_condition`.
|`copy_min_score`|optional|
Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the
label.
diff --git a/WORKSPACE b/WORKSPACE
index 0caf8c3..428a6a4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -991,5 +991,5 @@
maven_jar(
name = "testcontainers-elasticsearch",
artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
- sha1 = "6b778a270b7529fcb9b7a6f62f3ae9d38544ce2f",
+ sha1 = "595e3a50f59cd3c1d281ca6c1bc4037e277a1353",
)
diff --git a/e2e-tests/src/test/resources/hooks/commit-msg b/e2e-tests/src/test/resources/hooks/commit-msg
deleted file mode 100644
index b05a671..0000000
--- a/e2e-tests/src/test/resources/hooks/commit-msg
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/sh
-#
-# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
-#
-# Copyright (C) 2009 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.
-
-# avoid [[ which is not POSIX sh.
-if test "$#" != 1 ; then
- echo "$0 requires an argument."
- exit 1
-fi
-
-if test ! -f "$1" ; then
- echo "file does not exist: $1"
- exit 1
-fi
-
-if test ! -s "$1" ; then
- echo "file is empty: $1"
- exit 1
-fi
-
-# $RANDOM will be undefined if not using bash, so don't use set -u
-random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin)
-dest="$1.tmp.${random}"
-
-# Avoid the --in-place option which only appeared in Git 2.8
-# Avoid the --if-exists option which only appeared in Git 2.15
-cat "$1" \
-| git -c trailer.ifexists=doNothing interpret-trailers --trailer "Change-Id: I${random}" > "${dest}" \
-&& mv "${dest}" "$1"
diff --git a/e2e-tests/src/test/resources/hooks/commit-msg b/e2e-tests/src/test/resources/hooks/commit-msg
new file mode 120000
index 0000000..6066256
--- /dev/null
+++ b/e2e-tests/src/test/resources/hooks/commit-msg
@@ -0,0 +1 @@
+../../../../../resources/com/google/gerrit/server/tools/root/hooks/commit-msg
\ No newline at end of file
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 3e8cf3b..fd78bd8 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.OptionalSubject.optionals;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -103,6 +104,7 @@
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.change.BatchAbandon;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.ChangeResource;
@@ -127,6 +129,8 @@
import com.google.gerrit.server.notedb.AbstractChangeNotes;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeNotesCommit;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
import com.google.gerrit.server.plugins.TestServerPlugin;
import com.google.gerrit.server.project.ProjectCache;
@@ -168,6 +172,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -297,6 +302,8 @@
protected TestRepository<InMemoryRepository> testRepo;
protected String resourcePrefix;
protected Description description;
+ protected GerritServer.Description testMethodDescription;
+
protected boolean testRequiresSsh;
protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
@@ -343,6 +350,13 @@
}
@After
+ public void verifyNoPiiInChangeNotes() throws RestApiException, IOException {
+ if (testMethodDescription.verifyNoPiiInChangeNotes()) {
+ verifyNoAccountDetailsInChangeNotes();
+ }
+ }
+
+ @After
public void closeEventRecorder() {
if (eventRecorder != null) {
eventRecorder.close();
@@ -430,6 +444,7 @@
GerritServer.Description.forTestClass(description, configName);
GerritServer.Description methodDesc =
GerritServer.Description.forTestMethod(description, configName);
+ testMethodDescription = methodDesc;
testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
if (!testRequiresSsh) {
@@ -463,7 +478,7 @@
toClose = Collections.synchronizedList(new ArrayList<>());
admin = accountCreator.admin();
- user = accountCreator.user();
+ user = accountCreator.user1();
// Evict and reindex accounts in case tests modify them.
reindexAccount(admin.id());
@@ -699,6 +714,58 @@
}
}
+ /**
+ * Verify that NoteDB commits do not persist user-sensitive information, by running checks for all
+ * commits in {@link RefNames#changeMetaRef} for all changes, created during the test.
+ *
+ * <p>These tests prevent regression, assuming appropriate test coverage for new features. The
+ * verification is disabled by default and can be enabled using {@link VerifyNoPiiInChangeNotes}
+ * annotation either on test class or method.
+ */
+ protected void verifyNoAccountDetailsInChangeNotes() throws RestApiException, IOException {
+ List<ChangeInfo> allChanges = gApi.changes().query().get();
+
+ List<AccountState> allAccounts = accounts.all();
+ for (ChangeInfo change : allChanges) {
+ try (Repository repo = repoManager.openRepository(Project.nameKey(change.project))) {
+ String metaRefName =
+ RefNames.changeMetaRef(Change.Id.tryParse(change._number.toString()).get());
+ ObjectId metaTip = repo.getRefDatabase().exactRef(metaRefName).getObjectId();
+ ChangeNotesRevWalk revWalk = ChangeNotesCommit.newRevWalk(repo);
+ revWalk.reset();
+ revWalk.markStart(revWalk.parseCommit(metaTip));
+ ChangeNotesCommit commit;
+ while ((commit = revWalk.next()) != null) {
+ String fullMessage = commit.getFullMessage();
+ for (AccountState accountState : allAccounts) {
+ Account account = accountState.account();
+ assertThat(fullMessage).doesNotContain(account.getName());
+ if (account.fullName() != null) {
+ assertThat(fullMessage).doesNotContain(account.fullName());
+ }
+ if (account.displayName() != null) {
+ assertThat(fullMessage).doesNotContain(account.displayName());
+ }
+ if (account.preferredEmail() != null) {
+ assertThat(fullMessage).doesNotContain(account.preferredEmail());
+ }
+ if (accountState.userName().isPresent()) {
+ assertThat(fullMessage).doesNotContain(accountState.userName().get());
+ }
+ List<String> allEmails =
+ accountState.externalIds().stream()
+ .map(ExternalId::email)
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
+ for (String email : allEmails) {
+ assertThat(fullMessage).doesNotContain(email);
+ }
+ }
+ }
+ }
+ }
+ }
+
protected TestRepository<?>.CommitBuilder commitBuilder() throws Exception {
return testRepo.branch("HEAD").commit().insertChangeId();
}
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index 127f92b..aa13339 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -145,8 +145,8 @@
return create("admin2", "admin2@example.com", "Administrator2", null, "Administrators");
}
- public TestAccount user() throws Exception {
- return create("user", "user@example.com", "User", null);
+ public TestAccount user1() throws Exception {
+ return create("user1", "user1@example.com", "User1", null);
}
public TestAccount user2() throws Exception {
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 1ea9ebc..93c1237 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -111,6 +111,8 @@
public abstract static class Description {
public static Description forTestClass(
org.junit.runner.Description testDesc, String configName) {
+ VerifyNoPiiInChangeNotes verifyNoPiiInChangeNotes =
+ get(VerifyNoPiiInChangeNotes.class, testDesc.getTestClass());
return new AutoValue_GerritServer_Description(
testDesc,
configName,
@@ -119,6 +121,7 @@
has(Sandboxed.class, testDesc.getTestClass()),
has(SkipProjectClone.class, testDesc.getTestClass()),
has(UseSsh.class, testDesc.getTestClass()),
+ verifyNoPiiInChangeNotes != null && verifyNoPiiInChangeNotes.value(),
false, // @UseSystemTime is only valid on methods.
get(UseClockStep.class, testDesc.getTestClass()),
get(UseTimezone.class, testDesc.getTestClass()),
@@ -138,6 +141,11 @@
// on class level.
useClockStep = get(UseClockStep.class, testDesc.getTestClass());
}
+ VerifyNoPiiInChangeNotes verifyNoPiiInChangeNotes =
+ testDesc.getAnnotation(VerifyNoPiiInChangeNotes.class);
+ if (verifyNoPiiInChangeNotes == null) {
+ verifyNoPiiInChangeNotes = get(VerifyNoPiiInChangeNotes.class, testDesc.getTestClass());
+ }
return new AutoValue_GerritServer_Description(
testDesc,
@@ -153,6 +161,7 @@
|| has(SkipProjectClone.class, testDesc.getTestClass()),
testDesc.getAnnotation(UseSsh.class) != null
|| has(UseSsh.class, testDesc.getTestClass()),
+ verifyNoPiiInChangeNotes != null && verifyNoPiiInChangeNotes.value(),
testDesc.getAnnotation(UseSystemTime.class) != null,
useClockStep,
testDesc.getAnnotation(UseTimezone.class) != null
@@ -198,6 +207,8 @@
abstract boolean useSshAnnotation();
+ abstract boolean verifyNoPiiInChangeNotes();
+
boolean useSsh() {
return useSshAnnotation() && SshMode.useSsh();
}
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 67e26ec..d46fb78 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -30,7 +30,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.query.change.ChangeData;
@@ -282,6 +282,11 @@
return this;
}
+ public PushOneCommit noParent() throws Exception {
+ commitBuilder.noParents();
+ return this;
+ }
+
public PushOneCommit addSymlink(String path, String target) throws Exception {
RevBlob blobId = testRepo.blob(target);
commitBuilder.edit(
diff --git a/java/com/google/gerrit/acceptance/VerifyNoPiiInChangeNotes.java b/java/com/google/gerrit/acceptance/VerifyNoPiiInChangeNotes.java
new file mode 100644
index 0000000..1bdaa6e
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/VerifyNoPiiInChangeNotes.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for the acceptance tests, inherited from {@link AbstractDaemonTest}, to enable
+ * verification that NoteDB commits do not persist user-sensitive information. See {@link
+ * AbstractDaemonTest#verifyNoAccountDetailsInChangeNotes}.
+ *
+ * <p>Disabled by default, can be enabled per test class/test method.
+ */
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface VerifyNoPiiInChangeNotes {
+ boolean value() default false;
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeKindCreator.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeKindCreator.java
new file mode 100644
index 0000000..cb987da
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeKindCreator.java
@@ -0,0 +1,348 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.inject.Inject;
+import java.util.List;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/** Helper to create changes of a certain {@link ChangeKind}. */
+public class ChangeKindCreator {
+ private GerritApi gApi;
+ private PushOneCommit.Factory pushFactory;
+ private RequestScopeOperations requestScopeOperations;
+ private ProjectOperations projectOperations;
+
+ @Inject
+ private ChangeKindCreator(
+ GerritApi gApi,
+ PushOneCommit.Factory pushFactory,
+ RequestScopeOperations requestScopeOperations,
+ ProjectOperations projectOperations) {
+ this.gApi = gApi;
+ this.pushFactory = pushFactory;
+ this.requestScopeOperations = requestScopeOperations;
+ this.projectOperations = projectOperations;
+ }
+
+ /** Creates a change with the given {@link ChangeKind} and returns the change id. */
+ public String createChange(
+ ChangeKind kind, TestRepository<InMemoryRepository> testRepo, TestAccount user)
+ throws Exception {
+ switch (kind) {
+ case NO_CODE_CHANGE:
+ case REWORK:
+ case TRIVIAL_REBASE:
+ case NO_CHANGE:
+ return createChange(testRepo, user).getChangeId();
+ case MERGE_FIRST_PARENT_UPDATE:
+ return createChangeForMergeCommit(testRepo, user);
+ default:
+ throw new IllegalStateException("unexpected change kind: " + kind);
+ }
+ }
+
+ /** Updates a change with the given {@link ChangeKind}. */
+ public void updateChange(
+ String changeId,
+ ChangeKind changeKind,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user,
+ Project.NameKey project)
+ throws Exception {
+ switch (changeKind) {
+ case NO_CODE_CHANGE:
+ noCodeChange(changeId, testRepo, user, project);
+ return;
+ case REWORK:
+ rework(changeId, testRepo, user, project);
+ return;
+ case TRIVIAL_REBASE:
+ trivialRebase(changeId, testRepo, user, project);
+ return;
+ case MERGE_FIRST_PARENT_UPDATE:
+ updateFirstParent(changeId, testRepo, user);
+ return;
+ case NO_CHANGE:
+ noChange(changeId, testRepo, user, project);
+ return;
+ default:
+ assertWithMessage("unexpected change kind: " + changeKind).fail();
+ }
+ }
+
+ /**
+ * Creates a cherry pick of the provided change with the given {@link ChangeKind} and returns the
+ * change id.
+ */
+ public String cherryPick(
+ String changeId,
+ ChangeKind changeKind,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user,
+ Project.NameKey project)
+ throws Exception {
+ switch (changeKind) {
+ case REWORK:
+ case TRIVIAL_REBASE:
+ break;
+ case NO_CODE_CHANGE:
+ case NO_CHANGE:
+ case MERGE_FIRST_PARENT_UPDATE:
+ default:
+ assertWithMessage("unexpected change kind: " + changeKind).fail();
+ }
+
+ testRepo.reset(projectOperations.project(project).getHead("master"));
+ PushOneCommit.Result r =
+ pushFactory
+ .create(
+ user.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ "other.txt",
+ "new content " + System.nanoTime())
+ .to("refs/for/master");
+ r.assertOkStatus();
+ vote(user, r.getChangeId(), 2, 1);
+ merge(r);
+
+ String subject =
+ ChangeKind.TRIVIAL_REBASE.equals(changeKind)
+ ? PushOneCommit.SUBJECT
+ : "Reworked change " + System.nanoTime();
+ CherryPickInput in = new CherryPickInput();
+ in.destination = "master";
+ in.message = String.format("%s\n\nChange-Id: %s", subject, changeId);
+ ChangeInfo c = gApi.changes().id(changeId).current().cherryPick(in).get();
+ return c.changeId;
+ }
+
+ /** Creates a change that is a merge {@link ChangeKind} and returns the change id. */
+ public String createChangeForMergeCommit(
+ TestRepository<InMemoryRepository> testRepo, TestAccount user) throws Exception {
+ ObjectId initial = testRepo.getRepository().exactRef(HEAD).getLeaf().getObjectId();
+
+ PushOneCommit.Result parent1 = createChange("parent 1", "p1.txt", "content 1", testRepo, user);
+
+ testRepo.reset(initial);
+ PushOneCommit.Result parent2 = createChange("parent 2", "p2.txt", "content 2", testRepo, user);
+
+ testRepo.reset(parent1.getCommit());
+
+ PushOneCommit merge = pushFactory.create(user.newIdent(), testRepo);
+ merge.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
+ PushOneCommit.Result result = merge.to("refs/for/master");
+ result.assertOkStatus();
+ return result.getChangeId();
+ }
+
+ /** Update the first parent of a merge. */
+ public void updateFirstParent(
+ String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user)
+ throws Exception {
+ ChangeInfo c = detailedChange(changeId);
+ List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents;
+ String parent1 = parents.get(0).commit;
+ String parent2 = parents.get(1).commit;
+ RevCommit commitParent2 = testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent2));
+
+ testRepo.reset(parent1);
+ PushOneCommit.Result newParent1 =
+ createChange("new parent 1", "p1-1.txt", "content 1-1", testRepo, user);
+
+ PushOneCommit merge = pushFactory.create(user.newIdent(), testRepo, changeId);
+ merge.setParents(ImmutableList.of(newParent1.getCommit(), commitParent2));
+ PushOneCommit.Result result = merge.to("refs/for/master");
+ result.assertOkStatus();
+
+ assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.MERGE_FIRST_PARENT_UPDATE);
+ }
+
+ /** Update the second parent of a merge. */
+ public void updateSecondParent(
+ String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user)
+ throws Exception {
+ ChangeInfo c = detailedChange(changeId);
+ List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents;
+ String parent1 = parents.get(0).commit;
+ String parent2 = parents.get(1).commit;
+ RevCommit commitParent1 = testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent1));
+
+ testRepo.reset(parent2);
+ PushOneCommit.Result newParent2 =
+ createChange("new parent 2", "p2-2.txt", "content 2-2", testRepo, user);
+
+ PushOneCommit merge = pushFactory.create(user.newIdent(), testRepo, changeId);
+ merge.setParents(ImmutableList.of(commitParent1, newParent2.getCommit()));
+ PushOneCommit.Result result = merge.to("refs/for/master");
+ result.assertOkStatus();
+
+ assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.REWORK);
+ }
+
+ private void noCodeChange(
+ String changeId,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user,
+ Project.NameKey project)
+ throws Exception {
+ TestRepository<?>.CommitBuilder commitBuilder =
+ testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
+ commitBuilder
+ .message("New subject " + System.nanoTime())
+ .author(user.newIdent())
+ .committer(new PersonIdent(user.newIdent(), testRepo.getDate()));
+ commitBuilder.create();
+ GitUtil.pushHead(testRepo, "refs/for/master", false);
+ assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.NO_CODE_CHANGE);
+ }
+
+ private void noChange(
+ String changeId,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user,
+ Project.NameKey project)
+ throws Exception {
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ String commitMessage = change.revisions.get(change.currentRevision).commit.message;
+
+ TestRepository<?>.CommitBuilder commitBuilder =
+ testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
+ commitBuilder
+ .message(commitMessage)
+ .author(user.newIdent())
+ .committer(new PersonIdent(user.newIdent(), testRepo.getDate()));
+ commitBuilder.create();
+ GitUtil.pushHead(testRepo, "refs/for/master", false);
+ assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.NO_CHANGE);
+ }
+
+ private void rework(
+ String changeId,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user,
+ Project.NameKey project)
+ throws Exception {
+ PushOneCommit push =
+ pushFactory.create(
+ user.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ PushOneCommit.FILE_NAME,
+ "new content " + System.nanoTime(),
+ changeId);
+ push.to("refs/for/master").assertOkStatus();
+ assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.REWORK);
+ }
+
+ private void trivialRebase(
+ String changeId,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user,
+ Project.NameKey project)
+ throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ testRepo.reset(projectOperations.project(project).getHead("master"));
+ PushOneCommit push =
+ pushFactory.create(
+ user.newIdent(),
+ testRepo,
+ "Other Change",
+ "a" + System.nanoTime() + ".txt",
+ PushOneCommit.FILE_CONTENT);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ ReviewInput in = new ReviewInput().label(LabelId.CODE_REVIEW, 2).label(LabelId.VERIFIED, 1);
+ revision.review(in);
+ revision.submit();
+
+ gApi.changes().id(changeId).current().rebase();
+ assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.TRIVIAL_REBASE);
+ }
+
+ private ChangeKind getChangeKind(String changeId) throws Exception {
+ ChangeInfo c = gApi.changes().id(changeId).get(ListChangesOption.CURRENT_REVISION);
+ return c.revisions.get(c.currentRevision).kind;
+ }
+
+ private PushOneCommit.Result createChange(
+ TestRepository<InMemoryRepository> testRepo, TestAccount user) throws Exception {
+ PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ result.assertOkStatus();
+ return result;
+ }
+
+ private ChangeInfo detailedChange(String changeId) throws Exception {
+ return gApi.changes()
+ .id(changeId)
+ .get(
+ ListChangesOption.DETAILED_LABELS,
+ ListChangesOption.CURRENT_REVISION,
+ ListChangesOption.CURRENT_COMMIT);
+ }
+
+ private PushOneCommit.Result createChange(
+ String subject,
+ String fileName,
+ String content,
+ TestRepository<InMemoryRepository> testRepo,
+ TestAccount user)
+ throws Exception {
+ PushOneCommit push = pushFactory.create(user.newIdent(), testRepo, subject, fileName, content);
+ return push.to("refs/for/master");
+ }
+
+ private void vote(TestAccount user, String changeId, int codeReviewVote, int verifiedVote)
+ throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ ReviewInput in =
+ new ReviewInput()
+ .label(LabelId.CODE_REVIEW, codeReviewVote)
+ .label(LabelId.VERIFIED, verifiedVote);
+ gApi.changes().id(changeId).current().review(in);
+ }
+
+ private void merge(PushOneCommit.Result r) throws Exception {
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+ }
+}
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index 9649642..d254752 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -24,6 +24,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
@AutoValue
public abstract class LabelType {
@@ -128,6 +129,8 @@
public abstract boolean isCanOverride();
+ public abstract Optional<String> getCopyCondition();
+
@Nullable
public abstract ImmutableList<String> getRefPatterns();
@@ -239,6 +242,8 @@
public abstract Builder setCopyAnyScore(boolean copyAnyScore);
+ public abstract Builder setCopyCondition(@Nullable String copyCondition);
+
public abstract Builder setCopyMinScore(boolean copyMinScore);
public abstract Builder setCopyMaxScore(boolean copyMaxScore);
diff --git a/java/com/google/gerrit/entities/SubmitRequirement.java b/java/com/google/gerrit/entities/SubmitRequirement.java
index 36f7b53..df03fd5 100644
--- a/java/com/google/gerrit/entities/SubmitRequirement.java
+++ b/java/com/google/gerrit/entities/SubmitRequirement.java
@@ -29,23 +29,23 @@
/**
* Expression of the condition that makes the requirement applicable. The expression should be
* evaluated for a specific {@link Change} and if it returns false, the requirement becomes
- * irrelevant for the change (i.e. {@link #blockingExpression()} and {@link #overrideExpression()}
- * become irrelevant).
+ * irrelevant for the change (i.e. {@link #submittabilityExpression()} and {@link
+ * #overrideExpression()} become irrelevant).
*
* <p>An empty {@link Optional} indicates that the requirement is applicable for any change.
*/
public abstract Optional<SubmitRequirementExpression> applicabilityExpression();
/**
- * Expression of the condition that blocks the submission of a change. The expression should be
- * evaluated for a specific {@link Change} and if it returns false, the requirement becomes
+ * Expression of the condition that allows the submission of a change. The expression should be
+ * evaluated for a specific {@link Change} and if it returns true, the requirement becomes
* fulfilled for the change.
*/
- public abstract SubmitRequirementExpression blockingExpression();
+ public abstract SubmitRequirementExpression submittabilityExpression();
/**
* Expression that, if evaluated to true, causes the submit requirement to be fulfilled,
- * regardless of the blocking expression. This expression should be evaluated for a specific
+ * regardless of the submittability expression. This expression should be evaluated for a specific
* {@link Change}.
*
* <p>An empty {@link Optional} indicates that the requirement is not overridable.
@@ -72,7 +72,8 @@
public abstract Builder setApplicabilityExpression(
Optional<SubmitRequirementExpression> applicabilityExpression);
- public abstract Builder setBlockingExpression(SubmitRequirementExpression blockingExpression);
+ public abstract Builder setSubmittabilityExpression(
+ SubmitRequirementExpression submittabilityExpression);
public abstract Builder setOverrideExpression(
Optional<SubmitRequirementExpression> overrideExpression);
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpression.java b/java/com/google/gerrit/entities/SubmitRequirementExpression.java
index 7b31304..c978347 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpression.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpression.java
@@ -19,12 +19,7 @@
import com.google.gerrit.common.Nullable;
import java.util.Optional;
-/**
- * Describe a applicability, blocking or override expression of a {@link SubmitRequirement}.
- *
- * <p>TODO: Store the tree representation of the parsed expression internally and throw an exception
- * upon creation if the expression syntax is invalid.
- */
+/** Describe a applicability, blocking or override expression of a {@link SubmitRequirement}. */
@AutoValue
public abstract class SubmitRequirementExpression {
@@ -45,5 +40,5 @@
}
/** Returns the underlying String representing this {@link SubmitRequirementExpression}. */
- public abstract String expression();
+ public abstract String expressionString();
}
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
new file mode 100644
index 0000000..94c0e91
--- /dev/null
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.Optional;
+
+/** Result of evaluating a submit requirement expression on a given Change. */
+@AutoValue
+public abstract class SubmitRequirementExpressionResult {
+
+ /**
+ * Entity detailing the result of evaluating a Submit requirement expression. Contains an empty
+ * {@link Optional} if {@link #status()} is equal to {@link Status#ERROR}.
+ */
+ public abstract Optional<PredicateResult> predicateResult();
+
+ public abstract Optional<String> errorMessage();
+
+ public Status status() {
+ if (predicateResult().isPresent()) {
+ return predicateResult().get().status() ? Status.PASS : Status.FAIL;
+ }
+ return Status.ERROR;
+ }
+
+ public static SubmitRequirementExpressionResult create(PredicateResult predicateResult) {
+ return new AutoValue_SubmitRequirementExpressionResult(
+ Optional.of(predicateResult), Optional.empty());
+ }
+
+ public static SubmitRequirementExpressionResult error(String errorMessage) {
+ return new AutoValue_SubmitRequirementExpressionResult(
+ Optional.empty(), Optional.of(errorMessage));
+ }
+
+ /**
+ * Returns a list of leaf predicate results whose {@link PredicateResult#status()} is true. If
+ * {@link #status()} is equal to {@link Status#ERROR}, an empty list is returned.
+ */
+ public ImmutableList<PredicateResult> getPassingAtoms() {
+ if (predicateResult().isPresent()) {
+ return predicateResult().get().getAtoms(/* status= */ true);
+ }
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns a list of leaf predicate results whose {@link PredicateResult#status()} is false. If
+ * {@link #status()} is equal to {@link Status#ERROR}, an empty list is returned.
+ */
+ public ImmutableList<PredicateResult> getFailingAtoms() {
+ if (predicateResult().isPresent()) {
+ return predicateResult().get().getAtoms(/* status= */ false);
+ }
+ return ImmutableList.of();
+ }
+
+ public enum Status {
+ /** Submit requirement expression is fulfilled for a given change. */
+ PASS,
+
+ /** Submit requirement expression is failing for a given change. */
+ FAIL,
+
+ /** Submit requirement expression contains invalid syntax and is not parsable. */
+ ERROR
+ }
+
+ /**
+ * Entity detailing the result of evaluating a predicate.
+ *
+ * <p>Example - branch:refs/heads/foo and has:unresolved
+ *
+ * <p>The above predicate is an "And" predicate having two child predicates:
+ *
+ * <ul>
+ * <li>branch:refs/heads/foo
+ * <li>has:unresolved
+ * </ul>
+ *
+ * <p>Each child predicate as well as the parent contains the result of its evaluation.
+ */
+ @AutoValue
+ public abstract static class PredicateResult {
+ abstract ImmutableList<PredicateResult> childPredicateResults();
+
+ abstract String predicateString();
+
+ /** true if the predicate is passing for a given change. */
+ abstract boolean status();
+
+ /**
+ * Returns the list of leaf {@link PredicateResult} whose {@link #status()} is equal to the
+ * {@code status} parameter.
+ */
+ ImmutableList<PredicateResult> getAtoms(boolean status) {
+ ImmutableList.Builder<PredicateResult> atomsList = ImmutableList.builder();
+ getAtomsRecursively(atomsList, status);
+ return atomsList.build();
+ }
+
+ private void getAtomsRecursively(ImmutableList.Builder<PredicateResult> list, boolean status) {
+ if (childPredicateResults().isEmpty() && status() == status) {
+ list.add(this);
+ return;
+ }
+ childPredicateResults().forEach(c -> c.getAtomsRecursively(list, status));
+ }
+
+ public static Builder builder() {
+ return new AutoValue_SubmitRequirementExpressionResult_PredicateResult.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder childPredicateResults(ImmutableList<PredicateResult> value);
+
+ protected abstract ImmutableList.Builder<PredicateResult> childPredicateResultsBuilder();
+
+ public abstract Builder predicateString(String value);
+
+ public abstract Builder status(boolean value);
+
+ public Builder addChildPredicateResult(PredicateResult result) {
+ childPredicateResultsBuilder().add(result);
+ return this;
+ }
+
+ public abstract PredicateResult build();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
new file mode 100644
index 0000000..7b4d609
--- /dev/null
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
+import java.util.Optional;
+
+/** Result of evaluating a {@link SubmitRequirement} on a given Change. */
+@AutoValue
+public abstract class SubmitRequirementResult {
+ /** Result of evaluating a {@link SubmitRequirement#applicabilityExpression()} on a change. */
+ public abstract Optional<SubmitRequirementExpressionResult> applicabilityExpressionResult();
+
+ /**
+ * Result of evaluating a {@link SubmitRequirement#submittabilityExpression()} ()} on a change.
+ */
+ public abstract SubmitRequirementExpressionResult submittabilityExpressionResult();
+
+ /** Result of evaluating a {@link SubmitRequirement#overrideExpression()} ()} on a change. */
+ public abstract Optional<SubmitRequirementExpressionResult> overrideExpressionResult();
+
+ @Memoized
+ public Status status() {
+ if (assertError(submittabilityExpressionResult())
+ || assertError(applicabilityExpressionResult())
+ || assertError(overrideExpressionResult())) {
+ return Status.ERROR;
+ } else if (assertFail(applicabilityExpressionResult())) {
+ return Status.NOT_APPLICABLE;
+ } else if (assertPass(overrideExpressionResult())) {
+ return Status.OVERRIDDEN;
+ } else if (assertPass(submittabilityExpressionResult())) {
+ return Status.SATISFIED;
+ } else {
+ return Status.UNSATISFIED;
+ }
+ }
+
+ public static Builder builder() {
+ return new AutoValue_SubmitRequirementResult.Builder();
+ }
+
+ public enum Status {
+ /** Submit requirement is fulfilled. */
+ SATISFIED,
+
+ /**
+ * Submit requirement is not satisfied. Happens when {@link
+ * SubmitRequirement#submittabilityExpression()} evaluates to false.
+ */
+ UNSATISFIED,
+
+ /**
+ * Submit requirement is overridden. Happens when {@link SubmitRequirement#overrideExpression()}
+ * evaluates to true.
+ */
+ OVERRIDDEN,
+
+ /**
+ * Submit requirement is not applicable for a given change. Happens when {@link
+ * SubmitRequirement#applicabilityExpression()} evaluates to false.
+ */
+ NOT_APPLICABLE,
+
+ /**
+ * Any of the applicability, blocking or override expressions contain invalid syntax and are not
+ * parsable.
+ */
+ ERROR
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder applicabilityExpressionResult(
+ Optional<SubmitRequirementExpressionResult> value);
+
+ public abstract Builder submittabilityExpressionResult(SubmitRequirementExpressionResult value);
+
+ public abstract Builder overrideExpressionResult(
+ Optional<SubmitRequirementExpressionResult> value);
+
+ public abstract SubmitRequirementResult build();
+ }
+
+ private boolean assertPass(Optional<SubmitRequirementExpressionResult> expressionResult) {
+ return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.PASS);
+ }
+
+ private boolean assertPass(SubmitRequirementExpressionResult expressionResult) {
+ return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.PASS);
+ }
+
+ private boolean assertFail(Optional<SubmitRequirementExpressionResult> expressionResult) {
+ return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.FAIL);
+ }
+
+ private boolean assertError(Optional<SubmitRequirementExpressionResult> expressionResult) {
+ return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.ERROR);
+ }
+
+ private boolean assertError(SubmitRequirementExpressionResult expressionResult) {
+ return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.ERROR);
+ }
+
+ private boolean assertStatus(
+ SubmitRequirementExpressionResult expressionResult,
+ SubmitRequirementExpressionResult.Status status) {
+ return expressionResult.status() == status;
+ }
+
+ private boolean assertStatus(
+ Optional<SubmitRequirementExpressionResult> expressionResult,
+ SubmitRequirementExpressionResult.Status status) {
+ return expressionResult.isPresent() && assertStatus(expressionResult.get(), status);
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 647dead..0fff0ba 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -70,12 +70,26 @@
@SuppressWarnings("unchecked") // reflection is used to construct instances of T
private static <T> T getAdded(T oldValue, T newValue) {
+ if (newValue instanceof Collection) {
+ List result = getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
+ return (T) result;
+ }
+
+ if (newValue instanceof Map) {
+ Map result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
+ return (T) result;
+ }
+
T toPopulate = (T) construct(newValue.getClass());
if (toPopulate == null) {
return null;
}
for (Field field : newValue.getClass().getDeclaredFields()) {
+ if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+
Object newFieldObj = get(field, newValue);
if (oldValue == null || newFieldObj == null) {
set(field, toPopulate, newFieldObj);
@@ -89,13 +103,8 @@
if (isSimple(field.getType()) || oldFieldObj == null) {
set(field, toPopulate, newFieldObj);
- } else if (newFieldObj instanceof Collection) {
- set(
- field,
- toPopulate,
- getAddedForCollection((Collection<?>) oldFieldObj, (Collection<?>) newFieldObj));
- } else if (newFieldObj instanceof Map) {
- set(field, toPopulate, getAddedForMap((Map<?, ?>) oldFieldObj, (Map<?, ?>) newFieldObj));
+ } else if (newFieldObj instanceof Collection || newFieldObj instanceof Map) {
+ set(field, toPopulate, getAdded(oldFieldObj, newFieldObj));
} else {
// Recurse to set all fields in the non-primitive object.
set(field, toPopulate, getAdded(oldFieldObj, newFieldObj));
@@ -143,6 +152,9 @@
private static ImmutableList<Object> getAdditions(
Collection<?> oldCollection, Collection<?> newCollection) {
+ if (oldCollection == null)
+ return newCollection != null ? ImmutableList.copyOf(newCollection) : null;
+
Map<Object, List<Object>> duplicatesMap = newCollection.stream().collect(groupingBy(v -> v));
oldCollection.forEach(
v -> {
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
index 9a6d086..6f733d6 100644
--- a/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
+++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
@@ -26,6 +26,7 @@
public List<String> branches;
public Boolean canOverride;
public Boolean copyAnyScore;
+ public String copyCondition;
public Boolean copyMinScore;
public Boolean copyMaxScore;
public Boolean copyAllScoresIfListOfFilesDidNotChange;
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
index 87cae86..38b76c1 100644
--- a/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
+++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
@@ -25,6 +25,8 @@
public List<String> branches;
public Boolean canOverride;
public Boolean copyAnyScore;
+ public String copyCondition;
+ public Boolean unsetCopyCondition;
public Boolean copyMinScore;
public Boolean copyMaxScore;
public Boolean copyAllScoresIfListOfFilesDidNotChange;
diff --git a/java/com/google/gerrit/index/query/IndexPredicate.java b/java/com/google/gerrit/index/query/IndexPredicate.java
index aac6682..7bbe70b 100644
--- a/java/com/google/gerrit/index/query/IndexPredicate.java
+++ b/java/com/google/gerrit/index/query/IndexPredicate.java
@@ -14,11 +14,30 @@
package com.google.gerrit.index.query;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.FieldType;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.StreamSupport;
/** Predicate that is mapped to a field in the index. */
-public abstract class IndexPredicate<I> extends OperatorPredicate<I> {
+public abstract class IndexPredicate<I> extends OperatorPredicate<I> implements Matchable<I> {
+ /**
+ * Text segmentation to be applied to both the query string and the indexed field for full-text
+ * queries. This is inspired by http://unicode.org/reports/tr29/ which is what Lucene uses, but
+ * complexity was reduced to the bare minimum at the cost of small discrepancies to the Unicode
+ * spec.
+ */
+ private static final Splitter FULL_TEXT_SPLITTER = Splitter.on(CharMatcher.anyOf(" ,.-\\/_\n"));
+
private final FieldDef<I, ?> def;
protected IndexPredicate(FieldDef<I, ?> def, String value) {
@@ -38,4 +57,70 @@
public FieldType<?> getType() {
return def.getType();
}
+
+ /**
+ * This method matches documents without calling an index subsystem. For primitive fields (e.g.
+ * integer, long) , the matching logic is consistent across this method and all known index
+ * implementations. For text fields (i.e. prefix and full-text) the semantics vary between this
+ * implementation and known index implementations:
+ * <li>Prefix: Lucene as well as {@link #match(I)} matches terms as true prefixes (prefix:foo ->
+ * `foo bar` matches, but `baz foo bar` does not match). The index implementation at Google
+ * tokenizes both the query and the indexed text and matches tokens individually (prefix:fo ba
+ * -> `baz foo bar` matches).
+ * <li>Full text: Lucene uses a {@code PhraseQuery} to search for terms in full text fields
+ * in-order. The index implementation at Google as well as {@link #match(I)} tokenizes both
+ * the query and the indexed text and matches tokens individually.
+ *
+ * @return true if the predicate matches the provided {@link I}.
+ */
+ @Override
+ public boolean match(I doc) {
+ if (getField().isRepeatable()) {
+ Iterable<Object> values = (Iterable<Object>) getField().get(doc);
+ for (Object v : values) {
+ if (matchesSingleObject(v)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return matchesSingleObject(getField().get(doc));
+ }
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+
+ private boolean matchesSingleObject(Object fieldValueFromObject) {
+ String fieldTypeName = getField().getType().getName();
+ if (fieldTypeName.equals(FieldType.INTEGER.getName())) {
+ return Objects.equals(fieldValueFromObject, Ints.tryParse(value));
+ } else if (fieldTypeName.equals(FieldType.EXACT.getName())) {
+ return Objects.equals(fieldValueFromObject, value);
+ } else if (fieldTypeName.equals(FieldType.LONG.getName())) {
+ return Objects.equals(fieldValueFromObject, Longs.tryParse(value));
+ } else if (fieldTypeName.equals(FieldType.PREFIX.getName())) {
+ return String.valueOf(fieldValueFromObject).startsWith(value);
+ } else if (fieldTypeName.equals(FieldType.FULL_TEXT.getName())) {
+ Set<String> tokenizedField = tokenizeString(String.valueOf(fieldValueFromObject));
+ Set<String> tokenizedValue = tokenizeString(value);
+ return !Sets.intersection(tokenizedField, tokenizedValue).isEmpty();
+ } else if (fieldTypeName.equals(FieldType.STORED_ONLY.getName())) {
+ throw new IllegalStateException("can't filter for storedOnly field " + getField().getName());
+ } else if (fieldTypeName.equals(FieldType.TIMESTAMP.getName())) {
+ throw new IllegalStateException("timestamp queries must be handled in subclasses");
+ } else if (fieldTypeName.equals(FieldType.INTEGER_RANGE.getName())) {
+ throw new IllegalStateException("integer range queries must be handled in subclasses");
+ } else {
+ throw new IllegalStateException("unrecognized field " + fieldTypeName);
+ }
+ }
+
+ private static ImmutableSet<String> tokenizeString(String value) {
+ return StreamSupport.stream(FULL_TEXT_SPLITTER.split(value.toLowerCase()).spliterator(), false)
+ .filter(s -> !s.trim().isEmpty())
+ .collect(toImmutableSet());
+ }
}
diff --git a/java/com/google/gerrit/index/query/IntegerRangePredicate.java b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
index 6780867..850c4a5 100644
--- a/java/com/google/gerrit/index/query/IntegerRangePredicate.java
+++ b/java/com/google/gerrit/index/query/IntegerRangePredicate.java
@@ -31,6 +31,7 @@
protected abstract Integer getValueInt(T object);
+ @Override
public boolean match(T object) {
Integer valueInt = getValueInt(object);
if (valueInt == null) {
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 2cfc49f..30de2f5 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
+import com.google.gerrit.server.approval.ApprovalCacheImpl;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -79,6 +80,7 @@
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.approval.ApprovalModule;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
import com.google.gerrit.server.restapi.group.GroupModule;
@@ -171,6 +173,7 @@
modules.add(new GroupModule());
modules.add(new NoteDbModule());
modules.add(AccountCacheImpl.module());
+ modules.add(ApprovalCacheImpl.module());
modules.add(DefaultPreferencesCacheImpl.module());
modules.add(GroupCacheImpl.module());
modules.add(GroupIncludeCacheImpl.module());
@@ -181,6 +184,7 @@
modules.add(ServiceUserClassifierImpl.module());
modules.add(TagCache.module());
modules.add(PureRevertCache.module());
+ modules.add(new ApprovalModule());
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
factory(ChangeIsVisibleToPredicate.Factory.class);
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 404906d..f9195c0 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -131,6 +131,7 @@
"//lib/ow2:ow2-asm-util",
"//lib/prolog:runtime",
"//proto:cache_java_proto",
+ "//proto:entities_java_proto",
],
)
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 005ae3b..d60bc8f 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -29,6 +29,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -149,8 +150,7 @@
projectCache.get(notes.getProjectName()).orElseThrow(illegalState(notes.getProjectName()));
ApprovalsUtil approvalsUtil = approvalsUtilProvider.get();
- for (PatchSetApproval ap :
- approvalsUtil.byPatchSet(notes, change.currentPatchSetId(), null, null)) {
+ for (PatchSetApproval ap : approvalsUtil.byPatchSet(notes, change.currentPatchSetId())) {
LabelType type = projectState.getLabelTypes(notes).byLabel(ap.label());
if (type != null && ap.value() == 1 && type.getFunction() == LabelFunction.PATCH_SET_LOCK) {
return true;
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 2665b9a..75a2b38 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -531,6 +531,19 @@
return searchImpl(input, searchers, visibilitySupplierCanSee(), accountActivityPredicate);
}
+ /**
+ * As opposed to {@link #resolve}, the returned result includes all inactive accounts for the
+ * input search.
+ *
+ * <p>This can be used to resolve Gerrit Account from email to its {@link Account.Id}, to make
+ * sure that if {@link Account} with such email exists in Gerrit (even inactive), user data (email
+ * address) won't be recorded as it is, but instead will be stored as a link to the corresponding
+ * Gerrit Account.
+ */
+ public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException {
+ return searchImpl(input, searchers, visibilitySupplierCanSee(), all());
+ }
+
public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
return searchImpl(input, searchers, visibilitySupplierAll(), accountActivityPredicate());
}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index ab96c6b..764c46d 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -57,9 +57,9 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.FileResource;
import com.google.gerrit.server.change.RebaseUtil;
import com.google.gerrit.server.change.RevisionResource;
@@ -642,7 +642,7 @@
ListMultimapBuilder.treeKeys().arrayListValues().build();
try {
Iterable<PatchSetApproval> approvals =
- approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().id(), null, null);
+ approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().id());
AccountLoader accountLoader =
accountLoaderFactory.create(
EnumSet.of(
diff --git a/java/com/google/gerrit/server/approval/ApprovalCache.java b/java/com/google/gerrit/server/approval/ApprovalCache.java
new file mode 100644
index 0000000..5637249
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/ApprovalCache.java
@@ -0,0 +1,28 @@
+// 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.approval;
+
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.server.notedb.ChangeNotes;
+
+/**
+ * Cache that holds approvals per patch set and NoteDb state. This includes approvals copied forward
+ * from older patch sets.
+ */
+public interface ApprovalCache {
+ /** Returns {@link PatchSetApproval}s for the given patch set. */
+ Iterable<PatchSetApproval> get(ChangeNotes notes, PatchSet.Id psId);
+}
diff --git a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java b/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
new file mode 100644
index 0000000..93099eb
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
@@ -0,0 +1,133 @@
+// 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.approval;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.proto.Cache;
+import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.protobuf.ByteString;
+import java.util.concurrent.ExecutionException;
+
+/** @see ApprovalCache */
+public class ApprovalCacheImpl implements ApprovalCache {
+ private static final String CACHE_NAME = "approvals";
+
+ public static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ bind(ApprovalCache.class).to(ApprovalCacheImpl.class);
+ persist(
+ CACHE_NAME,
+ Cache.PatchSetApprovalsKeyProto.class,
+ Cache.AllPatchSetApprovalsProto.class)
+ .version(1)
+ .loader(Loader.class)
+ .keySerializer(new ProtobufSerializer<>(Cache.PatchSetApprovalsKeyProto.parser()))
+ .valueSerializer(new ProtobufSerializer<>(Cache.AllPatchSetApprovalsProto.parser()));
+ }
+ };
+ }
+
+ private final LoadingCache<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto>
+ cache;
+
+ @Inject
+ ApprovalCacheImpl(
+ @Named(CACHE_NAME)
+ LoadingCache<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto> cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public Iterable<PatchSetApproval> get(ChangeNotes notes, PatchSet.Id psId) {
+ try {
+ return fromProto(
+ cache.get(
+ Cache.PatchSetApprovalsKeyProto.newBuilder()
+ .setChangeId(notes.getChangeId().get())
+ .setPatchSetId(psId.get())
+ .setProject(notes.getProjectName().get())
+ .setId(
+ ByteString.copyFrom(
+ ObjectIdCacheSerializer.INSTANCE.serialize(notes.getMetaId())))
+ .build()));
+ } catch (ExecutionException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Singleton
+ static class Loader
+ extends CacheLoader<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto> {
+ private final ApprovalInference approvalInference;
+ private final ChangeNotes.Factory changeNotesFactory;
+
+ @Inject
+ Loader(ApprovalInference approvalInference, ChangeNotes.Factory changeNotesFactory) {
+ this.approvalInference = approvalInference;
+ this.changeNotesFactory = changeNotesFactory;
+ }
+
+ @Override
+ public Cache.AllPatchSetApprovalsProto load(Cache.PatchSetApprovalsKeyProto key)
+ throws Exception {
+ Change.Id changeId = Change.id(key.getChangeId());
+ return toProto(
+ approvalInference.forPatchSet(
+ changeNotesFactory.createChecked(
+ Project.nameKey(key.getProject()),
+ changeId,
+ ObjectIdCacheSerializer.INSTANCE.deserialize(key.getId().toByteArray())),
+ PatchSet.id(changeId, key.getPatchSetId()),
+ null
+ /* revWalk= */ ,
+ null
+ /* repoConfig= */ ));
+ }
+ }
+
+ private static Iterable<PatchSetApproval> fromProto(Cache.AllPatchSetApprovalsProto proto) {
+ ImmutableList.Builder<PatchSetApproval> builder = ImmutableList.builder();
+ for (Entities.PatchSetApproval psa : proto.getApprovalList()) {
+ builder.add(PatchSetApprovalProtoConverter.INSTANCE.fromProto(psa));
+ }
+ return builder.build();
+ }
+
+ private static Cache.AllPatchSetApprovalsProto toProto(Iterable<PatchSetApproval> autoValue) {
+ Cache.AllPatchSetApprovalsProto.Builder builder = Cache.AllPatchSetApprovalsProto.newBuilder();
+ for (PatchSetApproval psa : autoValue) {
+ builder.addApproval(PatchSetApprovalProtoConverter.INSTANCE.toProto(psa));
+ }
+ return builder.build();
+ }
+}
diff --git a/java/com/google/gerrit/server/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
similarity index 87%
rename from java/com/google/gerrit/server/ApprovalInference.java
rename to java/com/google/gerrit/server/approval/ApprovalInference.java
index 675c470..0185598 100644
--- a/java/com/google/gerrit/server/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server;
+package com.google.gerrit.server.approval;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -26,12 +26,12 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
-import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.logging.Metadata;
@@ -44,6 +44,11 @@
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.approval.ApprovalContext;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
+import com.google.gerrit.server.query.approval.ListOfFilesUnchangedPredicate;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
@@ -61,24 +66,33 @@
* submit time, or refreshed on demand, as when reading approvals from the NoteDb.
*/
@Singleton
-public class ApprovalInference {
+class ApprovalInference {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ProjectCache projectCache;
private final ChangeKindCache changeKindCache;
private final LabelNormalizer labelNormalizer;
private final PatchListCache patchListCache;
+ private final ApprovalQueryBuilder approvalQueryBuilder;
+ private final OneOffRequestContext requestContext;
+ private final ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate;
@Inject
ApprovalInference(
ProjectCache projectCache,
ChangeKindCache changeKindCache,
LabelNormalizer labelNormalizer,
- PatchListCache patchListCache) {
+ PatchListCache patchListCache,
+ ApprovalQueryBuilder approvalQueryBuilder,
+ OneOffRequestContext requestContext,
+ ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate) {
this.projectCache = projectCache;
this.changeKindCache = changeKindCache;
this.labelNormalizer = labelNormalizer;
this.patchListCache = patchListCache;
+ this.approvalQueryBuilder = approvalQueryBuilder;
+ this.requestContext = requestContext;
+ this.listOfFilesUnchangedPredicate = listOfFilesUnchangedPredicate;
}
/**
@@ -105,7 +119,7 @@
}
}
- private static boolean canCopy(
+ private boolean canCopyBasedOnBooleanLabelConfigs(
ProjectState project,
PatchSetApproval psa,
PatchSet.Id psId,
@@ -172,12 +186,7 @@
project.getName());
return true;
} else if (type.isCopyAllScoresIfListOfFilesDidNotChange()
- && patchList.getPatches().stream()
- .noneMatch(
- p ->
- p.getChangeType() == ChangeType.ADDED
- || p.getChangeType() == ChangeType.DELETED
- || p.getChangeType() == ChangeType.RENAMED)) {
+ && listOfFilesUnchangedPredicate.match(patchList)) {
logger.atFine().log(
"approval %d on label %s of patch set %d of change %d can be copied"
+ " to patch set %d because the label has set "
@@ -309,6 +318,31 @@
}
}
+ private boolean canCopyBasedOnCopyCondition(
+ ChangeNotes changeNotes,
+ PatchSetApproval psa,
+ PatchSet.Id psId,
+ LabelType type,
+ ChangeKind changeKind) {
+ if (!type.getCopyCondition().isPresent()) {
+ return false;
+ }
+ ApprovalContext ctx = ApprovalContext.create(changeNotes, psa, psId, changeKind);
+ try {
+ // Use a request context to run checks as an internal user with expanded visibility. This is
+ // so that the output of the copy condition does not depend on who is running the current
+ // request (e.g. a group used in this query might not be visible to the person sending this
+ // request).
+ try (ManualRequestContext ignored = requestContext.open()) {
+ return approvalQueryBuilder.parse(type.getCopyCondition().get()).asMatchable().match(ctx);
+ }
+ } catch (QueryParseException e) {
+ logger.atWarning().withCause(e).log(
+ "Unable to copy label because config is invalid. This should have been caught before.");
+ return false;
+ }
+ }
+
private Collection<PatchSetApproval> getForPatchSetWithoutNormalization(
ChangeNotes notes,
ProjectState project,
@@ -380,7 +414,8 @@
if (patchList == null && type != null && type.isCopyAllScoresIfListOfFilesDidNotChange()) {
patchList = getPatchList(project, ps, priorPatchSet);
}
- if (!canCopy(project, psa, ps.id(), kind, type, patchList)) {
+ if (!canCopyBasedOnBooleanLabelConfigs(project, psa, ps.id(), kind, type, patchList)
+ && !canCopyBasedOnCopyCondition(notes, psa, ps.id(), type, kind)) {
continue;
}
resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(ps.id()));
diff --git a/java/com/google/gerrit/server/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
similarity index 95%
rename from java/com/google/gerrit/server/ApprovalsUtil.java
rename to java/com/google/gerrit/server/approval/ApprovalsUtil.java
index 411768d..b1e85e9 100644
--- a/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server;
+package com.google.gerrit.server.approval;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
@@ -39,6 +39,9 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -94,16 +97,19 @@
private final ApprovalInference approvalInference;
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
+ private final ApprovalCache approvalCache;
@VisibleForTesting
@Inject
public ApprovalsUtil(
ApprovalInference approvalInference,
PermissionBackend permissionBackend,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ ApprovalCache approvalCache) {
this.approvalInference = approvalInference;
this.permissionBackend = permissionBackend;
this.projectCache = projectCache;
+ this.approvalCache = approvalCache;
}
/**
@@ -338,6 +344,10 @@
return approvalInference.forPatchSet(notes, psId, rw, repoConfig);
}
+ public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
+ return approvalCache.get(notes, psId);
+ }
+
public Iterable<PatchSetApproval> byPatchSetUser(
ChangeNotes notes,
PatchSet.Id psId,
@@ -347,6 +357,11 @@
return filterApprovals(byPatchSet(notes, psId, rw, repoConfig), accountId);
}
+ public Iterable<PatchSetApproval> byPatchSetUser(
+ ChangeNotes notes, PatchSet.Id psId, Account.Id accountId) {
+ return filterApprovals(byPatchSet(notes, psId), accountId);
+ }
+
public PatchSetApproval getSubmitter(ChangeNotes notes, PatchSet.Id c) {
if (c == null) {
return null;
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
index 4627cdb..c00961f 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
@@ -18,6 +18,7 @@
import com.google.common.base.Converter;
import com.google.common.base.Enums;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Shorts;
import com.google.gerrit.entities.LabelFunction;
@@ -39,6 +40,7 @@
.setAllowPostSubmit(proto.getAllowPostSubmit())
.setIgnoreSelfApproval(proto.getIgnoreSelfApproval())
.setDefaultValue(Shorts.saturatedCast(proto.getDefaultValue()))
+ .setCopyCondition(Strings.emptyToNull(proto.getCopyCondition()))
.setCopyAnyScore(proto.getCopyAnyScore())
.setCopyMinScore(proto.getCopyMinScore())
.setCopyMaxScore(proto.getCopyMaxScore())
@@ -67,6 +69,7 @@
.map(LabelValueSerializer::serialize)
.collect(toImmutableList()))
.setFunction(FUNCTION_CONVERTER.reverse().convert(autoValue.getFunction()))
+ .setCopyCondition(autoValue.getCopyCondition().orElse(""))
.setCopyAnyScore(autoValue.isCopyAnyScore())
.setCopyMinScore(autoValue.isCopyMinScore())
.setCopyMaxScore(autoValue.isCopyMaxScore())
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializer.java
index ad015d1..47a377f 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializer.java
@@ -28,7 +28,8 @@
.setDescription(Optional.ofNullable(Strings.emptyToNull(proto.getDescription())))
.setApplicabilityExpression(
SubmitRequirementExpression.of(proto.getApplicabilityExpression()))
- .setBlockingExpression(SubmitRequirementExpression.create(proto.getBlockingExpression()))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(proto.getSubmittabilityExpression()))
.setOverrideExpression(SubmitRequirementExpression.of(proto.getOverrideExpression()))
.setAllowOverrideInChildProjects(proto.getAllowOverrideInChildProjects())
.build();
@@ -40,10 +41,11 @@
.setName(submitRequirement.name())
.setDescription(submitRequirement.description().orElse(""))
.setApplicabilityExpression(
- submitRequirement.applicabilityExpression().orElse(emptyExpression).expression())
- .setBlockingExpression(submitRequirement.blockingExpression().expression())
+ submitRequirement.applicabilityExpression().orElse(emptyExpression).expressionString())
+ .setSubmittabilityExpression(
+ submitRequirement.submittabilityExpression().expressionString())
.setOverrideExpression(
- submitRequirement.overrideExpression().orElse(emptyExpression).expression())
+ submitRequirement.overrideExpression().orElse(emptyExpression).expressionString())
.setAllowOverrideInChildProjects(submitRequirement.allowOverrideInChildProjects())
.build();
}
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index 1d6fb3c..a333ce5 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -33,10 +33,10 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index c067fcb..6728ba2 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -45,10 +45,10 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ReviewerModifier.InternalReviewerInput;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 3729b59..27b71d6 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -30,12 +30,12 @@
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestResource.HasETag;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 64472ea..3a12ad4 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -29,12 +29,12 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.extensions.events.ReviewerDeleted;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
diff --git a/java/com/google/gerrit/server/change/DeleteReviewersUtil.java b/java/com/google/gerrit/server/change/DeleteReviewersUtil.java
index a4f306b..3212c8d 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewersUtil.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewersUtil.java
@@ -20,8 +20,8 @@
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 647fdf0..d25dba0 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -29,11 +29,11 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index 761b57d..d5b74a8 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -27,8 +27,8 @@
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -94,7 +94,7 @@
out,
reviewerAccountId,
cd,
- approvalsUtil.byPatchSetUser(cd.notes(), psId, reviewerAccountId, null, null));
+ approvalsUtil.byPatchSetUser(cd.notes(), psId, reviewerAccountId));
}
public ReviewerInfo format(
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index 6f05072..f3c5193 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -265,7 +265,7 @@
IdentifiedUser reviewerUser;
boolean exactMatchFound = false;
try {
- reviewerUser = accountResolver.resolve(input.reviewer).asUniqueUser();
+ reviewerUser = accountResolver.resolveIncludeInactive(input.reviewer).asUniqueUser();
if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
|| input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
exactMatchFound = true;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 4794858..5a74c78 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -77,7 +77,6 @@
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.DynamicOptions;
@@ -101,6 +100,8 @@
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
+import com.google.gerrit.server.approval.ApprovalCacheImpl;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
import com.google.gerrit.server.avatar.AvatarProvider;
@@ -179,6 +180,7 @@
import com.google.gerrit.server.project.ProjectNameLockManager;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.approval.ApprovalModule;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -237,6 +239,7 @@
bind(RulesCache.class);
bind(BlameCache.class).to(BlameCacheImpl.class);
install(AccountCacheImpl.module());
+ install(ApprovalCacheImpl.module());
install(BatchUpdate.module());
install(ChangeKindCacheImpl.module());
install(ChangeFinder.module());
@@ -270,6 +273,7 @@
install(new SshAddressesModule());
install(new FileInfoJsonModule(cfg));
install(ThreadLocalRequestContext.module());
+ install(new ApprovalModule());
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index caf495f..d3faac1 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -35,12 +35,12 @@
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.data.AccountAttribute;
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 2dbafd2..0e0185a 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -29,12 +29,12 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommonConverters;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.NotifyResolver;
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 58df343..1da14f8 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -47,9 +47,9 @@
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -650,7 +650,7 @@
private Iterable<PatchSetApproval> safeGetApprovals(ChangeNotes notes, PatchSet.Id psId) {
try {
- return approvalsUtil.byPatchSet(notes, psId, null, null);
+ return approvalsUtil.byPatchSet(notes, psId);
} catch (StorageException e) {
logger.atSevere().withCause(e).log("Can't read approval records for %s", psId);
return Collections.emptyList();
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 15bc603..ec2ed4f 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -100,7 +100,6 @@
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
@@ -111,6 +110,7 @@
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.SetHashtagsOp;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 5e951d3..cc203ad 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -30,7 +30,6 @@
import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.query.change.ProjectPredicate;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Provider;
import java.io.IOException;
@@ -116,7 +115,7 @@
.setLimit(limit)
.query(
Predicate.and(
- new ProjectPredicate(projectName.get()),
+ ChangePredicates.project(projectName),
ChangeStatusPredicate.open(),
ChangePredicates.owner(user)))) {
PatchSet ps = cd.currentPatchSet();
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index cc908e4..b55e91b 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -43,11 +43,11 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.AddReviewersOp;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.NotifyResolver;
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 7131d44..8aab647a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -107,6 +107,7 @@
private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
+ // TODO: Rename LEGACY_ID to NUMERIC_ID
/** Legacy change ID. */
public static final FieldDef<ChangeData, Integer> LEGACY_ID =
integer("legacy_id").stored().build(cd -> cd.getId().get());
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index b8a5cd9..05c5c77 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -20,7 +20,6 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangePredicates;
-import com.google.gerrit.server.query.change.LegacyChangeIdStrPredicate;
/**
* Index for Gerrit changes. This class is mainly used for typing the generic parent class that
@@ -33,6 +32,6 @@
default Predicate<ChangeData> keyPredicate(Change.Id id) {
return getSchema().useLegacyNumericFields()
? ChangePredicates.id(id)
- : new LegacyChangeIdStrPredicate(id);
+ : ChangePredicates.idStr(id);
}
}
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 3a35d80..d805e39 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -43,7 +43,6 @@
import com.google.gerrit.mail.MailMessage;
import com.google.gerrit.mail.MailMetadata;
import com.google.gerrit.mail.TextParser;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
@@ -51,6 +50,7 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.extensions.events.CommentAdded;
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index 808d6a4..7fff232 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -18,13 +18,13 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.GerritInstanceName;
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index ea76ab8..6af2345 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -78,8 +78,7 @@
try {
Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
- for (PatchSetApproval ca :
- args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.id(), null, null)) {
+ for (PatchSetApproval ca : args.approvalsUtil.byPatchSet(changeData.notes(), patchSet.id())) {
LabelType lt = labelTypes.byLabel(ca.labelId());
if (lt == null) {
continue;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
index 71cb8c9..76573f6 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -15,9 +15,12 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Sets;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.InsertedObject;
import java.io.IOException;
@@ -127,4 +130,14 @@
}
return footerLines.get(key.getName().toLowerCase());
}
+
+ public boolean isAttentionSetCommitOnly(boolean hasChangeMessage) {
+ return !hasChangeMessage
+ && footerLines
+ .keySet()
+ .equals(
+ Sets.newHashSet(
+ FOOTER_PATCH_SET.getName().toLowerCase(),
+ FOOTER_ATTENTION.getName().toLowerCase()));
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index f4d6cd3..f12176b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -327,7 +327,6 @@
}
private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
- updateCount++;
Timestamp commitTimestamp = getCommitTimestamp(commit);
createdOn = commitTimestamp;
@@ -370,7 +369,8 @@
originalSubject = currSubject;
}
- parseChangeMessage(psId, accountId, realAccountId, commit, commitTimestamp);
+ boolean hasChangeMessage =
+ parseChangeMessage(psId, accountId, realAccountId, commit, commitTimestamp);
if (topic == null) {
topic = parseTopic(commit);
}
@@ -435,6 +435,9 @@
previousWorkInProgressFooter = null;
parseWorkInProgress(commit);
+ if (countTowardsMaxUpdatesLimit(commit, hasChangeMessage)) {
+ updateCount++;
+ }
}
private void parseSubmission(ChangeNotesCommit commit, Timestamp commitTimestamp)
@@ -720,7 +723,7 @@
}
}
- private void parseChangeMessage(
+ private boolean parseChangeMessage(
PatchSet.Id psId,
Account.Id accountId,
Account.Id realAccountId,
@@ -728,7 +731,7 @@
Timestamp ts) {
Optional<String> changeMsgString = getChangeMessageString(commit);
if (!changeMsgString.isPresent()) {
- return;
+ return false;
}
ChangeMessage changeMessage =
@@ -740,7 +743,7 @@
changeMsgString.get(),
realAccountId,
tag);
- allChangeMessages.add(changeMessage);
+ return allChangeMessages.add(changeMessage);
}
public static Optional<String> getChangeMessageString(ChangeNotesCommit commit) {
@@ -1197,4 +1200,9 @@
.orElseThrow(
() -> parseException("cannot retrieve account id: %s", ident.getEmailAddress()));
}
+
+ protected boolean countTowardsMaxUpdatesLimit(
+ ChangeNotesCommit commit, boolean hasChangeMessage) {
+ return !commit.isAttentionSetCommitOnly(hasChangeMessage);
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 9d23137..f56e933 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -579,10 +579,19 @@
@Override
protected boolean bypassMaxUpdates() {
- // Allow abandoning or submitting a change even if it would exceed the max update count.
+ return isAbandonChange() || isAttentionSetChangeOnly();
+ }
+
+ private boolean isAbandonChange() {
return status != null && status.isClosed();
}
+ private boolean isAttentionSetChangeOnly() {
+ return (plannedAttentionSetUpdates != null
+ && plannedAttentionSetUpdates.size() > 0
+ && comments.isEmpty());
+ }
+
@Override
protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
throws IOException {
diff --git a/java/com/google/gerrit/server/notedb/OpenRepo.java b/java/com/google/gerrit/server/notedb/OpenRepo.java
index 351f31d..d02ec87 100644
--- a/java/com/google/gerrit/server/notedb/OpenRepo.java
+++ b/java/com/google/gerrit/server/notedb/OpenRepo.java
@@ -178,9 +178,9 @@
&& !update.bypassMaxUpdates()) {
throw new LimitExceededException(
String.format(
- "Change %s may not exceed %d updates. It may still be abandoned or submitted. To"
- + " continue working on this change, recreate it with a new Change-Id, then"
- + " abandon this one.",
+ "Change %s may not exceed %d updates. It may still be abandoned, submitted and you can add/remove"
+ + " reviewers to/from the attention-set. To continue working on this change, recreate it with a new"
+ + " Change-Id, then abandon this one.",
update.getId(), maxUpdates.get()));
}
curr = next;
diff --git a/java/com/google/gerrit/server/patch/ComparisonType.java b/java/com/google/gerrit/server/patch/ComparisonType.java
index eca2658..e450779 100644
--- a/java/com/google/gerrit/server/patch/ComparisonType.java
+++ b/java/com/google/gerrit/server/patch/ComparisonType.java
@@ -30,7 +30,7 @@
/**
* 1-based parent. Available if the old commit is the parent of the new commit and old commit is
- * not the auto-merge.
+ * not the auto-merge. If set to 0, then comparison is for a root commit.
*/
abstract Optional<Integer> parentNum();
@@ -48,6 +48,10 @@
return new AutoValue_ComparisonType(Optional.empty(), true);
}
+ public static ComparisonType againstRoot() {
+ return new AutoValue_ComparisonType(Optional.of(0), false);
+ }
+
private static ComparisonType create(Optional<Integer> parent, boolean automerge) {
return new AutoValue_ComparisonType(parent, automerge);
}
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 6217239..dbbb7a6 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
@@ -341,6 +342,10 @@
abstract ObjectId newCommit();
+ /**
+ * Base commit represents the old commit of the diff. For diffs against the root commit, this
+ * should be set to {@code EMPTY_TREE_ID}.
+ */
abstract ObjectId baseCommit();
abstract ComparisonType comparisonType();
@@ -386,6 +391,11 @@
return result.build();
}
int numParents = baseCommitUtil.getNumParents(project, newCommit);
+ if (numParents == 0) {
+ result.baseCommit(EMPTY_TREE_ID);
+ result.comparisonType(ComparisonType.againstRoot());
+ return result.build();
+ }
if (numParents == 1) {
result.baseCommit(baseCommitUtil.getBaseCommit(project, newCommit, parent));
result.comparisonType(ComparisonType.againstParent(1));
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index 395312f..2133474 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -26,6 +26,7 @@
import com.google.common.collect.Multimap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
@@ -93,7 +94,7 @@
persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
.maximumWeight(10 << 20)
.weigher(FileDiffWeigher.class)
- .version(4)
+ .version(5)
.keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
.valueSerializer(FileDiffOutput.Serializer.INSTANCE)
.loader(FileDiffLoader.class);
@@ -189,6 +190,9 @@
private ComparisonType getComparisonType(
RevWalk rw, ObjectReader reader, ObjectId oldCommitId, ObjectId newCommitId)
throws IOException {
+ if (oldCommitId.equals(EMPTY_TREE_ID)) {
+ return ComparisonType.againstRoot();
+ }
RevCommit oldCommit = DiffUtil.getRevCommit(rw, oldCommitId);
RevCommit newCommit = DiffUtil.getRevCommit(rw, newCommitId);
for (int i = 0; i < newCommit.getParentCount(); i++) {
@@ -211,7 +215,7 @@
}
/**
- * Creates a {@link FileDiffOutput} entry for the "Commit message" and "Merge list" file paths.
+ * Creates a {@link FileDiffOutput} entry for the "Commit message" or "Merge list" magic paths.
*/
private FileDiffOutput createMagicPathEntry(
FileDiffCacheKey key, ObjectReader reader, RevWalk rw, MagicPath magicPath) {
@@ -219,7 +223,10 @@
RawTextComparator cmp = comparatorFor(key.whitespace());
ComparisonType comparisonType =
getComparisonType(rw, reader, key.oldCommit(), key.newCommit());
- RevCommit aCommit = DiffUtil.getRevCommit(rw, key.oldCommit());
+ RevCommit aCommit =
+ key.oldCommit().equals(EMPTY_TREE_ID)
+ ? null
+ : DiffUtil.getRevCommit(rw, key.oldCommit());
RevCommit bCommit = DiffUtil.getRevCommit(rw, key.newCommit());
return magicPath == MagicPath.COMMIT
? createCommitEntry(reader, aCommit, bCommit, comparisonType, cmp, key.diffAlgorithm())
@@ -248,16 +255,19 @@
}
}
+ /**
+ * Creates a commit entry. {@code oldCommit} is null if the comparison is against a root commit.
+ */
private FileDiffOutput createCommitEntry(
ObjectReader reader,
- RevCommit oldCommit,
+ @Nullable RevCommit oldCommit,
RevCommit newCommit,
ComparisonType comparisonType,
RawTextComparator rawTextComparator,
GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
throws IOException {
Text aText =
- comparisonType.isAgainstParentOrAutoMerge()
+ oldCommit == null || comparisonType.isAgainstParentOrAutoMerge()
? Text.EMPTY
: Text.forCommit(reader, oldCommit);
Text bText = Text.forCommit(reader, newCommit);
@@ -272,16 +282,20 @@
diffAlgorithm);
}
+ /**
+ * Creates a merge list entry. {@code oldCommit} is null if the comparison is against a root
+ * commit.
+ */
private FileDiffOutput createMergeListEntry(
ObjectReader reader,
- RevCommit oldCommit,
+ @Nullable RevCommit oldCommit,
RevCommit newCommit,
ComparisonType comparisonType,
RawTextComparator rawTextComparator,
GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
throws IOException {
Text aText =
- comparisonType.isAgainstParentOrAutoMerge()
+ oldCommit == null || comparisonType.isAgainstParentOrAutoMerge()
? Text.EMPTY
: Text.forMergeList(comparisonType, reader, oldCommit);
Text bText = Text.forMergeList(comparisonType, reader, newCommit);
@@ -297,7 +311,7 @@
}
private static FileDiffOutput createMagicFileDiffOutput(
- ObjectId oldCommit,
+ @Nullable ObjectId oldCommit,
ObjectId newCommit,
ComparisonType comparisonType,
RawTextComparator rawTextComparator,
@@ -317,7 +331,7 @@
FileHeader fileHeader = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
Patch.ChangeType changeType = FileHeaderUtil.getChangeType(fileHeader);
return FileDiffOutput.builder()
- .oldCommitId(oldCommit)
+ .oldCommitId(oldCommit == null ? EMPTY_TREE_ID : oldCommit)
.newCommitId(newCommit)
.comparisonType(comparisonType)
.oldPath(FileHeaderUtil.getOldPath(fileHeader))
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java
index a478fcf..7ac9343 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java
@@ -26,6 +26,7 @@
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl.DiffAlgorithm;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
/** Cache key for the {@link FileDiffCache}. */
@@ -35,7 +36,10 @@
/** A specific git project / repository. */
public abstract Project.NameKey project();
- /** The 20 bytes SHA-1 commit ID of the old commit used in the diff. */
+ /**
+ * The 20 bytes SHA-1 commit ID of the old commit used in the diff. If set to {@link
+ * Constants#EMPTY_TREE_ID}, the diff is performed against the root commit.
+ */
public abstract ObjectId oldCommit();
/** The 20 bytes SHA-1 commit ID of the new commit used in the diff. */
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
index 730162f..63c9d22 100644
--- a/java/com/google/gerrit/server/project/LabelDefinitionJson.java
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -32,6 +32,7 @@
label.defaultValue = labelType.getDefaultValue();
label.branches = labelType.getRefPatterns() != null ? labelType.getRefPatterns() : null;
label.canOverride = toBoolean(labelType.isCanOverride());
+ label.copyCondition = labelType.getCopyCondition().orElse(null);
label.copyAnyScore = toBoolean(labelType.isCopyAnyScore());
label.copyMinScore = toBoolean(labelType.isCopyMinScore());
label.copyMaxScore = toBoolean(labelType.isCopyMaxScore());
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 1b11ba2..e69967c 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -81,7 +81,13 @@
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
-/** Cache of project information, including access rights. */
+/**
+ * Cache of project information, including access rights.
+ *
+ * <p>The data of a project is the project's project.config in refs/meta/config parsed out as an
+ * immutable value. It's keyed purely by the refs/meta/config SHA-1. We also cache the same value
+ * keyed by name. The latter mapping can become outdated, so data must be evicted explicitly.
+ */
@Singleton
public class ProjectCacheImpl implements ProjectCache {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 5ac5ac7..9f898d9 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -112,6 +112,7 @@
public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
public static final String KEY_COPY_ANY_SCORE = "copyAnyScore";
+ public static final String KEY_COPY_CONDITION = "copyCondition";
public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
public static final String KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE =
"copyAllScoresIfListOfFilesDidNotChange";
@@ -129,7 +130,7 @@
public static final String KEY_SR_NAME = "name";
public static final String KEY_SR_DESCRIPTION = "description";
public static final String KEY_SR_APPLICABILITY_EXPRESSION = "applicabilityExpression";
- public static final String KEY_SR_BLOCKING_EXPRESSION = "blockingExpression";
+ public static final String KEY_SR_SUBMITTABILITY_EXPRESSION = "submittabilityExpression";
public static final String KEY_SR_OVERRIDE_EXPRESSION = "overrideExpression";
public static final String KEY_SR_OVERRIDE_IN_CHILD_PROJECTS = "canOverrideInChildProjects";
@@ -984,7 +985,7 @@
lowerNames.put(lower, name);
String description = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_DESCRIPTION);
String appExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION);
- String blockExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_BLOCKING_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);
@@ -994,7 +995,7 @@
ValidationError.create(
PROJECT_CONFIG,
(String.format(
- "Submit requirement \"%s\" does not define a blocking expression."
+ "Submit requirement \"%s\" does not define a submittability expression."
+ " Skipping this requirement.",
name))));
continue;
@@ -1008,7 +1009,7 @@
.setName(name)
.setDescription(Optional.ofNullable(description))
.setApplicabilityExpression(SubmitRequirementExpression.of(appExpr))
- .setBlockingExpression(SubmitRequirementExpression.create(blockExpr))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create(blockExpr))
.setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
.setAllowOverrideInChildProjects(canInherit)
.build();
@@ -1075,6 +1076,7 @@
KEY_FUNCTION, name, Joiner.on(", ").join(LabelFunction.ALL.keySet()))));
}
label.setFunction(function.orElse(null));
+ label.setCopyCondition(rc.getString(LABEL, name, KEY_COPY_CONDITION));
if (!values.isEmpty()) {
short dv = (short) rc.getInt(LABEL, name, KEY_DEFAULT_VALUE, 0);
@@ -1664,6 +1666,11 @@
values.add(value.format().trim());
}
rc.setStringList(LABEL, name, KEY_VALUE, values);
+ if (label.getCopyCondition().isPresent()) {
+ rc.setString(LABEL, name, KEY_COPY_CONDITION, label.getCopyCondition().get());
+ } else {
+ rc.unset(LABEL, name, KEY_COPY_CONDITION);
+ }
List<String> refPatterns = label.getRefPatterns();
if (refPatterns != null && !refPatterns.isEmpty()) {
@@ -1694,19 +1701,19 @@
SUBMIT_REQUIREMENT,
name,
KEY_SR_APPLICABILITY_EXPRESSION,
- sr.applicabilityExpression().get().expression());
+ sr.applicabilityExpression().get().expressionString());
}
rc.setString(
SUBMIT_REQUIREMENT,
name,
- KEY_SR_BLOCKING_EXPRESSION,
- sr.blockingExpression().expression());
+ KEY_SR_SUBMITTABILITY_EXPRESSION,
+ sr.submittabilityExpression().expressionString());
if (sr.overrideExpression().isPresent()) {
rc.setString(
SUBMIT_REQUIREMENT,
name,
KEY_SR_OVERRIDE_EXPRESSION,
- sr.overrideExpression().get().expression());
+ sr.overrideExpression().get().expressionString());
}
rc.setBoolean(
SUBMIT_REQUIREMENT,
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 249eb35..03d38b3 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -63,8 +63,8 @@
import org.eclipse.jgit.lib.Config;
/**
- * Cached information on a project. Must not contain any data derived from parents other than it's
- * immediate parent's {@link com.google.gerrit.entities.Project.NameKey}.
+ * State of a project, aggregated from the project and its parents. This is obtained from the {@link
+ * ProjectCache}. It should not be persisted across requests
*/
public class ProjectState {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index 812d98d..fca1b36 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -48,10 +48,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeIdPredicate;
-import com.google.gerrit.server.query.change.CommitPredicate;
-import com.google.gerrit.server.query.change.ProjectPredicate;
-import com.google.gerrit.server.query.change.RefPredicate;
+import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.gerrit.server.update.RetryHelper;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -151,7 +148,7 @@
// Base predicate which is fixed for every change query.
Predicate<ChangeData> basePredicate =
- and(new ProjectPredicate(projectName.get()), new RefPredicate(branch), open());
+ and(ChangePredicates.project(projectName), ChangePredicates.ref(branch), open());
int maxLeafPredicates = indexConfig.maxTerms() - basePredicate.getLeafCount();
@@ -231,11 +228,11 @@
}
// Find changes that have a matching Change-Id.
- predicates.add(new ChangeIdPredicate(changeId));
+ predicates.add(ChangePredicates.idPrefix(changeId));
});
// Find changes that have a matching commit.
- predicates.add(new CommitPredicate(commit.name()));
+ predicates.add(ChangePredicates.commitPrefix(commit.name()));
}
if (!predicates.isEmpty()) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
new file mode 100644
index 0000000..1d154dd
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -0,0 +1,101 @@
+// 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.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.Optional;
+
+/** Evaluates submit requirements for different change data. */
+@Singleton
+public class SubmitRequirementsEvaluator {
+ private final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
+
+ @Inject
+ private SubmitRequirementsEvaluator(Provider<ChangeQueryBuilder> changeQueryBuilderProvider) {
+ this.changeQueryBuilderProvider = changeQueryBuilderProvider;
+ }
+
+ /**
+ * Validate a {@link SubmitRequirementExpression}. Callers who wish to validate submit
+ * requirements upon creation or update should use this method.
+ *
+ * @param expression entity containing the expression string.
+ * @throws QueryParseException the expression string contains invalid syntax and can't be parsed.
+ */
+ public void validateExpression(SubmitRequirementExpression expression)
+ throws QueryParseException {
+ changeQueryBuilderProvider.get().parse(expression.expressionString());
+ }
+
+ /** Evaluate a {@link SubmitRequirement} on a given {@link ChangeData}. */
+ public SubmitRequirementResult evaluate(SubmitRequirement sr, ChangeData cd) {
+ SubmitRequirementExpressionResult blockingResult =
+ evaluateExpression(sr.submittabilityExpression(), cd);
+
+ Optional<SubmitRequirementExpressionResult> applicabilityResult =
+ sr.applicabilityExpression().isPresent()
+ ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
+ : Optional.empty();
+
+ Optional<SubmitRequirementExpressionResult> overrideResult =
+ sr.overrideExpression().isPresent()
+ ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
+ : Optional.empty();
+
+ return SubmitRequirementResult.builder()
+ .submittabilityExpressionResult(blockingResult)
+ .applicabilityExpressionResult(applicabilityResult)
+ .overrideExpressionResult(overrideResult)
+ .build();
+ }
+
+ /** Evaluate a {@link SubmitRequirementExpression} using change data. */
+ public SubmitRequirementExpressionResult evaluateExpression(
+ SubmitRequirementExpression expression, ChangeData changeData) {
+ try {
+ Predicate<ChangeData> predicate =
+ changeQueryBuilderProvider.get().parse(expression.expressionString());
+ PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData);
+ return SubmitRequirementExpressionResult.create(predicateResult);
+ } catch (QueryParseException e) {
+ return SubmitRequirementExpressionResult.error(e.getMessage());
+ }
+ }
+
+ /** Evaluate the predicate recursively using change data. */
+ private PredicateResult evaluatePredicateTree(
+ Predicate<ChangeData> predicate, ChangeData changeData) {
+ PredicateResult.Builder predicateResult =
+ PredicateResult.builder()
+ .predicateString(predicate.toString())
+ .status(predicate.asMatchable().match(changeData));
+ predicate
+ .getChildren()
+ .forEach(
+ c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c, changeData)));
+ return predicateResult.build();
+ }
+}
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index e4da946..8f94089 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -21,7 +21,6 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.IndexPredicate;
-import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.server.account.AccountState;
@@ -132,8 +131,7 @@
}
/** Predicate that is mapped to a field in the account index. */
- static class AccountPredicate extends IndexPredicate<AccountState>
- implements Matchable<AccountState> {
+ static class AccountPredicate extends IndexPredicate<AccountState> {
AccountPredicate(FieldDef<AccountState, ?> def, String value) {
super(def, value);
}
@@ -141,16 +139,6 @@
AccountPredicate(FieldDef<AccountState, ?> def, String name, String value) {
super(def, name, value);
}
-
- @Override
- public boolean match(AccountState object) {
- return true;
- }
-
- @Override
- public int getCost() {
- return 1;
- }
}
private AccountPredicates() {}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
new file mode 100644
index 0000000..4c2c7e8
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.server.notedb.ChangeNotes;
+
+/** Entity representing all required information to match predicates for copying approvals. */
+@AutoValue
+public abstract class ApprovalContext {
+ /** Approval on the source patch set to be copied. */
+ public abstract PatchSetApproval patchSetApproval();
+
+ /** Target change and patch set for the approval. */
+ public abstract PatchSet.Id target();
+
+ /** {@link ChangeNotes} of the change in question. */
+ public abstract ChangeNotes changeNotes();
+
+ /** {@link ChangeKind} of the delta between the origin and target patch set. */
+ public abstract ChangeKind changeKind();
+
+ public static ApprovalContext create(
+ ChangeNotes changeNotes, PatchSetApproval psa, PatchSet.Id id, ChangeKind changeKind) {
+ checkState(
+ psa.patchSetId().changeId().equals(id.changeId()),
+ "approval and target must be the same change. got: %s, %s",
+ psa.patchSetId(),
+ id);
+ checkState(
+ psa.patchSetId().get() + 1 == id.get(),
+ "approvals can only be copied to the next consecutive patch set. got: %s, %s",
+ psa.patchSetId(),
+ id);
+ return new AutoValue_ApprovalContext(psa, id, changeNotes, changeKind);
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalModule.java b/java/com/google/gerrit/server/query/approval/ApprovalModule.java
new file mode 100644
index 0000000..ff4d5ad
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/ApprovalModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+
+/** Module to bind logic related to approval copying. */
+public class ApprovalModule extends FactoryModule {
+
+ @Override
+ protected void configure() {
+ factory(MagicValuePredicate.Factory.class);
+ factory(UserInPredicate.Factory.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalPredicate.java b/java/com/google/gerrit/server/query/approval/ApprovalPredicate.java
new file mode 100644
index 0000000..a6f8153
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/ApprovalPredicate.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.Predicate;
+
+public abstract class ApprovalPredicate extends Predicate<ApprovalContext>
+ implements Matchable<ApprovalContext> {
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
new file mode 100644
index 0000000..c3594f5
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.inject.Inject;
+import java.util.Arrays;
+
+public class ApprovalQueryBuilder extends QueryBuilder<ApprovalContext, ApprovalQueryBuilder> {
+ private static final QueryBuilder.Definition<ApprovalContext, ApprovalQueryBuilder> mydef =
+ new QueryBuilder.Definition<>(ApprovalQueryBuilder.class);
+
+ private final MagicValuePredicate.Factory magicValuePredicate;
+ private final UserInPredicate.Factory userInPredicate;
+ private final GroupResolver groupResolver;
+ private final GroupControl.Factory groupControl;
+ private final ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate;
+
+ @Inject
+ protected ApprovalQueryBuilder(
+ MagicValuePredicate.Factory magicValuePredicate,
+ UserInPredicate.Factory userInPredicate,
+ GroupResolver groupResolver,
+ GroupControl.Factory groupControl,
+ ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate) {
+ super(mydef, null);
+ this.magicValuePredicate = magicValuePredicate;
+ this.userInPredicate = userInPredicate;
+ this.groupResolver = groupResolver;
+ this.groupControl = groupControl;
+ this.listOfFilesUnchangedPredicate = listOfFilesUnchangedPredicate;
+ }
+
+ @Operator
+ public Predicate<ApprovalContext> changeKind(String term) throws QueryParseException {
+ return new ChangeKindPredicate(toEnumValue(ChangeKind.class, term));
+ }
+
+ @Operator
+ public Predicate<ApprovalContext> is(String term) throws QueryParseException {
+ return magicValuePredicate.create(toEnumValue(MagicValuePredicate.MagicValue.class, term));
+ }
+
+ @Operator
+ public Predicate<ApprovalContext> approverin(String group) throws QueryParseException {
+ return userInPredicate.create(UserInPredicate.Field.APPROVER, parseGroupOrThrow(group));
+ }
+
+ @Operator
+ public Predicate<ApprovalContext> uploaderin(String group) throws QueryParseException {
+ return userInPredicate.create(UserInPredicate.Field.UPLOADER, parseGroupOrThrow(group));
+ }
+
+ @Operator
+ public Predicate<ApprovalContext> has(String value) throws QueryParseException {
+ if (value.equals("unchanged-files")) {
+ return listOfFilesUnchangedPredicate;
+ }
+ throw error(
+ String.format(
+ "'%s' is not a supported argument for has. only 'unchanged-files' is supported",
+ value));
+ }
+
+ private static <T extends Enum<T>> T toEnumValue(Class<T> clazz, String term)
+ throws QueryParseException {
+ try {
+ return Enum.valueOf(clazz, term.toUpperCase().replace('-', '_'));
+ } catch (
+ @SuppressWarnings("UnusedException")
+ IllegalArgumentException unused) {
+ throw new QueryParseException(
+ String.format(
+ "%s is not a valid term. valid options: %s",
+ term, Arrays.asList(clazz.getEnumConstants())));
+ }
+ }
+
+ private AccountGroup.UUID parseGroupOrThrow(String maybeUUID) throws QueryParseException {
+ GroupDescription.Basic g = groupResolver.parseId(maybeUUID);
+ if (g == null || !groupControl.controlFor(g).isVisible()) {
+ throw error("Group " + maybeUUID + " not found");
+ }
+ return g.getGroupUUID();
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java b/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java
new file mode 100644
index 0000000..78711fd
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.index.query.Predicate;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Predicate that matches patch set approvals we want to copy if the diff between the old and new
+ * patch set is of a certain kind.
+ */
+public class ChangeKindPredicate extends ApprovalPredicate {
+ private final ChangeKind changeKind;
+
+ ChangeKindPredicate(ChangeKind changeKind) {
+ this.changeKind = changeKind;
+ }
+
+ @Override
+ public boolean match(ApprovalContext ctx) {
+ return ctx.changeKind().equals(changeKind);
+ }
+
+ @Override
+ public Predicate<ApprovalContext> copy(
+ Collection<? extends Predicate<ApprovalContext>> children) {
+ return new ChangeKindPredicate(changeKind);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(changeKind);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof ChangeKindPredicate)) {
+ return false;
+ }
+ return ((ChangeKindPredicate) other).changeKind.equals(changeKind);
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
new file mode 100644
index 0000000..30097d8
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/** Predicate that matches when the new patch-set includes the same files as the old patch-set. */
+@Singleton
+public class ListOfFilesUnchangedPredicate extends ApprovalPredicate {
+ private final PatchListCache patchListCache;
+
+ @Inject
+ public ListOfFilesUnchangedPredicate(PatchListCache patchListCache) {
+ this.patchListCache = patchListCache;
+ }
+
+ @Override
+ public boolean match(ApprovalContext ctx) {
+ PatchSet currentPatchset = ctx.changeNotes().getCurrentPatchSet();
+ Map.Entry<PatchSet.Id, PatchSet> priorPatchSet =
+ ctx.changeNotes().getPatchSets().lowerEntry(currentPatchset.id());
+ PatchListKey key =
+ PatchListKey.againstCommit(
+ priorPatchSet.getValue().commitId(),
+ currentPatchset.commitId(),
+ DiffPreferencesInfo.Whitespace.IGNORE_NONE);
+ try {
+ return match(patchListCache.get(key, ctx.changeNotes().getProjectName()));
+ } catch (PatchListNotAvailableException ex) {
+ throw new StorageException(
+ "failed to compute difference in files, so won't copy"
+ + " votes on labels even if list of files is the same and "
+ + "copyAllIfListOfFilesDidNotChange",
+ ex);
+ }
+ }
+
+ public boolean match(PatchList patchList) {
+ return patchList.getPatches().stream()
+ .noneMatch(
+ p ->
+ p.getChangeType() == ChangeType.ADDED
+ || p.getChangeType() == ChangeType.DELETED
+ || p.getChangeType() == ChangeType.RENAMED);
+ }
+
+ @Override
+ public Predicate<ApprovalContext> copy(
+ Collection<? extends Predicate<ApprovalContext>> children) {
+ return new ListOfFilesUnchangedPredicate(patchListCache);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(patchListCache);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof ListOfFilesUnchangedPredicate)) {
+ return false;
+ }
+ ListOfFilesUnchangedPredicate o = (ListOfFilesUnchangedPredicate) other;
+ return Objects.equals(o.patchListCache, patchListCache);
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
new file mode 100644
index 0000000..2924e6e
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Collection;
+import java.util.Objects;
+
+/** Predicate that matches patch set approvals we want to copy based on the value. */
+public class MagicValuePredicate extends ApprovalPredicate {
+ enum MagicValue {
+ MIN,
+ MAX,
+ ANY
+ }
+
+ public interface Factory {
+ MagicValuePredicate create(MagicValue value);
+ }
+
+ private final MagicValue value;
+ private final ProjectCache projectCache;
+
+ @Inject
+ MagicValuePredicate(ProjectCache projectCache, @Assisted MagicValue value) {
+ this.projectCache = projectCache;
+ this.value = value;
+ }
+
+ @Override
+ public boolean match(ApprovalContext ctx) {
+ short pValue;
+ switch (value) {
+ case ANY:
+ return true;
+ case MIN:
+ pValue =
+ getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId())
+ .getMaxNegative();
+ break;
+ case MAX:
+ pValue =
+ getLabelType(ctx.changeNotes().getProjectName(), ctx.patchSetApproval().labelId())
+ .getMaxPositive();
+ break;
+ default:
+ throw new IllegalArgumentException("unrecognized label value: " + value);
+ }
+ return pValue == ctx.patchSetApproval().value();
+ }
+
+ private LabelType getLabelType(Project.NameKey project, LabelId labelId) {
+ return projectCache
+ .get(project)
+ .orElseThrow(() -> new IllegalStateException(project + " absent"))
+ .getLabelTypes()
+ .byLabel(labelId);
+ }
+
+ @Override
+ public Predicate<ApprovalContext> copy(
+ Collection<? extends Predicate<ApprovalContext>> children) {
+ return new MagicValuePredicate(projectCache, value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MagicValuePredicate)) {
+ return false;
+ }
+ return ((MagicValuePredicate) other).value.equals(value);
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/UserInPredicate.java b/java/com/google/gerrit/server/query/approval/UserInPredicate.java
new file mode 100644
index 0000000..7e16fcb
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/UserInPredicate.java
@@ -0,0 +1,71 @@
+package com.google.gerrit.server.query.approval;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Collection;
+import java.util.Objects;
+
+/** Predicate that matches group memberships of users such as uploader or approver. */
+public class UserInPredicate extends ApprovalPredicate {
+ interface Factory {
+ UserInPredicate create(Field field, AccountGroup.UUID group);
+ }
+
+ enum Field {
+ UPLOADER,
+ APPROVER
+ }
+
+ private final IdentifiedUser.GenericFactory identifiedUserFactory;
+ private final Field field;
+ private final AccountGroup.UUID group;
+
+ @Inject
+ UserInPredicate(
+ IdentifiedUser.GenericFactory identifiedUserFactory,
+ @Assisted Field field,
+ @Assisted AccountGroup.UUID group) {
+ this.identifiedUserFactory = identifiedUserFactory;
+ this.field = field;
+ this.group = group;
+ }
+
+ @Override
+ public boolean match(ApprovalContext ctx) {
+ Account.Id accountId;
+ if (field == Field.UPLOADER) {
+ PatchSet patchSet = ctx.changeNotes().getPatchSets().get(ctx.target());
+ accountId = patchSet.uploader();
+ } else if (field == Field.APPROVER) {
+ accountId = ctx.patchSetApproval().accountId();
+ } else {
+ throw new IllegalStateException("unknown field in group membership check: " + field);
+ }
+ return identifiedUserFactory.create(accountId).getEffectiveGroups().contains(group);
+ }
+
+ @Override
+ public Predicate<ApprovalContext> copy(
+ Collection<? extends Predicate<ApprovalContext>> children) {
+ return new UserInPredicate(identifiedUserFactory, field, group);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(field, group);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof UserInPredicate)) {
+ return false;
+ }
+ UserInPredicate o = (UserInPredicate) other;
+ return Objects.equals(o.field, field) && Objects.equals(o.group, group);
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
deleted file mode 100644
index 6e362ad..0000000
--- a/java/com/google/gerrit/server/query/change/AuthorPredicate.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.AUTHOR;
-import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
-
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class AuthorPredicate extends ChangeIndexPredicate {
- public AuthorPredicate(String value) {
- super(AUTHOR, FIELD_AUTHOR, value.toLowerCase());
- }
-
- @Override
- public boolean match(ChangeData object) {
- return ChangeField.getAuthorParts(object).contains(getValue().toLowerCase());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 84c6de0..6f8b097 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -58,7 +58,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.index.RefState;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
@@ -68,6 +67,7 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.StarredChangesUtil.StarRef;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.CommentThread;
import com.google.gerrit.server.change.CommentThreads;
import com.google.gerrit.server.change.MergeabilityCache;
@@ -586,8 +586,7 @@
} else {
try {
currentApprovals =
- ImmutableList.copyOf(
- approvalsUtil.byPatchSet(notes(), c.currentPatchSetId(), null, null));
+ ImmutableList.copyOf(approvalsUtil.byPatchSet(notes(), c.currentPatchSetId()));
} catch (StorageException e) {
if (e.getCause() instanceof NoSuchChangeException) {
currentApprovals = Collections.emptyList();
@@ -1323,7 +1322,7 @@
}
draftsByUser = new HashMap<>();
- for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
+ for (Ref ref : commentsUtil.getDraftRefs(notes().getChangeId())) {
Account.Id account = Account.Id.fromRefSuffix(ref.getName());
if (account != null
// Double-check that any drafts exist for this user after
diff --git a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
deleted file mode 100644
index f06d1f2..0000000
--- a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.index.change.ChangeField;
-
-/** Predicate over Change-Id strings (aka Change.Key). */
-public class ChangeIdPredicate extends ChangeIndexPredicate {
- public ChangeIdPredicate(String id) {
- super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id);
- }
-
- @Override
- public boolean match(ChangeData cd) {
- Change change = cd.change();
- if (change == null) {
- return false;
- }
-
- String key = change.getKey().get();
- if (key.equals(getValue()) || key.startsWith(getValue())) {
- return true;
- }
- return false;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index 28bfb0b..ccd4109 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -14,17 +14,12 @@
package com.google.gerrit.server.query.change;
-import com.google.common.primitives.Ints;
import com.google.gerrit.index.FieldDef;
-import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.query.IndexPredicate;
-import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
-import java.util.Objects;
/** Predicate that is mapped to a field in the change index. */
-public class ChangeIndexPredicate extends IndexPredicate<ChangeData>
- implements Matchable<ChangeData> {
+public class ChangeIndexPredicate extends IndexPredicate<ChangeData> {
/**
* Returns an index predicate that matches no changes in the index.
*
@@ -44,32 +39,4 @@
protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String name, String value) {
super(def, name, value);
}
-
- @Override
- public boolean match(ChangeData cd) {
- if (getField().isRepeatable()) {
- Iterable<Object> values = (Iterable<Object>) getField().get(cd);
- for (Object v : values) {
- if (matchesSingleObject(v)) {
- return true;
- }
- }
- return false;
- } else {
- return matchesSingleObject(getField().get(cd));
- }
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-
- private boolean matchesSingleObject(Object fieldValueFromObject) {
- String fieldTypeName = getField().getType().getName();
- if (fieldTypeName.equals(FieldType.INTEGER.getName())) {
- return Objects.equals(fieldValueFromObject, Ints.tryParse(value));
- }
- throw new UnsupportedOperationException("match function must be provided in subclass");
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 568916d..617002d 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -14,14 +14,19 @@
package com.google.gerrit.server.query.change;
+import com.google.common.base.CharMatcher;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.index.query.Predicate;
+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;
/** Predicates that match against {@link ChangeData}. */
public class ChangePredicates {
@@ -97,6 +102,12 @@
ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
}
+ /** Returns a predicate that matches the change with the provided {@link Change.Id}. */
+ public static Predicate<ChangeData> idStr(Change.Id id) {
+ return new ChangeIndexPredicate(
+ ChangeField.LEGACY_ID_STR, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+ }
+
/** Returns a predicate that matches changes owned by the provided {@link Account.Id}. */
public static Predicate<ChangeData> owner(Account.Id id) {
return new ChangeIndexPredicate(ChangeField.OWNER, id.toString());
@@ -119,4 +130,152 @@
cherryPickOf(psId.changeId()),
new ChangeIndexPredicate(ChangeField.CHERRY_PICK_OF_PATCHSET, String.valueOf(psId.get())));
}
+
+ /** Returns a predicate that matches changes in the provided {@link Project.NameKey}. */
+ public static Predicate<ChangeData> project(Project.NameKey id) {
+ return new ChangeIndexPredicate(ChangeField.PROJECT, id.get());
+ }
+
+ /** Returns a predicate that matches changes targeted at the provided {@code refName}. */
+ public static Predicate<ChangeData> ref(String refName) {
+ return new ChangeIndexPredicate(ChangeField.REF, refName);
+ }
+
+ /** Returns a predicate that matches changes in the provided {@code topic}. */
+ public static Predicate<ChangeData> exactTopic(String topic) {
+ return new ChangeIndexPredicate(ChangeField.EXACT_TOPIC, topic);
+ }
+
+ /** Returns a predicate that matches changes in the provided {@code topic}. */
+ public static Predicate<ChangeData> fuzzyTopic(String topic) {
+ return new ChangeIndexPredicate(ChangeField.FUZZY_TOPIC, topic);
+ }
+
+ /** Returns a predicate that matches changes submitted in the provided {@code changeSet}. */
+ public static Predicate<ChangeData> submissionId(String changeSet) {
+ return new ChangeIndexPredicate(ChangeField.SUBMISSIONID, changeSet);
+ }
+
+ /** Returns a predicate that matches changes that modified the provided {@code path}. */
+ public static Predicate<ChangeData> path(String path) {
+ return new ChangeIndexPredicate(ChangeField.PATH, path);
+ }
+
+ /** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
+ public static Predicate<ChangeData> hashtag(String hashtag) {
+ // Use toLowerCase without locale to match behavior in ChangeField.
+ return new ChangeIndexPredicate(
+ ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+ }
+
+ /** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
+ public static Predicate<ChangeData> fuzzyHashtag(String hashtag) {
+ // Use toLowerCase without locale to match behavior in ChangeField.
+ return new ChangeIndexPredicate(
+ ChangeField.FUZZY_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+ }
+
+ /** Returns a predicate that matches changes that modified the provided {@code file}. */
+ public static Predicate<ChangeData> file(ChangeQueryBuilder.Arguments args, String file) {
+ Predicate<ChangeData> eqPath = path(file);
+ if (!args.getSchema().hasField(ChangeField.FILE_PART)) {
+ return eqPath;
+ }
+ return Predicate.or(eqPath, new ChangeIndexPredicate(ChangeField.FILE_PART, file));
+ }
+
+ /**
+ * Returns a predicate that matches changes with the provided {@code footer} in their commit
+ * message.
+ */
+ public static Predicate<ChangeData> footer(String footer) {
+ int indexEquals = footer.indexOf('=');
+ int indexColon = footer.indexOf(':');
+
+ // footer key cannot contain '='
+ if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
+ footer = footer.substring(0, indexEquals) + ": " + footer.substring(indexEquals + 1);
+ }
+ return new ChangeIndexPredicate(ChangeField.FOOTER, footer.toLowerCase(Locale.US));
+ }
+
+ /**
+ * Returns a predicate that matches changes that modified files in the provided {@code directory}.
+ */
+ public static Predicate<ChangeData> directory(String directory) {
+ return new ChangeIndexPredicate(
+ ChangeField.DIRECTORY, CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US));
+ }
+
+ /** Returns a predicate that matches changes with the provided {@code trackingId}. */
+ public static Predicate<ChangeData> trackingId(String trackingId) {
+ return new ChangeIndexPredicate(ChangeField.TR, trackingId);
+ }
+
+ /** Returns a predicate that matches changes authored by the provided {@code exactAuthor}. */
+ public static Predicate<ChangeData> exactAuthor(String exactAuthor) {
+ return new ChangeIndexPredicate(ChangeField.EXACT_AUTHOR, exactAuthor.toLowerCase(Locale.US));
+ }
+
+ /** Returns a predicate that matches changes authored by the provided {@code author}. */
+ public static Predicate<ChangeData> author(String author) {
+ return new ChangeIndexPredicate(ChangeField.AUTHOR, author);
+ }
+
+ /**
+ * Returns a predicate that matches changes where the patch set was committed by {@code
+ * exactCommitter}.
+ */
+ public static Predicate<ChangeData> exactCommitter(String exactCommitter) {
+ return new ChangeIndexPredicate(
+ ChangeField.EXACT_COMMITTER, exactCommitter.toLowerCase(Locale.US));
+ }
+
+ /**
+ * Returns a predicate that matches changes where the patch set was committed by {@code
+ * committer}.
+ */
+ public static Predicate<ChangeData> committer(String comitter) {
+ return new ChangeIndexPredicate(ChangeField.COMMITTER, comitter.toLowerCase(Locale.US));
+ }
+
+ /** Returns a predicate that matches changes whose ID starts with the provided {@code id}. */
+ public static Predicate<ChangeData> idPrefix(String id) {
+ return new ChangeIndexPredicate(ChangeField.ID, id);
+ }
+
+ /**
+ * Returns a predicate that matches changes in a project that has the provided {@code prefix} in
+ * its name.
+ */
+ public static Predicate<ChangeData> projectPrefix(String prefix) {
+ return new ChangeIndexPredicate(ChangeField.PROJECTS, prefix);
+ }
+
+ /**
+ * Returns a predicate that matches changes where a patch set has the provided {@code commitId}
+ * either as prefix or as full {@link org.eclipse.jgit.lib.ObjectId}.
+ */
+ public static Predicate<ChangeData> commitPrefix(String commitId) {
+ if (commitId.length() == ObjectIds.STR_LEN) {
+ return new ChangeIndexPredicate(ChangeField.EXACT_COMMIT, commitId);
+ }
+ return new ChangeIndexPredicate(ChangeField.COMMIT, commitId);
+ }
+
+ /**
+ * Returns a predicate that matches changes where the provided {@code message} appears in the
+ * commit message. Uses full-text search semantics.
+ */
+ public static Predicate<ChangeData> message(String message) {
+ return new ChangeIndexPredicate(ChangeField.COMMIT_MESSAGE, message);
+ }
+
+ /**
+ * Returns a predicate that matches changes where the provided {@code comment} appears in any
+ * comment on any patch set of the change. Uses full-text search semantics.
+ */
+ public static Predicate<ChangeData> comment(String comment) {
+ return new ChangeIndexPredicate(ChangeField.COMMENT, comment);
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 7ebaec7..94b5442 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -36,6 +36,7 @@
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.exceptions.NotSignedInException;
@@ -527,17 +528,17 @@
return Predicate.and(
project(triplet.get().project().get()),
branch(triplet.get().branch().branch()),
- new ChangeIdPredicate(parseChangeId(triplet.get().id().get())));
+ ChangePredicates.idPrefix(parseChangeId(triplet.get().id().get())));
}
if (PAT_LEGACY_ID.matcher(query).matches()) {
Integer id = Ints.tryParse(query);
if (id != null) {
return args.getSchema().useLegacyNumericFields()
? ChangePredicates.id(Change.id(id))
- : new LegacyChangeIdStrPredicate(Change.id(id));
+ : ChangePredicates.idStr(Change.id(id));
}
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
- return new ChangeIdPredicate(parseChangeId(query));
+ return ChangePredicates.idPrefix(parseChangeId(query));
}
throw new QueryParseException("Invalid change format");
@@ -545,7 +546,7 @@
@Operator
public Predicate<ChangeData> comment(String value) {
- return new CommentPredicate(args.index, value);
+ return ChangePredicates.comment(value);
}
@Operator
@@ -704,7 +705,7 @@
@Operator
public Predicate<ChangeData> commit(String id) {
- return new CommitPredicate(id);
+ return ChangePredicates.commitPrefix(id);
}
@Operator
@@ -727,12 +728,12 @@
if (name.startsWith("^")) {
return new RegexProjectPredicate(name);
}
- return new ProjectPredicate(name);
+ return ChangePredicates.project(Project.nameKey(name));
}
@Operator
public Predicate<ChangeData> projects(String name) {
- return new ProjectPrefixPredicate(name);
+ return ChangePredicates.projectPrefix(name);
}
@Operator
@@ -790,7 +791,7 @@
@Operator
public Predicate<ChangeData> hashtag(String hashtag) {
- return new ExactHashtagPredicate(hashtag);
+ return ChangePredicates.hashtag(hashtag);
}
@Operator
@@ -799,19 +800,19 @@
return new RegexHashtagPredicate(hashtag);
}
if (hashtag.isEmpty()) {
- return new ExactHashtagPredicate(hashtag);
+ return ChangePredicates.hashtag(hashtag);
}
if (!args.index.getSchema().hasField(ChangeField.FUZZY_HASHTAG)) {
throw new QueryParseException(
"'inhashtag' operator is not supported by change index version");
}
- return new FuzzyHashtagPredicate(hashtag, args.index);
+ return ChangePredicates.fuzzyHashtag(hashtag);
}
@Operator
public Predicate<ChangeData> topic(String name) {
- return new ExactTopicPredicate(name);
+ return ChangePredicates.exactTopic(name);
}
@Operator
@@ -820,9 +821,9 @@
return new RegexTopicPredicate(name);
}
if (name.isEmpty()) {
- return new ExactTopicPredicate(name);
+ return ChangePredicates.exactTopic(name);
}
- return new FuzzyTopicPredicate(name, args.index);
+ return ChangePredicates.fuzzyTopic(name);
}
@Operator
@@ -830,7 +831,7 @@
if (ref.startsWith("^")) {
return new RegexRefPredicate(ref);
}
- return new RefPredicate(ref);
+ return ChangePredicates.ref(ref);
}
@Operator
@@ -843,7 +844,7 @@
if (file.startsWith("^")) {
return new RegexPathPredicate(file);
}
- return EqualsFilePredicate.create(args, file);
+ return ChangePredicates.file(args, file);
}
@Operator
@@ -851,7 +852,7 @@
if (path.startsWith("^")) {
return new RegexPathPredicate(path);
}
- return new EqualsPathPredicate(FIELD_PATH, path);
+ return ChangePredicates.path(path);
}
@Operator
@@ -884,7 +885,7 @@
@Operator
public Predicate<ChangeData> footer(String footer) throws QueryParseException {
if (args.getSchema().hasField(ChangeField.FOOTER)) {
- return new FooterPredicate(footer);
+ return ChangePredicates.footer(footer);
}
throw new QueryParseException("'footer' operator is not supported by change index version");
}
@@ -900,7 +901,7 @@
if (directory.startsWith("^")) {
return new RegexDirectoryPredicate(directory);
}
- return new DirectoryPredicate(directory);
+ return ChangePredicates.directory(directory);
}
throw new QueryParseException("'directory' operator is not supported by change index version");
}
@@ -997,7 +998,7 @@
@Operator
public Predicate<ChangeData> message(String text) {
- return new MessagePredicate(args.index, text);
+ return ChangePredicates.message(text);
}
@Operator
@@ -1231,7 +1232,7 @@
@Operator
public Predicate<ChangeData> tr(String trackingId) {
- return new TrackingIdPredicate(trackingId);
+ return ChangePredicates.trackingId(trackingId);
}
@Operator
@@ -1389,18 +1390,18 @@
public Predicate<ChangeData> author(String who) throws QueryParseException {
if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
return getAuthorOrCommitterPredicate(
- who.trim(), ExactAuthorPredicate::new, AuthorPredicate::new);
+ who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
}
- return getAuthorOrCommitterFullTextPredicate(who.trim(), AuthorPredicate::new);
+ return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::author);
}
@Operator
public Predicate<ChangeData> committer(String who) throws QueryParseException {
if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
return getAuthorOrCommitterPredicate(
- who.trim(), ExactCommitterPredicate::new, CommitterPredicate::new);
+ who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
}
- return getAuthorOrCommitterFullTextPredicate(who.trim(), CommitterPredicate::new);
+ return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::committer);
}
@Operator
@@ -1432,7 +1433,7 @@
@Operator
public Predicate<ChangeData> submissionId(String value) throws QueryParseException {
if (args.getSchema().hasField(ChangeField.SUBMISSIONID)) {
- return new SubmissionIdPredicate(value);
+ return ChangePredicates.submissionId(value);
}
throw new QueryParseException(
"'submissionid' operator is not supported by change index version");
diff --git a/java/com/google/gerrit/server/query/change/CommentPredicate.java b/java/com/google/gerrit/server/query/change/CommentPredicate.java
deleted file mode 100644
index 0abe45d..0000000
--- a/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.IndexedChangeQuery;
-
-public class CommentPredicate extends ChangeIndexPredicate {
- protected final ChangeIndex index;
-
- public CommentPredicate(ChangeIndex index, String value) {
- super(ChangeField.COMMENT, value);
- this.index = index;
- }
-
- @Override
- public boolean match(ChangeData object) {
- try {
- Change.Id id = object.getId();
- Predicate<ChangeData> p =
- Predicate.and(
- index.getSchema().useLegacyNumericFields()
- ? ChangePredicates.id(id)
- : new LegacyChangeIdStrPredicate(id),
- this);
- for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
- if (cData.getId().equals(id)) {
- return true;
- }
- }
- } catch (QueryParseException e) {
- throw new StorageException(e);
- }
-
- return false;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/CommitPredicate.java b/java/com/google/gerrit/server/query/change/CommitPredicate.java
deleted file mode 100644
index 2b3b345..0000000
--- a/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.git.ObjectIds.matchesAbbreviation;
-import static com.google.gerrit.server.index.change.ChangeField.COMMIT;
-import static com.google.gerrit.server.index.change.ChangeField.EXACT_COMMIT;
-
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.git.ObjectIds;
-import com.google.gerrit.index.FieldDef;
-
-public class CommitPredicate extends ChangeIndexPredicate {
- static FieldDef<ChangeData, ?> commitField(String id) {
- if (id.length() == ObjectIds.STR_LEN) {
- return EXACT_COMMIT;
- }
- return COMMIT;
- }
-
- public CommitPredicate(String id) {
- super(commitField(id), id);
- }
-
- @Override
- public boolean match(ChangeData object) {
- String id = getValue().toLowerCase();
- for (PatchSet p : object.patchSets()) {
- if (equals(p, id)) {
- return true;
- }
- }
- return false;
- }
-
- protected boolean equals(PatchSet p, String id) {
- if (getField() == EXACT_COMMIT) {
- return p.commitId().name().equals(id);
- }
- return matchesAbbreviation(p.commitId(), id);
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
deleted file mode 100644
index 65034a2..0000000
--- a/java/com/google/gerrit/server/query/change/CommitterPredicate.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.COMMITTER;
-import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
-
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class CommitterPredicate extends ChangeIndexPredicate {
- public CommitterPredicate(String value) {
- super(COMMITTER, FIELD_COMMITTER, value.toLowerCase());
- }
-
- @Override
- public boolean match(ChangeData object) {
- return ChangeField.getCommitterParts(object).contains(getValue().toLowerCase());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 80e3cb9..1ad9dba 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -81,17 +81,17 @@
List<Predicate<ChangeData>> filePredicates = new ArrayList<>(files.size());
for (String file : files) {
- filePredicates.add(new EqualsPathPredicate(ChangeQueryBuilder.FIELD_PATH, file));
+ filePredicates.add(ChangePredicates.path(file));
}
List<Predicate<ChangeData>> and = new ArrayList<>(5);
- and.add(new ProjectPredicate(c.getProject().get()));
- and.add(new RefPredicate(c.getDest().branch()));
+ and.add(ChangePredicates.project(c.getProject()));
+ and.add(ChangePredicates.ref(c.getDest().branch()));
and.add(
Predicate.not(
args.getSchema().useLegacyNumericFields()
? ChangePredicates.id(c.getId())
- : new LegacyChangeIdStrPredicate(c.getId())));
+ : ChangePredicates.idStr(c.getId())));
and.add(Predicate.or(filePredicates));
ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
diff --git a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
deleted file mode 100644
index 9249137..0000000
--- a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.common.base.CharMatcher;
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Locale;
-
-public class DirectoryPredicate extends ChangeIndexPredicate {
- private static String clean(String directory) {
- return CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US);
- }
-
- DirectoryPredicate(String value) {
- super(ChangeField.DIRECTORY, clean(value));
- }
-
- @Override
- public boolean match(ChangeData cd) {
- return ChangeField.getDirectories(cd).contains(value);
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
deleted file mode 100644
index 4e454fa..0000000
--- a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-
-public class EqualsFilePredicate extends ChangeIndexPredicate {
- public static Predicate<ChangeData> create(Arguments args, String value) {
- Predicate<ChangeData> eqPath = new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value);
- if (!args.getSchema().hasField(ChangeField.FILE_PART)) {
- return eqPath;
- }
- return Predicate.or(eqPath, new EqualsFilePredicate(value));
- }
-
- private EqualsFilePredicate(String value) {
- super(ChangeField.FILE_PART, ChangeQueryBuilder.FIELD_FILE, value);
- }
-
- @Override
- public boolean match(ChangeData object) {
- return ChangeField.getFileParts(object).contains(value);
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
deleted file mode 100644
index 63975d0..0000000
--- a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Collections;
-
-public class EqualsPathPredicate extends ChangeIndexPredicate {
- public EqualsPathPredicate(String fieldName, String value) {
- super(ChangeField.PATH, fieldName, value);
- }
-
- @Override
- public boolean match(ChangeData object) {
- return Collections.binarySearch(object.currentFilePaths(), value) >= 0;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
deleted file mode 100644
index 3b3cb90..0000000
--- a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.EXACT_AUTHOR;
-import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTAUTHOR;
-
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Locale;
-
-public class ExactAuthorPredicate extends ChangeIndexPredicate {
- public ExactAuthorPredicate(String value) {
- super(EXACT_AUTHOR, FIELD_EXACTAUTHOR, value.toLowerCase(Locale.US));
- }
-
- @Override
- public boolean match(ChangeData object) {
- return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
deleted file mode 100644
index 390d4ab..0000000
--- a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.EXACT_COMMITTER;
-import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTCOMMITTER;
-
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Locale;
-
-public class ExactCommitterPredicate extends ChangeIndexPredicate {
- public ExactCommitterPredicate(String value) {
- super(EXACT_COMMITTER, FIELD_EXACTCOMMITTER, value.toLowerCase(Locale.US));
- }
-
- @Override
- public boolean match(ChangeData object) {
- return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java b/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
deleted file mode 100644
index 76ced90..0000000
--- a/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.server.change.HashtagsUtil;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class ExactHashtagPredicate extends ChangeIndexPredicate {
- public ExactHashtagPredicate(String hashtag) {
- // Use toLowerCase without locale to match behavior in ChangeField.
- // TODO(dborowitz): Change both.
- super(ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
- }
-
- @Override
- public boolean match(ChangeData cd) {
- if (Strings.isNullOrEmpty(getValue())) {
- return cd.hashtags().isEmpty();
- }
- for (String hashtag : cd.hashtags()) {
- if (hashtag.equalsIgnoreCase(getValue())) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
deleted file mode 100644
index 5d8d2c0..0000000
--- a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
-
-import com.google.gerrit.entities.Change;
-
-public class ExactTopicPredicate extends ChangeIndexPredicate {
- public ExactTopicPredicate(String topic) {
- super(EXACT_TOPIC, topic);
- }
-
- @Override
- public boolean match(ChangeData object) {
- Change change = object.change();
- if (change == null) {
- return false;
- }
- return getValue().equals(change.getTopic());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/FooterPredicate.java b/java/com/google/gerrit/server/query/change/FooterPredicate.java
deleted file mode 100644
index 37bd6b1..0000000
--- a/java/com/google/gerrit/server/query/change/FooterPredicate.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Locale;
-
-public class FooterPredicate extends ChangeIndexPredicate {
- private static String clean(String value) {
- int indexEquals = value.indexOf('=');
- int indexColon = value.indexOf(':');
-
- // footer key cannot contain '='
- if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
- value = value.substring(0, indexEquals) + ": " + value.substring(indexEquals + 1);
- }
- return value.toLowerCase(Locale.US);
- }
-
- FooterPredicate(String value) {
- super(ChangeField.FOOTER, clean(value));
- }
-
- @Override
- public boolean match(ChangeData cd) {
- return ChangeField.getFooters(cd).contains(value);
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
deleted file mode 100644
index b4d6b5f..0000000
--- a/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.FUZZY_HASHTAG;
-
-import com.google.gerrit.server.index.change.ChangeIndex;
-
-public class FuzzyHashtagPredicate extends ChangeIndexPredicate {
- protected final ChangeIndex index;
-
- public FuzzyHashtagPredicate(String hashtag, ChangeIndex index) {
- super(FUZZY_HASHTAG, hashtag.toLowerCase());
- this.index = index;
- }
-
- @Override
- public boolean match(ChangeData cd) {
- return cd.hashtags().stream().anyMatch(ht -> ht.toLowerCase().contains(getValue()));
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
deleted file mode 100644
index 47652b8..0000000
--- a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.FUZZY_TOPIC;
-
-import com.google.common.collect.Iterables;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.IndexedChangeQuery;
-
-public class FuzzyTopicPredicate extends ChangeIndexPredicate {
- protected final ChangeIndex index;
-
- public FuzzyTopicPredicate(String topic, ChangeIndex index) {
- super(FUZZY_TOPIC, topic);
- this.index = index;
- }
-
- @Override
- public boolean match(ChangeData cd) {
- Change change = cd.change();
- if (change == null) {
- return false;
- }
- String t = change.getTopic();
- if (t == null) {
- return false;
- }
- try {
- Predicate<ChangeData> thisId =
- index.getSchema().useLegacyNumericFields()
- ? ChangePredicates.id(cd.getId())
- : new LegacyChangeIdStrPredicate(cd.getId());
- Iterable<ChangeData> results =
- index.getSource(and(thisId, this), IndexedChangeQuery.oneResult()).read();
- return !Iterables.isEmpty(results);
- } catch (QueryParseException e) {
- throw new StorageException(e);
- }
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index ed0f237..76ebd81 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -61,15 +61,15 @@
}
private static Predicate<ChangeData> ref(BranchNameKey branch) {
- return new RefPredicate(branch.branch());
+ return ChangePredicates.ref(branch.branch());
}
private static Predicate<ChangeData> change(Change.Key key) {
- return new ChangeIdPredicate(key.get());
+ return ChangePredicates.idPrefix(key.get());
}
private static Predicate<ChangeData> project(Project.NameKey project) {
- return new ProjectPredicate(project.get());
+ return ChangePredicates.project(project);
}
private static Predicate<ChangeData> status(Change.Status status) {
@@ -77,7 +77,7 @@
}
private static Predicate<ChangeData> commit(String id) {
- return new CommitPredicate(id);
+ return ChangePredicates.commitPrefix(id);
}
private final ChangeData.Factory changeDataFactory;
@@ -100,7 +100,7 @@
(id) ->
schema().useLegacyNumericFields()
? ChangePredicates.id(id)
- : new LegacyChangeIdStrPredicate(id);
+ : ChangePredicates.idStr(id);
}
public List<ChangeData> byKey(Change.Key key) {
@@ -108,7 +108,7 @@
}
public List<ChangeData> byKeyPrefix(String prefix) {
- return query(new ChangeIdPredicate(prefix));
+ return query(ChangePredicates.idPrefix(prefix));
}
public List<ChangeData> byLegacyChangeId(Change.Id id) {
@@ -225,7 +225,7 @@
}
public List<ChangeData> byTopicOpen(String topic) {
- return query(and(new ExactTopicPredicate(topic), open()));
+ return query(and(ChangePredicates.exactTopic(topic), open()));
}
public List<ChangeData> byCommit(ObjectId id) {
@@ -269,14 +269,17 @@
private static Predicate<ChangeData> byBranchCommitPred(
String project, String branch, String hash) {
- return and(new ProjectPredicate(project), new RefPredicate(branch), commit(hash));
+ return and(
+ ChangePredicates.project(Project.nameKey(project)),
+ ChangePredicates.ref(branch),
+ commit(hash));
}
public List<ChangeData> bySubmissionId(String cs) {
if (Strings.isNullOrEmpty(cs)) {
return Collections.emptyList();
}
- return query(new SubmissionIdPredicate(cs));
+ return query(ChangePredicates.submissionId(cs));
}
private static Predicate<ChangeData> byProjectGroupsPredicate(
diff --git a/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java b/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
deleted file mode 100644
index 60cfd8f..0000000
--- a/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
-
-import com.google.gerrit.entities.Change;
-
-/** Predicate over change number (aka legacy ID or Change.Id). */
-public class LegacyChangeIdStrPredicate extends ChangeIndexPredicate {
- protected final Change.Id id;
-
- public LegacyChangeIdStrPredicate(Change.Id id) {
- super(LEGACY_ID_STR, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
- this.id = id;
- }
-
- @Override
- public boolean match(ChangeData object) {
- return id.equals(object.getId());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/MessagePredicate.java b/java/com/google/gerrit/server/query/change/MessagePredicate.java
deleted file mode 100644
index caf751e..0000000
--- a/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.IndexedChangeQuery;
-
-/** Predicate to match changes that contains specified text in commit messages body. */
-public class MessagePredicate extends ChangeIndexPredicate {
- protected final ChangeIndex index;
-
- public MessagePredicate(ChangeIndex index, String value) {
- super(ChangeField.COMMIT_MESSAGE, value);
- this.index = index;
- }
-
- @Override
- public boolean match(ChangeData object) {
- try {
- Predicate<ChangeData> p =
- Predicate.and(
- index.getSchema().useLegacyNumericFields()
- ? ChangePredicates.id(object.getId())
- : new LegacyChangeIdStrPredicate(object.getId()),
- this);
- for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
- if (cData.getId().equals(object.getId())) {
- return true;
- }
- }
- } catch (QueryParseException e) {
- throw new StorageException(e);
- }
-
- return false;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 2c82075..4a54c03 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -47,10 +47,10 @@
}
List<Predicate<ChangeData>> r = new ArrayList<>();
- r.add(new ProjectPredicate(projectState.get().getName()));
+ r.add(ChangePredicates.project(projectState.get().getNameKey()));
try {
for (ProjectInfo p : childProjects.list(projectState.get().getNameKey())) {
- r.add(new ProjectPredicate(p.name));
+ r.add(ChangePredicates.project(Project.nameKey(p.name)));
}
} catch (PermissionBackendException e) {
logger.atWarning().withCause(e).log("cannot check permissions to expand child projects");
diff --git a/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
deleted file mode 100644
index b76207c..0000000
--- a/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class ProjectPredicate extends ChangeIndexPredicate {
- public ProjectPredicate(String id) {
- super(ChangeField.PROJECT, id);
- }
-
- protected Project.NameKey getValueKey() {
- return Project.nameKey(getValue());
- }
-
- @Override
- public boolean match(ChangeData object) {
- Change change = object.change();
- if (change == null) {
- return false;
- }
-
- Project.NameKey p = change.getDest().project();
- return p.equals(getValueKey());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
deleted file mode 100644
index b89fffe..0000000
--- a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class ProjectPrefixPredicate extends ChangeIndexPredicate {
- public ProjectPrefixPredicate(String prefix) {
- super(ChangeField.PROJECTS, prefix);
- }
-
- @Override
- public boolean match(ChangeData object) {
- Change c = object.change();
- return c != null && c.getDest().project().get().startsWith(getValue());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/RefPredicate.java b/java/com/google/gerrit/server/query/change/RefPredicate.java
deleted file mode 100644
index 8dced69..0000000
--- a/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class RefPredicate extends ChangeIndexPredicate {
- public RefPredicate(String ref) {
- super(ChangeField.REF, ref);
- }
-
- @Override
- public boolean match(ChangeData object) {
- Change change = object.change();
- if (change == null) {
- return false;
- }
- return getValue().equals(change.getDest().branch());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
deleted file mode 100644
index b653eb5..0000000
--- a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class SubmissionIdPredicate extends ChangeIndexPredicate {
- public SubmissionIdPredicate(String changeSet) {
- super(ChangeField.SUBMISSIONID, changeSet);
- }
-
- @Override
- public boolean match(ChangeData object) {
- Change change = object.change();
- if (change == null) {
- return false;
- }
- if (change.getSubmissionId() == null) {
- return false;
- }
- return getValue().equals(change.getSubmissionId());
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
deleted file mode 100644
index 4f58be2..0000000
--- a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class TrackingIdPredicate extends ChangeIndexPredicate {
- public TrackingIdPredicate(String trackingId) {
- super(ChangeField.TR, trackingId);
- }
-
- @Override
- public boolean match(ChangeData cd) {
- return cd.trackingFooters().containsValue(getValue());
- }
-}
diff --git a/java/com/google/gerrit/server/query/group/GroupPredicates.java b/java/com/google/gerrit/server/query/group/GroupPredicates.java
index 9d8171c..8a2dc8d 100644
--- a/java/com/google/gerrit/server/query/group/GroupPredicates.java
+++ b/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -19,7 +19,6 @@
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.IndexPredicate;
-import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.index.group.GroupField;
import java.util.Locale;
@@ -45,7 +44,7 @@
}
public static Predicate<InternalGroup> name(String name) {
- return new NameGroupPredicate(name);
+ return new GroupPredicate(GroupField.NAME, name);
}
public static Predicate<InternalGroup> owner(AccountGroup.UUID ownerUuid) {
@@ -76,25 +75,5 @@
}
}
- // TODO(hiesel): This is just a one-off to make index tests work. Remove in favor of a more
- // generic solution.
- // This is required because Gerrit needs to look up groups by name on every request.
- static class NameGroupPredicate extends IndexPredicate<InternalGroup>
- implements Matchable<InternalGroup> {
- NameGroupPredicate(String value) {
- super(GroupField.NAME, value);
- }
-
- @Override
- public boolean match(InternalGroup group) {
- return group.getName().equals(getValue());
- }
-
- @Override
- public int getCost() {
- return 1;
- }
- }
-
private GroupPredicates() {}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index dc1cd10..5375936 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -33,11 +33,11 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 8ae902d..35cadb7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -33,12 +33,12 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerResource;
diff --git a/java/com/google/gerrit/server/restapi/change/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 3d07d43..12dbf4e 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -19,7 +19,7 @@
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ReviewerJson;
import com.google.gerrit.server.change.ReviewerResource;
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index b44f637..2df2d29 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -20,7 +20,7 @@
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ReviewerJson;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.RevisionResource;
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 22fcbc7..50b7516 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -40,11 +40,11 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 8d413fa..6816361 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -81,7 +81,6 @@
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
@@ -91,6 +90,7 @@
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.ModifyReviewersEmail;
@@ -1418,11 +1418,7 @@
return !del.isEmpty() || !ups.isEmpty();
}
- /**
- * Approval is copied over if it doesn't exist in the approvals of the current patch-set
- * according to change notes (which means it was computed in {@link
- * com.google.gerrit.server.ApprovalInference})
- */
+ /** Approval is copied over if it doesn't exist in the approvals of the current patch-set. */
private boolean isApprovalCopiedOver(
PatchSetApproval patchSetApproval, ChangeNotes changeNotes) {
return !changeNotes.getApprovals().get(changeNotes.getChange().currentPatchSetId()).stream()
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index 17ee92e..dcf616c 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -26,11 +26,11 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ReviewerModifier;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index 4723d70..0356cdd 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -27,11 +27,11 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.AttentionSetUnchangedOp;
import com.google.gerrit.server.change.CommentThread;
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index d80ab696..9e74eec 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -24,10 +24,10 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ReviewerSuggestion;
import com.google.gerrit.server.change.SuggestedReviewer;
import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index 4bfcf14..9da7c88 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -22,8 +22,8 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index 2651ab5..97383cda 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -24,7 +24,7 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.restapi.account.AccountsCollection;
diff --git a/java/com/google/gerrit/server/restapi/change/Votes.java b/java/com/google/gerrit/server/restapi/change/Votes.java
index d899002..0f4b960 100644
--- a/java/com/google/gerrit/server/restapi/change/Votes.java
+++ b/java/com/google/gerrit/server/restapi/change/Votes.java
@@ -24,7 +24,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.VoteResource;
import com.google.inject.Inject;
@@ -83,9 +83,7 @@
approvalsUtil.byPatchSetUser(
rsrc.getChangeResource().getNotes(),
rsrc.getChange().currentPatchSetId(),
- rsrc.getReviewerUser().getAccountId(),
- null,
- null);
+ rsrc.getReviewerUser().getAccountId());
for (PatchSetApproval psa : byPatchSetUser) {
votes.put(psa.label(), psa.value());
}
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 033463c..ae7f540 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -34,8 +34,7 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.Reachable;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.CommitPredicate;
-import com.google.gerrit.server.query.change.ProjectPredicate;
+import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.gerrit.server.update.RetryHelper;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -149,10 +148,10 @@
// branches to check, by seeing if its parents were associated to changes.
Predicate<ChangeData> pred =
Predicate.and(
- new ProjectPredicate(project.get()),
+ ChangePredicates.project(project),
Predicate.or(
Arrays.stream(commit.getParents())
- .map(parent -> new CommitPredicate(parent.getId().getName()))
+ .map(parent -> ChangePredicates.commitPrefix(parent.getId().getName()))
.collect(toImmutableList())));
changes =
retryHelper
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index eceab43..92038b0 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -30,7 +30,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index 2ae1b05..d2f4161 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -36,6 +37,7 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -51,6 +53,7 @@
private final MetaDataUpdate.User updateFactory;
private final ProjectConfig.Factory projectConfigFactory;
private final ProjectCache projectCache;
+ private final ApprovalQueryBuilder approvalQueryBuilder;
@Inject
public CreateLabel(
@@ -58,12 +61,14 @@
PermissionBackend permissionBackend,
MetaDataUpdate.User updateFactory,
ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ ApprovalQueryBuilder approvalQueryBuilder) {
this.user = user;
this.permissionBackend = permissionBackend;
this.updateFactory = updateFactory;
this.projectConfigFactory = projectConfigFactory;
this.projectCache = projectCache;
+ this.approvalQueryBuilder = approvalQueryBuilder;
}
@Override
@@ -166,6 +171,23 @@
labelType.setCopyAnyScore(input.copyAnyScore);
}
+ if (input.copyCondition != null) {
+ try {
+ approvalQueryBuilder.parse(input.copyCondition);
+ } catch (QueryParseException e) {
+ throw new BadRequestException(
+ "unable to parse copy condition. got: " + input.copyCondition + ". " + e.getMessage(),
+ e);
+ }
+ if (Boolean.TRUE.equals(input.unsetCopyCondition)) {
+ throw new BadRequestException("can't set and unset copyCondition in the same request");
+ }
+ labelType.setCopyCondition(Strings.emptyToNull(input.copyCondition));
+ }
+ if (Boolean.TRUE.equals(input.unsetCopyCondition)) {
+ labelType.setCopyCondition(null);
+ }
+
if (input.copyMinScore != null) {
labelType.setCopyMinScore(input.copyMinScore);
}
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index b1bcb15..d69abef 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -32,6 +33,7 @@
import com.google.gerrit.server.project.LabelResource;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -45,6 +47,7 @@
private final MetaDataUpdate.User updateFactory;
private final ProjectConfig.Factory projectConfigFactory;
private final ProjectCache projectCache;
+ private final ApprovalQueryBuilder approvalQueryBuilder;
@Inject
public SetLabel(
@@ -52,12 +55,14 @@
PermissionBackend permissionBackend,
MetaDataUpdate.User updateFactory,
ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ ApprovalQueryBuilder approvalQueryBuilder) {
this.user = user;
this.permissionBackend = permissionBackend;
this.updateFactory = updateFactory;
this.projectConfigFactory = projectConfigFactory;
this.projectCache = projectCache;
+ this.approvalQueryBuilder = approvalQueryBuilder;
}
@Override
@@ -174,6 +179,26 @@
dirty = true;
}
+ input.copyCondition = Strings.emptyToNull(input.copyCondition);
+ if (input.copyCondition != null) {
+ try {
+ approvalQueryBuilder.parse(input.copyCondition);
+ } catch (QueryParseException e) {
+ throw new BadRequestException(
+ "unable to parse copy condition. got: " + input.copyCondition + ". " + e.getMessage(),
+ e);
+ }
+ labelTypeBuilder.setCopyCondition(input.copyCondition);
+ dirty = true;
+ if (Boolean.TRUE.equals(input.unsetCopyCondition)) {
+ throw new BadRequestException("can't set and unset copyCondition in the same request");
+ }
+ }
+ if (Boolean.TRUE.equals(input.unsetCopyCondition)) {
+ labelTypeBuilder.setCopyCondition(null);
+ dirty = true;
+ }
+
if (input.copyAnyScore != null) {
labelTypeBuilder.setCopyAnyScore(input.copyAnyScore);
dirty = true;
diff --git a/java/com/google/gerrit/server/submit/CommitMergeStatus.java b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
index bf8b840..4638bfa 100644
--- a/java/com/google/gerrit/server/submit/CommitMergeStatus.java
+++ b/java/com/google/gerrit/server/submit/CommitMergeStatus.java
@@ -77,7 +77,14 @@
EMPTY_COMMIT(
"Change could not be merged because the commit is empty.\n"
+ "\n"
- + "Project policy requires all commits to contain modifications to at least one file.");
+ + "Project policy requires all commits to contain modifications to at least one file."),
+
+ FAST_FORWARD_INDEPENDENT_CHANGES(
+ "Change could not be merged because the submission has two independent changes "
+ + "with the same destination branch.\n"
+ + "\n"
+ + "Independent changes can't be submitted to the same destination branch with "
+ + "FAST_FORWARD_ONLY submit strategy");
private final String description;
diff --git a/java/com/google/gerrit/server/submit/FastForwardOnly.java b/java/com/google/gerrit/server/submit/FastForwardOnly.java
index 176b063..8a30898 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOnly.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOnly.java
@@ -14,11 +14,15 @@
package com.google.gerrit.server.submit;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.update.RepoContext;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class FastForwardOnly extends SubmitStrategy {
FastForwardOnly(SubmitStrategy.Arguments args) {
@@ -28,6 +32,21 @@
@Override
public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+
+ Map<BranchNameKey, CodeReviewCommit> branchToCommit = new HashMap<>();
+ for (CodeReviewCommit codeReviewCommit : sorted) {
+ BranchNameKey branchNameKey = codeReviewCommit.change().getDest();
+ CodeReviewCommit otherCommitInBranch = branchToCommit.get(branchNameKey);
+ if (otherCommitInBranch == null) {
+ branchToCommit.put(branchNameKey, codeReviewCommit);
+ } else {
+ // we found another change with the same destination branch.
+ codeReviewCommit.setStatusCode(CommitMergeStatus.FAST_FORWARD_INDEPENDENT_CHANGES);
+ otherCommitInBranch.setStatusCode(CommitMergeStatus.FAST_FORWARD_INDEPENDENT_CHANGES);
+ return ImmutableList.of();
+ }
+ }
+
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
CodeReviewCommit newTipCommit =
args.mergeUtil.getFirstFastForward(args.mergeTip.getInitialTip(), args.rw, sorted);
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index 530c53f..6291e6c 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -26,12 +26,12 @@
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.SetPrivateOp;
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
index b533bebc..59c6b81 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
@@ -134,6 +134,7 @@
case NOT_FAST_FORWARD:
case EMPTY_COMMIT:
case MISSING_DEPENDENCY:
+ case FAST_FORWARD_INDEPENDENT_CHANGES:
// TODO(dborowitz): Reformat these messages to be more appropriate for
// short problem descriptions.
String message = s.getDescription();
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index a63c7dc..09470d4 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -32,9 +32,9 @@
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 7130339..1d50e82 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -576,7 +576,7 @@
"Account 'user' only matches inactive accounts. To use an inactive account, retry"
+ " with one of the following exact account IDs:\n"
+ id
- + ": User <user@example.com>");
+ + ": User1 <user1@example.com>");
assertThat(gApi.accounts().id(id).getActive()).isFalse();
gApi.accounts().id(id).setActive(true);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 2abbe1a..529ce73 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -79,6 +79,7 @@
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseTimezone;
+import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
@@ -212,6 +213,7 @@
@NoHttpd
@UseTimezone(timezone = "US/Eastern")
+@VerifyNoPiiInChangeNotes(true)
public class ChangeIT extends AbstractDaemonTest {
@Inject private AccountOperations accountOperations;
@@ -439,25 +441,25 @@
.newAccount()
.username(name("user1"))
.preferredEmail(email1)
- .fullname("User 1")
+ .fullname("User1")
.create();
accountOperations
.newAccount()
.username(name("user2"))
.preferredEmail(email2)
- .fullname("User 2")
+ .fullname("User2")
.create();
accountOperations
.newAccount()
.username(name("user3"))
.preferredEmail(email3)
- .fullname("User 3")
+ .fullname("User3")
.create();
accountOperations
.newAccount()
.username(name("user4"))
.preferredEmail(email4)
- .fullname("User 4")
+ .fullname("User4")
.create();
ReviewInput in =
ReviewInput.noScore()
@@ -644,7 +646,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void reviewWithWorkInProgressChangeOwner() throws Exception {
PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
@@ -659,7 +661,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void reviewWithWithWorkInProgressAdmin() throws Exception {
PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
@@ -997,7 +999,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void deleteNewChangeAsNormalUser() throws Exception {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
@@ -1022,7 +1024,7 @@
@Test
public void deleteNewChangeAsUserWithDeleteChangesPermissionForProjectOwners() throws Exception {
GroupApi groupApi = gApi.groups().create(name("delete-change"));
- groupApi.addMembers("user");
+ groupApi.addMembers("user1");
Project.NameKey nameKey = Project.nameKey(name("delete-change"));
ProjectInput in = new ProjectInput();
@@ -1151,7 +1153,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void deleteAbandonedChangeAsNormalUser() throws Exception {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
@@ -1166,7 +1168,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
@@ -1192,7 +1194,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception {
projectOperations
.project(project)
@@ -1804,7 +1806,7 @@
}
@Test
- public void addReviewerThatIsInactive() throws Exception {
+ public void addReviewerThatIsInactiveByUsername() throws Exception {
PushOneCommit.Result result = createChange();
String username = name("new-user");
@@ -1815,19 +1817,11 @@
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(in.reviewer);
- assertThat(r.error)
- .isEqualTo(
- "Account '"
- + username
- + "' only matches inactive accounts. To use an inactive account, retry with one of"
- + " the following exact account IDs:\n"
- + id
- + ": Name of user not set ("
- + id
- + ")\n"
- + username
- + " does not identify a registered user or group");
- assertThat(r.reviewers).isNull();
+ assertThat(r.error).isNull();
+ assertThat(r.reviewers).hasSize(1);
+ ReviewerInfo reviewer = r.reviewers.get(0);
+ assertThat(reviewer._accountId).isEqualTo(id.get());
+ assertThat(reviewer.username).isEqualTo(username);
}
@Test
@@ -1850,15 +1844,13 @@
}
@Test
- public void addReviewerThatIsInactiveEmailFallback() throws Exception {
+ public void addReviewerThatIsInactiveByEmail() throws Exception {
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
-
PushOneCommit.Result result = createChange();
-
String username = "user@domain.com";
- accountOperations.newAccount().username(username).inactive().create();
+ Account.Id id = accountOperations.newAccount().username(username).inactive().create();
ReviewerInput in = new ReviewerInput();
in.reviewer = username;
@@ -1867,9 +1859,10 @@
assertThat(r.input).isEqualTo(username);
assertThat(r.error).isNull();
- // When adding by email, the reviewers field is also empty because we can't
- // render a ReviewerInfo object for a non-account.
- assertThat(r.reviewers).isNull();
+ assertThat(r.ccs).hasSize(1);
+ AccountInfo reviewer = r.ccs.get(0);
+ assertThat(reviewer._accountId).isEqualTo(id.get());
+ assertThat(reviewer.username).isEqualTo(username);
}
@Test
@@ -1962,7 +1955,7 @@
.newAccount()
.username(username1)
.preferredEmail(email1)
- .fullname("User 1")
+ .fullname("User1")
.create();
in.reviewer = email1;
in.state = ReviewerState.CC;
@@ -2656,7 +2649,7 @@
.isEqualTo(
"Removed Code-Review+1 by " + ChangeMessagesUtil.getAccountTemplate(user.id()) + "\n");
assertThat(gApi.changes().id(r.getChangeId()).message(message.id).get().message)
- .isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
+ .isEqualTo("Removed Code-Review+1 by User1 <user1@example.com>\n");
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
}
@@ -3886,7 +3879,7 @@
for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) {
requestScopeOperations.setApiUser(acc.id());
String newMessage =
- "modified commit by " + acc.username() + "\n\nChange-Id: " + r.getChangeId() + "\n";
+ "modified commit by " + acc.id() + "\n\nChange-Id: " + r.getChangeId() + "\n";
gApi.changes().id(r.getChangeId()).setMessage(newMessage);
RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 503ab11..53a9364 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -28,15 +28,14 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
import static com.google.gerrit.server.project.testing.TestLabels.value;
-import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.change.ChangeKindCreator;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -45,26 +44,19 @@
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import java.util.EnumSet;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -73,6 +65,7 @@
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ChangeOperations changeOperations;
+ @Inject private ChangeKindCreator changeKindCreator;
@Inject
@Named("change_kind")
@@ -135,11 +128,11 @@
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
testRepo.reset(projectOperations.project(project).getHead("master"));
- String changeId = createChange(changeKind);
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, 1, -1);
- updateChange(changeId, changeKind);
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, changeKind);
assertVotes(c, user, 1, 0, changeKind);
@@ -147,6 +140,49 @@
}
@Test
+ public void stickyWhenCopyConditionIsTrue() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("is:ANY"));
+ u.save();
+ }
+
+ for (ChangeKind changeKind :
+ EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
+ testRepo.reset(projectOperations.project(project).getHead("master"));
+
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
+ vote(admin, changeId, 2, 1);
+ vote(user, changeId, 1, -1);
+
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
+ ChangeInfo c = detailedChange(changeId);
+ assertVotes(c, admin, 2, 0, changeKind);
+ assertVotes(c, user, 1, 0, changeKind);
+ }
+ }
+
+ @Test
+ public void stickyEvenWhenUserCantSeeUploaderInGroup() throws Exception {
+ // user can't see admin group
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ String administratorsUUID = gApi.groups().query("name:Administrators").get().get(0).id;
+ u.getConfig()
+ .updateLabelType(
+ LabelId.CODE_REVIEW, b -> b.setCopyCondition("approverin:" + administratorsUUID));
+ u.save();
+ }
+
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+ amendChange(changeId);
+ vote(user, changeId, 1, -1); // Invalidate cache
+ requestScopeOperations.setApiUser(user.id());
+ ChangeInfo c = detailedChange(changeId);
+ assertVotes(c, admin, 2, 0);
+ assertVotes(c, user, 1, -1);
+ }
+
+ @Test
public void stickyOnMinScore() throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyMinScore(true));
@@ -157,11 +193,11 @@
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
testRepo.reset(projectOperations.project(project).getHead("master"));
- String changeId = createChange(changeKind);
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
vote(admin, changeId, -1, 1);
vote(user, changeId, -2, -1);
- updateChange(changeId, changeKind);
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 0, 0, changeKind);
assertVotes(c, user, -2, 0, changeKind);
@@ -169,6 +205,30 @@
}
@Test
+ public void stickyWhenEitherBooleanConfigsOrCopyConditionAreTrue() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .updateLabelType(
+ LabelId.CODE_REVIEW, b -> b.setCopyCondition("is:MAX").setCopyMinScore(true));
+ u.save();
+ }
+
+ for (ChangeKind changeKind :
+ EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
+ testRepo.reset(projectOperations.project(project).getHead("master"));
+
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
+ vote(admin, changeId, 2, 1);
+ vote(user, changeId, -2, -1);
+
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
+ ChangeInfo c = detailedChange(changeId);
+ assertVotes(c, admin, 2, 0, changeKind);
+ assertVotes(c, user, -2, 0, changeKind);
+ }
+ }
+
+ @Test
public void stickyOnMaxScore() throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyMaxScore(true));
@@ -179,11 +239,11 @@
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
testRepo.reset(projectOperations.project(project).getHead("master"));
- String changeId = createChange(changeKind);
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, 1, -1);
- updateChange(changeId, changeKind);
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, changeKind);
assertVotes(c, user, 0, 0, changeKind);
@@ -205,12 +265,12 @@
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
testRepo.reset(projectOperations.project(project).getHead("master"));
- String changeId = createChange(changeKind);
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
vote(admin, changeId, -1, 1);
vote(user, changeId, -2, -1);
vote(user2, changeId, 1, -1);
- updateChange(changeId, changeKind);
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, -1, 0, changeKind);
assertVotes(c, user, 0, 0, changeKind);
@@ -226,16 +286,16 @@
u.save();
}
- String changeId = createChange(TRIVIAL_REBASE);
+ String changeId = changeKindCreator.createChange(TRIVIAL_REBASE, testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
- updateChange(changeId, NO_CHANGE);
+ changeKindCreator.updateChange(changeId, NO_CHANGE, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, NO_CHANGE);
assertVotes(c, user, -2, 0, NO_CHANGE);
- updateChange(changeId, TRIVIAL_REBASE);
+ changeKindCreator.updateChange(changeId, TRIVIAL_REBASE, testRepo, admin, project);
c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, TRIVIAL_REBASE);
assertVotes(c, user, -2, 0, TRIVIAL_REBASE);
@@ -248,7 +308,8 @@
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
- String cherryPickChangeId = cherryPick(changeId, TRIVIAL_REBASE);
+ String cherryPickChangeId =
+ changeKindCreator.cherryPick(changeId, TRIVIAL_REBASE, testRepo, admin, project);
c = detailedChange(cherryPickChangeId);
assertVotes(c, admin, 2, 0);
assertVotes(c, user, -2, 0);
@@ -259,7 +320,7 @@
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
- cherryPickChangeId = cherryPick(changeId, REWORK);
+ cherryPickChangeId = changeKindCreator.cherryPick(changeId, REWORK, testRepo, admin, project);
c = detailedChange(cherryPickChangeId);
assertVotes(c, admin, 0, 0);
assertVotes(c, user, 0, 0);
@@ -272,16 +333,16 @@
u.save();
}
- String changeId = createChange(NO_CODE_CHANGE);
+ String changeId = changeKindCreator.createChange(NO_CODE_CHANGE, testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
- updateChange(changeId, NO_CHANGE);
+ changeKindCreator.updateChange(changeId, NO_CHANGE, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 0, 1, NO_CHANGE);
assertVotes(c, user, 0, -1, NO_CHANGE);
- updateChange(changeId, NO_CODE_CHANGE);
+ changeKindCreator.updateChange(changeId, NO_CODE_CHANGE, testRepo, admin, project);
c = detailedChange(changeId);
assertVotes(c, admin, 0, 1, NO_CODE_CHANGE);
assertVotes(c, user, 0, -1, NO_CODE_CHANGE);
@@ -298,16 +359,16 @@
u.save();
}
- String changeId = createChange(MERGE_FIRST_PARENT_UPDATE);
+ String changeId = changeKindCreator.createChange(MERGE_FIRST_PARENT_UPDATE, testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
- updateChange(changeId, NO_CHANGE);
+ changeKindCreator.updateChange(changeId, NO_CHANGE, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, NO_CHANGE);
assertVotes(c, user, -2, 0, NO_CHANGE);
- updateChange(changeId, MERGE_FIRST_PARENT_UPDATE);
+ changeKindCreator.updateChange(changeId, MERGE_FIRST_PARENT_UPDATE, testRepo, admin, project);
c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, MERGE_FIRST_PARENT_UPDATE);
assertVotes(c, user, -2, 0, MERGE_FIRST_PARENT_UPDATE);
@@ -322,11 +383,11 @@
u.save();
}
- String changeId = createChangeForMergeCommit();
+ String changeId = changeKindCreator.createChangeForMergeCommit(testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
- updateSecondParent(changeId);
+ changeKindCreator.updateSecondParent(changeId, testRepo, admin);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 0, 0, null);
assertVotes(c, user, 0, 0, null);
@@ -439,7 +500,7 @@
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
testRepo.reset(projectOperations.project(project).getHead("master"));
- String changeId = createChange(changeKind);
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
vote(admin, changeId, 2, 1);
vote(user, changeId, -2, -1);
@@ -450,7 +511,7 @@
assertVotes(c, admin, 0, 0, null);
assertVotes(c, user, 0, 0, null);
- updateChange(changeId, changeKind);
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
c = detailedChange(changeId);
assertVotes(c, admin, 0, 0, changeKind);
assertVotes(c, user, 0, 0, changeKind);
@@ -465,16 +526,16 @@
u.save();
}
- String changeId = createChange(REWORK);
+ String changeId = changeKindCreator.createChange(REWORK, testRepo, admin);
vote(admin, changeId, 2, 1);
for (int i = 0; i < 5; i++) {
- updateChange(changeId, NO_CODE_CHANGE);
+ changeKindCreator.updateChange(changeId, NO_CODE_CHANGE, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 1, NO_CODE_CHANGE);
}
- updateChange(changeId, REWORK);
+ changeKindCreator.updateChange(changeId, REWORK, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, REWORK);
}
@@ -491,11 +552,11 @@
u.save();
}
- String changeId = createChange(REWORK);
+ String changeId = changeKindCreator.createChange(REWORK, testRepo, admin);
vote(admin, changeId, 2, 1);
- updateChange(changeId, NO_CODE_CHANGE);
- updateChange(changeId, NO_CODE_CHANGE);
- updateChange(changeId, NO_CODE_CHANGE);
+ changeKindCreator.updateChange(changeId, NO_CODE_CHANGE, testRepo, admin, project);
+ changeKindCreator.updateChange(changeId, NO_CODE_CHANGE, testRepo, admin, project);
+ changeKindCreator.updateChange(changeId, NO_CODE_CHANGE, testRepo, admin, project);
Map<Integer, ObjectId> revisions = new HashMap<>();
gApi.changes()
@@ -524,24 +585,24 @@
}
// Vote max score on PS1
- String changeId = createChange(REWORK);
+ String changeId = changeKindCreator.createChange(REWORK, testRepo, admin);
vote(admin, changeId, 2, 1);
// Have someone else vote min score on PS2
- updateChange(changeId, REWORK);
+ changeKindCreator.updateChange(changeId, REWORK, testRepo, admin, project);
vote(user, changeId, -2, 0);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, REWORK);
assertVotes(c, user, -2, 0, REWORK);
// No vote changes on PS3
- updateChange(changeId, REWORK);
+ changeKindCreator.updateChange(changeId, REWORK, testRepo, admin, project);
c = detailedChange(changeId);
assertVotes(c, admin, 2, 0, REWORK);
assertVotes(c, user, -2, 0, REWORK);
// Both users revote on PS4
- updateChange(changeId, REWORK);
+ changeKindCreator.updateChange(changeId, REWORK, testRepo, admin, project);
vote(admin, changeId, 1, 1);
vote(user, changeId, 1, 1);
c = detailedChange(changeId);
@@ -549,7 +610,7 @@
assertVotes(c, user, 1, 1, REWORK);
// New approvals shouldn't carry through to PS5
- updateChange(changeId, REWORK);
+ changeKindCreator.updateChange(changeId, REWORK, testRepo, admin, project);
c = detailedChange(changeId);
assertVotes(c, admin, 0, 0, REWORK);
assertVotes(c, user, 0, 0, REWORK);
@@ -564,10 +625,10 @@
}
// Vote max score on PS1
- String changeId = createChange(REWORK);
+ String changeId = changeKindCreator.createChange(REWORK, testRepo, admin);
vote(admin, changeId, label, 2);
assertVotes(detailedChange(changeId), admin, label, 2, null);
- updateChange(changeId, REWORK);
+ changeKindCreator.updateChange(changeId, REWORK, testRepo, admin, project);
assertVotes(detailedChange(changeId), admin, label, 2, REWORK);
// Delete vote that was copied via sticky approval
@@ -622,209 +683,17 @@
for (ChangeKind changeKind : changeKinds) {
testRepo.reset(projectOperations.project(project).getHead("master"));
- String changeId = createChange(changeKind);
+ String changeId = changeKindCreator.createChange(changeKind, testRepo, admin);
vote(admin, changeId, +2, 1);
vote(user, changeId, -2, -1);
- updateChange(changeId, changeKind);
+ changeKindCreator.updateChange(changeId, changeKind, testRepo, admin, project);
ChangeInfo c = detailedChange(changeId);
assertVotes(c, admin, 0, 0, changeKind);
assertVotes(c, user, 0, 0, changeKind);
}
}
- private String createChange(ChangeKind kind) throws Exception {
- switch (kind) {
- case NO_CODE_CHANGE:
- case REWORK:
- case TRIVIAL_REBASE:
- case NO_CHANGE:
- return createChange().getChangeId();
- case MERGE_FIRST_PARENT_UPDATE:
- return createChangeForMergeCommit();
- default:
- throw new IllegalStateException("unexpected change kind: " + kind);
- }
- }
-
- private void updateChange(String changeId, ChangeKind changeKind) throws Exception {
- switch (changeKind) {
- case NO_CODE_CHANGE:
- noCodeChange(changeId);
- return;
- case REWORK:
- rework(changeId);
- return;
- case TRIVIAL_REBASE:
- trivialRebase(changeId);
- return;
- case MERGE_FIRST_PARENT_UPDATE:
- updateFirstParent(changeId);
- return;
- case NO_CHANGE:
- noChange(changeId);
- return;
- default:
- assertWithMessage("unexpected change kind: " + changeKind).fail();
- }
- }
-
- private void noCodeChange(String changeId) throws Exception {
- TestRepository<?>.CommitBuilder commitBuilder =
- testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
- commitBuilder
- .message("New subject " + System.nanoTime())
- .author(admin.newIdent())
- .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
- commitBuilder.create();
- GitUtil.pushHead(testRepo, "refs/for/master", false);
- assertThat(getChangeKind(changeId)).isEqualTo(NO_CODE_CHANGE);
- }
-
- private void noChange(String changeId) throws Exception {
- ChangeInfo change = gApi.changes().id(changeId).get();
- String commitMessage = change.revisions.get(change.currentRevision).commit.message;
-
- TestRepository<?>.CommitBuilder commitBuilder =
- testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
- commitBuilder
- .message(commitMessage)
- .author(admin.newIdent())
- .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()));
- commitBuilder.create();
- GitUtil.pushHead(testRepo, "refs/for/master", false);
- assertThat(getChangeKind(changeId)).isEqualTo(NO_CHANGE);
- }
-
- private void rework(String changeId) throws Exception {
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(),
- testRepo,
- PushOneCommit.SUBJECT,
- PushOneCommit.FILE_NAME,
- "new content " + System.nanoTime(),
- changeId);
- push.to("refs/for/master").assertOkStatus();
- assertThat(getChangeKind(changeId)).isEqualTo(REWORK);
- }
-
- private void trivialRebase(String changeId) throws Exception {
- requestScopeOperations.setApiUser(admin.id());
- testRepo.reset(projectOperations.project(project).getHead("master"));
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(),
- testRepo,
- "Other Change",
- "a" + System.nanoTime() + ".txt",
- PushOneCommit.FILE_CONTENT);
- PushOneCommit.Result r = push.to("refs/for/master");
- r.assertOkStatus();
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- ReviewInput in = new ReviewInput().label(LabelId.CODE_REVIEW, 2).label(LabelId.VERIFIED, 1);
- revision.review(in);
- revision.submit();
-
- gApi.changes().id(changeId).current().rebase();
- assertThat(getChangeKind(changeId)).isEqualTo(TRIVIAL_REBASE);
- }
-
- private String createChangeForMergeCommit() throws Exception {
- ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
-
- PushOneCommit.Result parent1 = createChange("parent 1", "p1.txt", "content 1");
-
- testRepo.reset(initial);
- PushOneCommit.Result parent2 = createChange("parent 2", "p2.txt", "content 2");
-
- testRepo.reset(parent1.getCommit());
-
- PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo);
- merge.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
- PushOneCommit.Result result = merge.to("refs/for/master");
- result.assertOkStatus();
- return result.getChangeId();
- }
-
- private void updateFirstParent(String changeId) throws Exception {
- ChangeInfo c = detailedChange(changeId);
- List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents;
- String parent1 = parents.get(0).commit;
- String parent2 = parents.get(1).commit;
- RevCommit commitParent2 = testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent2));
-
- testRepo.reset(parent1);
- PushOneCommit.Result newParent1 = createChange("new parent 1", "p1-1.txt", "content 1-1");
-
- PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo, changeId);
- merge.setParents(ImmutableList.of(newParent1.getCommit(), commitParent2));
- PushOneCommit.Result result = merge.to("refs/for/master");
- result.assertOkStatus();
-
- assertThat(getChangeKind(changeId)).isEqualTo(MERGE_FIRST_PARENT_UPDATE);
- }
-
- private void updateSecondParent(String changeId) throws Exception {
- ChangeInfo c = detailedChange(changeId);
- List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents;
- String parent1 = parents.get(0).commit;
- String parent2 = parents.get(1).commit;
- RevCommit commitParent1 = testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent1));
-
- testRepo.reset(parent2);
- PushOneCommit.Result newParent2 = createChange("new parent 2", "p2-2.txt", "content 2-2");
-
- PushOneCommit merge = pushFactory.create(admin.newIdent(), testRepo, changeId);
- merge.setParents(ImmutableList.of(commitParent1, newParent2.getCommit()));
- PushOneCommit.Result result = merge.to("refs/for/master");
- result.assertOkStatus();
-
- assertThat(getChangeKind(changeId)).isEqualTo(REWORK);
- }
-
- private String cherryPick(String changeId, ChangeKind changeKind) throws Exception {
- switch (changeKind) {
- case REWORK:
- case TRIVIAL_REBASE:
- break;
- case NO_CODE_CHANGE:
- case NO_CHANGE:
- case MERGE_FIRST_PARENT_UPDATE:
- default:
- assertWithMessage("unexpected change kind: " + changeKind).fail();
- }
-
- testRepo.reset(projectOperations.project(project).getHead("master"));
- PushOneCommit.Result r =
- pushFactory
- .create(
- admin.newIdent(),
- testRepo,
- PushOneCommit.SUBJECT,
- "other.txt",
- "new content " + System.nanoTime())
- .to("refs/for/master");
- r.assertOkStatus();
- vote(admin, r.getChangeId(), 2, 1);
- merge(r);
-
- String subject =
- TRIVIAL_REBASE.equals(changeKind)
- ? PushOneCommit.SUBJECT
- : "Reworked change " + System.nanoTime();
- CherryPickInput in = new CherryPickInput();
- in.destination = "master";
- in.message = String.format("%s\n\nChange-Id: %s", subject, changeId);
- ChangeInfo c = gApi.changes().id(changeId).current().cherryPick(in).get();
- return c.changeId;
- }
-
- private ChangeKind getChangeKind(String changeId) throws Exception {
- ChangeInfo c = gApi.changes().id(changeId).get(CURRENT_REVISION);
- return c.revisions.get(c.currentRevision).kind;
- }
-
private void vote(TestAccount user, String changeId, String label, int vote) throws Exception {
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(changeId).current().review(new ReviewInput().label(label, vote));
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 8b0f61e..81b9ba8 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -201,10 +201,10 @@
public void addRemoveMember() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
- gApi.groups().id(group.get()).addMembers("user");
+ gApi.groups().id(group.get()).addMembers("user1");
assertMembers(group.get(), user);
- gApi.groups().id(group.get()).removeMembers("user");
+ gApi.groups().id(group.get()).removeMembers("user1");
ImmutableSet<Account.Id> members = groupOperations.group(group).get().members();
assertThat(members).isEmpty();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 45a895a..b0de1c1 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -236,10 +236,10 @@
Permission.VIEW_PRIVATE_CHANGES,
403,
ImmutableList.of(
- "'user' can perform 'read' with force=false on project '"
+ "'user1' can perform 'read' with force=false on project '"
+ normalProject.get()
+ "' for ref 'refs/heads/*'",
- "'user' cannot perform 'viewPrivateChanges' with force=false on project '"
+ "'user1' cannot perform 'viewPrivateChanges' with force=false on project '"
+ normalProject.get()
+ "' for ref 'refs/heads/master'")),
// Test 2
@@ -248,7 +248,7 @@
normalProject.get(),
200,
ImmutableList.of(
- "'user' can perform 'read' with force=false on project '"
+ "'user1' can perform 'read' with force=false on project '"
+ normalProject.get()
+ "' for ref 'refs/heads/*'")),
// Test 3
@@ -257,10 +257,10 @@
secretProject.get(),
403,
ImmutableList.of(
- "'user' cannot perform 'read' with force=false on project '"
+ "'user1' cannot perform 'read' with force=false on project '"
+ secretProject.get()
+ "' for ref 'refs/heads/*' because this permission is blocked",
- "'user' cannot perform 'read' with force=false on project '"
+ "'user1' cannot perform 'read' with force=false on project '"
+ secretProject.get()
+ "' for ref 'refs/meta/version' because this permission is blocked")),
// Test 4
@@ -270,10 +270,10 @@
"refs/heads/secret/master",
403,
ImmutableList.of(
- "'user' can perform 'read' with force=false on project '"
+ "'user1' can perform 'read' with force=false on project '"
+ secretRefProject.get()
+ "' for ref 'refs/heads/*'",
- "'user' cannot perform 'read' with force=false on project '"
+ "'user1' cannot perform 'read' with force=false on project '"
+ secretRefProject.get()
+ "' for ref 'refs/heads/secret/master' because this permission is blocked")),
// Test 5
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 9bdc420..1a55b82 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
@@ -25,6 +26,7 @@
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
@@ -35,7 +37,10 @@
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.changes.FileApi;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -46,6 +51,8 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.webui.EditWebLink;
+import com.google.gerrit.server.patch.DiffOperations;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.inject.Inject;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
@@ -57,6 +64,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
+import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.imageio.ImageIO;
import org.eclipse.jgit.lib.ObjectId;
@@ -81,11 +89,14 @@
private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private DiffOperations diffOperations;
+ @Inject private ProjectOperations projectOperations;
private boolean intraline;
private boolean useNewDiffCacheListFiles;
private boolean useNewDiffCacheGetDiff;
+ private ObjectId initialCommit;
private ObjectId commit1;
private String changeId;
private String initialPatchSetId;
@@ -104,6 +115,8 @@
baseConfig.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
ObjectId headCommit = testRepo.getRepository().resolve("HEAD");
+ initialCommit = headCommit;
+
commit1 =
addCommit(headCommit, ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2));
@@ -124,10 +137,33 @@
assertDiffForNewFile(result, COMMIT_MSG, result.getCommit().getFullMessage());
}
- @Ignore
@Test
public void diffWithRootCommit() throws Exception {
- // TODO(ghareeb): Implement this test
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
+ .update();
+
+ testRepo.reset(initialCommit);
+ PushOneCommit push =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "subject", ImmutableMap.of("f.txt", "content"))
+ .noParent();
+ push.setForce(true);
+ PushOneCommit.Result result = push.to("refs/heads/master");
+
+ Map<String, FileDiffOutput> modifiedFiles =
+ diffOperations.listModifiedFilesAgainstParent(project, result.getCommit(), null);
+
+ assertThat(modifiedFiles.keySet()).containsExactly("/COMMIT_MSG", "f.txt");
+ assertThat(
+ modifiedFiles.values().stream()
+ .map(FileDiffOutput::oldCommitId)
+ .collect(Collectors.toSet()))
+ .containsExactly(EMPTY_TREE_ID);
+ assertThat(modifiedFiles.get("/COMMIT_MSG").changeType()).isEqualTo(Patch.ChangeType.ADDED);
+ assertThat(modifiedFiles.get("f.txt").changeType()).isEqualTo(Patch.ChangeType.ADDED);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
index 23bcdec..31292d5 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
@@ -36,7 +36,7 @@
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.events.ChangeMergedEvent;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.change.ChangeData;
@@ -396,7 +396,7 @@
.add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
.update();
- TestAccount user = accountCreator.user();
+ TestAccount user = accountCreator.user1();
String pushSpec = "refs/for/master%reviewer=" + user.email();
sender.clear();
@@ -433,7 +433,7 @@
.add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
.update();
- TestAccount user = accountCreator.user();
+ TestAccount user = accountCreator.user1();
String pushSpec = "refs/for/master%reviewer=" + user.email() + ",cc=" + user.email();
TestAccount user2 = accountCreator.user2();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index bf8de93..650334f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -63,10 +63,10 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 796ce38..d967f48 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -85,8 +85,8 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 36cd3cb..be94cdf 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -147,7 +147,7 @@
+ "' only matches inactive accounts. To use an inactive account, retry with one"
+ " of the following exact account IDs:\n"
+ user.id()
- + ": User <user@example.com>");
+ + ": User1 <user1@example.com>");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 5b31fd8..d480eb1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -28,6 +28,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
@@ -71,6 +72,7 @@
@NoHttpd
@UseClockStep(clockStepUnit = TimeUnit.MINUTES)
+@VerifyNoPiiInChangeNotes(true)
public class AttentionSetIT extends AbstractDaemonTest {
@Inject private ChangeOperations changeOperations;
@@ -703,7 +705,7 @@
assertThat(exception.getMessage())
.isEqualTo(
- "user can not be added/removed twice, and can not be added and removed at the same"
+ "user1 can not be added/removed twice, and can not be added and removed at the same"
+ " time");
}
@@ -720,7 +722,7 @@
assertThat(exception.getMessage())
.isEqualTo(
- "user can not be added/removed twice, and can not be added and removed at the same"
+ "user1 can not be added/removed twice, and can not be added and removed at the same"
+ " time");
}
@@ -757,7 +759,7 @@
assertThat(exception.getMessage())
.isEqualTo(
- "user can not be added/removed twice, and can not be added and removed at the same"
+ "user1 can not be added/removed twice, and can not be added and removed at the same"
+ " time");
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 6bffdf7..6a748a5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -51,20 +51,20 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void testChangeOwner_OwnerACLNotGranted() throws Exception {
assertApproveFails(user, createMyChange(testRepo));
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void testChangeOwner_OwnerACLGranted() throws Exception {
grantApproveToChangeOwner(project);
approve(user, createMyChange(testRepo));
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void testChangeOwner_NotOwnerACLGranted() throws Exception {
grantApproveToChangeOwner(project);
assertApproveFails(user2, createMyChange(testRepo));
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index ed21050..695bb90 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -60,14 +60,14 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void updateProjectConfig() throws Exception {
String id = testUpdateProjectConfig();
assertThat(gApi.changes().id(id).get().revisions).hasSize(1);
}
@Test
- @TestProjectInput(cloneAs = "user", submitType = SubmitType.CHERRY_PICK)
+ @TestProjectInput(cloneAs = "user1", submitType = SubmitType.CHERRY_PICK)
public void updateProjectConfigWithCherryPick() throws Exception {
String id = testUpdateProjectConfig();
assertThat(gApi.changes().id(id).get().revisions).hasSize(2);
@@ -102,7 +102,7 @@
}
@Test
- @TestProjectInput(cloneAs = "user")
+ @TestProjectInput(cloneAs = "user1")
public void onlyAdminMayUpdateProjectParent() throws Exception {
requestScopeOperations.setApiUser(admin.id());
ProjectInput parent = new ProjectInput();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java b/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java
index 2cb96e8..29dd227 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java
@@ -20,14 +20,21 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInfoDifference;
+import com.google.inject.Inject;
+import java.util.Collection;
import org.junit.Test;
public class GetMetaDiffIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
private static final String UNSAVED_REV_ID = "0000000000000000000000000000000000000001";
private static final String TOPIC = "topic";
@@ -197,4 +204,22 @@
assertThat(difference.added().currentRevision).isEqualTo(newInfo.currentRevision);
assertThat(difference.removed().currentRevision).isEqualTo(oldInfo.currentRevision);
}
+
+ @Test
+ public void staticField() throws Exception {
+ PushOneCommit.Result result = createChange();
+ ReviewInput in = new ReviewInput();
+ in.message("hello");
+
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(result.getChangeId()).revision("current").review(in);
+ ChangeApi chApi = gApi.changes().id(result.getChangeId());
+ ChangeInfoDifference difference = chApi.metaDiff(null, null, ListChangesOption.LABELS);
+ assertThat(difference.added().reviewers).containsKey(ReviewerState.CC);
+ assertThat(difference.added().reviewers).hasSize(1);
+ Collection<AccountInfo> reviewers = difference.added().reviewers.get(ReviewerState.CC);
+ assertThat(reviewers).hasSize(1);
+ AccountInfo info = reviewers.iterator().next();
+ assertThat(info._accountId).isEqualTo(user.id().get());
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index def4ed8..99aa273 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -67,7 +67,7 @@
TestAccount user2 = accountCreator.user2();
AccountGroup.UUID groupId = groupOperations.newGroup().name("test").create();
String group = groupOperations.group(groupId).get().name();
- gApi.groups().id(group).addMembers("admin", "user", user2.username());
+ gApi.groups().id(group).addMembers("admin", user.username(), user2.username());
// Create a project and restrict its visibility to the group
Project.NameKey p = projectOperations.newProject().create();
@@ -103,7 +103,7 @@
// Remove the user from the group so they can no longer see the project
requestScopeOperations.setApiUser(admin.id());
- gApi.groups().id(group).removeMembers("user");
+ gApi.groups().id(group).removeMembers(user.username());
// User can no longer see the change
requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 66eb48c..58e48e9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -20,6 +20,7 @@
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -107,6 +108,39 @@
}
@Test
+ @GerritConfig(name = "change.submitWholeTopic", value = "true")
+ public void submitTwoIndependentChangesWithFastForwardFail() throws Throwable {
+ RevCommit initialHead = projectOperations.project(project).getHead("master");
+ PushOneCommit.Result change1 = createChange("subject1", "file1.txt", "content", "topic");
+
+ testRepo.reset(initialHead);
+ PushOneCommit.Result change2 = createChange("subject2", "file2.txt", "content", "topic");
+
+ approve(change1.getChangeId());
+ approve(change2.getChangeId());
+
+ String fastForwardIndependentChangesError =
+ "Change could not be merged because the submission"
+ + " has two independent changes with the same destination branch. Independent changes can't "
+ + "be submitted to the same destination branch with FAST_FORWARD_ONLY submit strategy";
+
+ submitWithConflict(
+ change2.getChangeId(),
+ String.format(
+ "Failed to submit 2 changes due to the following problems:\n"
+ + "Change %d: %s\nChange %d: %s",
+ change1.getChange().getId().get(),
+ fastForwardIndependentChangesError,
+ change2.getChange().getId().get(),
+ fastForwardIndependentChangesError));
+
+ RevCommit updatedHead = projectOperations.project(project).getHead("master");
+ assertThat(updatedHead.getId()).isEqualTo(initialHead.getId());
+ assertRefUpdatedEvents();
+ assertChangeMergedEvents();
+ }
+
+ @Test
public void submitFastForwardNotPossible_Conflict() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
index 6a98b8b..dfe69f9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -430,6 +430,57 @@
}
@Test
+ public void createWithCopyCondition() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyCondition = "is:MAX";
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyCondition).isEqualTo("is:MAX");
+ }
+
+ @Test
+ public void createCopyConditionPerformsGroupVisibilityCheckWhenUserInPredicateIsUsed()
+ throws Exception {
+ String administratorsUUID = gApi.groups().query("name:Administrators").get().get(0).id;
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyCondition = "uploaderin:" + administratorsUUID;
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ // User can't see admin group
+ requestScopeOperations.setApiUser(user.id());
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("Group " + administratorsUUID + " not found");
+
+ // Admin can see admin group
+ requestScopeOperations.setApiUser(admin.id());
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(updatedLabel.copyCondition).isEqualTo(input.copyCondition);
+ }
+
+ @Test
+ public void createWithInvalidCopyCondition() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyCondition = "blarg::asd";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Bar").create(input));
+ assertThat(thrown).hasMessageThat().contains("unable to parse copy condition");
+ }
+
+ @Test
public void createWithCopyMaxScore() throws Exception {
LabelDefinitionInput input = new LabelDefinitionInput();
input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
index 2e68b54..b4938c1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
@@ -516,6 +516,83 @@
}
@Test
+ public void setCopyCondition() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyCondition).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyCondition = "is:MAX";
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyCondition).isEqualTo("is:MAX");
+ }
+
+ @Test
+ public void setCopyConditionPerformsGroupVisibilityCheckWhenUserInPredicateIsUsed()
+ throws Exception {
+ String administratorsUUID = gApi.groups().query("name:Administrators").get().get(0).id;
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyCondition).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyCondition = "uploaderin:" + administratorsUUID;
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ // User can't see admin group
+ requestScopeOperations.setApiUser(user.id());
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("foo").update(input));
+ assertThat(thrown).hasMessageThat().contains("Group " + administratorsUUID + " not found");
+
+ // Admin can see admin group
+ requestScopeOperations.setApiUser(admin.id());
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyCondition).isEqualTo(input.copyCondition);
+ }
+
+ @Test
+ public void setInvalidCopyCondition() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyCondition).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyCondition = "foo:::bar";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("foo").update(input));
+ assertThat(thrown).hasMessageThat().contains("unable to parse copy condition");
+ }
+
+ @Test
+ public void unsetCopyCondition() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateLabelType("foo", lt -> lt.setCopyCondition("is:MAX"));
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyCondition)
+ .isEqualTo("is:MAX");
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.unsetCopyCondition = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyCondition).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyCondition).isNull();
+ }
+
+ @Test
public void setCopyMinScore() throws Exception {
configLabel("foo", LabelFunction.NO_OP);
assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isNull();
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 00aacbb5..cbf8438 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -1453,7 +1453,7 @@
.to("refs/for/master");
// Add drafts with user scope
- requestScopeOperations.setApiUser(accountCreator.user().id());
+ requestScopeOperations.setApiUser(accountCreator.user1().id());
CommentInfo draft =
addDraft(
r1.getChangeId(),
@@ -1477,7 +1477,7 @@
.isEqualTo(String.format("Non-existing draft IDs: [%s]", draft.id));
// Request will succeed if done by user
- requestScopeOperations.setApiUser(accountCreator.user().id());
+ requestScopeOperations.setApiUser(accountCreator.user1().id());
gApi.changes().id(r1.getChangeId()).current().review(reviewInput);
assertThat(
gApi.changes().id(r1.getChangeId()).commentsRequest().getAsList().stream()
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
index 49b184b..df66e9f 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -48,7 +48,7 @@
@GerritConfig(name = "receiveemail.filter.mode", value = "ALLOW")
@GerritConfig(
name = "receiveemail.filter.patterns",
- values = {".+ser@example\\.com", "a@b\\.com"})
+ values = {".+ser1@example\\.com", "a@b\\.com"})
public void listFilterAllowDoesNotFilterListedUser() throws Exception {
ChangeInfo changeInfo = createChangeAndReplyByEmail();
// Check that the comments from the email have been persisted
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
new file mode 100644
index 0000000..23047a4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -0,0 +1,265 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
+
+import com.google.common.collect.MoreCollectors;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+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.LabelFunction;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
+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.restapi.RestApiException;
+import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class SubmitRequirementsEvaluatorIT extends AbstractDaemonTest {
+ @Inject SubmitRequirementsEvaluator evaluator;
+ @Inject private ProjectOperations projectOperations;
+ @Inject private Provider<InternalChangeQuery> changeQueryProvider;
+
+ private ChangeData changeData;
+ private String changeId;
+
+ @Before
+ public void setUp() throws Exception {
+ PushOneCommit.Result pushResult =
+ createChange(testRepo, "refs/heads/master", "Fix a bug", "file.txt", "content", "topic");
+ changeData = pushResult.getChange();
+ changeId = pushResult.getChangeId();
+ }
+
+ @Test
+ public void invalidExpression() throws Exception {
+ SubmitRequirementExpression expression =
+ SubmitRequirementExpression.create("invalid_field:invalid_value");
+ SubmitRequirementExpressionResult result = evaluator.evaluateExpression(expression, changeData);
+
+ assertThat(result.status()).isEqualTo(Status.ERROR);
+ assertThat(result.errorMessage().get())
+ .isEqualTo("Unsupported operator invalid_field:invalid_value");
+ }
+
+ @Test
+ public void expressionWithPassingPredicate() throws Exception {
+ SubmitRequirementExpression expression =
+ SubmitRequirementExpression.create("branch:refs/heads/master");
+ SubmitRequirementExpressionResult result = evaluator.evaluateExpression(expression, changeData);
+
+ assertThat(result.status()).isEqualTo(Status.PASS);
+ assertThat(result.errorMessage()).isEqualTo(Optional.empty());
+ }
+
+ @Test
+ public void expressionWithFailingPredicate() throws Exception {
+ SubmitRequirementExpression expression =
+ SubmitRequirementExpression.create("branch:refs/heads/foo");
+ SubmitRequirementExpressionResult result = evaluator.evaluateExpression(expression, changeData);
+
+ assertThat(result.status()).isEqualTo(Status.FAIL);
+ assertThat(result.errorMessage()).isEqualTo(Optional.empty());
+ }
+
+ @Test
+ public void compositeExpression() throws Exception {
+ SubmitRequirementExpression expression =
+ SubmitRequirementExpression.create(
+ String.format(
+ "(project:%s AND branch:refs/heads/foo) OR message:\"Fix a bug\"", project.get()));
+
+ SubmitRequirementExpressionResult result = evaluator.evaluateExpression(expression, changeData);
+
+ assertThat(result.status()).isEqualTo(Status.PASS);
+
+ assertThat(result.getPassingAtoms())
+ .containsExactly(
+ PredicateResult.builder()
+ .predicateString(String.format("project:%s", project.get()))
+ .status(true)
+ .build(),
+ PredicateResult.builder()
+ .predicateString("message:\"Fix a bug\"")
+ .status(true)
+ .build());
+
+ assertThat(result.getFailingAtoms())
+ .containsExactly(
+ PredicateResult.builder()
+ // TODO(ghareeb): querying "branch:" creates a RefPredicate. Fix names so that they
+ // match
+ .predicateString(String.format("ref:refs/heads/foo"))
+ .status(false)
+ .build());
+ }
+
+ @Test
+ public void submitRequirementIsNotApplicable_whenApplicabilityExpressionIsFalse()
+ throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:non-existent-project",
+ /* submittabilityExpr= */ "message:\"Fix bug\"",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.NOT_APPLICABLE);
+ }
+
+ @Test
+ public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsTrue() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "message:\"Fix a bug\"",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+
+ @Test
+ public void submitRequirementIsUnsatisfied_whenSubmittabilityExpressionIsFalse()
+ throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
+ }
+
+ @Test
+ public void submitRequirementIsOverridden_whenOverrideExpressionIsTrue() throws Exception {
+ addLabel("build-cop-override");
+ voteLabel(changeId, "build-cop-override", 1);
+
+ // Reload change data after applying the vote
+ changeData =
+ changeQueryProvider.get().byLegacyChangeId(changeData.getId()).stream()
+ .collect(MoreCollectors.onlyElement());
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* overrideExpr= */ "label:\"build-cop-override=+1\"");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.OVERRIDDEN);
+ }
+
+ @Test
+ public void submitRequirementIsError_whenApplicabilityExpressionHasInvalidSyntax()
+ throws Exception {
+ addLabel("build-cop-override");
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "invalid_field:invalid_value",
+ /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* overrideExpr= */ "label:\"build-cop-override=+1\"");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
+ assertThat(result.applicabilityExpressionResult().get().errorMessage().get())
+ .isEqualTo("Unsupported operator invalid_field:invalid_value");
+ }
+
+ @Test
+ public void submitRequirementIsError_whenSubmittabilityExpressionHasInvalidSyntax()
+ throws Exception {
+ addLabel("build-cop-override");
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "invalid_field:invalid_value",
+ /* overrideExpr= */ "label:\"build-cop-override=+1\"");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
+ assertThat(result.submittabilityExpressionResult().errorMessage().get())
+ .isEqualTo("Unsupported operator invalid_field:invalid_value");
+ }
+
+ @Test
+ public void submitRequirementIsError_whenOverrideExpressionHasInvalidSyntax() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* overrideExpr= */ "invalid_field:invalid_value");
+
+ SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
+ assertThat(result.overrideExpressionResult().get().errorMessage().get())
+ .isEqualTo("Unsupported operator invalid_field:invalid_value");
+ }
+
+ private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
+ gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
+ }
+
+ private void addLabel(String labelName) throws Exception {
+ configLabel(
+ project,
+ labelName,
+ LabelFunction.NO_OP,
+ value(1, "ok"),
+ value(0, "No score"),
+ value(-1, "Needs work"));
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allowLabel(labelName).ref("refs/heads/master").group(REGISTERED_USERS).range(-1, +1))
+ .update();
+ }
+
+ private SubmitRequirement createSubmitRequirement(
+ @Nullable String applicabilityExpr,
+ String submittabilityExpr,
+ @Nullable String overrideExpr) {
+ return SubmitRequirement.builder()
+ .setName("sr-name")
+ .setDescription(Optional.of("sr-description"))
+ .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
+ .setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
+ .setAllowOverrideInChildProjects(false)
+ .build();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
new file mode 100644
index 0000000..9392219
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -0,0 +1,274 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.query;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.change.ChangeKindCreator;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.approval.ApprovalContext;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
+import com.google.inject.Inject;
+import java.util.Date;
+import org.junit.Test;
+
+public class ApprovalQueryIT extends AbstractDaemonTest {
+ @Inject private ApprovalQueryBuilder queryBuilder;
+ @Inject private ChangeKindCreator changeKindCreator;
+ @Inject private ChangeNotes.Factory changeNotesFactory;
+ @Inject private ChangeKindCache changeKindCache;
+ @Inject private ChangeOperations changeOperations;
+
+ @Test
+ public void magicValuePredicate() throws Exception {
+ assertTrue(queryBuilder.parse("is:MAX").asMatchable().match(contextForCodeReviewLabel(2)));
+ assertTrue(queryBuilder.parse("is:mAx").asMatchable().match(contextForCodeReviewLabel(2)));
+ assertFalse(queryBuilder.parse("is:MAX").asMatchable().match(contextForCodeReviewLabel(-2)));
+ assertFalse(queryBuilder.parse("is:MAX").asMatchable().match(contextForCodeReviewLabel(1)));
+ assertFalse(queryBuilder.parse("is:MAX").asMatchable().match(contextForCodeReviewLabel(5000)));
+
+ assertTrue(queryBuilder.parse("is:MIN").asMatchable().match(contextForCodeReviewLabel(-2)));
+ assertTrue(queryBuilder.parse("is:mIn").asMatchable().match(contextForCodeReviewLabel(-2)));
+ assertFalse(queryBuilder.parse("is:MIN").asMatchable().match(contextForCodeReviewLabel(2)));
+ assertFalse(queryBuilder.parse("is:MIN").asMatchable().match(contextForCodeReviewLabel(-1)));
+ assertFalse(queryBuilder.parse("is:MIN").asMatchable().match(contextForCodeReviewLabel(5000)));
+
+ assertTrue(queryBuilder.parse("is:ANY").asMatchable().match(contextForCodeReviewLabel(-2)));
+ assertTrue(queryBuilder.parse("is:ANY").asMatchable().match(contextForCodeReviewLabel(2)));
+ assertTrue(queryBuilder.parse("is:aNy").asMatchable().match(contextForCodeReviewLabel(2)));
+ }
+
+ @Test
+ public void changeKindPredicate_noCodeChange() throws Exception {
+ String change = changeKindCreator.createChange(ChangeKind.NO_CODE_CHANGE, testRepo, admin);
+ changeKindCreator.updateChange(change, ChangeKind.NO_CODE_CHANGE, testRepo, admin, project);
+ PatchSet.Id ps1 =
+ PatchSet.id(Change.id(gApi.changes().id(change).get()._number), /* psId= */ 1);
+ assertTrue(
+ queryBuilder
+ .parse("changekind:no-code-change")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ -2, ps1, admin.id())));
+
+ changeKindCreator.updateChange(change, ChangeKind.TRIVIAL_REBASE, testRepo, admin, project);
+ PatchSet.Id ps2 =
+ PatchSet.id(Change.id(gApi.changes().id(change).get()._number), /* psId= */ 2);
+ assertFalse(
+ queryBuilder
+ .parse("changekind:no-code-change")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ -2, ps2, admin.id())));
+ }
+
+ @Test
+ public void changeKindPredicate_trivialRebase() throws Exception {
+ String change = changeKindCreator.createChange(ChangeKind.TRIVIAL_REBASE, testRepo, admin);
+ changeKindCreator.updateChange(change, ChangeKind.TRIVIAL_REBASE, testRepo, admin, project);
+ PatchSet.Id ps1 =
+ PatchSet.id(Change.id(gApi.changes().id(change).get()._number), /* psId= */ 1);
+ assertTrue(
+ queryBuilder
+ .parse("changekind:trivial-rebase")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ -2, ps1, admin.id())));
+
+ changeKindCreator.updateChange(change, ChangeKind.REWORK, testRepo, admin, project);
+ PatchSet.Id ps2 =
+ PatchSet.id(Change.id(gApi.changes().id(change).get()._number), /* psId= */ 2);
+ assertFalse(
+ queryBuilder
+ .parse("changekind:trivial-rebase")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ -2, ps2, admin.id())));
+ }
+
+ @Test
+ public void changeKindPredicate_reworkAndNotRework() throws Exception {
+ String change = changeKindCreator.createChange(ChangeKind.REWORK, testRepo, admin);
+ changeKindCreator.updateChange(change, ChangeKind.REWORK, testRepo, admin, project);
+ PatchSet.Id ps1 =
+ PatchSet.id(Change.id(gApi.changes().id(change).get()._number), /* psId= */ 1);
+ assertTrue(
+ queryBuilder
+ .parse("changekind:rework")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ -2, ps1, admin.id())));
+
+ changeKindCreator.updateChange(change, ChangeKind.REWORK, testRepo, admin, project);
+ PatchSet.Id ps2 =
+ PatchSet.id(Change.id(gApi.changes().id(change).get()._number), /* psId= */ 2);
+ assertFalse(
+ queryBuilder
+ .parse("-changekind:rework")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ -2, ps2, admin.id())));
+ }
+
+ @Test
+ public void uploaderInPredicate() throws Exception {
+ String administratorsUUID = gApi.groups().query("name:Administrators").get().get(0).id;
+
+ PushOneCommit.Result pushResult = createChange();
+ String changeCreatedByAdmin = pushResult.getChangeId();
+ approve(changeCreatedByAdmin);
+ // PS2 uploaded by admin
+ amendChange(changeCreatedByAdmin);
+ // PS3 uploaded by user
+ amendChangeWithUploader(pushResult, project, user).assertOkStatus();
+
+ // can copy approval from patchset 1 -> 2
+ assertTrue(
+ queryBuilder
+ .parse("uploaderin:" + administratorsUUID)
+ .asMatchable()
+ .match(
+ contextForCodeReviewLabel(
+ /* value= */ 2,
+ PatchSet.id(pushResult.getChange().getId(), /* psId= */ 1),
+ admin.id())));
+ // can not copy approval from patchset 2 -> 3
+ assertFalse(
+ queryBuilder
+ .parse("uploaderin:" + administratorsUUID)
+ .asMatchable()
+ .match(
+ contextForCodeReviewLabel(
+ /* value= */ 2,
+ PatchSet.id(pushResult.getChange().getId(), /* psId= */ 2),
+ admin.id())));
+ }
+
+ @Test
+ public void approverInPredicate() throws Exception {
+ String administratorsUUID = gApi.groups().query("name:Administrators").get().get(0).id;
+
+ PushOneCommit.Result pushResult = createChange();
+ amendChange(pushResult.getChangeId());
+ amendChange(pushResult.getChangeId());
+ // can copy approval from patchset 1 -> 2
+ assertTrue(
+ queryBuilder
+ .parse("approverin:" + administratorsUUID)
+ .asMatchable()
+ .match(
+ contextForCodeReviewLabel(
+ /* value= */ 2,
+ PatchSet.id(pushResult.getChange().getId(), /* psId= */ 1),
+ admin.id())));
+ // can not copy approval from patchset 2 -> 3
+ assertFalse(
+ queryBuilder
+ .parse("approverin:" + administratorsUUID)
+ .asMatchable()
+ .match(
+ contextForCodeReviewLabel(
+ /* value= */ 2,
+ PatchSet.id(pushResult.getChange().getId(), /* psId= */ 2),
+ user.id())));
+ }
+
+ @Test
+ public void userInPredicate_groupNotFound() {
+ QueryParseException thrown =
+ assertThrows(
+ QueryParseException.class,
+ () ->
+ queryBuilder
+ .parse("uploaderin:foobar")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ 2)));
+ assertThat(thrown).hasMessageThat().contains("Group foobar not found");
+ }
+
+ @Test
+ public void hasChangedFilesPredicate() throws Exception {
+ Change.Id changeId =
+ changeOperations.newChange().project(project).file("file").content("content").create();
+ changeOperations.change(changeId).newPatchset().file("file").content("new content").create();
+
+ // can copy approval from patch-set 1 -> 2
+ assertTrue(
+ queryBuilder
+ .parse("has:unchanged-files")
+ .asMatchable()
+ .match(
+ contextForCodeReviewLabel(
+ /* value= */ 2, PatchSet.id(changeId, /* psId= */ 1), admin.id())));
+ changeOperations.change(changeId).newPatchset().file("file").delete().create();
+
+ // can not copy approval from patch-set 2 -> 3
+ assertFalse(
+ queryBuilder
+ .parse("has:unchanged-files")
+ .asMatchable()
+ .match(
+ contextForCodeReviewLabel(
+ /* value= */ 2, PatchSet.id(changeId, /* psId= */ 2), admin.id())));
+ }
+
+ @Test
+ public void hasChangedFilesPredicate_unsupportedOperator() {
+ QueryParseException thrown =
+ assertThrows(
+ QueryParseException.class,
+ () ->
+ queryBuilder
+ .parse("has:invalid")
+ .asMatchable()
+ .match(contextForCodeReviewLabel(/* value= */ 2)));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ "'invalid' is not a supported argument for has. only 'unchanged-files' is supported");
+ }
+
+ private ApprovalContext contextForCodeReviewLabel(int value) throws Exception {
+ PushOneCommit.Result result = createChange();
+ amendChange(result.getChangeId());
+ PatchSet.Id psId = PatchSet.id(result.getChange().getId(), 1);
+ return contextForCodeReviewLabel(value, psId, admin.id());
+ }
+
+ private ApprovalContext contextForCodeReviewLabel(
+ int value, PatchSet.Id psId, Account.Id approver) {
+ ChangeNotes changeNotes = changeNotesFactory.create(project, psId.changeId());
+ PatchSet.Id newPsId = PatchSet.id(psId.changeId(), psId.get() + 1);
+ ChangeKind changeKind =
+ changeKindCache.getChangeKind(
+ changeNotes.getChange(), changeNotes.getPatchSets().get(newPsId));
+ PatchSetApproval approval =
+ PatchSetApproval.builder()
+ .postSubmit(false)
+ .granted(new Date())
+ .key(PatchSetApproval.key(psId, approver, LabelId.create("Code-Review")))
+ .value(value)
+ .build();
+ return ApprovalContext.create(changeNotes, approval, newPsId, changeKind);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/query/BUILD b/javatests/com/google/gerrit/acceptance/server/query/BUILD
new file mode 100644
index 0000000..f7d13a0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/query/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "server_query",
+ labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 09b0438..4352fe8 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -18,9 +18,11 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.client.ReviewerState;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import org.junit.Test;
@@ -305,6 +307,32 @@
buildObjectWithFullFields(ChangeInfo.class);
}
+ @Test
+ public void getDiff_arrayListInMap() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+
+ AccountInfo i1 = new AccountInfo();
+ i1._accountId = 1;
+ AccountInfo i2 = new AccountInfo();
+ i2._accountId = 2;
+
+ ArrayList<AccountInfo> a1 = new ArrayList<>();
+ ArrayList<AccountInfo> a2 = new ArrayList<>();
+
+ a1.add(i1);
+ a2.add(i1);
+ a2.add(i2);
+ oldChangeInfo.reviewers = ImmutableMap.of(ReviewerState.REVIEWER, a1);
+ newChangeInfo.reviewers = ImmutableMap.of(ReviewerState.REVIEWER, a2);
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+ assertThat(diff.added().reviewers).hasSize(1);
+ assertThat(diff.added().reviewers).containsKey(ReviewerState.REVIEWER);
+ assertThat(diff.added().reviewers.get(ReviewerState.REVIEWER)).containsExactly(i2);
+ assertThat(diff.removed().reviewers).isNull();
+ }
+
private static Object buildObjectWithFullFields(Class<?> c) throws Exception {
if (c == null) {
return null;
diff --git a/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
index f9df375..b114acc 100644
--- a/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
+++ b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
@@ -114,7 +114,7 @@
setUpUserAuthentication(admin.username());
// Create non-admin user
- TestAccount user = accountCreator.user();
+ TestAccount user = accountCreator.user1();
setUpUserAuthentication(user.username());
// Prepare data for new change on master branch
diff --git a/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java b/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java
index c33ef8e..ad8a486 100644
--- a/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java
+++ b/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java
@@ -64,7 +64,7 @@
private void setup(ServerContext ctx) throws Exception {
ctx.getInjector().injectMembers(this);
- TestAccount user = accountCreator.user();
+ TestAccount user = accountCreator.user1();
gApi.accounts().id(user.username()).setHttpPassword(PASSWORD);
String canonical = config.getString("gerrit", null, "canonicalweburl");
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
index ad460cd..614dcf0 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
@@ -35,6 +35,7 @@
.setIgnoreSelfApproval(!LabelType.DEF_IGNORE_SELF_APPROVAL)
.setRefPatterns(ImmutableList.of("refs/heads/*", "refs/tags/*"))
.setDefaultValue((short) 1)
+ .setCopyCondition("is:ANY")
.setCopyAnyScore(!LabelType.DEF_COPY_ANY_SCORE)
.setCopyMaxScore(!LabelType.DEF_COPY_MAX_SCORE)
.setCopyMinScore(!LabelType.DEF_COPY_MIN_SCORE)
@@ -57,7 +58,8 @@
@Test
public void roundTripWithMinimalValues() {
- LabelType autoValue = ALL_VALUES_SET.toBuilder().setRefPatterns(null).build();
+ LabelType autoValue =
+ ALL_VALUES_SET.toBuilder().setRefPatterns(null).setCopyCondition(null).build();
assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
}
}
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 c2e8e0c..a1dee1a 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
@@ -29,7 +29,7 @@
.setName("code-review")
.setDescription(Optional.of("require code review +2"))
.setApplicabilityExpression(SubmitRequirementExpression.of("branch(refs/heads/master)"))
- .setBlockingExpression(SubmitRequirementExpression.create("label(code-review, 2+)"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label(code-review, 2+)"))
.setOverrideExpression(Optional.empty())
.setAllowOverrideInChildProjects(true)
.build();
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
new file mode 100644
index 0000000..f105cf1
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
@@ -0,0 +1,124 @@
+// 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.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.util.time.TimeUtil;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChangeNotesCommitTest extends AbstractChangeNotesTest {
+ private TestRepository<InMemoryRepository> testRepo;
+ private ChangeNotesCommit.ChangeNotesRevWalk walk;
+
+ @Before
+ public void setUpTestRepo() throws Exception {
+ testRepo = new TestRepository<>(repo);
+ walk = ChangeNotesCommit.newRevWalk(repo);
+ }
+
+ @After
+ public void tearDownTestRepo() throws Exception {
+ walk.close();
+ }
+
+ @Test
+ public void attentionSetCommitOnlyWhenNoChangeMessageIsPresentAndCorrectFooter()
+ throws Exception {
+ RevCommit commit =
+ writeCommit(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Attention: {\"person_ident\":\"Gerrit User 1000000 \\u003c1000000@adce0b11-8f2e-4ab6-ac69-e675f183d871\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}");
+
+ newParser(commit).parseAll();
+ assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(false)).isEqualTo(true);
+ }
+
+ @Test
+ public void noAttentionSetCommitOnlyWhenNoChangeMessageIsPresentAndFooterNotOnlyAS()
+ throws Exception {
+ RevCommit commit =
+ writeCommit(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Subject: Change subject\n"
+ + "Attention: {\"person_ident\":\"Gerrit User 1000000 \\u003c1000000@adce0b11-8f2e-4ab6-ac69-e675f183d871\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}");
+
+ newParser(commit).parseAll();
+ assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(false)).isEqualTo(false);
+ }
+
+ @Test
+ public void noAttentionSetCommitOnlyWhenNoChangeMessageIsPresentAndGenericFooter()
+ throws Exception {
+ RevCommit commit = writeCommit("Update patch set 1\n" + "\n" + "Patch-set: 1\n");
+
+ newParser(commit).parseAll();
+ assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(false)).isEqualTo(false);
+ }
+
+ @Test
+ public void noAttentionSetCommitOnlyWhenChangeMessageIsPresent() throws Exception {
+ RevCommit commit =
+ writeCommit(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Attention: {\"person_ident\":\"Gerrit User 1000000 \\u003c1000000@adce0b11-8f2e-4ab6-ac69-e675f183d871\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}");
+
+ newParser(commit).parseAll();
+ assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(true)).isEqualTo(false);
+ }
+
+ private ChangeNotesParser newParser(ObjectId tip) throws Exception {
+ walk.reset();
+ ChangeNoteJson changeNoteJson = injector.getInstance(ChangeNoteJson.class);
+ return new ChangeNotesParser(newChange().getId(), tip, walk, changeNoteJson, args.metrics);
+ }
+
+ private RevCommit writeCommit(String body) throws Exception {
+ Change change = newChange(true);
+ ChangeNotes notes = newNotes(change).load();
+ ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
+ PersonIdent author =
+ noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent);
+ try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
+ CommitBuilder cb = new CommitBuilder();
+ cb.setParentId(notes.getRevision());
+ cb.setAuthor(author);
+ cb.setCommitter(new PersonIdent(serverIdent, author.getWhen()));
+ cb.setTreeId(testRepo.tree());
+ cb.setMessage(body);
+ ObjectId id = ins.insert(cb);
+ ins.flush();
+ RevCommit commit = walk.parseCommit(id);
+ walk.parseBody(commit);
+ return commit;
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 5bfe97c..6a32fa1 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -497,6 +497,73 @@
}
@Test
+ public void attentionSetOnlyShouldNotCountTowardsMaxUpdatesLimit() throws Exception {
+ RevCommit commit =
+ writeCommit(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Attention: {\"person_ident\":\"Gerrit User 1000000 \\u003c1000000@adce0b11-8f2e-4ab6-ac69-e675f183d871\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}",
+ false);
+ ChangeNotesParser changeNotesParser = newParser(commit);
+ changeNotesParser.parseAll();
+ final boolean hasChangeMessage = false;
+ assertThat(
+ changeNotesParser.countTowardsMaxUpdatesLimit(
+ (ChangeNotesCommit) commit, hasChangeMessage))
+ .isEqualTo(false);
+ }
+
+ @Test
+ public void attentionSetWithExtraFooterShouldCountTowardsMaxUpdatesLimit() throws Exception {
+ RevCommit commit =
+ writeCommit(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Subject: Change subject\n"
+ + "Attention: {\"person_ident\":\"Gerrit User 1000000 \\u003c1000000@adce0b11-8f2e-4ab6-ac69-e675f183d871\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}",
+ false);
+ ChangeNotesParser changeNotesParser = newParser(commit);
+ changeNotesParser.parseAll();
+ final boolean hasChangeMessage = false;
+ assertThat(
+ changeNotesParser.countTowardsMaxUpdatesLimit(
+ (ChangeNotesCommit) commit, hasChangeMessage))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void changeWithoutAttentionSetShouldCountTowardsMaxUpdatesLimit() throws Exception {
+ RevCommit commit = writeCommit("Update WIP change\n" + "\n" + "Patch-set: 1\n", true);
+ ChangeNotesParser changeNotesParser = newParser(commit);
+ changeNotesParser.parseAll();
+ final boolean hasChangeMessage = false;
+ assertThat(
+ changeNotesParser.countTowardsMaxUpdatesLimit(
+ (ChangeNotesCommit) commit, hasChangeMessage))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void attentionSetWithCommentShouldCountTowardsMaxUpdatesLimit() throws Exception {
+ RevCommit commit =
+ writeCommit(
+ "Update patch set 1\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Attention: {\"person_ident\":\"Gerrit User 1000000 \\u003c1000000@adce0b11-8f2e-4ab6-ac69-e675f183d871\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}",
+ false);
+ ChangeNotesParser changeNotesParser = newParser(commit);
+ changeNotesParser.parseAll();
+ final boolean hasChangeMessage = true;
+ assertThat(
+ changeNotesParser.countTowardsMaxUpdatesLimit(
+ (ChangeNotesCommit) commit, hasChangeMessage))
+ .isEqualTo(true);
+ }
+
+ @Test
public void caseInsensitiveFooters() throws Exception {
assertParseSucceeds(
"Update change\n"
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
new file mode 100644
index 0000000..dbd1939
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.CommentRange;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.server.util.time.TimeUtil;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class ChangeUpdateTest extends AbstractChangeNotesTest {
+
+ @Test
+ public void bypassMaxUpdatesShouldBeTrueWhenChangingAttentionSetOnly() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+
+ // Add to attention set
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(
+ otherUser.getAccountId(), AttentionSetUpdate.Operation.ADD, "test");
+ update.addToPlannedAttentionSetUpdates(ImmutableSet.of(attentionSetUpdate));
+
+ update.commit();
+
+ assertThat(update.bypassMaxUpdates()).isTrue();
+ }
+
+ @Test
+ public void bypassMaxUpdatesShouldBeTrueWhenClosingChange() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+
+ update.setStatus(Change.Status.ABANDONED);
+
+ update.commit();
+
+ assertThat(update.bypassMaxUpdates()).isTrue();
+ }
+
+ @Test
+ public void bypassMaxUpdatesShouldBeFalseWhenNotAbandoningChangeAndNotChangingAttentionSetOnly()
+ throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+
+ update.commit();
+
+ assertThat(update.bypassMaxUpdates()).isFalse();
+ }
+
+ @Test
+ public void bypassMaxUpdatesShouldBeFalseWhenCommentsAndChangesToAttentionSetCoexist()
+ throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+
+ // Add to attention set
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(
+ otherUser.getAccountId(), AttentionSetUpdate.Operation.ADD, "test");
+ update.addToPlannedAttentionSetUpdates(ImmutableSet.of(attentionSetUpdate));
+
+ // Add a comment
+ RevCommit commit = tr.commit().message("PS2").create();
+ update.putComment(
+ HumanComment.Status.PUBLISHED,
+ newComment(
+ c.currentPatchSetId(),
+ "a.txt",
+ "uuid1",
+ new CommentRange(1, 2, 3, 4),
+ 1,
+ changeOwner,
+ null,
+ TimeUtil.nowTs(),
+ "Comment",
+ (short) 1,
+ commit,
+ false));
+ update.commit();
+
+ assertThat(update.bypassMaxUpdates()).isFalse();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java b/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java
new file mode 100644
index 0000000..e4c2196
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java
@@ -0,0 +1,143 @@
+// 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.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.update.ChainedReceiveCommands;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Test;
+
+public class OpenRepoTest extends AbstractChangeNotesTest {
+
+ private final Optional<Integer> NO_UPDATES_AT_ALL = Optional.of(0);
+ private final Optional<Integer> ONLY_ONE_UPDATE = Optional.of(1);
+ private final Optional<Integer> ONLY_TWO_UPDATES = Optional.of(2);
+ private final Optional<Integer> MAX_PATCH_SETS = Optional.empty();
+
+ private FakeChainedReceiveCommands fakeChainedReceiveCommands;
+
+ @Override
+ public void setUpTestEnvironment() throws Exception {
+ super.setUpTestEnvironment();
+ fakeChainedReceiveCommands = new FakeChainedReceiveCommands(repo);
+ }
+
+ @Test
+ public void throwExceptionWhenExceedingMaxUpdatesLimit() throws Exception {
+ try (OpenRepo openRepo = openRepo()) {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setStatus(Change.Status.NEW);
+
+ ListMultimap<String, ChangeUpdate> changeUpdates =
+ new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("one", update).build();
+
+ assertThrows(
+ LimitExceededException.class,
+ () -> openRepo.addUpdates(changeUpdates, NO_UPDATES_AT_ALL, MAX_PATCH_SETS));
+ }
+ }
+
+ @Test
+ public void allowExceedingLimitWhenAttentionSetUpdateOnly() throws Exception {
+ try (OpenRepo openRepo = openRepo()) {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setStatus(Change.Status.NEW);
+
+ // Add to attention set
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(
+ otherUser.getAccountId(), AttentionSetUpdate.Operation.ADD, "test");
+ update.addToPlannedAttentionSetUpdates(ImmutableSet.of(attentionSetUpdate));
+
+ ListMultimap<String, ChangeUpdate> changeUpdates =
+ new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("one", update).build();
+
+ openRepo.addUpdates(changeUpdates, NO_UPDATES_AT_ALL, MAX_PATCH_SETS);
+
+ assertThat(fakeChainedReceiveCommands.commands.size()).isEqualTo(1);
+ }
+ }
+
+ @Test
+ public void attentionSetUpdateShouldNotContributeToOperationsCount() throws Exception {
+ try (OpenRepo openRepo = openRepo()) {
+ Change c1 = newChange();
+
+ ChangeUpdate update1 = newUpdateForNewChange(c1, changeOwner);
+ // Add to attention set
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(
+ otherUser.getAccountId(), AttentionSetUpdate.Operation.ADD, "test");
+ update1.addToPlannedAttentionSetUpdates(ImmutableSet.of(attentionSetUpdate));
+
+ ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
+ update2.setStatus(Change.Status.NEW);
+
+ ListMultimap<String, ChangeUpdate> changeUpdates =
+ new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
+
+ openRepo.addUpdates(changeUpdates, ONLY_TWO_UPDATES, MAX_PATCH_SETS);
+
+ assertThat(fakeChainedReceiveCommands.commands.size()).isEqualTo(1);
+ }
+ }
+
+ @Test
+ public void normalChangeShouldContributeToOperationsCount() throws Exception {
+ try (OpenRepo openRepo = openRepo()) {
+ Change c1 = newChange();
+
+ ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
+ update2.setStatus(Change.Status.NEW);
+
+ ListMultimap<String, ChangeUpdate> changeUpdates =
+ new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
+
+ assertThrows(
+ LimitExceededException.class,
+ () -> openRepo.addUpdates(changeUpdates, ONLY_ONE_UPDATE, MAX_PATCH_SETS));
+ }
+ }
+
+ private static class FakeChainedReceiveCommands extends ChainedReceiveCommands {
+ Map<String, ReceiveCommand> commands = new HashMap<>();
+
+ public FakeChainedReceiveCommands(Repository repo) {
+ super(repo);
+ }
+
+ @Override
+ public void add(ReceiveCommand cmd) {
+ commands.put(cmd.getRefName(), cmd);
+ }
+ }
+
+ private OpenRepo openRepo() {
+ return new OpenRepo(repo, rw, null, fakeChainedReceiveCommands, false);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 9dad9ae..2e934e9 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -212,11 +212,11 @@
"[submitRequirement \"Code-review\"]\n"
+ " description = At least one Code Review +2\n"
+ " applicabilityExpression = branch(refs/heads/master)\n"
- + " blockingExpression = label(code-review, +2)\n"
+ + " submittabilityExpression = label(code-review, +2)\n"
+ "[submitRequirement \"api-review\"]\n"
+ " description = Additional review required for API modifications\n"
+ " applicabilityExpression = commit_filepath_contains(\\\"/api/.*\\\")\n"
- + " blockingExpression = label(api-review, +2)\n"
+ + " submittabilityExpression = label(api-review, +2)\n"
+ " overrideExpression = label(build-cop-override, +1)\n"
+ " canOverrideInChildProjects = true\n")
.create();
@@ -231,7 +231,8 @@
.setDescription(Optional.of("At least one Code Review +2"))
.setApplicabilityExpression(
SubmitRequirementExpression.of("branch(refs/heads/master)"))
- .setBlockingExpression(SubmitRequirementExpression.create("label(code-review, +2)"))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label(code-review, +2)"))
.setOverrideExpression(Optional.empty())
.setAllowOverrideInChildProjects(false)
.build(),
@@ -241,7 +242,8 @@
.setDescription(Optional.of("Additional review required for API modifications"))
.setApplicabilityExpression(
SubmitRequirementExpression.of("commit_filepath_contains(\"/api/.*\")"))
- .setBlockingExpression(SubmitRequirementExpression.create("label(api-review, +2)"))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label(api-review, +2)"))
.setOverrideExpression(
SubmitRequirementExpression.of("label(build-cop-override, +1)"))
.setAllowOverrideInChildProjects(true)
@@ -256,7 +258,7 @@
.add(
"project.config",
"[submitRequirement \"code-review\"]\n"
- + " blockingExpression = label(code-review, +2)\n")
+ + " submittabilityExpression = label(code-review, +2)\n")
.create();
ProjectConfig cfg = read(rev);
@@ -266,7 +268,8 @@
"code-review",
SubmitRequirement.builder()
.setName("code-review")
- .setBlockingExpression(SubmitRequirementExpression.create("label(code-review, +2)"))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label(code-review, +2)"))
.setAllowOverrideInChildProjects(false)
.build());
}
@@ -280,10 +283,10 @@
"project.config",
"[submitRequirement \"code-review\"]\n"
+ " description = At least one Code Review +2\n"
- + " blockingExpression = label(code-review, +2)\n"
+ + " submittabilityExpression = label(code-review, +2)\n"
+ "[submitRequirement \"Code-Review\"]\n"
+ " description = Another code review label\n"
- + " blockingExpression = label(code-review, +2)\n"
+ + " submittabilityExpression = label(code-review, +2)\n"
+ " canOverrideInChildProjects = true\n")
.create();
@@ -295,7 +298,8 @@
SubmitRequirement.builder()
.setName("code-review")
.setDescription(Optional.of("At least one Code Review +2"))
- .setBlockingExpression(SubmitRequirementExpression.create("label(code-review, +2)"))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label(code-review, +2)"))
.setAllowOverrideInChildProjects(false)
.build());
assertThat(cfg.getValidationErrors()).hasSize(1);
@@ -307,7 +311,7 @@
}
@Test
- public void readSubmitRequirementNoBlockingExpression() throws Exception {
+ public void readSubmitRequirementNoSubmittabilityExpression() throws Exception {
RevCommit rev =
tr.commit()
.add("groups", group(developers))
@@ -323,7 +327,7 @@
assertThat(cfg.getValidationErrors()).hasSize(1);
assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
.isEqualTo(
- "project.config: Submit requirement \"code-review\" does not define a blocking expression."
+ "project.config: Submit requirement \"code-review\" does not define a submittability expression."
+ " Skipping this requirement.");
}
@@ -942,7 +946,7 @@
"[submitRequirement \"code-review\"]\n"
+ " description = At least one Code Review +2\n"
+ " applicabilityExpression = branch(refs/heads/master)\n"
- + " blockingExpression = label(code-review, +2)\n"
+ + " submittabilityExpression = label(code-review, +2)\n"
+ "[notify \"name\"]\n"
+ " email = example@example.com\n")
.create();
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index f6b3317..b7be40b 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -607,7 +607,6 @@
@Test
public void reindex() throws Exception {
AccountInfo user1 = newAccountWithFullName("tester", "Test Usre");
-
// update account without reindex so that account index is stale
Account.Id accountId = Account.id(user1._accountId);
String newName = "Test User";
@@ -621,10 +620,11 @@
.setAccountDelta(AccountDelta.builder().setFullName(newName).build())
.commit(md);
}
-
- assertQuery("name:" + quote(user1.name), user1);
- assertQuery("name:" + quote(newName));
-
+ // Querying for the account here will not result in a stale document because
+ // we load AccountStates from the cache after reading documents from the index
+ // which means we always read fresh data when matching.
+ //
+ // Reindex document
gApi.accounts().id(user1.username).index();
assertQuery("name:" + quote(user1.name));
assertQuery("name:" + quote(newName), user1);
diff --git a/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
index b742bd8..31d256e 100644
--- a/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
@@ -14,10 +14,6 @@
package com.google.gerrit.server.query.account;
-import static org.junit.Assume.assumeFalse;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -25,7 +21,6 @@
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -52,16 +47,4 @@
fakeConfig.setString("index", null, "type", "fake");
return Guice.createInjector(new InMemoryModule(fakeConfig));
}
-
- @Override
- protected void validateAssumptions() {
- // TODO(hiesel): Account predicates are always matching (they return true on match), so we need
- // to skip all tests here. We are doing this to document existing behavior. We want to remove
- // this assume statement and make group predicates matchable.
- assumeFalse(
- AccountPredicates.equalsName("test")
- .asMatchable()
- .match(
- AccountState.forAccount(Account.builder(Account.id(1), new Timestamp(0)).build())));
- }
}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index 385d4b2..1e23420 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -18,8 +18,6 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.eclipse.jgit.lib.Config;
-import org.junit.Ignore;
-import org.junit.Test;
/**
* Test against {@link com.google.gerrit.index.testing.AbstractFakeIndex}. This test might seem
@@ -34,156 +32,4 @@
fakeConfig.setString("index", null, "type", "fake");
return Guice.createInjector(new InMemoryModule(fakeConfig));
}
-
- @Ignore
- @Test
- @Override
- public void byDefault() throws Exception {
- // TODO(hiesel): Fix bug in predicate and remove @Ignore.
- super.byDefault();
- }
-
- @Ignore
- @Test
- @Override
- public void byMergedBefore() throws Exception {
- // TODO(hiesel): Used predicate is not a matchable. Fix.
- super.byMergedBefore();
- }
-
- @Ignore
- @Test
- @Override
- public void reviewerAndCcByEmail() throws Exception {
- // TODO(hiesel): Fix bug in predicate and remove @Ignore.
- super.reviewerAndCcByEmail();
- }
-
- @Ignore
- @Test
- @Override
- public void byMessageExact() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.byMessageExact();
- }
-
- @Ignore
- @Test
- @Override
- public void fullTextWithNumbers() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.fullTextWithNumbers();
- }
-
- @Ignore
- @Test
- @Override
- public void byTriplet() throws Exception {
- // TODO(hiesel): Fix bug in predicate and remove @Ignore.
- super.byTriplet();
- }
-
- @Ignore
- @Test
- @Override
- public void byAge() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.byAge();
- }
-
- @Ignore
- @Test
- @Override
- public void byMessageSubstring() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.byMessageSubstring();
- }
-
- @Ignore
- @Test
- @Override
- public void byBeforeUntil() throws Exception {
- // TODO(hiesel): Used predicate is not a matchable. Fix.
- super.byBeforeUntil();
- }
-
- @Ignore
- @Test
- @Override
- public void byTopic() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.byTopic();
- }
-
- @Ignore
- @Test
- @Override
- public void userQuery() throws Exception {
- // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
- super.userQuery();
- }
-
- @Ignore
- @Test
- @Override
- public void visible() throws Exception {
- // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
- super.visible();
- }
-
- @Ignore
- @Test
- @Override
- public void userDestination() throws Exception {
- // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
- super.userDestination();
- }
-
- @Ignore
- @Test
- @Override
- public void byAfterSince() throws Exception {
- // TODO(hiesel): Used predicate is not a matchable. Fix.
- super.byAfterSince();
- }
-
- @Ignore
- @Test
- @Override
- public void byMessageMixedCase() throws Exception {
- // TODO(hiesel): Used predicate is not a matchable. Fix.
- super.byMessageMixedCase();
- }
-
- @Ignore
- @Test
- @Override
- public void byCommit() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.byCommit();
- }
-
- @Ignore
- @Test
- @Override
- public void byComment() throws Exception {
- // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
- super.byComment();
- }
-
- @Ignore
- @Test
- @Override
- public void byMergedAfter() throws Exception {
- // TODO(hiesel): Used predicate is not a matchable. Fix.
- super.byMergedAfter();
- }
-
- @Ignore
- @Test
- @Override
- public void byOwnerInvalidQuery() throws Exception {
- // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
- super.byMergedAfter();
- }
}
diff --git a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
index 8bc1c30..e4f228a 100644
--- a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.query.group;
-import static org.junit.Assume.assumeTrue;
-
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -48,12 +46,4 @@
fakeConfig.setString("index", null, "type", "fake");
return Guice.createInjector(new InMemoryModule(fakeConfig));
}
-
- @Override
- protected void validateAssumptions() {
- // TODO(hiesel): Group predicates are not matchable, so we need to skip all tests here.
- // We are doing this to document existing behavior. We want to remove this assume statement and
- // make group predicates matchable.
- assumeTrue(GroupPredicates.inname("test").isMatchable());
- }
}
diff --git a/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
index 8517ad2..6fc0568 100644
--- a/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.query.project;
-import static org.junit.Assume.assumeTrue;
-
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -50,12 +48,4 @@
fakeConfig.setString("index", null, "type", "fake");
return Guice.createInjector(new InMemoryModule(fakeConfig));
}
-
- @Override
- protected void validateAssumptions() {
- // TODO(hiesel): Project predicates are not matchable, so we need to skip all tests here.
- // We are doing this to document existing behavior. We want to remove this assume statement and
- // make group predicates matchable.
- assumeTrue(ProjectPredicates.inname("test").isMatchable());
- }
}
diff --git a/plugins/replication b/plugins/replication
index 13cefb7..dc9bb2e 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 13cefb724df786d254ecbc24261589ab473be267
+Subproject commit dc9bb2e946e4c6c31e8a4665f30eca6d00017523
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index fb0390a..35e6449 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit fb0390a8b49f0d601e11f8a1ac0658c429727f21
+Subproject commit 35e6449a517691a880c94e7467bc07360f8e6666
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index 454b3d5..2682158 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {CommentRange} from './core';
export declare interface ChecksPluginApi {
/**
@@ -382,6 +383,11 @@
links?: Link[];
/**
+ * Links to lines of code. The referenced path must be part of this patchset.
+ */
+ codePointers?: CodePointer[];
+
+ /**
* Callbacks to the plugin. Must be implemented individually by each
* plugin. Actions are rendered as buttons. If there are more than two actions
* per result, then further actions are put into an overflow menu. Sort order
@@ -430,6 +436,11 @@
icon: LinkIcon;
}
+export declare interface CodePointer {
+ path: string;
+ range: CommentRange;
+}
+
export enum LinkIcon {
EXTERNAL = 'external',
IMAGE = 'image',
@@ -438,4 +449,5 @@
DOWNLOAD_MOBILE = 'download_mobile',
HELP_PAGE = 'help_page',
REPORT_BUG = 'report_bug',
+ CODE = 'code',
}
diff --git a/polygerrit-ui/app/api/core.ts b/polygerrit-ui/app/api/core.ts
index 5820139..af7fc40 100644
--- a/polygerrit-ui/app/api/core.ts
+++ b/polygerrit-ui/app/api/core.ts
@@ -45,3 +45,25 @@
/** The character position in the end line. (0-based) */
end_character: number;
}
+
+/**
+ * Return type for cursor moves, that indicate whether a move was possible.
+ */
+export enum CursorMoveResult {
+ /** The cursor was successfully moved. */
+ MOVED,
+ /** There were no stops - the cursor was reset. */
+ NO_STOPS,
+ /**
+ * There was no more matching stop to move to - the cursor was clipped to the
+ * end.
+ */
+ CLIPPED,
+ /** The abort condition would have been fulfilled for the new target. */
+ ABORTED,
+}
+
+/** A sentinel that can be inserted to disallow moving across. */
+export class AbortStop {}
+
+export type Stop = HTMLElement | AbortStop;
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 8e1ffef..ad83fb8 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -20,7 +20,7 @@
* limitations under the License.
*/
-import {CommentRange} from './core';
+import {CommentRange, CursorMoveResult} from './core';
/**
* Diff type in preferences
@@ -287,6 +287,11 @@
lineNum: LineNumber;
}
+export declare interface LineNumberEventDetail {
+ side: Side;
+ lineNum: LineNumber;
+}
+
/** All types of button for expanding diff sections */
export enum ContextButtonType {
ABOVE = 'above',
@@ -400,3 +405,38 @@
className: string
): void;
}
+
+// The current setup requires API users to register GrDiff instances with the
+// cursor, but we do not at this point want to expose the API that GrDiffCursor
+// uses to the public as it is likely to change. So for now, we allow any type
+// and cast. This works fine so long as API users do provide whatever the
+// gr-diff tag creates.
+export type GrDiff = unknown;
+
+/** A service to interact with the line cursor in gr-diff instances. */
+export declare interface GrDiffCursor {
+ replaceDiffs(diffs: GrDiff[]): void;
+ unregisterDiff(diff: GrDiff): void;
+
+ isAtStart(): boolean;
+ isAtEnd(): boolean;
+
+ moveLeft(): void;
+ moveRight(): void;
+
+ moveDown(): CursorMoveResult;
+ moveUp(): CursorMoveResult;
+
+ moveToFirstChunk(): void;
+ moveToLastChunk(): void;
+
+ moveToNextChunk(): CursorMoveResult;
+ moveToPreviousChunk(): CursorMoveResult;
+
+ moveToNextCommentThread(): CursorMoveResult;
+ moveToPreviousCommentThread(): CursorMoveResult;
+
+ createCommentInPlace(): void;
+ resetScrollMode(): void;
+ moveToLineNumber(lineNum: number, side: Side, path?: string): void;
+}
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index b9918d3..b1b7f34 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -20,12 +20,13 @@
* limitations under the License.
*/
-import {DiffLayer, GrAnnotation} from './diff';
+import {DiffLayer, GrAnnotation, GrDiffCursor} from './diff';
declare global {
interface Window {
grdiff: {
GrAnnotation: GrAnnotation;
+ GrDiffCursor: {new (): GrDiffCursor};
TokenHighlightLayer: {new (): DiffLayer};
};
}
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 0738825..d22026a 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -90,3 +90,9 @@
// This measures the same interval as ExpandAllDiffs, but the result is divided by the number of diffs expanded.
FILE_EXPAND_ALL_AVG = 'ExpandAllPerDiff',
}
+
+export enum Interaction {
+ TOGGLE_SHOW_ALL_BUTTON = 'toggle show all button',
+ SHOW_TAB = 'show-tab',
+ ATTENTION_SET_CHIP = 'attention-set-chip',
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index 8a4cbbe..a9de24a 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -118,19 +118,15 @@
assert.isTrue(saveStub.called);
});
- test('_getRepoBranchesSuggestions empty', done => {
- element._getRepoBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
+ test('_getRepoBranchesSuggestions empty', async () => {
+ const branches = await element._getRepoBranchesSuggestions('nonexistent');
+ assert.equal(branches.length, 0);
});
- test('_getRepoBranchesSuggestions non-empty', done => {
- element._getRepoBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
+ test('_getRepoBranchesSuggestions non-empty', async () => {
+ const branches = await element._getRepoBranchesSuggestions('test-branch');
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
});
test('_computeBranchClass', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
index c8e058e..4864af5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
@@ -23,20 +23,22 @@
const basicFixture = fixtureFromElement('gr-repo-list');
-let counter;
-const repoGenerator = () => {
+function createRepo(name, counter) {
return {
- id: `test${++counter}`,
- name: `test`,
+ id: `${name}${counter}`,
+ name: `${name}`,
state: 'ACTIVE',
web_links: [
{
name: 'diffusion',
- url: `https://phabricator.example.org/r/project/test${counter}`,
+ url: `https://phabricator.example.org/r/project/${name}${counter}`,
},
],
};
-};
+}
+
+let counter;
+const repoGenerator = () => createRepo('test', ++counter);
suite('gr-repo-list tests', () => {
let element;
@@ -123,6 +125,15 @@
done();
});
});
+
+ test('filter is case insensitive', async () => {
+ const repoStub = stubRestApi('getRepos');
+ const repos = [createRepo('aSDf', 0)];
+ repoStub.withArgs('asdf').returns(Promise.resolve(repos));
+ element._filter = 'asdf';
+ await element._getRepos('asdf', 25, 0);
+ assert.equal(element._repos.length, 1);
+ });
});
suite('loading', () => {
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 2869750..fa3c9c6 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
@@ -48,6 +48,7 @@
} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
import {pluralize} from '../../../utils/string-util';
+import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
enum ChangeSize {
XS = 10,
@@ -105,7 +106,7 @@
changeURL?: string;
@property({type: Array, computed: '_changeStatuses(change)'})
- statuses?: string[];
+ statuses?: ChangeStates[];
@property({type: Boolean})
showStar = false;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index d6f4bdd..41a7598 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -252,7 +252,7 @@
})
.catch(err => {
fireTitleChange(this, title || this._computeTitle(user));
- console.warn(err);
+ this.reporting.error(err);
})
.then(() => {
this._loading = false;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
index 1924a66..42a6847 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
@@ -29,7 +29,7 @@
</style>
<gr-avatar
account="[[_accountDetails]]"
- image-size="100"
+ imageSize="100"
aria-label="Account avatar"
></gr-avatar>
<div class="info">
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 1790651..02e4161 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -30,6 +30,7 @@
} from '../../../test/test-data-generators';
import {ChangeStatus, HttpMethod} from '../../../constants/constants';
import {
+ mockPromise,
query,
queryAll,
queryAndAssert,
@@ -144,13 +145,11 @@
return element.reload();
});
- test('show-revision-actions event should fire', done => {
+ test('show-revision-actions event should fire', async () => {
const spy = sinon.spy(element, '_sendShowRevisionActions');
element.reload();
- flush(() => {
- assert.isTrue(spy.called);
- done();
- });
+ await flush();
+ assert.isTrue(spy.called);
});
test('primary and secondary actions split properly', () => {
@@ -198,7 +197,7 @@
);
});
- test('plugin revision actions', done => {
+ test('plugin revision actions', async () => {
const stub = stubRestApi('getChangeActionURL').returns(
Promise.resolve('the-url')
);
@@ -206,20 +205,18 @@
'plugin~action': {},
};
assert.isOk(element.revisionActions['plugin~action']);
- flush(() => {
- assert.isTrue(
- stub.calledWith(
- element.changeNum,
- element.latestPatchNum,
- '/plugin~action'
- )
- );
- assert.equal(
- (element.revisionActions['plugin~action'] as UIActionInfo)!.__url,
- 'the-url'
- );
- done();
- });
+ await flush();
+ assert.isTrue(
+ stub.calledWith(
+ element.changeNum,
+ element.latestPatchNum,
+ '/plugin~action'
+ )
+ );
+ assert.equal(
+ (element.revisionActions['plugin~action'] as UIActionInfo)!.__url,
+ 'the-url'
+ );
});
test('plugin change actions', async () => {
@@ -266,71 +263,61 @@
);
});
- test('hide revision action', done => {
- flush(() => {
- const buttonEl = queryAndAssert(element, '[data-action-key="submit"]');
- assert.isOk(buttonEl);
- element.setActionHidden(
- element.ActionType.REVISION,
- element.RevisionActions.SUBMIT,
- true
- );
- assert.lengthOf(element._hiddenActions, 1);
- element.setActionHidden(
- element.ActionType.REVISION,
- element.RevisionActions.SUBMIT,
- true
- );
- assert.lengthOf(element._hiddenActions, 1);
- flush(() => {
- const buttonEl = element.shadowRoot?.querySelector(
- '[data-action-key="submit"]'
- );
- assert.isNotOk(buttonEl);
+ test('hide revision action', async () => {
+ await flush();
+ let buttonEl: Element | undefined = queryAndAssert(
+ element,
+ '[data-action-key="submit"]'
+ );
+ assert.isOk(buttonEl);
+ element.setActionHidden(
+ element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT,
+ true
+ );
+ assert.lengthOf(element._hiddenActions, 1);
+ element.setActionHidden(
+ element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT,
+ true
+ );
+ assert.lengthOf(element._hiddenActions, 1);
+ await flush();
+ buttonEl = query(element, '[data-action-key="submit"]');
+ assert.isNotOk(buttonEl);
- element.setActionHidden(
- element.ActionType.REVISION,
- element.RevisionActions.SUBMIT,
- false
- );
- flush(() => {
- const buttonEl = queryAndAssert(
- element,
- '[data-action-key="submit"]'
- );
- assert.isFalse(buttonEl.hasAttribute('hidden'));
- done();
- });
- });
- });
+ element.setActionHidden(
+ element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT,
+ false
+ );
+ await flush();
+ buttonEl = queryAndAssert(element, '[data-action-key="submit"]');
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
});
- test('buttons exist', done => {
+ test('buttons exist', async () => {
element._loading = false;
- flush(() => {
- const buttonEls = queryAll(element, 'gr-button');
- const menuItems = element.$.moreActions.items;
+ await flush();
+ const buttonEls = queryAll(element, 'gr-button');
+ const menuItems = element.$.moreActions.items;
- // Total button number is one greater than the number of total actions
- // due to the existence of the overflow menu trigger.
- assert.equal(
- buttonEls!.length + menuItems!.length,
- element._allActionValues.length + 1
- );
- assert.isFalse(element.hidden);
- done();
- });
+ // Total button number is one greater than the number of total actions
+ // due to the existence of the overflow menu trigger.
+ assert.equal(
+ buttonEls!.length + menuItems!.length,
+ element._allActionValues.length + 1
+ );
+ assert.isFalse(element.hidden);
});
- test('delete buttons have explicit labels', done => {
- flush(() => {
- const deleteItems = element.$.moreActions.items!.filter(item =>
- item.id!.startsWith('delete')
- );
- assert.equal(deleteItems.length, 1);
- assert.equal(deleteItems[0].name, 'Delete change');
- done();
- });
+ test('delete buttons have explicit labels', async () => {
+ await flush();
+ const deleteItems = element.$.moreActions.items!.filter(item =>
+ item.id!.startsWith('delete')
+ );
+ assert.equal(deleteItems.length, 1);
+ assert.equal(deleteItems[0].name, 'Delete change');
});
test('get revision object from change', () => {
@@ -369,7 +356,7 @@
assert.deepEqual(result, actions);
});
- test('submit change', () => {
+ test('submit change', async () => {
const showSpy = sinon.spy(element, '_showActionDialog');
stubRestApi('getFromProjectLookup').returns(
Promise.resolve('test' as RepoName)
@@ -390,12 +377,15 @@
);
tap(submitButton);
- flush();
+ await flush();
assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
});
- test('submit change, tap on icon', done => {
- sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake(done);
+ test('submit change, tap on icon', async () => {
+ const submitted = mockPromise();
+ sinon
+ .stub(element.$.confirmSubmitDialog, 'resetFocus')
+ .callsFake(() => submitted.resolve());
stubRestApi('getFromProjectLookup').returns(
Promise.resolve('test' as RepoName)
);
@@ -414,6 +404,7 @@
'gr-button[data-action-key="submit"] iron-icon'
);
tap(submitIcon);
+ await submitted;
});
test('_handleSubmitConfirm', () => {
@@ -435,19 +426,16 @@
assert.isFalse(fireStub.called);
});
- test('submit change with plugin hook', done => {
+ test('submit change with plugin hook', async () => {
sinon.stub(element, '_canSubmitChange').callsFake(() => false);
const fireActionStub = sinon.stub(element, '_fireAction');
- flush(() => {
- const submitButton = queryAndAssert(
- element,
- 'gr-button[data-action-key="submit"]'
- );
- tap(submitButton);
- assert.equal(fireActionStub.callCount, 0);
-
- done();
- });
+ await flush();
+ const submitButton = queryAndAssert(
+ element,
+ 'gr-button[data-action-key="submit"]'
+ );
+ tap(submitButton);
+ assert.equal(fireActionStub.callCount, 0);
});
test('chain state', () => {
@@ -490,55 +478,51 @@
);
});
- test('rebase change', done => {
+ test('rebase change', async () => {
const fireActionStub = sinon.stub(element, '_fireAction');
const fetchChangesStub = sinon
.stub(element.$.confirmRebase, 'fetchRecentChanges')
.returns(Promise.resolve([]));
element._hasKnownChainState = true;
- flush(() => {
- const rebaseButton = queryAndAssert(
- element,
- 'gr-button[data-action-key="rebase"]'
- );
- tap(rebaseButton);
- const rebaseAction = {
- __key: 'rebase',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Rebase',
- method: HttpMethod.POST,
- title: 'Rebase onto tip of branch or parent change',
- };
- assert.isTrue(fetchChangesStub.called);
- element._handleRebaseConfirm(
- new CustomEvent('', {detail: {base: '1234'}})
- );
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/rebase',
- assertUIActionInfo(rebaseAction),
- true,
- {base: '1234'},
- ]);
- done();
- });
+ await flush();
+ const rebaseButton = queryAndAssert(
+ element,
+ 'gr-button[data-action-key="rebase"]'
+ );
+ tap(rebaseButton);
+ const rebaseAction = {
+ __key: 'rebase',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Rebase',
+ method: HttpMethod.POST,
+ title: 'Rebase onto tip of branch or parent change',
+ };
+ assert.isTrue(fetchChangesStub.called);
+ element._handleRebaseConfirm(
+ new CustomEvent('', {detail: {base: '1234'}})
+ );
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/rebase',
+ assertUIActionInfo(rebaseAction),
+ true,
+ {base: '1234'},
+ ]);
});
- test('rebase change fires reload event', done => {
+ test('rebase change fires reload event', async () => {
const eventStub = sinon.stub(element, 'dispatchEvent');
element._handleResponse(
{__key: 'rebase', __type: ActionType.CHANGE, label: 'l'},
new Response()
);
- flush(() => {
- assert.isTrue(eventStub.called);
- assert.equal(eventStub.lastCall.args[0].type, 'reload');
- done();
- });
+ await flush();
+ assert.isTrue(eventStub.called);
+ assert.equal(eventStub.lastCall.args[0].type, 'reload');
});
- test("rebase dialog gets recent changes each time it's opened", done => {
+ test("rebase dialog gets recent changes each time it's opened", async () => {
const fetchChangesStub = sinon
.stub(element.$.confirmRebase, 'fetchRecentChanges')
.returns(Promise.resolve([]));
@@ -550,17 +534,15 @@
tap(rebaseButton);
assert.isTrue(fetchChangesStub.calledOnce);
- flush(() => {
- element.$.confirmRebase.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: true,
- })
- );
- tap(rebaseButton);
- assert.isTrue(fetchChangesStub.calledTwice);
- done();
- });
+ await flush();
+ element.$.confirmRebase.dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ tap(rebaseButton);
+ assert.isTrue(fetchChangesStub.calledTwice);
});
test('two dialogs are not shown at the same time', async () => {
@@ -624,7 +606,7 @@
});
suite('change edits', () => {
- test('disableEdit', () => {
+ test('disableEdit', async () => {
element.set('editMode', false);
element.set('editPatchsetLoaded', false);
element.change = {
@@ -632,7 +614,7 @@
status: ChangeStatus.NEW,
};
element.set('disableEdit', true);
- flush();
+ await flush();
assert.isNotOk(
query(element, 'gr-button[data-action-key="publishEdit"]')
@@ -647,7 +629,7 @@
assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
});
- test('shows confirm dialog for delete edit', () => {
+ test('shows confirm dialog for delete edit', async () => {
element.set('editMode', true);
element.set('editPatchsetLoaded', true);
@@ -660,12 +642,12 @@
'gr-button[primary]'
)
);
- flush();
+ await flush();
assert.equal(fireActionStub.lastCall.args[0], '/edit');
});
- test('edit patchset is loaded, needs rebase', () => {
+ test('edit patchset is loaded, needs rebase', async () => {
element.set('editMode', true);
element.set('editPatchsetLoaded', true);
element.change = {
@@ -673,7 +655,7 @@
status: ChangeStatus.NEW,
};
element.editBasedOnCurrentPatchSet = false;
- flush();
+ await flush();
assert.isNotOk(
query(element, 'gr-button[data-action-key="publishEdit"]')
@@ -684,7 +666,7 @@
assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
});
- test('edit patchset is loaded, does not need rebase', () => {
+ test('edit patchset is loaded, does not need rebase', async () => {
element.set('editMode', true);
element.set('editPatchsetLoaded', true);
element.change = {
@@ -692,7 +674,7 @@
status: ChangeStatus.NEW,
};
element.editBasedOnCurrentPatchSet = true;
- flush();
+ await flush();
assert.isOk(query(element, 'gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(
@@ -703,14 +685,14 @@
assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
});
- test('edit mode is loaded, no edit patchset', () => {
+ test('edit mode is loaded, no edit patchset', async () => {
element.set('editMode', true);
element.set('editPatchsetLoaded', false);
element.change = {
...createChangeViewChange(),
status: ChangeStatus.NEW,
};
- flush();
+ await flush();
assert.isNotOk(
query(element, 'gr-button[data-action-key="publishEdit"]')
@@ -725,14 +707,14 @@
assert.isOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
});
- test('normal patch set', () => {
+ test('normal patch set', async () => {
element.set('editMode', false);
element.set('editPatchsetLoaded', false);
element.change = {
...createChangeViewChange(),
status: ChangeStatus.NEW,
};
- flush();
+ await flush();
assert.isNotOk(
query(element, 'gr-button[data-action-key="publishEdit"]')
@@ -747,16 +729,17 @@
assert.isNotOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
});
- test('edit action', done => {
+ test('edit action', async () => {
+ const editTapped = mockPromise();
element.addEventListener('edit-tap', () => {
- done();
+ editTapped.resolve();
});
element.set('editMode', true);
element.change = {
...createChangeViewChange(),
status: ChangeStatus.NEW,
};
- flush();
+ await flush();
assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
assert.isOk(query(element, 'gr-button[data-action-key="stopEdit"]'));
@@ -764,7 +747,7 @@
...createChangeViewChange(),
status: ChangeStatus.MERGED,
};
- flush();
+ await flush();
assert.isNotOk(query(element, 'gr-button[data-action-key="edit"]'));
element.change = {
@@ -772,13 +755,14 @@
status: ChangeStatus.NEW,
};
element.set('editMode', false);
- flush();
+ await flush();
const editButton = queryAndAssert(
element,
'gr-button[data-action-key="edit"]'
);
tap(editButton);
+ await editTapped;
});
});
@@ -896,62 +880,57 @@
status: ChangeStatus.NEW,
},
];
- setup(done => {
+ setup(async () => {
stubRestApi('getChanges').returns(Promise.resolve(changes));
element._handleCherrypickTap();
- flush(() => {
- const radioButtons = queryAll(
- element.$.confirmCherrypick,
- "input[name='cherryPickOptions']"
- );
- assert.equal(radioButtons.length, 2);
- tap(radioButtons[1]);
- flush(() => {
- done();
- });
- });
+ await flush();
+ const radioButtons = queryAll(
+ element.$.confirmCherrypick,
+ "input[name='cherryPickOptions']"
+ );
+ assert.equal(radioButtons.length, 2);
+ tap(radioButtons[1]);
+ await flush();
});
- test('cherry pick topic dialog is rendered', done => {
+ test('cherry pick topic dialog is rendered', async () => {
const dialog = element.$.confirmCherrypick;
- flush(() => {
- const changesTable = queryAndAssert(dialog, 'table');
- const headers = Array.from(changesTable.querySelectorAll('th'));
- const expectedHeadings = [
- '',
- 'Change',
- 'Status',
- 'Subject',
- 'Project',
- 'Progress',
- '',
- ];
- const headings = headers.map(header => header.innerText);
- assert.equal(headings.length, expectedHeadings.length);
- for (let i = 0; i < headings.length; i++) {
- assert.equal(headings[i].trim(), expectedHeadings[i]);
- }
- const changeRows = queryAll(changesTable, 'tbody > tr');
- const change = Array.from(changeRows[0].querySelectorAll('td')).map(
- e => e.innerText
- );
- const expectedChange = [
- '',
- '1234567890',
- 'MERGED',
- 'random',
- 'A',
- 'NOT STARTED',
- '',
- ];
- for (let i = 0; i < change.length; i++) {
- assert.equal(change[i].trim(), expectedChange[i]);
- }
- done();
- });
+ await flush();
+ const changesTable = queryAndAssert(dialog, 'table');
+ const headers = Array.from(changesTable.querySelectorAll('th'));
+ const expectedHeadings = [
+ '',
+ 'Change',
+ 'Status',
+ 'Subject',
+ 'Project',
+ 'Progress',
+ '',
+ ];
+ const headings = headers.map(header => header.innerText);
+ assert.equal(headings.length, expectedHeadings.length);
+ for (let i = 0; i < headings.length; i++) {
+ assert.equal(headings[i].trim(), expectedHeadings[i]);
+ }
+ const changeRows = queryAll(changesTable, 'tbody > tr');
+ const change = Array.from(changeRows[0].querySelectorAll('td')).map(
+ e => e.innerText
+ );
+ const expectedChange = [
+ '',
+ '1234567890',
+ 'MERGED',
+ 'random',
+ 'A',
+ 'NOT STARTED',
+ '',
+ ];
+ for (let i = 0; i < change.length; i++) {
+ assert.equal(change[i].trim(), expectedChange[i]);
+ }
});
- test('changes with duplicate project show an error', done => {
+ test('changes with duplicate project show an error', async () => {
const dialog = element.$.confirmCherrypick;
const error = queryAndAssert(
dialog,
@@ -974,13 +953,11 @@
project: 'A' as RepoName,
},
]);
- flush(() => {
- assert.equal(
- error.innerText,
- 'Two changes cannot be of the same' + ' project'
- );
- done();
- });
+ await flush();
+ assert.equal(
+ error.innerText,
+ 'Two changes cannot be of the same' + ' project'
+ );
});
});
});
@@ -1021,24 +998,24 @@
});
});
- test('custom actions', done => {
+ test('custom actions', async () => {
// Add a button with the same key as a server-based one to ensure
// collisions are taken care of.
const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
- element.addEventListener(key + '-tap', e => {
+ const keyTapped = mockPromise();
+ element.addEventListener(key + '-tap', async e => {
assert.equal(
(e as CustomEvent).detail.node.getAttribute('data-action-key'),
key
);
element.removeActionButton(key);
- flush(() => {
- assert.notOk(query(element, '[data-action-key="' + key + '"]'));
- done();
- });
+ await flush();
+ assert.notOk(query(element, '[data-action-key="' + key + '"]'));
+ keyTapped.resolve();
});
- flush(() => {
- tap(queryAndAssert(element, '[data-action-key="' + key + '"]'));
- });
+ await flush();
+ tap(queryAndAssert(element, '[data-action-key="' + key + '"]'));
+ await keyTapped;
});
test('_setLoadingOnButtonWithKey top-level', () => {
@@ -1095,32 +1072,28 @@
return element.reload();
});
- test('abandon change with message', done => {
+ test('abandon change with message', async () => {
const newAbandonMsg = 'Test Abandon Message';
element.$.confirmAbandonDialog.message = newAbandonMsg;
- flush(() => {
- const abandonButton = queryAndAssert(
- element,
- 'gr-button[data-action-key="abandon"]'
- );
- tap(abandonButton);
+ await flush();
+ const abandonButton = queryAndAssert(
+ element,
+ 'gr-button[data-action-key="abandon"]'
+ );
+ tap(abandonButton);
- assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
- done();
- });
+ assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
});
- test('abandon change with no message', done => {
- flush(() => {
- const abandonButton = queryAndAssert(
- element,
- 'gr-button[data-action-key="abandon"]'
- );
- tap(abandonButton);
+ test('abandon change with no message', async () => {
+ await flush();
+ const abandonButton = queryAndAssert(
+ element,
+ 'gr-button[data-action-key="abandon"]'
+ );
+ tap(abandonButton);
- assert.isUndefined(element.$.confirmAbandonDialog.message);
- done();
- });
+ assert.isUndefined(element.$.confirmAbandonDialog.message);
});
test('works', () => {
@@ -1173,7 +1146,7 @@
return element.reload();
});
- test('revert change with plugin hook', done => {
+ test('revert change with plugin hook', async () => {
const newRevertMsg = 'Modified revert msg';
sinon
.stub(element.$.confirmRevertDialog, '_modifyRevertMsg')
@@ -1204,17 +1177,14 @@
'_populateRevertSubmissionMessage'
)
.callsFake(() => 'original msg');
- flush(() => {
- const revertButton = queryAndAssert(
- element,
- 'gr-button[data-action-key="revert"]'
- );
- tap(revertButton);
- flush(() => {
- assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
- done();
- });
- });
+ await flush();
+ const revertButton = queryAndAssert(
+ element,
+ 'gr-button[data-action-key="revert"]'
+ );
+ tap(revertButton);
+ await flush();
+ assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
});
suite('revert change submitted together', () => {
@@ -1243,60 +1213,56 @@
);
});
- test('confirm revert dialog shows both options', done => {
+ test('confirm revert dialog shows both options', async () => {
const revertButton = queryAndAssert(
element,
'gr-button[data-action-key="revert"]'
);
tap(revertButton);
- flush(() => {
- assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- const revertSingleChangeLabel = queryAndAssert(
- confirmRevertDialog,
- '.revertSingleChange'
- ) as HTMLLabelElement;
- const revertSubmissionLabel = queryAndAssert(
- confirmRevertDialog,
- '.revertSubmission'
- ) as HTMLLabelElement;
- assert(
- revertSingleChangeLabel.innerText.trim() ===
- 'Revert single change'
- );
- assert(
- revertSubmissionLabel.innerText.trim() ===
- 'Revert entire submission (2 Changes)'
- );
- let expectedMsg =
- 'Revert submission 199 0' +
- '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>' +
- '\n' +
- 'Reverted Changes:' +
- '\n' +
- '1234567890:random' +
- '\n' +
- '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
- '\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
- const radioInputs = queryAll(
- confirmRevertDialog,
- 'input[name="revertOptions"]'
- );
- tap(radioInputs[0]);
- flush(() => {
- expectedMsg =
- 'Revert "random commit message"\n\nThis reverts ' +
- 'commit 2000.\n\nReason' +
- ' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
- done();
- });
- });
+ await flush();
+ assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ const revertSingleChangeLabel = queryAndAssert(
+ confirmRevertDialog,
+ '.revertSingleChange'
+ ) as HTMLLabelElement;
+ const revertSubmissionLabel = queryAndAssert(
+ confirmRevertDialog,
+ '.revertSubmission'
+ ) as HTMLLabelElement;
+ assert(
+ revertSingleChangeLabel.innerText.trim() === 'Revert single change'
+ );
+ assert(
+ revertSubmissionLabel.innerText.trim() ===
+ 'Revert entire submission (2 Changes)'
+ );
+ let expectedMsg =
+ 'Revert submission 199 0' +
+ '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>' +
+ '\n' +
+ 'Reverted Changes:' +
+ '\n' +
+ '1234567890:random' +
+ '\n' +
+ '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '\n';
+ assert.equal(confirmRevertDialog._message, expectedMsg);
+ const radioInputs = queryAll(
+ confirmRevertDialog,
+ 'input[name="revertOptions"]'
+ );
+ tap(radioInputs[0]);
+ await flush();
+ expectedMsg =
+ 'Revert "random commit message"\n\nThis reverts ' +
+ 'commit 2000.\n\nReason' +
+ ' for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, expectedMsg);
});
- test('submit fails if message is not edited', done => {
+ test('submit fails if message is not edited', async () => {
const revertButton = queryAndAssert(
element,
'gr-button[data-action-key="revert"]'
@@ -1304,69 +1270,58 @@
const confirmRevertDialog = element.$.confirmRevertDialog;
tap(revertButton);
const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
- flush(() => {
- const confirmButton = queryAndAssert(
- queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
- '#confirm'
- );
- tap(confirmButton);
- flush(() => {
- assert.isTrue(confirmRevertDialog._showErrorMessage);
- assert.isFalse(fireStub.called);
- done();
- });
- });
+ await flush();
+ const confirmButton = queryAndAssert(
+ queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
+ '#confirm'
+ );
+ tap(confirmButton);
+ await flush();
+ assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isFalse(fireStub.called);
});
- test('message modification is retained on switching', done => {
+ test('message modification is retained on switching', async () => {
const revertButton = queryAndAssert(
element,
'gr-button[data-action-key="revert"]'
);
const confirmRevertDialog = element.$.confirmRevertDialog;
tap(revertButton);
- flush(() => {
- const radioInputs = queryAll(
- confirmRevertDialog,
- 'input[name="revertOptions"]'
- );
- const revertSubmissionMsg =
- 'Revert submission 199 0' +
- '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>' +
- '\n' +
- 'Reverted Changes:' +
- '\n' +
- '1234567890:random' +
- '\n' +
- '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
- '\n';
- const singleChangeMsg =
- 'Revert "random commit message"\n\nThis reverts ' +
- 'commit 2000.\n\nReason' +
- ' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
- const newRevertMsg = revertSubmissionMsg + 'random';
- const newSingleChangeMsg = singleChangeMsg + 'random';
- confirmRevertDialog._message = newRevertMsg;
- tap(radioInputs[0]);
- flush(() => {
- assert.equal(confirmRevertDialog._message, singleChangeMsg);
- confirmRevertDialog._message = newSingleChangeMsg;
- tap(radioInputs[1]);
- flush(() => {
- assert.equal(confirmRevertDialog._message, newRevertMsg);
- tap(radioInputs[0]);
- flush(() => {
- assert.equal(
- confirmRevertDialog._message,
- newSingleChangeMsg
- );
- done();
- });
- });
- });
- });
+ await flush();
+ const radioInputs = queryAll(
+ confirmRevertDialog,
+ 'input[name="revertOptions"]'
+ );
+ const revertSubmissionMsg =
+ 'Revert submission 199 0' +
+ '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>' +
+ '\n' +
+ 'Reverted Changes:' +
+ '\n' +
+ '1234567890:random' +
+ '\n' +
+ '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '\n';
+ const singleChangeMsg =
+ 'Revert "random commit message"\n\nThis reverts ' +
+ 'commit 2000.\n\nReason' +
+ ' for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
+ const newRevertMsg = revertSubmissionMsg + 'random';
+ const newSingleChangeMsg = singleChangeMsg + 'random';
+ confirmRevertDialog._message = newRevertMsg;
+ tap(radioInputs[0]);
+ await flush();
+ assert.equal(confirmRevertDialog._message, singleChangeMsg);
+ confirmRevertDialog._message = newSingleChangeMsg;
+ tap(radioInputs[1]);
+ await flush();
+ assert.equal(confirmRevertDialog._message, newRevertMsg);
+ tap(radioInputs[0]);
+ await flush();
+ assert.equal(confirmRevertDialog._message, newSingleChangeMsg);
});
});
@@ -1389,7 +1344,7 @@
);
});
- test('submit fails if message is not edited', done => {
+ test('submit fails if message is not edited', async () => {
const revertButton = queryAndAssert(
element,
'gr-button[data-action-key="revert"]'
@@ -1397,55 +1352,46 @@
const confirmRevertDialog = element.$.confirmRevertDialog;
tap(revertButton);
const fireStub = sinon.stub(confirmRevertDialog, 'dispatchEvent');
- flush(() => {
- const confirmButton = queryAndAssert(
- queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
- '#confirm'
- );
- tap(confirmButton);
- flush(() => {
- assert.isTrue(confirmRevertDialog._showErrorMessage);
- assert.isFalse(fireStub.called);
- done();
- });
- });
+ await flush();
+ const confirmButton = queryAndAssert(
+ queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
+ '#confirm'
+ );
+ tap(confirmButton);
+ await flush();
+ assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isFalse(fireStub.called);
});
- test('confirm revert dialog shows no radio button', done => {
+ test('confirm revert dialog shows no radio button', async () => {
const revertButton = queryAndAssert(
element,
'gr-button[data-action-key="revert"]'
);
tap(revertButton);
- flush(() => {
- const confirmRevertDialog = element.$.confirmRevertDialog;
- const radioInputs = queryAll(
- confirmRevertDialog,
- 'input[name="revertOptions"]'
- );
- assert.equal(radioInputs.length, 0);
- const msg =
- 'Revert "random commit message"\n\n' +
- 'This reverts commit 2000.\n\nReason ' +
- 'for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, msg);
- const editedMsg = msg + 'hello';
- confirmRevertDialog._message += 'hello';
- const confirmButton = queryAndAssert(
- queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
- '#confirm'
- );
- tap(confirmButton);
- flush(() => {
- assert.equal(fireActionStub.getCall(0).args[0], '/revert');
- assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
- assert.equal(
- fireActionStub.getCall(0).args[3].message,
- editedMsg
- );
- done();
- });
- });
+ await flush();
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ const radioInputs = queryAll(
+ confirmRevertDialog,
+ 'input[name="revertOptions"]'
+ );
+ assert.equal(radioInputs.length, 0);
+ const msg =
+ 'Revert "random commit message"\n\n' +
+ 'This reverts commit 2000.\n\nReason ' +
+ 'for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, msg);
+ const editedMsg = msg + 'hello';
+ confirmRevertDialog._message += 'hello';
+ const confirmButton = queryAndAssert(
+ queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
+ '#confirm'
+ );
+ tap(confirmButton);
+ await flush();
+ assert.equal(fireActionStub.getCall(0).args[0], '/revert');
+ assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
+ assert.equal(fireActionStub.getCall(0).args[3].message, editedMsg);
});
});
});
@@ -1477,27 +1423,23 @@
test(
'make sure the mark private change button is not outside of the ' +
'overflow menu',
- done => {
- flush(() => {
- assert.isNotOk(query(element, '[data-action-key="private"]'));
- done();
- });
+ async () => {
+ await flush();
+ assert.isNotOk(query(element, '[data-action-key="private"]'));
}
);
- test('private change', done => {
- flush(() => {
- assert.isOk(
- query(element.$.moreActions, 'span[data-id="private-change"]')
- );
- element.setActionOverflow(ActionType.CHANGE, 'private', false);
- flush();
- assert.isOk(query(element, '[data-action-key="private"]'));
- assert.isNotOk(
- query(element.$.moreActions, 'span[data-id="private-change"]')
- );
- done();
- });
+ test('private change', async () => {
+ await flush();
+ assert.isOk(
+ query(element.$.moreActions, 'span[data-id="private-change"]')
+ );
+ element.setActionOverflow(ActionType.CHANGE, 'private', false);
+ await flush();
+ assert.isOk(query(element, '[data-action-key="private"]'));
+ assert.isNotOk(
+ query(element.$.moreActions, 'span[data-id="private-change"]')
+ );
});
});
@@ -1528,35 +1470,23 @@
test(
'make sure the unmark private change button is not outside of the ' +
'overflow menu',
- done => {
- flush(() => {
- assert.isNotOk(
- query(element, '[data-action-key="private.delete"]')
- );
- done();
- });
+ async () => {
+ await flush();
+ assert.isNotOk(query(element, '[data-action-key="private.delete"]'));
}
);
- test('unmark the private change', done => {
- flush(() => {
- assert.isOk(
- query(
- element.$.moreActions,
- 'span[data-id="private.delete-change"]'
- )
- );
- element.setActionOverflow(ActionType.CHANGE, 'private.delete', false);
- flush();
- assert.isOk(query(element, '[data-action-key="private.delete"]'));
- assert.isNotOk(
- query(
- element.$.moreActions,
- 'span[data-id="private.delete-change"]'
- )
- );
- done();
- });
+ test('unmark the private change', async () => {
+ await flush();
+ assert.isOk(
+ query(element.$.moreActions, 'span[data-id="private.delete-change"]')
+ );
+ element.setActionOverflow(ActionType.CHANGE, 'private.delete', false);
+ await flush();
+ assert.isOk(query(element, '[data-action-key="private.delete"]'));
+ assert.isNotOk(
+ query(element.$.moreActions, 'span[data-id="private.delete-change"]')
+ );
});
});
@@ -1586,7 +1516,7 @@
assert.isFalse(fireActionStub.called);
});
- test('shows confirm dialog', () => {
+ test('shows confirm dialog', async () => {
element._handleDeleteTap();
assert.isFalse(
(queryAndAssert(element, '#confirmDeleteDialog') as GrDialog).hidden
@@ -1597,11 +1527,11 @@
'gr-button[primary]'
)
);
- flush();
+ await flush();
assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
});
- test('hides delete confirm on cancel', () => {
+ test('hides delete confirm on cancel', async () => {
element._handleDeleteTap();
tap(
queryAndAssert(
@@ -1609,7 +1539,7 @@
'gr-button:not([primary])'
)
);
- flush();
+ await flush();
assert.isTrue(
(queryAndAssert(element, '#confirmDeleteDialog') as GrDialog).hidden
);
@@ -1618,7 +1548,7 @@
});
suite('ignore change', () => {
- setup(done => {
+ setup(async () => {
sinon.stub(element, '_fireAction');
const IgnoreAction = {
@@ -1638,19 +1568,18 @@
element.changeNum = 2 as NumericChangeId;
element.latestPatchNum = 2 as PatchSetNum;
- element.reload().then(() => {
- flush(done);
- });
+ await element.reload();
+ await flush();
});
test('make sure the ignore button is not outside of the overflow menu', () => {
assert.isNotOk(query(element, '[data-action-key="ignore"]'));
});
- test('ignoring change', () => {
+ test('ignoring change', async () => {
queryAndAssert(element.$.moreActions, 'span[data-id="ignore-change"]');
element.setActionOverflow(ActionType.CHANGE, 'ignore', false);
- flush();
+ await flush();
queryAndAssert(element, '[data-action-key="ignore"]');
assert.isNotOk(
query(element.$.moreActions, 'span[data-id="ignore-change"]')
@@ -1659,7 +1588,7 @@
});
suite('unignore change', () => {
- setup(done => {
+ setup(async () => {
sinon.stub(element, '_fireAction');
const UnignoreAction = {
@@ -1679,21 +1608,20 @@
element.changeNum = 2 as NumericChangeId;
element.latestPatchNum = 2 as PatchSetNum;
- element.reload().then(() => {
- flush(done);
- });
+ await element.reload();
+ await flush();
});
test('unignore button is not outside of the overflow menu', () => {
assert.isNotOk(query(element, '[data-action-key="unignore"]'));
});
- test('unignoring change', () => {
+ test('unignoring change', async () => {
assert.isOk(
query(element.$.moreActions, 'span[data-id="unignore-change"]')
);
element.setActionOverflow(ActionType.CHANGE, 'unignore', false);
- flush();
+ await flush();
assert.isOk(query(element, '[data-action-key="unignore"]'));
assert.isNotOk(
query(element.$.moreActions, 'span[data-id="unignore-change"]')
@@ -1702,7 +1630,7 @@
});
suite('quick approve', () => {
- setup(() => {
+ setup(async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1719,7 +1647,7 @@
foo: ['-1', ' 0', '+1'],
},
};
- flush();
+ await flush();
});
test('added when can approve', () => {
@@ -1730,7 +1658,7 @@
assert.isNotNull(approveButton);
});
- test('hide quick approve', () => {
+ test('hide quick approve', async () => {
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -1740,7 +1668,7 @@
// Assert approve button gets removed from list of buttons.
element.hideQuickApproveAction();
- flush();
+ await flush();
const approveButtonUpdated = query(
element,
"gr-button[data-action-key='review']"
@@ -1756,18 +1684,21 @@
assert.equal(approveButton!.getAttribute('data-label'), 'foo+1');
});
- test('not added when change is merged', () => {
- element.change!.status = ChangeStatus.MERGED;
- flush(() => {
- const approveButton = query(
- element,
- "gr-button[data-action-key='review']"
- );
- assert.isNotOk(approveButton);
- });
+ test('not added when change is merged', async () => {
+ element.change = {
+ ...element.change!,
+ status: ChangeStatus.MERGED,
+ };
+
+ await flush();
+ const approveButton = query(
+ element,
+ "gr-button[data-action-key='review']"
+ );
+ assert.isNotOk(approveButton);
});
- test('not added when already approved', () => {
+ test('not added when already approved', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1781,7 +1712,7 @@
foo: [' 0', '+1'],
},
};
- flush();
+ await flush();
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -1789,7 +1720,7 @@
assert.isNotOk(approveButton);
});
- test('not added when label not permitted', () => {
+ test('not added when label not permitted', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1800,7 +1731,7 @@
bar: [],
},
};
- flush();
+ await flush();
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -1808,17 +1739,17 @@
assert.isNotOk(approveButton);
});
- test('approves when tapped', () => {
+ test('approves when tapped', async () => {
const fireActionStub = sinon.stub(element, '_fireAction');
tap(queryAndAssert(element, "gr-button[data-action-key='review']"));
- flush();
+ await flush();
assert.isTrue(fireActionStub.called);
assert.isTrue(fireActionStub.calledWith('/review'));
const payload = fireActionStub.lastCall.args[3];
assert.deepEqual((payload as ReviewInput).labels, {foo: 1});
});
- test('not added when multiple labels are required without code review', () => {
+ test('not added when multiple labels are required without code review', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1831,7 +1762,7 @@
bar: [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -1839,7 +1770,7 @@
assert.isNotOk(approveButton);
});
- test('code review shown with multiple missing approval', () => {
+ test('code review shown with multiple missing approval', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1861,7 +1792,7 @@
'Code-Review': [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = queryAndAssert(
element,
"gr-button[data-action-key='review']"
@@ -1869,7 +1800,7 @@
assert.isOk(approveButton);
});
- test('button label for missing approval', () => {
+ test('button label for missing approval', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1887,7 +1818,7 @@
bar: [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = queryAndAssert(
element,
"gr-button[data-action-key='review']"
@@ -1895,7 +1826,7 @@
assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
});
- test('no quick approve if score is not maximal for a label', () => {
+ test('no quick approve if score is not maximal for a label', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1913,7 +1844,7 @@
bar: [' 0', '+1'],
},
};
- flush();
+ await flush();
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -1921,7 +1852,7 @@
assert.isNotOk(approveButton);
});
- test('approving label with a non-max score', () => {
+ test('approving label with a non-max score', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1939,7 +1870,7 @@
bar: [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = queryAndAssert(
element,
"gr-button[data-action-key='review']"
@@ -1947,7 +1878,7 @@
assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
});
- test('added when can approve an already-approved code review label', () => {
+ test('added when can approve an already-approved code review label', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -1965,7 +1896,7 @@
'Code-Review': [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = queryAndAssert(
element,
"gr-button[data-action-key='review']"
@@ -1973,7 +1904,7 @@
assert.isNotNull(approveButton);
});
- test('not added when the user has already approved', () => {
+ test('not added when the user has already approved', async () => {
const vote = {
...createApproval(),
_account_id: 123 as AccountId,
@@ -1998,7 +1929,7 @@
'Code-Review': [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -2006,7 +1937,7 @@
assert.isNotOk(approveButton);
});
- test('not added when user owns the change', () => {
+ test('not added when user owns the change', async () => {
element.change = {
...createChangeViewChange(),
current_revision: 'abc1234' as CommitId,
@@ -2025,7 +1956,7 @@
'Code-Review': [' 0', '+1', '+2'],
},
};
- flush();
+ await flush();
const approveButton = query(
element,
"gr-button[data-action-key='review']"
@@ -2034,12 +1965,12 @@
});
});
- test('adds download revision action', () => {
+ test('adds download revision action', async () => {
const handler = sinon.stub();
element.addEventListener('download-tap', handler);
assert.ok(element.revisionActions.download);
element._handleDownloadTap();
- flush();
+ await flush();
assert.isTrue(handler.called);
});
@@ -2061,14 +1992,14 @@
});
suite('setActionOverflow', () => {
- test('move action from overflow', () => {
+ test('move action from overflow', async () => {
assert.isNotOk(query(element, '[data-action-key="cherrypick"]'));
assert.strictEqual(
element.$.moreActions!.items![0].id,
'cherrypick-revision'
);
element.setActionOverflow(ActionType.REVISION, 'cherrypick', false);
- flush();
+ await flush();
assert.isOk(query(element, '[data-action-key="cherrypick"]'));
assert.notEqual(
element.$.moreActions!.items![0].id,
@@ -2076,10 +2007,10 @@
);
});
- test('move action to overflow', () => {
+ test('move action to overflow', async () => {
assert.isOk(query(element, '[data-action-key="submit"]'));
element.setActionOverflow(ActionType.REVISION, 'submit', true);
- flush();
+ await flush();
assert.isNotOk(query(element, '[data-action-key="submit"]'));
assert.strictEqual(
element.$.moreActions.items![3].id,
@@ -2233,31 +2164,24 @@
);
});
- test('revert submission single change', done => {
- element
- ._send(
- HttpMethod.POST,
- {message: 'Revert submission'},
- '/revert_submission',
- false,
- cleanup,
- {} as UIActionInfo
- )
- .then(() => {
- element
- ._handleResponse(
- {
- __key: 'revert_submission',
- __type: ActionType.CHANGE,
- label: 'l',
- },
- new Response()
- )!
- .then(() => {
- assert.isTrue(navigateToSearchQueryStub.called);
- done();
- });
- });
+ test('revert submission single change', async () => {
+ await element._send(
+ HttpMethod.POST,
+ {message: 'Revert submission'},
+ '/revert_submission',
+ false,
+ cleanup,
+ {} as UIActionInfo
+ );
+ await element._handleResponse(
+ {
+ __key: 'revert_submission',
+ __type: ActionType.CHANGE,
+ label: 'l',
+ },
+ new Response()
+ );
+ assert.isTrue(navigateToSearchQueryStub.called);
});
});
@@ -2280,55 +2204,42 @@
);
});
- test('revert submission multiple change', done => {
- element
- ._send(
- HttpMethod.POST,
- {message: 'Revert submission'},
- '/revert_submission',
- false,
- cleanup,
- {} as UIActionInfo
- )
- .then(() => {
- element
- ._handleResponse(
- {
- __key: 'revert_submission',
- __type: ActionType.CHANGE,
- label: 'l',
- },
- new Response()
- )!
- .then(() => {
- assert.isFalse(showActionDialogStub.called);
- assert.isTrue(
- navigateToSearchQueryStub.calledWith('topic: T')
- );
- done();
- });
- });
+ test('revert submission multiple change', async () => {
+ await element._send(
+ HttpMethod.POST,
+ {message: 'Revert submission'},
+ '/revert_submission',
+ false,
+ cleanup,
+ {} as UIActionInfo
+ );
+ await element._handleResponse(
+ {
+ __key: 'revert_submission',
+ __type: ActionType.CHANGE,
+ label: 'l',
+ },
+ new Response()
+ );
+ assert.isFalse(showActionDialogStub.called);
+ assert.isTrue(navigateToSearchQueryStub.calledWith('topic: T'));
});
});
- test('revision action', done => {
- element
- ._send(
- HttpMethod.DELETE,
- payload,
- '/endpoint',
- true,
- cleanup,
- {} as UIActionInfo
- )
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isTrue(
- sendStub.calledWith(42, 'DELETE', '/endpoint', 12, payload)
- );
- done();
- });
+ test('revision action', async () => {
+ await element._send(
+ HttpMethod.DELETE,
+ payload,
+ '/endpoint',
+ true,
+ cleanup,
+ {} as UIActionInfo
+ );
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isTrue(
+ sendStub.calledWith(42, 'DELETE', '/endpoint', 12, payload)
+ );
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 3d55097..e1a1cbb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -86,6 +86,7 @@
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
+import {Interaction} from '../../../constants/reporting';
const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
@@ -619,7 +620,7 @@
_onShowAllClick() {
this._showAllSections = !this._showAllSections;
- this.reporting.reportInteraction('toggle show all button', {
+ this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
sectionName: 'metadata',
toState: this._showAllSections ? 'Show all' : 'Show less',
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
index 1d7db85..7fc3ddf 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
@@ -35,6 +35,7 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {appContext} from '../../../services/app-context';
import {labelCompare} from '../../../utils/label-util';
+import {Interaction} from '../../../constants/reporting';
interface ChangeRequirement extends Requirement {
satisfied: boolean;
@@ -181,7 +182,7 @@
_handleShowHide() {
this._showOptionalLabels = !this._showOptionalLabels;
- this.reporting.reportInteraction('toggle show all button', {
+ this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
sectionName: 'optional labels',
toState: this._showOptionalLabels ? 'Show all' : 'Show less',
});
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 8d47ab5..c34a6ae 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
@@ -20,18 +20,19 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {appContext} from '../../../services/app-context';
import {
- allRunsLatest$,
+ allRunsLatestPatchsetLatestAttempt$,
aPluginHasRegistered$,
CheckResult,
CheckRun,
- errorMessage$,
- loginCallback$,
- someProvidersAreLoading$,
+ errorMessageLatest$,
+ loginCallbackLatest$,
+ someProvidersAreLoadingLatest$,
} from '../../../services/checks/checks-model';
-import {Category, Link, RunStatus} from '../../../api/checks';
+import {Category, RunStatus} from '../../../api/checks';
import {fireShowPrimaryTab} from '../../../utils/event-util';
import '../../shared/gr-avatar/gr-avatar';
import {
+ firstPrimaryLink,
getResultsOf,
hasCompletedWithoutResults,
hasResultsOf,
@@ -260,7 +261,7 @@
}
/** What is the maximum number of expanded checks chips? */
-const DETAILS_QUOTA = 3;
+const DETAILS_QUOTA = 2;
@customElement('gr-change-summary')
export class GrChangeSummary extends GrLitElement {
@@ -292,7 +293,7 @@
* How many check chips may still be rendered as a detailed chip. Is reset
* when rendering begins and decreases while chips are rendered. So when
* there are two ERRORs, then those would consume 2 from this quota and then
- * there would only by DETAILS_QUOTA - 2 left for the other summary chips.
+ * there would only be DETAILS_QUOTA - 2 left for the other summary chips.
* Once there are more results than quota left we will stop rendering
* detailed chips and fall back to just icon+number rendering.
*/
@@ -307,11 +308,11 @@
constructor() {
super();
- this.subscribe('runs', allRunsLatest$);
+ this.subscribe('runs', allRunsLatestPatchsetLatestAttempt$);
this.subscribe('showChecksSummary', aPluginHasRegistered$);
- this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
- this.subscribe('errorMessage', errorMessage$);
- this.subscribe('loginCallback', loginCallback$);
+ this.subscribe('someProvidersAreLoading', someProvidersAreLoadingLatest$);
+ this.subscribe('errorMessage', errorMessageLatest$);
+ this.subscribe('loginCallback', loginCallbackLatest$);
}
static get styles() {
@@ -326,10 +327,9 @@
margin-bottom: var(--spacing-m);
}
.zeroState {
- color: var(--primary-text-color);
+ color: var(--deemphasized-text-color);
}
.loading.zeroState {
- color: var(--deemphasized-text-color);
margin-right: var(--spacing-m);
}
div.error {
@@ -455,13 +455,10 @@
this.detailsQuota -= runs.length;
return runs.map(run => {
this.detailsCheckNames.push(run.checkName);
- const allLinks = resultFilter(run)
- .reduce(
- (links, result) => links.concat(result.links ?? []),
- [] as Link[]
- )
- .filter(link => link.primary);
- const links = allLinks.length === 1 ? allLinks : [];
+ const allPrimaryLinks = resultFilter(run)
+ .map(firstPrimaryLink)
+ .filter(notUndefined);
+ const links = allPrimaryLinks.length === 1 ? allPrimaryLinks : [];
const text = `${run.checkName}`;
return html`<gr-checks-chip
class="${icon}"
@@ -563,7 +560,7 @@
account =>
html`<gr-avatar
.account="${account}"
- image-size="32"
+ imageSize="32"
></gr-avatar>`
)}
${countUnresolvedComments} unresolved</gr-summary-chip
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 f3b746a8..58cf489 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
@@ -72,7 +72,7 @@
hasEditPatchsetLoaded,
PatchSet,
} from '../../../utils/patch-set-util';
-import {changeStatuses} from '../../../utils/change-util';
+import {changeStatuses, isOwner, isReviewer} from '../../../utils/change-util';
import {EventType as PluginEventType} from '../../../api/plugin';
import {customElement, property, observe} from '@polymer/decorators';
import {GrApplyFixDialog} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
@@ -172,7 +172,7 @@
import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
import {Subject} from 'rxjs';
import {debounce, DelayedTask} from '../../../utils/async-util';
-import {Timing} from '../../../constants/reporting';
+import {Interaction, Timing} from '../../../constants/reporting';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
@@ -429,7 +429,7 @@
type: String,
computed: '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)',
})
- _changeStatuses?: string[];
+ _changeStatuses?: ChangeStates[];
/** If false, then the "Show more" button was used to expand. */
@property({type: Boolean})
@@ -634,7 +634,7 @@
this._dynamicTabContentEndpoints.length !==
this._dynamicTabHeaderEndpoints.length
) {
- console.warn('Different number of tab headers and tab content.');
+ this.reporting.error(new Error('Mismatch of headers and content.'));
}
})
.then(() => this._initActiveTabs(this.params));
@@ -767,7 +767,7 @@
}
}
if (activeIndex === -1) {
- console.warn('tab not found with given info', activeDetails);
+ this.reporting.error(new Error(`tab not found for ${activeDetails}`));
return;
}
const tabName = tabs[activeIndex].dataset['name'];
@@ -777,7 +777,7 @@
if (paperTabs.selected !== activeIndex) {
// paperTabs.selected is undefined during rendering
if (paperTabs.selected !== undefined) {
- this.reporting.reportInteraction('show-tab', {tabName, src});
+ this.reporting.reportInteraction(Interaction.SHOW_TAB, {tabName, src});
}
paperTabs.selected = activeIndex;
}
@@ -840,7 +840,7 @@
if (hasUnresolvedThreads) this.unresolvedOnly = true;
}
- this.reporting.reportInteraction('show-tab', {
+ this.reporting.reportInteraction(Interaction.SHOW_TAB, {
tabName,
src: 'paper-tab-click',
});
@@ -1225,10 +1225,7 @@
this._shownFileCount = e.detail.length;
}
- _expandAllDiffs(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
- return;
- }
+ _expandAllDiffs() {
this.$.fileList.expandAllDiffs();
}
@@ -2133,7 +2130,10 @@
.then(() => {
this.reporting.timeEnd(Timing.CHANGE_RELOAD);
if (isLocationChange) {
- this.reporting.changeDisplayed();
+ this.reporting.changeDisplayed({
+ isOwner: isOwner(this._change, this._account),
+ isReviewer: isReviewer(this._change, this._account),
+ });
}
});
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 b04792d..693406c 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
@@ -99,7 +99,7 @@
font-size: var(--font-size-mono);
line-height: var(--line-height-mono);
margin-right: var(--spacing-l);
- margin-bottom: var(--spacing-m);
+ margin-bottom: var(--spacing-l);
/* Account for border and padding and rounding errors. */
max-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
}
@@ -197,8 +197,7 @@
width: 100%;
}
gr-change-summary {
- /* temporary for old checks status */
- margin-bottom: var(--spacing-m);
+ margin-left: var(--spacing-m);
}
@media screen and (max-width: 75em) {
.relatedChanges {
@@ -430,6 +429,7 @@
></gr-linked-text>
</gr-editable-content>
</div>
+ <h3 class="assistive-tech-only">Comments and Checks Summary</h3>
<gr-change-summary
change-comments="[[_changeComments]]"
comment-threads="[[_commentThreads]]"
@@ -465,7 +465,7 @@
<paper-tab
on-click="_onPaperTabClick"
data-name$="[[_constants.PrimaryTab.FILES]]"
- >Files</paper-tab
+ ><span>Files</span></paper-tab
>
<paper-tab
on-click="_onPaperTabClick"
@@ -483,7 +483,7 @@
<paper-tab
data-name$="[[_constants.PrimaryTab.CHECKS]]"
on-click="_onPaperTabClick"
- >Checks</paper-tab
+ ><span>Checks</span></paper-tab
>
</template>
<template
@@ -504,7 +504,7 @@
data-name$="[[_constants.PrimaryTab.FINDINGS]]"
on-click="_onPaperTabClick"
>
- Findings
+ <span>Findings</span>
</paper-tab>
</paper-tabs>
@@ -564,6 +564,7 @@
is="dom-if"
if="[[_isTabActive(_constants.PrimaryTab.COMMENT_THREADS, _activeTabs)]]"
>
+ <h3 class="assistive-tech-only">Comments</h3>
<gr-thread-list
threads="[[_commentThreads]]"
change="[[_change]]"
@@ -579,6 +580,7 @@
is="dom-if"
if="[[_isTabActive(_constants.PrimaryTab.CHECKS, _activeTabs)]]"
>
+ <h3 class="assistive-tech-only">Checks</h3>
<gr-checks-tab
id="checksTab"
tab-state="[[_tabState.checksTab]]"
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 3d51ac6..5128948 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
@@ -1222,7 +1222,7 @@
},
};
element._mergeable = true;
- const expectedStatuses = ['Merged', 'WIP'];
+ const expectedStatuses = [ChangeStates.MERGED, ChangeStates.WIP];
assert.deepEqual(element._changeStatuses, expectedStatuses);
flush();
const statusChips = element.shadowRoot!.querySelectorAll(
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 532097b..ff303e3 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
@@ -44,6 +44,7 @@
import {DiffViewMode} from '../../../constants/constants';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fireEvent} from '../../../utils/event-util';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
declare global {
interface HTMLElementTagNameMap {
@@ -60,7 +61,7 @@
}
@customElement('gr-file-list-header')
-export class GrFileListHeader extends PolymerElement {
+export class GrFileListHeader extends KeyboardShortcutMixin(PolymerElement) {
static get template() {
return htmlTemplate;
}
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 69be729..8b9b80c 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
@@ -173,6 +173,7 @@
class="download"
title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
ShortcutSection.ACTIONS)]]"
+ has-tooltip=""
on-click="_handleDownloadTap"
>Download</gr-button
>
@@ -186,6 +187,7 @@
link=""
title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
ShortcutSection.FILE_LIST)]]"
+ has-tooltip=""
on-click="_expandAllDiffs"
>Expand All</gr-button
>
@@ -195,6 +197,7 @@
on-click="_collapseAllDiffs"
title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
ShortcutSection.FILE_LIST)]]"
+ has-tooltip=""
>Collapse All</gr-button
>
</template>
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 ca16ec0..5ce6e3b 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
@@ -95,7 +95,6 @@
export interface GrFileList {
$: {
diffPreferencesDialog: GrDiffPreferencesDialog;
- diffCursor: GrDiffCursor;
};
}
@@ -350,6 +349,8 @@
private fileCursor = new GrCursorManager();
+ private diffCursor = new GrDiffCursor();
+
constructor() {
super();
this.fileCursor.scrollMode = ScrollMode.KEEP_VISIBLE;
@@ -384,31 +385,26 @@
this._dynamicHeaderEndpoints.length !==
this._dynamicContentEndpoints.length
) {
- console.warn(
- 'Different number of dynamic file-list header and content.'
- );
+ this.reporting.error(new Error('dynamic header/content mismatch'));
}
if (
this._dynamicPrependedHeaderEndpoints.length !==
this._dynamicPrependedContentEndpoints.length
) {
- console.warn(
- 'Different number of dynamic file-list header and content.'
- );
+ this.reporting.error(new Error('dynamic header/content mismatch'));
}
if (
this._dynamicHeaderEndpoints.length !==
this._dynamicSummaryEndpoints.length
) {
- console.warn(
- 'Different number of dynamic file-list headers and summary.'
- );
+ this.reporting.error(new Error('dynamic header/content mismatch'));
}
});
}
/** @override */
disconnectedCallback() {
+ this.diffCursor.dispose();
this.fileCursor.unsetCursor();
this._cancelDiffs();
this.loadingTask?.cancel();
@@ -597,7 +593,7 @@
this._expandedFiles.length,
this._files.length
);
- this.$.diffCursor.handleDiffUpdate();
+ this.diffCursor.handleDiffUpdate();
}
/**
@@ -841,7 +837,7 @@
}
e.preventDefault();
- this.$.diffCursor.moveLeft();
+ this.diffCursor.moveLeft();
}
_handleRightPane(e: CustomKeyboardEvent) {
@@ -850,7 +846,7 @@
}
e.preventDefault();
- this.$.diffCursor.moveRight();
+ this.diffCursor.moveRight();
}
_handleToggleInlineDiff(e: CustomKeyboardEvent) {
@@ -891,7 +887,7 @@
if (this._showInlineDiffs) {
e.preventDefault();
- this.$.diffCursor.moveDown();
+ this.diffCursor.moveDown();
this._displayLine = true;
} else {
// Down key
@@ -911,7 +907,7 @@
if (this._showInlineDiffs) {
e.preventDefault();
- this.$.diffCursor.moveUp();
+ this.diffCursor.moveUp();
this._displayLine = true;
} else {
// Up key
@@ -930,7 +926,7 @@
}
e.preventDefault();
this.classList.remove('hideComments');
- this.$.diffCursor.createCommentInPlace();
+ this.diffCursor.createCommentInPlace();
}
_handleOpenLastFile(e: CustomKeyboardEvent) {
@@ -978,9 +974,9 @@
e.preventDefault();
if (isShiftPressed(e)) {
- this.$.diffCursor.moveToNextCommentThread();
+ this.diffCursor.moveToNextCommentThread();
} else {
- this.$.diffCursor.moveToNextChunk();
+ this.diffCursor.moveToNextChunk();
}
}
@@ -995,9 +991,9 @@
e.preventDefault();
if (isShiftPressed(e)) {
- this.$.diffCursor.moveToPreviousCommentThread();
+ this.diffCursor.moveToPreviousCommentThread();
} else {
- this.$.diffCursor.moveToPreviousChunk();
+ this.diffCursor.moveToPreviousChunk();
}
}
@@ -1033,7 +1029,7 @@
}
_openCursorFile() {
- const diff = this.$.diffCursor.getTargetDiffElement();
+ const diff = this.diffCursor.getTargetDiffElement();
if (!this.change || !diff || !this.patchRange || !diff.path) {
throw new Error('change, diff and patchRange must be all set and valid');
}
@@ -1240,12 +1236,7 @@
_updateDiffCursor() {
// Overwrite the cursor's list of diffs:
- this.$.diffCursor.splice(
- 'diffs',
- 0,
- this.$.diffCursor.diffs.length,
- ...this.diffs
- );
+ this.diffCursor.replaceDiffs(this.diffs);
}
_filesChanged() {
@@ -1382,7 +1373,7 @@
}
this._updateDiffCursor();
- this.$.diffCursor.reInitAndUpdateStops();
+ this.diffCursor.reInitAndUpdateStops();
}
private _clearCollapsedDiffs(collapsedDiffs: GrDiffHost[]) {
@@ -1432,7 +1423,9 @@
console.info('Expanding diff', iter, 'of', initialCount, ':', path);
const diffElem = this._findDiffByPath(path, diffElements);
if (!diffElem) {
- console.warn(`Did not find <gr-diff-host> element for ${path}`);
+ this.reporting.error(
+ new Error(`Did not find <gr-diff-host> element for ${path}`)
+ );
return Promise.resolve();
}
if (!this.changeComments || !this.patchRange || !this.diffPrefs) {
@@ -1470,7 +1463,7 @@
* prevented the issue of scrolling to top when we expand the second
* file individually.
*/
- this.$.diffCursor.reInitAndUpdateStops();
+ this.diffCursor.reInitAndUpdateStops();
})
);
}
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 40bd5bc..8cd35a5 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
@@ -735,5 +735,4 @@
on-reload-diff-preference="_handleReloadingDiffPreference"
>
</gr-diff-preferences-dialog>
- <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
`;
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 dcc2e46..4d793b9 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
@@ -543,7 +543,7 @@
assert.equal(element.fileCursor.index, 0);
assert.equal(element.selectedIndex, 0);
- const createCommentInPlaceStub = sinon.stub(element.$.diffCursor,
+ const createCommentInPlaceStub = sinon.stub(element.diffCursor,
'createCommentInPlace');
MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
assert.isTrue(createCommentInPlaceStub.called);
@@ -664,8 +664,8 @@
});
test('shift+left/shift+right', () => {
- const moveLeftStub = sinon.stub(element.$.diffCursor, 'moveLeft');
- const moveRightStub = sinon.stub(element.$.diffCursor, 'moveRight');
+ const moveLeftStub = sinon.stub(element.diffCursor, 'moveLeft');
+ const moveRightStub = sinon.stub(element.diffCursor, 'moveRight');
let noDiffsExpanded = true;
sinon.stub(element, '_noDiffsExpanded')
@@ -913,9 +913,9 @@
test('expandAllDiffs and collapseAllDiffs', () => {
const collapseStub = sinon.stub(element, '_clearCollapsedDiffs');
- const cursorUpdateStub = sinon.stub(element.$.diffCursor,
+ const cursorUpdateStub = sinon.stub(element.diffCursor,
'handleDiffUpdate');
- const reInitStub = sinon.stub(element.$.diffCursor,
+ const reInitStub = sinon.stub(element.diffCursor,
'reInitAndUpdateStops');
const path = 'path/to/my/file.txt';
@@ -1398,7 +1398,7 @@
}
element._updateDiffCursor();
- element.$.diffCursor.handleDiffUpdate();
+ element.diffCursor.handleDiffUpdate();
return diffs;
}
@@ -1484,10 +1484,11 @@
// The file cursor is now at 1.
assert.equal(element.fileCursor.index, 1);
+
MockInteractions.keyUpOn(element, 73, null, 'i');
flush();
-
diffs = await renderAndGetNewDiffs(1);
+
// Two diffs should be rendered.
assert.equal(diffs.length, 2);
const diffStopsFirst = diffs[0].getCursorStops();
@@ -1537,9 +1538,9 @@
setup(() => {
sinon.stub(element, '_renderInOrder').returns(Promise.resolve());
nKeySpy = sinon.spy(element, '_handleNextChunk');
- nextCommentStub = sinon.stub(element.$.diffCursor,
+ nextCommentStub = sinon.stub(element.diffCursor,
'moveToNextCommentThread');
- nextChunkStub = sinon.stub(element.$.diffCursor,
+ nextChunkStub = sinon.stub(element.diffCursor,
'moveToNextChunk');
fileRows =
element.root.querySelectorAll('.row:not(.header-row)');
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index 4a41316..57c1a30 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -20,7 +20,7 @@
<style include="gr-voting-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <style include="shared-styles">
+ <style>
:host {
display: block;
position: relative;
@@ -148,6 +148,7 @@
vertical-align: top;
}
.score {
+ box-sizing: border-box;
border-radius: var(--border-radius);
color: var(--vote-text-color);
display: inline-block;
@@ -199,6 +200,10 @@
font-weight: var(--font-weight-bold);
}
}
+ iron-icon {
+ --iron-icon-height: 20px;
+ --iron-icon-width: 20px;
+ }
@media screen and (max-width: 50em) {
.expanded .content {
padding-left: 0;
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 6be0c07..4e2aaf1 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
@@ -270,7 +270,9 @@
| undefined;
if (!el && this._showAllActivity) {
- console.warn(`Failed to scroll to message: ${messageID}`);
+ this.reporting.error(
+ new Error(`Failed to scroll to message: ${messageID}`)
+ );
return;
}
if (!el) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index 49c2486..921c45c 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -54,6 +54,7 @@
a {
display: block;
}
+ :host,
.changeContainer,
a {
max-width: 100%;
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 5595d15..360d7de 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
@@ -40,6 +40,7 @@
getRevisionKey,
isChangeInfo,
} from '../../../utils/change-util';
+import {Interaction} from '../../../constants/reporting';
/** What is the maximum number of shown changes in collapsed list? */
const DEFALT_NUM_CHANGES_WHEN_COLLAPSED = 3;
@@ -101,15 +102,37 @@
section {
margin-bottom: var(--spacing-l);
}
- gr-related-change {
+ .relatedChangeLine {
display: flex;
+ visibility: visible;
+ height: auto;
}
- .marker {
- position: absolute;
- margin-left: calc(-1 * var(--spacing-s));
+ .marker.arrow {
+ visibility: hidden;
+ min-width: 20px;
}
- .arrowToCurrentChange {
- position: absolute;
+ .marker.arrowToCurrentChange {
+ min-width: 20px;
+ text-align: center;
+ }
+ .marker.space {
+ height: 1px;
+ min-width: 20px;
+ }
+ gr-related-collapse[collapsed] .marker.arrow {
+ visibility: visible;
+ min-width: auto;
+ }
+ gr-related-collapse[collapsed] .relatedChangeLine.show-when-collapsed {
+ visibility: visible;
+ height: auto;
+ }
+ /* keep width, so width of section and position of show all button
+ * are set according to width of all (even hidden) elements
+ */
+ gr-related-collapse[collapsed] .relatedChangeLine {
+ visibility: hidden;
+ height: 0px;
}
`,
];
@@ -151,13 +174,16 @@
>
${this.relatedChanges.map(
(change, index) =>
- html`${this.renderMarkers(
+ html`<div
+ class="${classMap({
+ ['relatedChangeLine']: true,
+ ['show-when-collapsed']: relatedChangesMarkersPredicate(index)
+ .showWhenCollapsed,
+ })}"
+ >
+ ${this.renderMarkers(
relatedChangesMarkersPredicate(index)
)}<gr-related-change
- class="${classMap({
- ['show-when-collapsed']: relatedChangesMarkersPredicate(index)
- .showWhenCollapsed,
- })}"
.change="${change}"
.connectedRevisions="${connectedRevisions}"
.href="${change?._change_number
@@ -169,7 +195,8 @@
: ''}"
.showChangeStatus=${true}
>${change.commit.subject}</gr-related-change
- >`
+ >
+ </div>`
)}
</gr-related-collapse>
</section>`;
@@ -202,14 +229,17 @@
>
${submittedTogetherChanges.map(
(change, index) =>
- html`${this.renderMarkers(
+ html`<div
+ class="${classMap({
+ ['relatedChangeLine']: true,
+ ['show-when-collapsed']: submittedTogetherMarkersPredicate(
+ index
+ ).showWhenCollapsed,
+ })}"
+ >
+ ${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
- class="${classMap({
- ['show-when-collapsed']: submittedTogetherMarkersPredicate(
- index
- ).showWhenCollapsed,
- })}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
@@ -218,7 +248,8 @@
.showSubmittableCheck=${true}
>${change.project}: ${change.branch}:
${change.subject}</gr-related-change
- >`
+ >
+ </div>`
)}
</gr-related-collapse>
<div class="note" ?hidden=${!countNonVisibleChanges}>
@@ -246,13 +277,16 @@
>
${this.sameTopicChanges.map(
(change, index) =>
- html`${this.renderMarkers(
+ html`<div
+ class="${classMap({
+ ['relatedChangeLine']: true,
+ ['show-when-collapsed']: sameTopicMarkersPredicate(index)
+ .showWhenCollapsed,
+ })}"
+ >
+ ${this.renderMarkers(
sameTopicMarkersPredicate(index)
)}<gr-related-change
- class="${classMap({
- ['show-when-collapsed']: sameTopicMarkersPredicate(index)
- .showWhenCollapsed,
- })}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
@@ -260,7 +294,8 @@
)}"
>${change.project}: ${change.branch}:
${change.subject}</gr-related-change
- >`
+ >
+ </div>`
)}
</gr-related-collapse>
</section>`;
@@ -285,20 +320,24 @@
>
${this.conflictingChanges.map(
(change, index) =>
- html`${this.renderMarkers(
+ html`<div
+ class="${classMap({
+ ['relatedChangeLine']: true,
+ ['show-when-collapsed']: mergeConflictsMarkersPredicate(index)
+ .showWhenCollapsed,
+ })}"
+ >
+ ${this.renderMarkers(
mergeConflictsMarkersPredicate(index)
)}<gr-related-change
- class="${classMap({
- ['show-when-collapsed']: mergeConflictsMarkersPredicate(index)
- .showWhenCollapsed,
- })}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
>${change.subject}</gr-related-change
- >`
+ >
+ </div>`
)}
</gr-related-collapse>
</section>`;
@@ -323,20 +362,24 @@
>
${this.cherryPickChanges.map(
(change, index) =>
- html`${this.renderMarkers(
+ html`<div
+ class="${classMap({
+ ['relatedChangeLine']: true,
+ ['show-when-collapsed']: cherryPicksMarkersPredicate(index)
+ .showWhenCollapsed,
+ })}"
+ >
+ ${this.renderMarkers(
cherryPicksMarkersPredicate(index)
)}<gr-related-change
- class="${classMap({
- ['show-when-collapsed']: cherryPicksMarkersPredicate(index)
- .showWhenCollapsed,
- })}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
>${change.branch}: ${change.subject}</gr-related-change
- >`
+ >
+ </div>`
)}
</gr-related-collapse>
</section>`;
@@ -465,7 +508,7 @@
if (changeMarkers.showCurrentChangeArrow) {
return html`<span
role="img"
- class="arrowToCurrentChange"
+ class="marker arrowToCurrentChange"
aria-label="Arrow marking current change"
>âž”</span
>`;
@@ -473,7 +516,7 @@
if (changeMarkers.showTopArrow) {
return html`<span
role="img"
- class="marker"
+ class="marker arrow"
aria-label="Arrow marking change has collapsed ancestors"
><iron-icon icon="gr-icons:arrowDropUp"></iron-icon
></span> `;
@@ -481,12 +524,12 @@
if (changeMarkers.showBottomArrow) {
return html`<span
role="img"
- class="marker"
+ class="marker arrow"
aria-label="Arrow marking change has collapsed descendants"
><iron-icon icon="gr-icons:arrowDropDown"></iron-icon
></span> `;
}
- return nothing;
+ return html`<span class="marker space"></span>`;
}
reload(getRelatedChanges?: Promise<RelatedChangesInfo | undefined>) {
@@ -628,9 +671,12 @@
@property()
title = '';
- @property()
+ @property({type: Boolean})
showAll = false;
+ @property({type: Boolean, reflect: true})
+ collapsed = true;
+
@property()
length = 0;
@@ -655,36 +701,8 @@
gr-button {
display: flex;
}
- /* This is a hacky solution from old gr-related-change-list
- * TODO(milutin): find layout without needing it
- */
- h4:before,
- gr-button:before,
- ::slotted(gr-related-change):before {
- content: ' ';
- flex-shrink: 0;
- width: 1.2em;
- }
- .collapsed ::slotted(gr-related-change.show-when-collapsed) {
- visibility: visible;
- height: auto;
- }
- .collapsed ::slotted(.marker) {
- display: block;
- }
- .show-all ::slotted(.marker) {
- display: none;
- }
- /* keep width, so width of section and position of show all button
- * are set according to width of all (even hidden) elements
- */
- .collapsed ::slotted(gr-related-change) {
- visibility: hidden;
- height: 0px;
- }
- ::slotted(gr-related-change) {
- visibility: visible;
- height: auto;
+ h4 {
+ margin-left: 20px;
}
gr-button iron-icon {
color: inherit;
@@ -707,11 +725,7 @@
const title = html`<h4 class="title">${this.title}</h4>`;
const collapsible = this.length > this.numChangesWhenCollapsed;
- const items = html` <div
- class="${!this.showAll && collapsible ? 'collapsed' : 'show-all'}"
- >
- <slot></slot>
- </div>`;
+ this.collapsed = !this.showAll && collapsible;
let button: TemplateResult | typeof nothing = nothing;
if (collapsible) {
@@ -727,13 +741,13 @@
}
return html`<div class="container">${title}${button}</div>
- ${items}`;
+ <div><slot></slot></div>`;
}
private toggle(e: MouseEvent) {
e.stopPropagation();
this.showAll = !this.showAll;
- this.reporting.reportInteraction('toggle show all button', {
+ this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
sectionName: this.title,
toState: this.showAll ? 'Show all' : 'Show less',
});
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 5504877..939b23c 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
@@ -113,7 +113,7 @@
import {ErrorCallback} from '../../../api/rest';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {StorageLocation} from '../../../services/storage/gr-storage';
-import {Timing} from '../../../constants/reporting';
+import {Interaction, Timing} from '../../../constants/reporting';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -758,7 +758,7 @@
if (changeReviewers) {
for (const key of Object.keys(changeReviewers)) {
if (key !== 'REVIEWER' && key !== 'CC') {
- console.warn('unexpected reviewer state:', key);
+ this.reporting.error(new Error(`Unexpected reviewer state: ${key}`));
continue;
}
if (!changeReviewers[key]) continue;
@@ -820,12 +820,12 @@
if (this._newAttentionSet.has(id)) {
this._newAttentionSet.delete(id);
- this.reporting.reportInteraction('attention-set-chip', {
+ this.reporting.reportInteraction(Interaction.ATTENTION_SET_CHIP, {
action: `REMOVE${self}${role}`,
});
} else {
this._newAttentionSet.add(id);
- this.reporting.reportInteraction('attention-set-chip', {
+ this.reporting.reportInteraction(Interaction.ATTENTION_SET_CHIP, {
action: `ADD${self}${role}`,
});
}
@@ -1085,9 +1085,8 @@
} else if (isReviewerGroupSuggestion(suggestion)) {
entry = suggestion.group;
} else {
- console.warn(
- 'received suggestion that was neither account nor group:',
- suggestion
+ this.reporting.error(
+ new Error(`Suggestion is neither account nor group: ${suggestion}`)
);
return false;
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 17e5e26..c3c1d54 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -391,8 +391,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- deselected="[[!_computeHasNewAttention(account, _newAttentionSet)]]"
hide-hovercard=""
+ selection-chip-style
on-click="_handleAttentionClick"
></gr-account-label>
</template>
@@ -462,8 +462,8 @@
account="[[_owner]]"
force-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
- deselected="[[!_computeHasNewAttention(_owner, _newAttentionSet)]]"
hide-hovercard=""
+ selection-chip-style
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -477,8 +477,8 @@
account="[[_uploader]]"
force-attention="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
- deselected="[[!_computeHasNewAttention(_uploader, _newAttentionSet)]]"
hide-hovercard=""
+ selection-chip-style
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -496,9 +496,9 @@
<gr-account-label
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- deselected="[[!_computeHasNewAttention(account, _newAttentionSet)]]"
+ selected="[[_computeHasNewAttention(account, _newAttentionSet)]]
hide-hovercard=""
+ selection-chip-style
on-click="_handleAttentionClick"
>
</gr-account-label>
@@ -518,8 +518,8 @@
account="[[account]]"
force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- deselected="[[!_computeHasNewAttention(account, _newAttentionSet)]]"
hide-hovercard=""
+ selection-chip-style
on-click="_handleAttentionClick"
>
</gr-account-label>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 2f4adf5..8283be79 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -112,7 +112,7 @@
}
_computeEmptyThreadsMessage(threads: CommentThread[]) {
- return !threads.length ? 'No comments.' : 'No unresolved comments';
+ return !threads.length ? 'No comments' : 'No unresolved comments';
}
_showPartyPopper(threads: CommentThread[]) {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
index 1fe00ff..3d6cfd4 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
@@ -611,7 +611,7 @@
test('default empty message should show', () => {
assert.isTrue(
element.shadowRoot.querySelector('#threads').textContent.trim()
- .includes('No comments.'));
+ .includes('No comments'));
});
});
});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 4b7aec5..7796a6e 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -17,6 +17,7 @@
import {html} from 'lit-html';
import {classMap} from 'lit-html/directives/class-map';
import {repeat} from 'lit-html/directives/repeat';
+import {ifDefined} from 'lit-html/directives/if-defined';
import {
css,
customElement,
@@ -39,22 +40,23 @@
} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
import {
- allActions$,
- allLinks$,
CheckRun,
- checksPatchsetNumber$,
+ checksSelectedPatchsetNumber$,
RunResult,
- someProvidersAreLoading$,
+ someProvidersAreLoadingSelected$,
+ topLevelActionsSelected$,
+ topLevelLinksSelected$,
} from '../../services/checks/checks-model';
import {
allResults,
fireActionTriggered,
+ firstPrimaryLink,
hasCompletedWithoutResults,
iconForCategory,
iconForLink,
- otherLinks,
- primaryLink,
+ otherPrimaryLinks,
primaryRunAction,
+ secondaryLinks,
tooltipForLink,
} from '../../services/checks/checks-util';
import {assertIsDefined, check} from '../../utils/common-util';
@@ -77,6 +79,7 @@
getRepresentativeValue,
valueString,
} from '../../utils/label-util';
+import {GerritNav} from '../core/gr-navigation/gr-navigation';
@customElement('gr-result-row')
class GrResultRow extends GrLitElement {
@@ -113,7 +116,7 @@
gr-result-expanded {
cursor: default;
}
- tr {
+ tr.container {
border-top: 1px solid var(--border-color);
}
iron-icon.link {
@@ -169,27 +172,30 @@
overflow: hidden;
text-overflow: ellipsis;
}
- tr:hover {
+ tr.container:hover {
background: var(--hover-background-color);
}
- tr td .summary-cell .links,
- tr td .summary-cell .actions,
- tr.collapsed:hover td .summary-cell .links,
- tr.collapsed:hover td .summary-cell .actions,
+ tr.container td .summary-cell .links,
+ tr.container td .summary-cell .actions,
+ tr.container.collapsed:hover td .summary-cell .links,
+ tr.container.collapsed:hover td .summary-cell .actions,
:host(.dropdown-open) tr td .summary-cell .links,
:host(.dropdown-open) tr td .summary-cell .actions {
display: inline-block;
margin-left: var(--spacing-s);
}
- tr.collapsed td .summary-cell .message {
+ tr.container.collapsed td .summary-cell .message {
color: var(--deemphasized-text-color);
}
- tr.collapsed td .summary-cell .links,
- tr.collapsed td .summary-cell .actions {
+ tr.container.collapsed td .summary-cell .links,
+ tr.container.collapsed td .summary-cell .actions {
display: none;
}
- tr.collapsed:hover .summary-cell .hoverHide.tags,
- tr.collapsed:hover .summary-cell .hoverHide.label {
+ tr.container.collapsed:hover .summary-cell .hoverHide.tags,
+ tr.container.collapsed:hover .summary-cell .hoverHide.label {
+ display: none;
+ }
+ tr.detailsRow.collapsed {
display: none;
}
td .summary-cell .tags .tag {
@@ -294,19 +300,21 @@
return html`
<tr class="${classMap({container: true, collapsed: !this.isExpanded})}">
<td class="iconCol" @click="${this.toggleExpanded}">
- <div>${this.renderIcon()}</div>
+ <div>${this.runningIcon()}</div>
</td>
<td class="nameCol" @click="${this.toggleExpanded}">
<div class="flex">
<gr-hovercard-run .run="${this.result}"></gr-hovercard-run>
- <div class="name">${this.result.checkName}</div>
+ <div class="name">
+ ${this.result.checkName} ${this.renderStatus()}
+ </div>
<div class="space"></div>
${this.renderPrimaryRunAction()}
</div>
</td>
<td class="summaryCol">
<div class="summary-cell">
- ${this.renderLink(primaryLink(this.result))}
+ ${this.renderLink(firstPrimaryLink(this.result))}
${this.renderSummary(this.result.summary)}
<div class="message" @click="${this.toggleExpanded}">
${this.isExpanded ? '' : this.result.message}
@@ -316,7 +324,6 @@
</div>
${this.renderLabel()} ${this.renderLinks()} ${this.renderActions()}
</div>
- ${this.renderExpanded()}
</td>
<td class="expanderCol" @click="${this.toggleExpanded}">
<div
@@ -338,6 +345,10 @@
</div>
</td>
</tr>
+ <tr class="${classMap({detailsRow: true, collapsed: !this.isExpanded})}">
+ <td></td>
+ <td colspan="3">${this.renderExpanded()}</td>
+ </tr>
`;
}
@@ -376,7 +387,12 @@
`;
}
- renderIcon() {
+ private renderStatus() {
+ if (this.result?.status !== RunStatus.RUNNING) return;
+ return html`<span>(Running)</span>`;
+ }
+
+ private runningIcon() {
if (this.result?.status !== RunStatus.RUNNING) return;
return html`<iron-icon icon="gr-icons:timelapse"></iron-icon>`;
}
@@ -397,12 +413,23 @@
}
renderLinks() {
- const links = otherLinks(this.result);
+ const links = otherPrimaryLinks(this.result)
+ // Showing the same icons twice without text is super confusing.
+ .filter(
+ (link: Link, index: number, array: Link[]) =>
+ array.findIndex(other => link.icon === other.icon) === index
+ )
+ // 4 is enough for the summary row. All are shown in expanded state.
+ .slice(0, 4);
if (links.length === 0) return;
- return html`<div class="links">${links.map(this.renderLink)}</div>`;
+ return html`<div class="links">
+ ${links.map(link => this.renderLink(link))}
+ </div>`;
}
renderLink(link?: Link) {
+ // The expanded state renders all links in more detail. Hide in summary.
+ if (this.isExpanded) return;
if (!link) return;
const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
return html`<a href="${link.url}" target="_blank"
@@ -485,10 +512,23 @@
@property()
repoConfig?: ConfigInfo;
+ private changeService = appContext.changeService;
+
static get styles() {
return [
sharedStyles,
css`
+ .links {
+ white-space: normal;
+ padding: var(--spacing-s) 0;
+ }
+ .links a {
+ display: inline-block;
+ margin-right: var(--spacing-xl);
+ }
+ .links a iron-icon {
+ margin-right: var(--spacing-xs);
+ }
.message {
padding: var(--spacing-m) var(--spacing-m) var(--spacing-m) 0;
}
@@ -504,6 +544,8 @@
render() {
if (!this.result) return '';
return html`
+ ${this.renderFirstPrimaryLink()} ${this.renderOtherPrimaryLinks()}
+ ${this.renderSecondaryLinks()} ${this.renderCodePointers()}
<gr-endpoint-decorator name="check-result-expanded">
<gr-endpoint-param
name="run"
@@ -522,6 +564,67 @@
</gr-endpoint-decorator>
`;
}
+
+ private renderFirstPrimaryLink() {
+ const link = firstPrimaryLink(this.result);
+ if (!link) return;
+ return html`<div class="links">${this.renderLink(link)}</div>`;
+ }
+
+ private renderOtherPrimaryLinks() {
+ const links = otherPrimaryLinks(this.result);
+ if (links.length === 0) return;
+ return html`<div class="links">
+ ${links.map(link => this.renderLink(link))}
+ </div>`;
+ }
+
+ private renderSecondaryLinks() {
+ const links = secondaryLinks(this.result);
+ if (links.length === 0) return;
+ return html`<div class="links">
+ ${links.map(link => this.renderLink(link))}
+ </div>`;
+ }
+
+ private renderCodePointers() {
+ const pointers = this.result?.codePointers ?? [];
+ if (pointers.length === 0) return;
+ const links = pointers.map(pointer => {
+ let rangeText = '';
+ const start = pointer?.range?.start_line;
+ const end = pointer?.range?.end_line;
+ if (start) rangeText += `#${start}`;
+ if (end && start !== end) rangeText += `-${end}`;
+ const change = this.changeService.getChange();
+ assertIsDefined(change);
+ const path = pointer.path;
+ const patchset = this.result?.patchset as PatchSetNumber | undefined;
+ const line = pointer?.range?.start_line;
+ return {
+ icon: LinkIcon.CODE,
+ tooltip: `${path}${rangeText}`,
+ url: GerritNav.getUrlForDiff(change, path, patchset, undefined, line),
+ primary: true,
+ };
+ });
+ return links.map(
+ link => html`<div class="links">${this.renderLink(link, false)}</div>`
+ );
+ }
+
+ private renderLink(link?: Link, targetBlank = true) {
+ if (!link) return;
+ const text = link.tooltip ?? tooltipForLink(link.icon);
+ const target = targetBlank ? '_blank' : undefined;
+ return html`<a href="${link.url}" target="${ifDefined(target)}">
+ <iron-icon
+ class="link"
+ icon="gr-icons:${iconForLink(link.icon)}"
+ ></iron-icon
+ ><span>${text}</span>
+ </a>`;
+ }
}
const SHOW_ALL_THRESHOLDS: Map<Category, number> = new Map();
@@ -611,11 +714,11 @@
constructor() {
super();
- this.subscribe('actions', allActions$);
- this.subscribe('links', allLinks$);
- this.subscribe('checksPatchsetNumber', checksPatchsetNumber$);
+ this.subscribe('actions', topLevelActionsSelected$);
+ this.subscribe('links', topLevelLinksSelected$);
+ this.subscribe('checksPatchsetNumber', checksSelectedPatchsetNumber$);
this.subscribe('latestPatchsetNumber', latestPatchNum$);
- this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
+ this.subscribe('someProvidersAreLoading', someProvidersAreLoadingSelected$);
}
static get styles() {
@@ -634,6 +737,9 @@
var(--spacing-xl);
border-bottom: 1px solid var(--border-color);
}
+ .header.notLatest {
+ background-color: var(--emphasis-color);
+ }
.headerTopRow,
.headerBottomRow {
max-width: 1600px;
@@ -665,10 +771,20 @@
.headerBottomRow {
margin-top: var(--spacing-s);
}
+ .headerTopRow .right,
.headerBottomRow .right {
display: flex;
align-items: center;
}
+ .headerTopRow .right .goToLatest {
+ display: none;
+ }
+ .notLatest .headerTopRow .right .goToLatest {
+ display: block;
+ }
+ .headerTopRow .right .goToLatest gr-button {
+ margin-right: var(--spacing-m);
+ }
.headerBottomRow iron-icon {
color: var(--link-color);
}
@@ -812,16 +928,34 @@
private scrollElIntoView(selector: string) {
this.updateComplete.then(() => {
let el = this.shadowRoot?.querySelector(selector);
- // <gr-result-row> has display:contents and cannot be scrolled into view
- // itself. Thus we are preferring to scroll the first child into view.
- el = el?.shadowRoot?.firstElementChild ?? el;
- el?.scrollIntoView({block: 'center'});
+ // el might be a <gr-result-row> with an empty shadowRoot. Let's wait a
+ // moment before trying to find a child element in it.
+ setTimeout(() => {
+ // <gr-result-row> has display:contents and cannot be scrolled into view
+ // itself. Thus we are preferring to scroll the first child into view.
+ el = el?.shadowRoot?.firstElementChild ?? el;
+ el?.scrollIntoView({block: 'center'});
+ }, 0);
});
}
render() {
- return html`
- <div class="header">
+ // To pass CSS mixins for @apply to Polymer components, they need to appear
+ // in <style> inside the template.
+ const style = html`<style>
+ .headerTopRow .right .goToLatest gr-button {
+ --gr-button: {
+ padding: var(--spacing-s) var(--spacing-m);
+ text-transform: none;
+ }
+ }
+ </style>`;
+ const headerClasses = classMap({
+ header: true,
+ notLatest: !!this.checksPatchsetNumber,
+ });
+ return html`${style}
+ <div class="${headerClasses}">
<div class="headerTopRow">
<div class="left">
<h2 class="heading-2">Results</h2>
@@ -831,8 +965,13 @@
</div>
</div>
<div class="right">
+ <div class="goToLatest">
+ <gr-button @click="${this.goToLatestPatchset}" link
+ >Go to latest patchset</gr-button
+ >
+ </div>
<gr-dropdown-list
- value="${this.checksPatchsetNumber}"
+ value="${this.checksPatchsetNumber ?? this.latestPatchsetNumber}"
.items="${this.createPatchsetDropdownItems()}"
@value-change="${this.onPatchsetSelected}"
></gr-dropdown-list>
@@ -852,14 +991,20 @@
${this.renderSection(Category.WARNING)}
${this.renderSection(Category.INFO)}
${this.renderSection(Category.SUCCESS)}
- </div>
- `;
+ </div>`;
}
private renderLinks() {
const links = this.links ?? [];
if (links.length === 0) return;
- const primaryLinks = links.filter(a => a.primary).slice(0, 4);
+ const primaryLinks = links
+ .filter(a => a.primary)
+ // Showing the same icons twice without text is super confusing.
+ .filter(
+ (link: Link, index: number, array: Link[]) =>
+ array.findIndex(other => link.icon === other.icon) === index
+ )
+ .slice(0, 4);
const overflowLinks = links.filter(a => !primaryLinks.includes(a));
return html`
${primaryLinks.map(this.renderLink)}
@@ -957,6 +1102,10 @@
this.checksService.setPatchset(patchset as PatchSetNumber);
}
+ private goToLatestPatchset() {
+ this.checksService.setPatchset(undefined);
+ }
+
private createPatchsetDropdownItems() {
if (!this.latestPatchsetNumber) return [];
return Array.from(Array(this.latestPatchsetNumber), (_, i) => {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index edcc856..093eecb 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -32,15 +32,19 @@
import {
AttemptDetail,
compareByWorstCategory,
+ fireActionTriggered,
iconForCategory,
iconForRun,
+ PRIMARY_STATUS_ACTIONS,
primaryRunAction,
worstCategory,
} from '../../services/checks/checks-util';
import {
+ allRunsSelectedPatchset$,
CheckRun,
- allRuns$,
+ ChecksPatchset,
fakeActions,
+ fakeLinks,
fakeRun0,
fakeRun1,
fakeRun2,
@@ -50,7 +54,6 @@
fakeRun4_3,
fakeRun4_4,
updateStateSetResults,
- fakeLinks,
} from '../../services/checks/checks-model';
import {assertIsDefined} from '../../utils/common-util';
import {whenVisible} from '../../utils/dom-util';
@@ -134,10 +137,6 @@
div.chip.selected iron-icon.filter {
color: var(--selected-foreground);
}
- .chip.selected gr-checks-action,
- .chip.deselected gr-checks-action {
- display: none;
- }
gr-checks-action {
/* The button should fit into the 20px line-height. The negative
margin provides the extra space needed for the vertical padding.
@@ -314,6 +313,9 @@
@property()
runs: CheckRun[] = [];
+ @property({type: Boolean, reflect: true})
+ collapsed = false;
+
@property()
selectedRuns: string[] = [];
@@ -333,7 +335,7 @@
constructor() {
super();
- this.subscribe('runs', allRuns$);
+ this.subscribe('runs', allRunsSelectedPatchset$);
}
static get styles() {
@@ -342,12 +344,30 @@
css`
:host {
display: block;
+ }
+ :host(:not([collapsed])) {
+ min-width: 320px;
padding: var(--spacing-l) var(--spacing-xl) var(--spacing-xl)
var(--spacing-xl);
}
+ :host([collapsed]) {
+ padding: var(--spacing-l) 0;
+ }
.title {
display: flex;
- justify-content: space-between;
+ }
+ .title .flex-space {
+ flex-grow: 1;
+ }
+ .title gr-button {
+ --padding: var(--spacing-s) var(--spacing-m);
+ white-space: nowrap;
+ }
+ .title gr-button.expandButton {
+ --padding: var(--spacing-xs) var(--spacing-s);
+ }
+ :host(:not([collapsed])) .expandButton {
+ margin-right: calc(0px - var(--spacing-m));
}
.expandIcon {
width: var(--line-height-h3);
@@ -408,17 +428,14 @@
}
render() {
+ if (this.collapsed) {
+ return html`${this.renderCollapseButton()}`;
+ }
return html`
<h2 class="title">
<div class="heading-2">Runs</div>
- <div class="font-normal">
- <gr-button
- ?hidden="${this.selectedRuns.length < 2}"
- link
- @click="${() => fireRunSelectionReset(this)}"
- >Unselect All</gr-button
- >
- </div>
+ <div class="flex-space"></div>
+ ${this.renderTitleButtons()} ${this.renderCollapseButton()}
</h2>
<input
id="filterInput"
@@ -427,36 +444,102 @@
?hidden="${!this.showFilter()}"
@input="${this.onInput}"
/>
- ${this.renderSection(RunStatus.COMPLETED)}
${this.renderSection(RunStatus.RUNNING)}
+ ${this.renderSection(RunStatus.COMPLETED)}
${this.renderSection(RunStatus.RUNNABLE)} ${this.renderFakeControls()}
`;
}
+ private renderTitleButtons() {
+ if (this.selectedRuns.length < 2) return;
+ const actions = this.selectedRuns.map(selected => {
+ const run = this.runs.find(
+ run => run.isLatestAttempt && run.checkName === selected
+ );
+ return primaryRunAction(run);
+ });
+ const runButtonDisabled = !actions.every(
+ action =>
+ action?.name === PRIMARY_STATUS_ACTIONS.RUN ||
+ action?.name === PRIMARY_STATUS_ACTIONS.RERUN
+ );
+ return html`
+ <gr-button
+ class="font-normal"
+ link
+ @click="${() => fireRunSelectionReset(this)}"
+ >Unselect All</gr-button
+ >
+ <gr-button
+ class="font-normal"
+ link
+ title="${runButtonDisabled
+ ? 'Disabled. Unselect checks without a "Run" action to enable the button.'
+ : ''}"
+ has-tooltip="${runButtonDisabled}"
+ ?disabled="${runButtonDisabled}"
+ @click="${() => {
+ actions.forEach(action => fireActionTriggered(this, action));
+ }}"
+ >Run Selected</gr-button
+ >
+ `;
+ }
+
+ private renderCollapseButton() {
+ return html`
+ <gr-button
+ link
+ class="expandButton"
+ role="switch"
+ ?aria-checked="${this.collapsed}"
+ aria-label="${this.collapsed
+ ? 'Expand runs panel'
+ : 'Collapse runs panel'}"
+ has-tooltip="true"
+ title="${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}"
+ @click="${() => (this.collapsed = !this.collapsed)}"
+ ><iron-icon
+ class="expandIcon"
+ icon="${this.collapsed
+ ? 'gr-icons:chevron-right'
+ : 'gr-icons:chevron-left'}"
+ ></iron-icon>
+ </gr-button>
+ `;
+ }
+
onInput() {
assertIsDefined(this.filterInput, 'filter <input> element');
this.filterRegExp = new RegExp(this.filterInput.value, 'i');
}
none() {
- updateStateSetResults('f0', [], []);
- updateStateSetResults('f1', []);
- updateStateSetResults('f2', []);
- updateStateSetResults('f3', []);
- updateStateSetResults('f4', []);
+ updateStateSetResults('f0', [], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults('f1', [], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults('f2', [], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults('f3', [], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults('f4', [], [], [], ChecksPatchset.LATEST);
}
all() {
- updateStateSetResults('f0', [fakeRun0], fakeActions, fakeLinks);
- updateStateSetResults('f1', [fakeRun1]);
- updateStateSetResults('f2', [fakeRun2]);
- updateStateSetResults('f3', [fakeRun3]);
- updateStateSetResults('f4', [
- fakeRun4_1,
- fakeRun4_2,
- fakeRun4_3,
- fakeRun4_4,
- ]);
+ updateStateSetResults(
+ 'f0',
+ [fakeRun0],
+ fakeActions,
+ fakeLinks,
+ ChecksPatchset.LATEST
+ );
+ updateStateSetResults('f1', [fakeRun1], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults('f2', [fakeRun2], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults('f3', [fakeRun3], [], [], ChecksPatchset.LATEST);
+ updateStateSetResults(
+ 'f4',
+ [fakeRun4_1, fakeRun4_2, fakeRun4_3, fakeRun4_4],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
}
toggle(
@@ -466,7 +549,13 @@
links: Link[] = []
) {
const newRuns = this.runs.includes(runs[0]) ? [] : runs;
- updateStateSetResults(plugin, newRuns, actions, links);
+ updateStateSetResults(
+ plugin,
+ newRuns,
+ actions,
+ links,
+ ChecksPatchset.LATEST
+ );
}
renderSection(status: RunStatus) {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index b28596a..e7a7659 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -21,13 +21,13 @@
import {
CheckResult,
CheckRun,
- allResults$,
- allRuns$,
- checksPatchsetNumber$,
+ allResultsSelected$,
+ checksSelectedPatchsetNumber$,
+ allRunsSelectedPatchset$,
} from '../../services/checks/checks-model';
import './gr-checks-runs';
import './gr-checks-results';
-import {changeNum$} from '../../services/change/change-model';
+import {changeNum$, latestPatchNum$} from '../../services/change/change-model';
import {NumericChangeId, PatchSetNumber} from '../../types/common';
import {ActionTriggeredEvent} from '../../services/checks/checks-util';
import {AttemptSelectedEvent, RunSelectedEvent} from './gr-checks-util';
@@ -56,6 +56,9 @@
checksPatchsetNumber: PatchSetNumber | undefined = undefined;
@property()
+ latestPatchsetNumber: PatchSetNumber | undefined = undefined;
+
+ @property()
changeNum: NumericChangeId | undefined = undefined;
@state()
@@ -72,9 +75,10 @@
constructor() {
super();
- this.subscribe('runs', allRuns$);
- this.subscribe('results', allResults$);
- this.subscribe('checksPatchsetNumber', checksPatchsetNumber$);
+ this.subscribe('runs', allRunsSelectedPatchset$);
+ this.subscribe('results', allResultsSelected$);
+ this.subscribe('checksPatchsetNumber', checksSelectedPatchsetNumber$);
+ this.subscribe('latestPatchsetNumber', latestPatchNum$);
this.subscribe('changeNum', changeNum$);
this.addEventListener('action-triggered', (e: ActionTriggeredEvent) =>
@@ -91,7 +95,6 @@
display: flex;
}
.runs {
- min-width: 300px;
min-height: 400px;
border-right: 1px solid var(--border-color);
}
@@ -136,10 +139,11 @@
handleActionTriggered(action: Action, run?: CheckRun) {
if (!this.changeNum) return;
- if (!this.checksPatchsetNumber) return;
+ const patchSet = this.checksPatchsetNumber ?? this.latestPatchsetNumber;
+ if (!patchSet) return;
const promise = action.callback(
this.changeNum,
- this.checksPatchsetNumber,
+ patchSet,
run?.attempt,
run?.externalId,
run?.checkName,
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts
index 0fa2f1e..97f4a89 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts
@@ -45,7 +45,7 @@
account="[[account]]"
hidden$="[[!_hasAvatars]]"
hidden=""
- image-size="56"
+ imageSize="56"
aria-label="Account avatar"
></gr-avatar>
</gr-dropdown>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 4985c1b..8f9dfe2 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -217,6 +217,8 @@
url,
trace,
});
+ } else if (response.status === 429) {
+ this._showQuotaExceeded({status, statusText});
} else {
this._showErrorDialog(
this._constructServerErrorMsg({
@@ -229,7 +231,7 @@
);
}
}
- console.info(`server error: ${errorText}`);
+ this.reporting.error(new Error(`Server error: ${errorText}`));
});
};
@@ -260,6 +262,19 @@
});
}
+ _showQuotaExceeded({status, statusText}: ErrorMsg) {
+ const tip = 'Try again later';
+ const errorText = 'Too many requests from this client';
+ this._showErrorDialog(
+ this._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ tip,
+ })
+ );
+ }
+
_constructServerErrorMsg({
errorText,
status,
@@ -304,7 +319,7 @@
private readonly handleNetworkError = (e: NetworkErrorEvent) => {
this._showAlert('Server unavailable');
- console.error(e.detail.error.message);
+ this.reporting.error(new Error(`network error: ${e.detail.error.message}`));
};
// TODO(dhruvsr): allow less priority alerts to override high priority alerts
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
index 91f119e..fe4d9da 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
@@ -207,7 +207,6 @@
});
test('show network error', done => {
- const consoleErrorStub = sinon.stub(console, 'error');
const showAlertStub = sinon.stub(element, '_showAlert');
element.dispatchEvent(
new CustomEvent('network-error', {
@@ -218,8 +217,6 @@
assert.isTrue(showAlertStub.calledOnce);
assert.isTrue(showAlertStub.lastCall.calledWithExactly(
'Server unavailable'));
- assert.isTrue(consoleErrorStub.calledOnce);
- assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
done();
});
});
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.css.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.css.ts
deleted file mode 100644
index ccba289..0000000
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.css.ts
+++ /dev/null
@@ -1,30 +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 {css} from 'lit-element';
-
-export const cssTemplate = css`
- .key {
- background-color: var(--chip-background-color);
- color: var(--primary-text-color);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- display: inline-block;
- font-weight: var(--font-weight-bold);
- padding: var(--spacing-xxs) var(--spacing-m);
- text-align: center;
- }
-`;
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
index 091684f..657d7cc 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
@@ -16,9 +16,7 @@
*/
import {html} from 'lit-html';
import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement, property} from 'lit-element';
-import {cssTemplate} from './gr-key-binding-display.css';
-import {sharedStyles} from '../../../styles/shared-styles';
+import {css, customElement, property} from 'lit-element';
declare global {
interface HTMLElementTagNameMap {
@@ -29,7 +27,20 @@
@customElement('gr-key-binding-display')
export class GrKeyBindingDisplay extends GrLitElement {
static get styles() {
- return [sharedStyles, cssTemplate];
+ return [
+ css`
+ .key {
+ background-color: var(--chip-background-color);
+ color: var(--primary-text-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ display: inline-block;
+ font-weight: var(--font-weight-bold);
+ padding: var(--spacing-xxs) var(--spacing-m);
+ text-align: center;
+ }
+ `,
+ ];
}
render() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.ts
index 763a524..c66ea4f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.ts
@@ -18,6 +18,7 @@
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {queryAndAssert} from '../../../utils/common-util';
export class GrDiffBuilderBinary extends GrDiffBuilderUnified {
constructor(
@@ -32,7 +33,7 @@
const section = this._createElement('tbody', 'binary-diff');
const line = new GrDiffLine(GrDiffLineType.BOTH, 'FILE', 'FILE');
const fileRow = this._createRow(line);
- const contentTd = fileRow.querySelector('td.both.file')!;
+ const contentTd = queryAndAssert(fileRow, 'td.both.file')!;
contentTd.textContent = ' Difference in binary files';
section.appendChild(fileRow);
return section;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 96cb56a..f70974f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -17,6 +17,7 @@
import {
ContentLoadNeededEventDetail,
DiffContextExpandedExternalDetail,
+ LineNumberEventDetail,
MovedLinkClickedEventDetail,
RenderPreferences,
} from '../../../api/diff';
@@ -314,7 +315,9 @@
const firstGroupIsSkipped = !!contextGroups[0].skip;
const lastGroupIsSkipped = !!contextGroups[contextGroups.length - 1].skip;
- const showAbove = leftStart > 1 && !firstGroupIsSkipped;
+ const containsWholeFile = this._numLinesLeft === leftEnd - leftStart + 1;
+ const showAbove =
+ (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
const showBelow = leftEnd < this._numLinesLeft && !lastGroupIsSkipped;
if (showAbove) {
@@ -458,6 +461,30 @@
button.setAttribute('aria-label', `${number} added`);
}
}
+ button.addEventListener('mouseenter', () => {
+ button.dispatchEvent(
+ new CustomEvent<LineNumberEventDetail>('line-number-mouse-enter', {
+ detail: {
+ lineNum: number,
+ side,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
+ });
+ button.addEventListener('mouseleave', () => {
+ button.dispatchEvent(
+ new CustomEvent<LineNumberEventDetail>('line-number-mouse-leave', {
+ detail: {
+ lineNum: number,
+ side,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
+ });
}
return td;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
index 69c6139..68bdaa6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -15,54 +15,93 @@
* limitations under the License.
*/
-import '../../shared/gr-cursor-manager/gr-cursor-manager';
-import {
- AbortStop,
- CursorMoveResult,
- GrCursorManager,
- Stop,
- isTargetable,
-} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {customElement, observe, property} from '@polymer/decorators';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-cursor_html';
-import {DiffViewMode} from '../../../api/diff';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {Subscription} from 'rxjs';
+import {AbortStop, CursorMoveResult, Stop} from '../../../api/core';
+import {
+ DiffViewMode,
+ GrDiffCursor as GrDiffCursorApi,
+ LineNumberEventDetail,
+} from '../../../api/diff';
import {ScrollMode, Side} from '../../../constants/constants';
-import {customElement, property, observe} from '@polymer/decorators';
-import {GrDiffLineType} from '../gr-diff/gr-diff-line';
-import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
import {PolymerDomWrapper} from '../../../types/types';
+import {toggleClass} from '../../../utils/dom-util';
+import {
+ GrCursorManager,
+ isTargetable,
+} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {GrDiffLineType} from '../gr-diff/gr-diff-line';
import {GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {GrDiff} from '../gr-diff/gr-diff';
-import {Subscription} from 'rxjs';
-import {toggleClass} from '../../../utils/dom-util';
type GrDiffRowType = GrDiffLineType | GrDiffGroupType;
const LEFT_SIDE_CLASS = 'target-side-left';
const RIGHT_SIDE_CLASS = 'target-side-right';
-export interface GrDiffCursor {
- $: {};
+/** A subset of the GrDiff API that the cursor is using. */
+export interface GrDiffCursorable extends HTMLElement {
+ isRangeSelected(): boolean;
+ createRangeComment(): void;
+ getCursorStops(): Stop[];
+ path?: string;
}
-@customElement('gr-diff-cursor')
-export class GrDiffCursor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrDiffCursor implements GrDiffCursorApi {
private preventAutoScrollOnManualScroll = false;
- @property({type: String})
- side = Side.RIGHT;
+ set side(side: Side) {
+ if (this.sideInternal === side) {
+ return;
+ }
+ if (this.sideInternal && this.diffRow) {
+ this.fireCursorMoved(
+ 'line-cursor-moved-out',
+ this.diffRow,
+ this.sideInternal
+ );
+ }
+ this.sideInternal = side;
+ this.updateSideClass();
+ if (this.diffRow) {
+ this.fireCursorMoved('line-cursor-moved-in', this.diffRow, this.side);
+ }
+ }
- @property({type: Object, notify: true, observer: '_rowChanged'})
- diffRow?: HTMLElement;
+ get side(): Side {
+ return this.sideInternal;
+ }
- @property({type: Object})
- diffs: GrDiff[] = [];
+ private sideInternal = Side.RIGHT;
+
+ set diffRow(diffRow: HTMLElement | undefined) {
+ if (this.diffRowInternal) {
+ this.diffRowInternal.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
+ this.fireCursorMoved(
+ 'line-cursor-moved-out',
+ this.diffRowInternal,
+ this.side
+ );
+ }
+ this.diffRowInternal = diffRow;
+
+ this.updateSideClass();
+ if (this.diffRow) {
+ this.fireCursorMoved('line-cursor-moved-in', this.diffRow, this.side);
+ }
+ }
+
+ get diffRow(): HTMLElement | undefined {
+ return this.diffRowInternal;
+ }
+
+ private diffRowInternal?: HTMLElement;
+
+ private diffs: GrDiffCursorable[] = [];
/**
* If set, the cursor will attempt to move to the line number (instead of
@@ -72,62 +111,27 @@
* to that position. This parameter should be set at most for one gr-diff
* element in the page.
*/
- @property({type: Number})
initialLineNumber: number | null = null;
- @property({type: Boolean})
- _listeningForScroll = false;
-
private cursorManager = new GrCursorManager();
+ private targetSubscription?: Subscription;
+
constructor() {
- super();
this.cursorManager.cursorTargetClass = 'target-row';
this.cursorManager.scrollMode = ScrollMode.KEEP_VISIBLE;
this.cursorManager.focusOnMove = true;
- }
- /** @override */
- ready() {
- super.ready();
- afterNextRender(this, () => {
- /*
- This represents the diff cursor is ready for interaction coming from
- client components. It is more then Polymer "ready" lifecycle, as no
- "ready" events are automatically fired by Polymer, it means
- the cursor is completely interactable - in this case attached and
- painted on the page. We name it "ready" instead of "rendered" as the
- long-term goal is to make gr-diff-cursor a javascript class - not a DOM
- element with an actual lifecycle. This will be triggered only once
- per element.
- */
- this.dispatchEvent(
- new CustomEvent('ready', {
- composed: true,
- bubbles: false,
- })
- );
- });
- }
-
- private targetSubscription?: Subscription;
-
- /** @override */
- connectedCallback() {
- super.connectedCallback();
- // Catch when users are scrolling as the view loads.
window.addEventListener('scroll', this._boundHandleWindowScroll);
this.targetSubscription = this.cursorManager.target$.subscribe(target => {
this.diffRow = target || undefined;
});
}
- /** @override */
- disconnectedCallback() {
+ dispose() {
if (this.targetSubscription) this.targetSubscription.unsubscribe();
window.removeEventListener('scroll', this._boundHandleWindowScroll);
this.cursorManager.unsetCursor();
- super.disconnectedCallback();
}
// Don't remove - used by clients embedding gr-diff outside of Gerrit.
@@ -297,10 +301,10 @@
this.moveToFirstChunk();
}
}
- this.reInit();
+ this.resetScrollMode();
}
- reInit() {
+ resetScrollMode() {
this.cursorManager.scrollMode = ScrollMode.KEEP_VISIBLE;
}
@@ -313,7 +317,7 @@
};
reInitAndUpdateStops() {
- this.reInit();
+ this.resetScrollMode();
this._updateStops();
}
@@ -366,23 +370,25 @@
* {leftSide: false, number: 123} for line 123 of the revision, or
* {leftSide: true, number: 321} for line 321 of the base patch.
* Returns null if an address is not available.
- *
*/
getAddress() {
if (!this.diffRow) {
return null;
}
-
// Get the line-number cell targeted by the cursor. If the mode is unified
// then prefer the revision cell if available.
+ return this.getAddressFor(this.diffRow, this.side);
+ }
+
+ private getAddressFor(diffRow: HTMLElement, side: Side) {
let cell;
if (this._getViewMode() === DiffViewMode.UNIFIED) {
- cell = this.diffRow.querySelector('.lineNum.right');
+ cell = diffRow.querySelector('.lineNum.right');
if (!cell) {
- cell = this.diffRow.querySelector('.lineNum.left');
+ cell = diffRow.querySelector('.lineNum.left');
}
} else {
- cell = this.diffRow.querySelector('.lineNum.' + this.side);
+ cell = diffRow.querySelector('.lineNum.' + side);
}
if (!cell) {
return null;
@@ -456,15 +462,28 @@
);
}
- _rowChanged(_: HTMLElement, oldRow: HTMLElement) {
- if (oldRow) {
- oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
+ private fireCursorMoved(
+ event: 'line-cursor-moved-out' | 'line-cursor-moved-in',
+ row: HTMLElement,
+ side: Side
+ ) {
+ const address = this.getAddressFor(row, side);
+ if (address) {
+ const {leftSide, number} = address;
+ row.dispatchEvent(
+ new CustomEvent<LineNumberEventDetail>(event, {
+ detail: {
+ lineNum: number,
+ side: leftSide ? Side.LEFT : Side.RIGHT,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
}
- this._updateSideClass();
}
- @observe('side')
- _updateSideClass() {
+ private updateSideClass() {
if (!this.diffRow) {
return;
}
@@ -498,69 +517,53 @@
);
}
- /**
- * Setup and tear down on-render listeners for any diffs that are added or
- * removed from the cursor.
- */
- @observe('diffs.splices')
- _diffsChanged(changeRecord: PolymerSpliceChange<GrDiff[]>) {
- if (!changeRecord) {
- return;
+ replaceDiffs(diffs: GrDiffCursorable[]) {
+ for (const diff of this.diffs) {
+ this.removeEventListeners(diff);
}
-
+ this.diffs = [];
+ for (const diff of diffs) {
+ this.addEventListeners(diff);
+ }
+ this.diffs.push(...diffs);
this._updateStops();
+ }
- let splice;
- let i;
- for (
- let spliceIdx = 0;
- changeRecord.indexSplices && spliceIdx < changeRecord.indexSplices.length;
- spliceIdx++
- ) {
- splice = changeRecord.indexSplices[spliceIdx];
-
- // Removals must come before additions, because the gr-diff instances
- // might be the same.
- for (i = 0; i < splice?.removed.length; i++) {
- splice.removed[i].removeEventListener(
- 'loading-changed',
- this.boundHandleDiffLoadingChanged
- );
- splice.removed[i].removeEventListener(
- 'render-start',
- this._boundHandleDiffRenderStart
- );
- splice.removed[i].removeEventListener(
- 'render-content',
- this._boundHandleDiffRenderContent
- );
- splice.removed[i].removeEventListener(
- 'line-selected',
- this._boundHandleDiffLineSelected
- );
- }
-
- for (i = splice.index; i < splice.index + splice.addedCount; i++) {
- this.diffs[i].addEventListener(
- 'loading-changed',
- this.boundHandleDiffLoadingChanged
- );
- this.diffs[i].addEventListener(
- 'render-start',
- this._boundHandleDiffRenderStart
- );
- this.diffs[i].addEventListener(
- 'render-content',
- this._boundHandleDiffRenderContent
- );
- this.diffs[i].addEventListener(
- 'line-selected',
- this._boundHandleDiffLineSelected
- );
- }
+ unregisterDiff(diff: GrDiffCursorable) {
+ // This can happen during destruction - just don't unregister then.
+ if (!this.diffs) return;
+ const i = this.diffs.indexOf(diff);
+ if (i !== -1) {
+ this.diffs.splice(i, 1);
}
}
+ private removeEventListeners(diff: GrDiffCursorable) {
+ diff.removeEventListener(
+ 'loading-changed',
+ this.boundHandleDiffLoadingChanged
+ );
+ diff.removeEventListener('render-start', this._boundHandleDiffRenderStart);
+ diff.removeEventListener(
+ 'render-content',
+ this._boundHandleDiffRenderContent
+ );
+ diff.removeEventListener(
+ 'line-selected',
+ this._boundHandleDiffLineSelected
+ );
+ }
+
+ private addEventListeners(diff: GrDiffCursorable) {
+ diff.addEventListener(
+ 'loading-changed',
+ this.boundHandleDiffLoadingChanged
+ );
+ diff.addEventListener('render-start', this._boundHandleDiffRenderStart);
+ diff.addEventListener('render-content', this._boundHandleDiffRenderContent);
+ diff.addEventListener('line-selected', this._boundHandleDiffLineSelected);
+ }
+
_findRowByNumberAndFile(
targetNumber: number,
side: Side,
@@ -581,8 +584,97 @@
}
}
+// TODO(oler): Remove this once clients have migrated to using GrDiffCursor service.
+@customElement('gr-diff-cursor')
+export class GrDiffCursorElement extends PolymerElement {
+ static get template() {
+ return html``;
+ }
+
+ @property({type: String, observer: '_sideChanged'})
+ side: Side = Side.RIGHT;
+
+ _sideChanged(side: Side) {
+ this.cursor.side = side;
+ }
+
+ @property({type: Object}) diffs: GrDiff[] = [];
+
+ @observe('diffs.splices')
+ _diffsChanged() {
+ if (this.diffs) {
+ this.cursor.replaceDiffs(this.diffs);
+ }
+ }
+
+ private cursor = new GrDiffCursor();
+
+ /** @override */
+ ready() {
+ super.ready();
+ afterNextRender(this, () => {
+ /*
+ This represents the diff cursor is ready for interaction coming from
+ client components. It is more then Polymer "ready" lifecycle, as no
+ "ready" events are automatically fired by Polymer, it means
+ the cursor is completely interactable - in this case attached and
+ painted on the page. We name it "ready" instead of "rendered" as the
+ long-term goal is to make gr-diff-cursor a javascript class - not a DOM
+ element with an actual lifecycle. This will be triggered only once
+ per element.
+ */
+ this.dispatchEvent(
+ new CustomEvent('ready', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ });
+ }
+
+ /** @override */
+ disconnectedCallback() {
+ this.cursor.dispose();
+ super.disconnectedCallback();
+ }
+
+ isAtStart = this.cursor.isAtStart.bind(this.cursor);
+
+ isAtEnd = this.cursor.isAtEnd.bind(this.cursor);
+
+ moveLeft = this.cursor.moveLeft.bind(this.cursor);
+
+ moveRight = this.cursor.moveRight.bind(this.cursor);
+
+ moveDown = this.cursor.moveDown.bind(this.cursor);
+
+ moveUp = this.cursor.moveUp.bind(this.cursor);
+
+ moveToNextChunk = this.cursor.moveToNextChunk.bind(this.cursor);
+
+ moveToPreviousChunk = this.cursor.moveToPreviousChunk.bind(this.cursor);
+
+ moveToNextCommentThread = this.cursor.moveToNextCommentThread.bind(
+ this.cursor
+ );
+
+ moveToPreviousCommentThread = this.cursor.moveToPreviousCommentThread.bind(
+ this.cursor
+ );
+
+ moveToLineNumber = this.cursor.moveToLineNumber.bind(this.cursor);
+
+ moveToFirstChunk = this.cursor.moveToFirstChunk.bind(this.cursor);
+
+ moveToLastChunk = this.cursor.moveToLastChunk.bind(this.cursor);
+
+ reInit = this.cursor.resetScrollMode.bind(this.cursor);
+
+ createCommentInPlace = this.cursor.createCommentInPlace.bind(this.cursor);
+}
+
declare global {
interface HTMLElementTagNameMap {
- 'gr-diff-cursor': GrDiffCursor;
+ 'gr-diff-cursor': GrDiffCursorElement;
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.ts
deleted file mode 100644
index 1489006..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.ts
+++ /dev/null
@@ -1,19 +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``;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 7b72da8..3e63b0a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -22,26 +22,23 @@
import {listenOnce} from '../../../test/test-utils.js';
import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
import {createDefaultDiffPrefs} from '../../../constants/constants.js';
+import {GrDiffCursor} from './gr-diff-cursor.js';
const basicFixture = fixtureFromTemplate(html`
<gr-diff></gr-diff>
- <gr-diff-cursor></gr-diff-cursor>
`);
-const emptyFixture = fixtureFromElement('div');
-
suite('gr-diff-cursor tests', () => {
- let cursorElement;
+ let cursor;
let diffElement;
let diff;
setup(done => {
- const fixtureElems = basicFixture.instantiate();
- diffElement = fixtureElems[0];
- cursorElement = fixtureElems[1];
+ diffElement = basicFixture.instantiate();
+ cursor = new GrDiffCursor();
// Register the diff with the cursor.
- cursorElement.push('diffs', diffElement);
+ cursor.replaceDiffs([diffElement]);
diffElement.loggedIn = false;
diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
@@ -52,8 +49,8 @@
};
diffElement.path = 'some/path.ts';
const setupDone = () => {
- cursorElement._updateStops();
- cursorElement.moveToFirstChunk();
+ cursor._updateStops();
+ cursor.moveToFirstChunk();
diffElement.removeEventListener('render', setupDone);
done();
};
@@ -66,21 +63,21 @@
test('diff cursor functionality (side-by-side)', () => {
// The cursor has been initialized to the first delta.
- assert.isOk(cursorElement.diffRow);
+ assert.isOk(cursor.diffRow);
const firstDeltaRow = diffElement.shadowRoot
.querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursor.diffRow, firstDeltaRow);
- cursorElement.moveDown();
+ cursor.moveDown();
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
- cursorElement.moveUp();
+ cursor.moveUp();
- assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
+ assert.equal(cursor.diffRow, firstDeltaRow);
});
test('moveToFirstChunk', async () => {
@@ -116,24 +113,24 @@
// moveToFirstChunk() works correctly even if the button is not shown.
diffElement.prefs.show_file_comment_button = false;
await flush();
- cursorElement._updateStops();
+ cursor._updateStops();
const chunks = Array.from(diffElement.root.querySelectorAll(
'.section.delta'));
assert.equal(chunks.length, 2);
// Verify it works on fresh diff.
- cursorElement.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
- assert.equal(cursorElement.side, 'right');
+ cursor.moveToFirstChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
+ assert.equal(cursor.side, 'right');
// Verify it works from other cursor positions.
- cursorElement.moveToNextChunk();
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 1);
- assert.equal(cursorElement.side, 'left');
- cursorElement.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
- assert.equal(cursorElement.side, 'right');
+ cursor.moveToNextChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
+ assert.equal(cursor.side, 'left');
+ cursor.moveToFirstChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
+ assert.equal(cursor.side, 'right');
});
test('moveToLastChunk', async () => {
@@ -166,45 +163,45 @@
diffElement.diff = diff;
await flush();
- cursorElement._updateStops();
+ cursor._updateStops();
const chunks = Array.from(diffElement.root.querySelectorAll(
'.section.delta'));
assert.equal(chunks.length, 2);
// Verify it works on fresh diff.
- cursorElement.moveToLastChunk();
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 1);
- assert.equal(cursorElement.side, 'right');
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
+ assert.equal(cursor.side, 'right');
// Verify it works from other cursor positions.
- cursorElement.moveToPreviousChunk();
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
- assert.equal(cursorElement.side, 'left');
- cursorElement.moveToLastChunk();
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 1);
- assert.equal(cursorElement.side, 'right');
+ cursor.moveToPreviousChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
+ assert.equal(cursor.side, 'left');
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
+ assert.equal(cursor.side, 'right');
});
test('cursor scroll behavior', () => {
- assert.equal(cursorElement.cursorManager.scrollMode, 'keep-visible');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
diffElement.dispatchEvent(new Event('render-start'));
- assert.isTrue(cursorElement.cursorManager.focusOnMove);
+ assert.isTrue(cursor.cursorManager.focusOnMove);
window.dispatchEvent(new Event('scroll'));
- assert.equal(cursorElement.cursorManager.scrollMode, 'never');
- assert.isFalse(cursorElement.cursorManager.focusOnMove);
+ assert.equal(cursor.cursorManager.scrollMode, 'never');
+ assert.isFalse(cursor.cursorManager.focusOnMove);
diffElement.dispatchEvent(new Event('render-content'));
- assert.isTrue(cursorElement.cursorManager.focusOnMove);
+ assert.isTrue(cursor.cursorManager.focusOnMove);
- cursorElement.reInitCursor();
- assert.equal(cursorElement.cursorManager.scrollMode, 'keep-visible');
+ cursor.reInitCursor();
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
});
test('moves to selected line', () => {
- const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber');
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
diffElement.dispatchEvent(
new CustomEvent('line-selected', {
@@ -222,7 +219,7 @@
// We must allow the diff to re-render after setting the viewMode.
const renderHandler = function() {
diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
+ cursor.reInitCursor();
done();
};
diffElement.addEventListener('render', renderHandler);
@@ -231,25 +228,25 @@
test('diff cursor functionality (unified)', () => {
// The cursor has been initialized to the first delta.
- assert.isOk(cursorElement.diffRow);
+ assert.isOk(cursor.diffRow);
let firstDeltaRow = diffElement.shadowRoot
.querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursor.diffRow, firstDeltaRow);
firstDeltaRow = diffElement.shadowRoot
.querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursor.diffRow, firstDeltaRow);
- cursorElement.moveDown();
+ cursor.moveDown();
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
- cursorElement.moveUp();
+ cursor.moveUp();
- assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
+ assert.equal(cursor.diffRow, firstDeltaRow);
});
});
@@ -264,29 +261,29 @@
// Because the first delta in this diff is on the right, it should be set
// to the right side.
- assert.equal(cursorElement.side, 'right');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
- const firstIndex = cursorElement.cursorManager.index;
+ assert.equal(cursor.side, 'right');
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ const firstIndex = cursor.cursorManager.index;
// Move the side to the left. Because this delta only has a right side, we
// should be moved up to the previous line where there is content on the
// right. The previous row is part of the previous section.
- cursorElement.moveLeft();
+ cursor.moveLeft();
- assert.equal(cursorElement.side, 'left');
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.cursorManager.index, firstIndex - 1);
- assert.equal(cursorElement.diffRow.parentElement,
+ assert.equal(cursor.side, 'left');
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(cursor.cursorManager.index, firstIndex - 1);
+ assert.equal(cursor.diffRow.parentElement,
firstDeltaSection.previousSibling);
// If we move down, we should skip everything in the first delta because
// we are on the left side and the first delta has no content on the left.
- cursorElement.moveDown();
+ cursor.moveDown();
- assert.equal(cursorElement.side, 'left');
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.isTrue(cursorElement.cursorManager.index > firstIndex);
- assert.equal(cursorElement.diffRow.parentElement,
+ assert.equal(cursor.side, 'left');
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.isTrue(cursor.cursorManager.index > firstIndex);
+ assert.equal(cursor.diffRow.parentElement,
firstDeltaSection.nextSibling);
});
@@ -299,26 +296,26 @@
// We should be initialized to the first chunk. Since this chunk only has
// content on the right side, our side should be right.
- let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+ let currentIndex = indexOfChunk(cursor.diffRow.parentElement);
assert.equal(currentIndex, 0);
- assert.equal(cursorElement.side, 'right');
+ assert.equal(cursor.side, 'right');
// Move to the next chunk.
- cursorElement.moveToNextChunk();
+ cursor.moveToNextChunk();
// Since this chunk only has content on the left side. we should have been
// automatically moved over.
const previousIndex = currentIndex;
- currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+ currentIndex = indexOfChunk(cursor.diffRow.parentElement);
assert.equal(currentIndex, previousIndex + 1);
- assert.equal(cursorElement.side, 'left');
+ assert.equal(cursor.side, 'left');
});
suite('moved chunks without line range)', () => {
setup(done => {
const renderHandler = function() {
diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
+ cursor.reInitCursor();
done();
};
diffElement.addEventListener('render', renderHandler);
@@ -369,7 +366,7 @@
setup(done => {
const renderHandler = function() {
diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
+ cursor.reInitCursor();
done();
};
diffElement.addEventListener('render', renderHandler);
@@ -447,19 +444,19 @@
test('initialLineNumber not provided', done => {
let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber');
- const moveToChunkStub = sinon.stub(cursorElement, 'moveToFirstChunk')
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+ const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk')
.callsFake(() => {
- scrollBehaviorDuringMove = cursorElement.cursorManager.scrollMode;
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
});
function renderHandler() {
diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
+ cursor.reInitCursor();
assert.isFalse(moveToNumStub.called);
assert.isTrue(moveToChunkStub.called);
assert.equal(scrollBehaviorDuringMove, 'never');
- assert.equal(cursorElement.cursorManager.scrollMode, 'keep-visible');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
done();
}
diffElement.addEventListener('render', renderHandler);
@@ -468,34 +465,34 @@
test('initialLineNumber provided', done => {
let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber')
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber')
.callsFake(() => {
- scrollBehaviorDuringMove = cursorElement.cursorManager.scrollMode;
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
});
- const moveToChunkStub = sinon.stub(cursorElement, 'moveToFirstChunk');
+ const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
function renderHandler() {
diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
+ cursor.reInitCursor();
assert.isFalse(moveToChunkStub.called);
assert.isTrue(moveToNumStub.called);
assert.equal(moveToNumStub.lastCall.args[0], 10);
assert.equal(moveToNumStub.lastCall.args[1], 'right');
assert.equal(scrollBehaviorDuringMove, 'keep-visible');
- assert.equal(cursorElement.cursorManager.scrollMode, 'keep-visible');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
done();
}
diffElement.addEventListener('render', renderHandler);
- cursorElement.initialLineNumber = 10;
- cursorElement.side = 'right';
+ cursor.initialLineNumber = 10;
+ cursor.side = 'right';
diffElement._diffChanged(getMockDiffResponse());
});
test('getTargetDiffElement', () => {
- cursorElement.initialLineNumber = 1;
- assert.isTrue(!!cursorElement.diffRow);
+ cursor.initialLineNumber = 1;
+ assert.isTrue(!!cursor.diffRow);
assert.equal(
- cursorElement.getTargetDiffElement(),
+ cursor.getTargetDiffElement(),
diffElement
);
});
@@ -506,7 +503,7 @@
});
test('adds new draft for selected line on the left', done => {
- cursorElement.moveToLineNumber(2, 'left');
+ cursor.moveToLineNumber(2, 'left');
diffElement.addEventListener('create-comment', e => {
const {lineNum, range, side} = e.detail;
assert.equal(lineNum, 2);
@@ -514,11 +511,11 @@
assert.equal(side, 'left');
done();
});
- cursorElement.createCommentInPlace();
+ cursor.createCommentInPlace();
});
test('adds draft for selected line on the right', done => {
- cursorElement.moveToLineNumber(4, 'right');
+ cursor.moveToLineNumber(4, 'right');
diffElement.addEventListener('create-comment', e => {
const {lineNum, range, side} = e.detail;
assert.equal(lineNum, 4);
@@ -526,7 +523,7 @@
assert.equal(side, 'right');
done();
});
- cursorElement.createCommentInPlace();
+ cursor.createCommentInPlace();
});
test('creates comment for range if selected', done => {
@@ -547,15 +544,15 @@
assert.equal(side, 'right');
done();
});
- cursorElement.createCommentInPlace();
+ cursor.createCommentInPlace();
});
test('ignores call if nothing is selected', () => {
const createRangeCommentStub = sinon.stub(diffElement,
'createRangeComment');
const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
- cursorElement.diffRow = undefined;
- cursorElement.createCommentInPlace();
+ cursor.diffRow = undefined;
+ cursor.createCommentInPlace();
assert.isFalse(createRangeCommentStub.called);
assert.isFalse(addDraftAtLineStub.called);
});
@@ -563,31 +560,31 @@
test('getAddress', () => {
// It should initialize to the first chunk: line 5 of the revision.
- assert.deepEqual(cursorElement.getAddress(),
+ assert.deepEqual(cursor.getAddress(),
{leftSide: false, number: 5});
// Revision line 4 is up.
- cursorElement.moveUp();
- assert.deepEqual(cursorElement.getAddress(),
+ cursor.moveUp();
+ assert.deepEqual(cursor.getAddress(),
{leftSide: false, number: 4});
// Base line 4 is left.
- cursorElement.moveLeft();
- assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4});
+ cursor.moveLeft();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
// Moving to the next chunk takes it back to the start.
- cursorElement.moveToNextChunk();
- assert.deepEqual(cursorElement.getAddress(),
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(),
{leftSide: false, number: 5});
// The following chunk is a removal starting on line 10 of the base.
- cursorElement.moveToNextChunk();
- assert.deepEqual(cursorElement.getAddress(),
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(),
{leftSide: true, number: 10});
// Should be null if there is no selection.
- cursorElement.cursorManager.unsetCursor();
- assert.isNotOk(cursorElement.getAddress());
+ cursor.cursorManager.unsetCursor();
+ assert.isNotOk(cursor.getAddress());
});
test('_findRowByNumberAndFile', () => {
@@ -595,43 +592,25 @@
const row = diffElement.root.querySelectorAll('tr')[9];
// It should be line 8 on the right, but line 5 on the left.
- assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row);
- assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row);
+ assert.equal(cursor._findRowByNumberAndFile(8, 'right'), row);
+ assert.equal(cursor._findRowByNumberAndFile(5, 'left'), row);
});
test('expand context updates stops', done => {
- sinon.spy(cursorElement, '_updateStops');
+ sinon.spy(cursor, '_updateStops');
MockInteractions.tap(diffElement.shadowRoot
.querySelector('gr-context-controls').shadowRoot
.querySelector('.showContext'));
flush(() => {
- assert.isTrue(cursorElement._updateStops.called);
+ assert.isTrue(cursor._updateStops.called);
done();
});
});
test('updates stops when loading changes', () => {
- sinon.spy(cursorElement, '_updateStops');
+ sinon.spy(cursor, '_updateStops');
diffElement.dispatchEvent(new Event('loading-changed'));
- assert.isTrue(cursorElement._updateStops.called);
- });
-
- suite('gr-diff-cursor event tests', () => {
- let someEmptyDiv;
-
- setup(() => {
- someEmptyDiv = emptyFixture.instantiate();
- });
-
- teardown(() => sinon.restore());
-
- test('ready is fired after component is rendered', done => {
- const cursorElement = document.createElement('gr-diff-cursor');
- cursorElement.addEventListener('ready', () => {
- done();
- });
- someEmptyDiv.appendChild(cursorElement);
- });
+ assert.isTrue(cursor._updateStops.called);
});
suite('multi diff', () => {
@@ -639,18 +618,16 @@
<gr-diff></gr-diff>
<gr-diff></gr-diff>
<gr-diff></gr-diff>
- <gr-diff-cursor></gr-diff-cursor>
`);
let diffElements;
setup(() => {
- const fixtureElems = multiDiffFixture.instantiate();
- diffElements = fixtureElems.slice(0, 3);
- cursorElement = fixtureElems[3];
+ diffElements = multiDiffFixture.instantiate();
+ cursor = new GrDiffCursor();
// Register the diff with the cursor.
- cursorElement.push('diffs', ...diffElements);
+ cursor.replaceDiffs(diffElements);
for (const el of diffElements) {
el.prefs = createDefaultDiffPrefs();
@@ -664,7 +641,7 @@
// assertion because of the async nature assertion errors are handled and
// can cause the test simply timing out, causing a lot of debugging headache.
// Working with indices circumvents the problem.
- return diffElements.indexOf(cursorElement.getTargetDiffElement());
+ return diffElements.indexOf(cursor.getTargetDiffElement());
}
test('do not skip loading diffs', async () => {
@@ -678,28 +655,28 @@
const lastLine = diffElements[0].diff.meta_b.lines;
// Goto second last line of the first diff
- cursorElement.moveToLineNumber(lastLine - 1, 'right');
+ cursor.moveToLineNumber(lastLine - 1, 'right');
assert.equal(
- cursorElement.getTargetLineElement().textContent, lastLine - 1);
+ cursor.getTargetLineElement().textContent, lastLine - 1);
// Can move down until we reach the loading file
- cursorElement.moveDown();
+ cursor.moveDown();
assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursorElement.getTargetLineElement().textContent, lastLine);
+ assert.equal(cursor.getTargetLineElement().textContent, lastLine);
// Cannot move down while still loading the diff we would switch to
- cursorElement.moveDown();
+ cursor.moveDown();
assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursorElement.getTargetLineElement().textContent, lastLine);
+ assert.equal(cursor.getTargetLineElement().textContent, lastLine);
// Diff 1 finishing to load
diffElements[1].diff = getMockDiffResponse();
await diffRenderedPromises[1];
// Now we can go down
- cursorElement.moveDown();
+ cursor.moveDown();
assert.equal(getTargetDiffIndex(), 1);
- assert.equal(cursorElement.getTargetLineElement().textContent, 'File');
+ assert.equal(cursor.getTargetLineElement().textContent, 'File');
});
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
index 6216644..cbd5047 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -36,6 +36,7 @@
getSideByLineEl,
} from '../gr-diff/gr-diff-utils';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {queryAndAssert} from '../../../utils/common-util';
interface SidedRange {
side: Side;
@@ -574,7 +575,7 @@
_getLength(node: Node | null): number {
if (node === null) return 0;
if (node instanceof Element && node.classList.contains('content')) {
- return this._getLength(node.querySelector('.contentText')!);
+ return this._getLength(queryAndAssert(node, '.contentText'));
} else {
return GrAnnotation.getLength(node);
}
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 b42deda..72c7a62 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
@@ -398,7 +398,7 @@
if (e instanceof Response) {
this._handleGetDiffError(e);
} else {
- console.warn('Error encountered loading diff:', e);
+ this.reporting.error(e);
}
} finally {
this.reporting.timeEnd(Timing.DIFF_TOTAL);
@@ -483,12 +483,12 @@
});
})
.catch(err => {
- console.warn('Applying coverage from provider failed: ', err);
+ this.reporting.error(err);
});
});
})
.catch(err => {
- console.warn('Loading coverage ranges failed: ', err);
+ this.reporting.error(err);
});
}
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 b7ac0de..dacd8e7 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
@@ -71,7 +71,6 @@
ConfigInfo,
EditInfo,
EditPatchSetNum,
- ElementPropertyDeepChange,
FileInfo,
NumericChangeId,
ParentPatchSetNum,
@@ -106,7 +105,7 @@
import {GerritView} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
import {toggleClass, getKeyboardEvent} from '../../../utils/dom-util';
-import {CursorMoveResult} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+import {CursorMoveResult} from '../../../api/core';
const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const MSG_LOADING_BLAME = 'Loading blame...';
@@ -128,7 +127,6 @@
export interface GrDiffView {
$: {
commentAPI: GrCommentApi;
- cursor: GrDiffCursor;
diffHost: GrDiffHost;
reviewed: HTMLInputElement;
dropdown: GrDropdownList;
@@ -276,6 +274,11 @@
@property({type: Number})
_focusLineNum?: number;
+ private getReviewedParams: {
+ changeNum?: NumericChangeId;
+ patchNum?: PatchSetNum;
+ } = {};
+
get keyBindings() {
return {
esc: '_handleEscKey',
@@ -334,6 +337,8 @@
_onRenderHandler?: EventListener;
+ private cursor = new GrDiffCursor();
+
/** @override */
connectedCallback() {
super.connectedCallback();
@@ -345,22 +350,23 @@
});
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
- this.$.cursor.push('diffs', this.$.diffHost);
+ this.cursor.replaceDiffs([this.$.diffHost]);
this._onRenderHandler = (_: Event) => {
- this.$.cursor.reInitCursor();
+ this.cursor.reInitCursor();
};
this.$.diffHost.addEventListener('render', this._onRenderHandler);
}
/** @override */
disconnectedCallback() {
+ this.cursor.dispose();
if (this._onRenderHandler) {
this.$.diffHost.removeEventListener('render', this._onRenderHandler);
}
super.disconnectedCallback();
}
- _getLoggedIn() {
+ _getLoggedIn(): Promise<boolean> {
return this.restApiService.getLoggedIn();
}
@@ -453,8 +459,13 @@
_setReviewed(reviewed: boolean) {
if (this._editMode) return;
this.$.reviewed.checked = reviewed;
- if (!this._patchRange?.patchNum) return;
+ if (!this._patchRange?.patchNum || !this._path) return;
+ const path = this._path;
+ if (reviewed) this._reviewedFiles.add(path);
+ else this._reviewedFiles.delete(path);
this._saveReviewedState(reviewed).catch(err => {
+ if (this._reviewedFiles.has(path)) this._reviewedFiles.delete(path);
+ else this._reviewedFiles.add(path);
fireAlert(this, ERR_REVIEW_STATUS);
throw err;
});
@@ -492,14 +503,14 @@
if (this.shouldSuppressKeyboardShortcut(e)) return;
e.preventDefault();
- this.$.cursor.moveLeft();
+ this.cursor.moveLeft();
}
_handleRightPane(e: CustomKeyboardEvent) {
if (this.shouldSuppressKeyboardShortcut(e)) return;
e.preventDefault();
- this.$.cursor.moveRight();
+ this.cursor.moveRight();
}
_handlePrevLineOrFileWithComments(e: CustomKeyboardEvent) {
@@ -519,14 +530,14 @@
e.preventDefault();
this.$.diffHost.displayLine = true;
- this.$.cursor.moveUp();
+ this.cursor.moveUp();
}
_handleVisibleLine(e: CustomKeyboardEvent) {
if (this.shouldSuppressKeyboardShortcut(e)) return;
e.preventDefault();
- this.$.cursor.moveToVisibleArea();
+ this.cursor.moveToVisibleArea();
}
_onOpenFixPreview(e: OpenFixPreviewEvent) {
@@ -550,7 +561,7 @@
e.preventDefault();
this.$.diffHost.displayLine = true;
- this.$.cursor.moveDown();
+ this.cursor.moveDown();
}
_moveToPreviousFileWithComment() {
@@ -598,7 +609,7 @@
e.preventDefault();
this.classList.remove('hideComments');
- this.$.cursor.createCommentInPlace();
+ this.cursor.createCommentInPlace();
}
_handlePrevFile(e: CustomKeyboardEvent) {
@@ -628,18 +639,18 @@
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
- const result = this.$.cursor.moveToNextCommentThread();
+ const result = this.cursor.moveToNextCommentThread();
if (result === CursorMoveResult.CLIPPED) {
this._navigateToNextFileWithCommentThread();
}
} else {
if (this.modifierPressed(e)) return;
- const result = this.$.cursor.moveToNextChunk();
+ const result = this.cursor.moveToNextChunk();
// navigate to next file if key is not being held down
if (
!e.detail.keyboardEvent?.repeat &&
result === CursorMoveResult.CLIPPED &&
- this.$.cursor.isAtEnd()
+ this.cursor.isAtEnd()
) {
this.showToastAndNavigateFile('next', 'n');
}
@@ -689,11 +700,11 @@
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
- this.$.cursor.moveToPreviousCommentThread();
+ this.cursor.moveToPreviousCommentThread();
} else {
if (this.modifierPressed(e)) return;
- this.$.cursor.moveToPreviousChunk();
- if (!e.detail.keyboardEvent?.repeat && this.$.cursor.isAtStart()) {
+ this.cursor.moveToPreviousChunk();
+ if (!e.detail.keyboardEvent?.repeat && this.cursor.isAtStart()) {
this.showToastAndNavigateFile('previous', 'p');
}
}
@@ -848,7 +859,7 @@
if (!this._patchRange) return;
// TODO(taoalpha): add a shortcut for editing
- const cursorAddress = this.$.cursor.getAddress();
+ const cursorAddress = this.cursor.getAddress();
const editUrl = GerritNav.getEditUrlForDiff(
this._change,
this._path,
@@ -894,31 +905,26 @@
return {path: fileList[idx]};
}
- _getReviewedFiles(
- changeNum?: NumericChangeId,
- patchNum?: PatchSetNum
- ): Promise<Set<string>> {
- if (!changeNum || !patchNum) return Promise.resolve(new Set<string>());
- return this.restApiService
- .getReviewedFiles(changeNum, patchNum)
- .then(files => {
- this._reviewedFiles = new Set(files);
- return this._reviewedFiles;
- });
+ _getReviewedFiles(changeNum?: NumericChangeId, patchNum?: PatchSetNum) {
+ if (!changeNum || !patchNum) return;
+ if (
+ this.getReviewedParams.changeNum === changeNum &&
+ this.getReviewedParams.patchNum === patchNum
+ ) {
+ return;
+ }
+ this.getReviewedParams = {
+ changeNum,
+ patchNum,
+ };
+ this.restApiService.getReviewedFiles(changeNum, patchNum).then(files => {
+ this._reviewedFiles = new Set(files);
+ });
}
- _getReviewedStatus(
- editMode?: boolean,
- changeNum?: NumericChangeId,
- patchNum?: PatchSetNum,
- path?: string
- ) {
- if (editMode || !path) {
- return Promise.resolve(false);
- }
- return this._getReviewedFiles(changeNum, patchNum).then(files =>
- files.has(path)
- );
+ _getReviewedStatus(path: string) {
+ if (this._editMode) return false;
+ return this._reviewedFiles.has(path);
}
_initLineOfInterestAndCursor(leftSide: boolean) {
@@ -1093,7 +1099,9 @@
// the top-level change info view) and therefore undefined in `params`.
// If route is of type /comment/<commentId>/ then no patchNum is present
if (!value.patchNum && !value.commentLink) {
- console.warn('invalid url, no patchNum found');
+ this.reporting.error(
+ new Error(`Invalid diff view URL, no patchNum found: ${value}`)
+ );
return;
}
@@ -1197,44 +1205,39 @@
}
}
- @observe('_loggedIn', 'params.*', '_prefs', '_patchRange.*')
+ @observe('_path', '_prefs', '_reviewedFiles')
_setReviewedObserver(
+ path?: string,
+ prefs?: DiffPreferencesInfo,
+ reviewedFiles?: Set<string>
+ ) {
+ if (prefs === undefined) return;
+ if (path === undefined) return;
+ if (reviewedFiles === undefined) return;
+ if (prefs.manual_review) {
+ // Checkbox state needs to be set explicitly only when manual_review
+ // is specified.
+ this.$.reviewed.checked = this._getReviewedStatus(path);
+ } else {
+ this._setReviewed(true);
+ }
+ }
+
+ @observe('_loggedIn', '_changeNum', '_patchRange')
+ getReviewedFiles(
_loggedIn?: boolean,
- paramsRecord?: ElementPropertyDeepChange<GrDiffView, 'params'>,
- _prefs?: DiffPreferencesInfo,
- patchRangeRecord?: ElementPropertyDeepChange<GrDiffView, '_patchRange'>
+ _changeNum?: NumericChangeId,
+ patchRange?: PatchRange
) {
if (_loggedIn === undefined) return;
- if (paramsRecord === undefined) return;
- if (_prefs === undefined) return;
- if (patchRangeRecord === undefined) return;
- if (patchRangeRecord.base === undefined) return;
+ if (_changeNum === undefined) return;
+ if (patchRange === undefined) return;
- const patchRange = patchRangeRecord.base;
if (!_loggedIn) {
return;
}
- if (_prefs.manual_review) {
- // Checkbox state needs to be set explicitly only when manual_review
- // is specified.
-
- if (patchRange.patchNum) {
- this._getReviewedStatus(
- this._editMode,
- this._changeNum,
- patchRange.patchNum,
- this._path
- ).then((status: boolean) => {
- this.$.reviewed.checked = status;
- });
- }
- return;
- }
-
- if (paramsRecord.base?.view === GerritNav.View.DIFF) {
- this._setReviewed(true);
- }
+ this._getReviewedFiles(this._changeNum, patchRange.patchNum);
}
/**
@@ -1245,11 +1248,11 @@
return;
}
if (leftSide) {
- this.$.cursor.side = Side.LEFT;
+ this.cursor.side = Side.LEFT;
} else {
- this.$.cursor.side = Side.RIGHT;
+ this.cursor.side = Side.RIGHT;
}
- this.$.cursor.initialLineNumber = this._focusLineNum;
+ this.cursor.initialLineNumber = this._focusLineNum;
}
_getLineOfInterest(leftSide: boolean): LineOfInterest | undefined {
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 7897a4a..8d69007d 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
@@ -429,6 +429,5 @@
on-reload-diff-preference="_handleReloadingDiffPreference"
>
</gr-diff-preferences-dialog>
- <gr-diff-cursor id="cursor"></gr-diff-cursor>
<gr-comment-api id="commentAPI"></gr-comment-api>
`;
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 39ec384..ebcfa18 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
@@ -30,7 +30,7 @@
} from '../../../test/test-data-generators.js';
import {EditPatchSetNum} from '../../../types/common.js';
import sinon from 'sinon/pkg/sinon-esm';
-import {CursorMoveResult} from '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import {CursorMoveResult} from '../../../api/core.js';
const basicFixture = fixtureFromElement('gr-diff-view');
@@ -91,7 +91,6 @@
}
let getDiffChangeDetailStub;
- let getReviewedFilesStub;
setup(async () => {
clock = sinon.useFakeTimers();
stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
@@ -105,8 +104,6 @@
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getPortedComments').returns(Promise.resolve({}));
- getReviewedFilesStub = stubRestApi('getReviewedFiles').returns(
- Promise.resolve([]));
element = basicFixture.instantiate();
element._changeNum = '42';
@@ -422,19 +419,19 @@
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert(showPrefsStub.calledOnce);
- let scrollStub = sinon.stub(element.$.cursor, 'moveToNextChunk');
+ let scrollStub = sinon.stub(element.cursor, 'moveToNextChunk');
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
assert(scrollStub.calledOnce);
- scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousChunk');
+ scrollStub = sinon.stub(element.cursor, 'moveToPreviousChunk');
MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
assert(scrollStub.calledOnce);
- scrollStub = sinon.stub(element.$.cursor, 'moveToNextCommentThread');
+ scrollStub = sinon.stub(element.cursor, 'moveToNextCommentThread');
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
assert(scrollStub.calledOnce);
- scrollStub = sinon.stub(element.$.cursor,
+ scrollStub = sinon.stub(element.cursor,
'moveToPreviousCommentThread');
MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
assert(scrollStub.calledOnce);
@@ -470,7 +467,7 @@
test('moveToNextCommentThread navigates to next file', () => {
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
const diffChangeStub = sinon.stub(element, '_navigateToChange');
- sinon.stub(element.$.cursor, 'isAtEnd').returns(true);
+ sinon.stub(element.cursor, 'isAtEnd').returns(true);
element._changeNum = '42';
const comment = {
'wheatley.md': [{
@@ -857,7 +854,7 @@
b: {_number: 2, commit: {parents: []}},
},
};
- sinon.stub(element.$.cursor, 'getAddress')
+ sinon.stub(element.cursor, 'getAddress')
.returns({number: lineNumber, isLeftSide: false});
const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
flush(() => {
@@ -1157,10 +1154,11 @@
const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
.callsFake(() => Promise.resolve());
const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
- .callsFake(() => Promise.resolve());
+ .returns(false);
sinon.stub(element.$.diffHost, 'reload');
element._loggedIn = true;
+ element._prefs = {manual_review: true};
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
@@ -1172,17 +1170,19 @@
patchNum: 2,
basePatchNum: 1,
};
- element._prefs = {manual_review: true};
flush();
assert.isFalse(saveReviewedStub.called);
assert.isTrue(getReviewedStub.called);
+ const oldCount = getReviewedStub.callCount;
+
element._prefs = {};
+ element._path = 'abcd';
flush();
assert.isTrue(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.calledOnce);
+ assert.equal(getReviewedStub.callCount, oldCount);
});
test('file review status', () => {
@@ -1202,6 +1202,7 @@
patchNum: 2,
basePatchNum: 1,
};
+ element._path = 'abcd';
element._prefs = {};
flush();
@@ -1380,34 +1381,34 @@
});
test('_initCursor', () => {
- assert.isNotOk(element.$.cursor.initialLineNumber);
+ assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when params specify no cursor address:
element._initCursor(false);
- assert.isNotOk(element.$.cursor.initialLineNumber);
+ assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when params specify side but no number:
element._initCursor(true);
- assert.isNotOk(element.$.cursor.initialLineNumber);
+ assert.isNotOk(element.cursor.initialLineNumber);
// Revision hash: specifies lineNum but not side.
element._focusLineNum = 234;
element._initCursor(false);
- assert.equal(element.$.cursor.initialLineNumber, 234);
- assert.equal(element.$.cursor.side, 'right');
+ assert.equal(element.cursor.initialLineNumber, 234);
+ assert.equal(element.cursor.side, 'right');
// Base hash: specifies lineNum and side.
element._focusLineNum = 345;
element._initCursor(true);
- assert.equal(element.$.cursor.initialLineNumber, 345);
- assert.equal(element.$.cursor.side, 'left');
+ assert.equal(element.cursor.initialLineNumber, 345);
+ assert.equal(element.cursor.side, 'left');
// Specifies right side:
element._focusLineNum = 123;
element._initCursor(false);
- assert.equal(element.$.cursor.initialLineNumber, 123);
- assert.equal(element.$.cursor.side, 'right');
+ assert.equal(element.cursor.initialLineNumber, 123);
+ assert.equal(element.cursor.side, 'right');
});
test('_getLineOfInterest', () => {
@@ -1426,7 +1427,7 @@
test('_onLineSelected', () => {
const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
const replaceStateStub = sinon.stub(history, 'replaceState');
- sinon.stub(element.$.cursor, 'getAddress')
+ sinon.stub(element.cursor, 'getAddress')
.returns({number: 123, isLeftSide: false});
element._changeNum = 321;
@@ -1448,7 +1449,7 @@
test('line selected on left side', () => {
const getUrlStub = sinon.stub(GerritNav, 'getUrlForDiffById');
const replaceStateStub = sinon.stub(history, 'replaceState');
- sinon.stub(element.$.cursor, 'getAddress')
+ sinon.stub(element.cursor, 'getAddress')
.returns({number: 123, isLeftSide: true});
element._changeNum = 321;
@@ -1673,25 +1674,6 @@
[{value: '/foo'}, {value: '/bar'}]), 'show');
});
- test('_getReviewedStatus', () => {
- const promises = [];
- getReviewedFilesStub.returns(Promise.resolve(['path']));
-
- promises.push(element._getReviewedStatus(true, null, null, 'path')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, null, null, 'otherPath')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, null, null, 'path')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, 3, 5, 'path')
- .then(reviewed => assert.isTrue(reviewed)));
-
- return Promise.all(promises);
- });
-
test('f open file dropdown', () => {
assert.isFalse(element.$.dropdown.$.dropdown.opened);
MockInteractions.pressAndReleaseKeyOn(element, 70, null, 'f');
@@ -1751,11 +1733,11 @@
element, 'dispatchEvent').callThrough();
navToFileStub = sinon.stub(element, '_navToFile');
moveToPreviousChunkStub =
- sinon.stub(element.$.cursor, 'moveToPreviousChunk');
+ sinon.stub(element.cursor, 'moveToPreviousChunk');
moveToNextChunkStub =
- sinon.stub(element.$.cursor, 'moveToNextChunk');
- isAtStartStub = sinon.stub(element.$.cursor, 'isAtStart');
- isAtEndStub = sinon.stub(element.$.cursor, 'isAtEnd');
+ sinon.stub(element.cursor, 'moveToNextChunk');
+ isAtStartStub = sinon.stub(element.cursor, 'isAtStart');
+ isAtEndStub = sinon.stub(element.cursor, 'isAtEnd');
nowStub = sinon.stub(Date, 'now');
});
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 486bf3a..7c437fd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -62,11 +62,10 @@
import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
import {FlattenedNodesObserver} from '@polymer/polymer/lib/utils/flattened-nodes-observer';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {AbortStop} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {fireAlert, fireEvent} from '../../../utils/event-util';
import {MovedLinkClickedEvent} from '../../../types/events';
import {getContentEditableRange} from '../../../utils/safari-selection-util';
-
+import {AbortStop} from '../../../api/core';
import {
CreateCommentEventDetail as CreateCommentEventDetailApi,
RenderPreferences,
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
index 131825a..948578d 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
@@ -36,9 +36,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
index ac015e1..df0d71a 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
@@ -118,9 +118,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
index 0c6dd4d..0c36cd5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts
@@ -57,7 +57,7 @@
try {
mayContinue = callback(e);
} catch (exception) {
- console.warn(`Plugin error handing event: ${exception}`);
+ this.reporting.error(exception);
}
if (mayContinue === false) {
e.stopImmediatePropagation();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 7ef5a73..d278d22 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -105,6 +105,8 @@
})
);
+ promises.push(this.restApiService.invalidateAccountsDetailCache());
+
promises.push(
this.restApiService.getAccount().then(account => {
if (!account) return;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
index 6ca484d..e530ac8 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
@@ -35,7 +35,7 @@
<section>
<span class="title"></span>
<span class="value">
- <gr-avatar account="[[_account]]" image-size="120"></gr-avatar>
+ <gr-avatar account="[[_account]]" imageSize="120"></gr-avatar>
</span>
</section>
<section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
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 ab4c5a5..d7078ed 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
@@ -97,6 +97,13 @@
_config?: ServerInfo;
@property({type: Boolean, reflectToAttribute: true})
+ selectionChipStyle = false;
+
+ @property({
+ type: Boolean,
+ reflectToAttribute: true,
+ observer: 'selectedChanged',
+ })
selected = false;
@property({type: Boolean, reflectToAttribute: true})
@@ -126,6 +133,10 @@
});
}
+ selectedChanged(selected?: boolean) {
+ this.deselected = !selected;
+ }
+
_isAttentionSetEnabled(
highlight: boolean,
account: AccountInfo,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
index c5b66ce3..a642337 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
@@ -37,13 +37,13 @@
:host::after {
content: var(--account-label-suffix);
}
- :host([deselected]) {
+ :host([deselected][selection-chip-style]) {
background-color: var(--background-color-primary);
border: 1px solid var(--comment-separator-color);
border-radius: 8px;
color: var(--deemphasized-text-color);
}
- :host([selected]) {
+ :host([selected][selection-chip-style]) {
background-color: var(--chip-selected-background-color);
border: 1px solid var(--chip-selected-background-color);
border-radius: 8px;
@@ -128,7 +128,7 @@
class$="[[_computeHasAttentionClass(highlightAttention, account, change, forceAttention)]]"
>
<template is="dom-if" if="[[!hideAvatar]]">
- <gr-avatar account="[[account]]" image-size="32"></gr-avatar>
+ <gr-avatar account="[[account]]" imageSize="32"></gr-avatar>
</template>
<span class="text">
<span class="name">[[_computeName(account, _config, firstName)]]</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 231fc36..130969e 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
@@ -326,7 +326,9 @@
return;
}
}
- console.warn('received remove event for missing account', toRemove);
+ this.reporting.error(
+ new Error(`Received "remove" event for missing account: ${toRemove}`)
+ );
}
_getNativeInput(paperInput: PaperInputElementExt) {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
index e30e995..80576a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -14,22 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-avatar_html';
import {getBaseUrl} from '../../../utils/url-util';
import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
-import {customElement, property} from '@polymer/decorators';
import {AccountInfo} from '../../../types/common';
import {appContext} from '../../../services/app-context';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property} from 'lit-element';
@customElement('gr-avatar')
-export class GrAvatar extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Object, observer: '_accountChanged'})
+export class GrAvatar extends GrLitElement {
+ @property({type: Object})
account?: AccountInfo;
@property({type: Number})
@@ -40,6 +34,27 @@
private readonly restApiService = appContext.restApiService;
+ static get styles() {
+ return [
+ css`
+ :host {
+ display: inline-block;
+ border-radius: 50%;
+ background-size: cover;
+ background-color: var(
+ --avatar-background-color,
+ var(--gray-background)
+ );
+ }
+ `,
+ ];
+ }
+
+ render() {
+ this._updateAvatarURL();
+ return html``;
+ }
+
/** @override */
connectedCallback() {
super.connectedCallback();
@@ -57,10 +72,6 @@
return this.restApiService.getConfig();
}
- _accountChanged() {
- this._updateAvatarURL();
- }
-
_updateAvatarURL() {
if (!this._hasAvatars || !this.account) {
this.hidden = true;
@@ -80,7 +91,7 @@
);
}
- _buildAvatarURL(account: AccountInfo) {
+ _buildAvatarURL(account?: AccountInfo) {
if (!account) {
return '';
}
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
deleted file mode 100644
index a1e51df..0000000
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- :host {
- display: inline-block;
- border-radius: 50%;
- background-size: cover;
- background-color: var(--avatar-background-color, var(--gray-background));
- }
- </style>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index a4f46db..65e8e9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -28,13 +28,15 @@
import {ParsedChangeInfo} from '../../../types/types';
export enum ChangeStates {
- MERGED = 'Merged',
ABANDONED = 'Abandoned',
+ ACTIVE = 'Active',
MERGE_CONFLICT = 'Merge Conflict',
- WIP = 'WIP',
+ MERGED = 'Merged',
PRIVATE = 'Private',
+ READY_TO_SUBMIT = 'Ready to submit',
REVERT_CREATED = 'Revert Created',
REVERT_SUBMITTED = 'Revert Submitted',
+ WIP = 'WIP',
}
const WIP_TOOLTIP =
@@ -52,7 +54,7 @@
'current reviewers (or anyone with "View Private Changes" permission).';
@customElement('gr-change-status')
-class GrChangeStatus extends PolymerElement {
+export class GrChangeStatus extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -91,8 +93,12 @@
resolveWeblinks?: GeneratedWebLink[],
status?: ChangeStates
): boolean {
+ const isRevertCreatedOrSubmitted =
+ (status === ChangeStates.REVERT_SUBMITTED ||
+ status === ChangeStates.REVERT_CREATED) &&
+ revertedChange !== undefined;
return (
- revertedChange !== undefined ||
+ isRevertCreatedOrSubmitted ||
!!(status === ChangeStates.MERGE_CONFLICT && resolveWeblinks?.length)
);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 116dcaf..2ca2744b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
@@ -87,7 +87,7 @@
>
<template
is="dom-if"
- if="[[!!hasStatusLink(revertedChange, resolveWeblinks, status)]]">
+ if="[[hasStatusLink(revertedChange, resolveWeblinks, status)]]">
<a class="status-link"
href="[[getStatusLink(revertedChange, resolveWeblinks, status)]]">
<div class="chip" aria-label$="Label: [[status]]">
@@ -100,7 +100,7 @@
</div>
</a>
</template>
- <template is="dom-if" if="[[!hasStatusLink(revertedChange)]]">
+ <template is="dom-if" if="[[!hasStatusLink(revertedChange, resolveWeblinks, status)]]">
<div class="chip" aria-label$="Label: [[status]]">
[[_computeStatusString(status)]]
</div>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
deleted file mode 100644
index 7bc2466..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 sinon from 'sinon/pkg/sinon-esm';
-import '../../../test/common-test-setup-karma.js';
-import {createChange} from '../../../test/test-data-generators.js';
-import './gr-change-status.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {MERGE_CONFLICT_TOOLTIP} from './gr-change-status.js';
-
-const basicFixture = fixtureFromElement('gr-change-status');
-
-const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
- 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
- 'and email notifications will be silenced until the review is started.';
-
-const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
- 'current reviewers (or anyone with "View Private Changes" permission).';
-
-suite('gr-change-status tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('WIP', () => {
- element.status = 'WIP';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, 'Work in Progress');
- assert.equal(element.tooltipText, WIP_TOOLTIP);
- assert.isTrue(element.classList.contains('wip'));
- });
-
- test('WIP flat', () => {
- element.flat = true;
- element.status = 'WIP';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, 'WIP');
- assert.isDefined(element.tooltipText);
- assert.isTrue(element.classList.contains('wip'));
- assert.isTrue(element.hasAttribute('flat'));
- });
-
- test('merged', () => {
- element.status = 'Merged';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('merged'));
- assert.isFalse(
- element.showResolveIcon([{url: 'http://google.com'}], 'Merged'));
- });
-
- test('abandoned', () => {
- element.status = 'Abandoned';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('abandoned'));
- });
-
- test('merge conflict', () => {
- const status = 'Merge Conflict';
- element.status = status;
- flush();
-
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
- assert.isTrue(element.classList.contains('merge-conflict'));
- assert.isFalse(element.hasStatusLink(undefined, [], status));
- assert.isFalse(element.showResolveIcon([], status));
- });
-
- test('merge conflict with resolve link', () => {
- const status = 'Merge Conflict';
- const url = 'http://google.com';
- const weblinks = [{url}];
-
- assert.isTrue(element.hasStatusLink(undefined, weblinks, status));
- assert.equal(element.getStatusLink(undefined, weblinks, status), url);
- assert.isTrue(element.showResolveIcon(weblinks, status));
- });
-
- test('reverted change', () => {
- const url = 'http://google.com';
- const status = 'Revert Submitted';
- const revertedChange = createChange();
- sinon.stub(GerritNav, 'getUrlForSearchQuery').returns(url);
-
- assert.isTrue(element.hasStatusLink(revertedChange, [], status));
- assert.equal(element.getStatusLink(revertedChange, [], status), url);
- });
-
- test('private', () => {
- element.status = 'Private';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
- assert.isTrue(element.classList.contains('private'));
- });
-
- test('active', () => {
- element.status = 'Active';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('active'));
- });
-
- test('ready to submit', () => {
- element.status = 'Ready to submit';
- flush();
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('ready-to-submit'));
- });
-
- test('updating status removes the previous class', () => {
- element.status = 'Private';
- flush();
- assert.isTrue(element.classList.contains('private'));
- assert.isFalse(element.classList.contains('wip'));
-
- element.status = 'WIP';
- flush();
- assert.isFalse(element.classList.contains('private'));
- assert.isTrue(element.classList.contains('wip'));
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
new file mode 100644
index 0000000..a56f6f1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
@@ -0,0 +1,172 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 sinon from 'sinon/pkg/sinon-esm';
+import '../../../test/common-test-setup-karma';
+import {createChange} from '../../../test/test-data-generators';
+import './gr-change-status';
+import {ChangeStates, GrChangeStatus} from './gr-change-status';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {MERGE_CONFLICT_TOOLTIP} from './gr-change-status';
+
+const basicFixture = fixtureFromElement('gr-change-status');
+
+const WIP_TOOLTIP =
+ "This change isn't ready to be reviewed or submitted. " +
+ "It will not appear on dashboards unless you are CC'ed or assigned, " +
+ 'and email notifications will be silenced until the review is started.';
+
+const PRIVATE_TOOLTIP =
+ 'This change is only visible to its owner and ' +
+ 'current reviewers (or anyone with "View Private Changes" permission).';
+
+suite('gr-change-status tests', () => {
+ let element: GrChangeStatus;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ test('WIP', () => {
+ element.status = ChangeStates.WIP;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Work in Progress'
+ );
+ assert.equal(element.tooltipText, WIP_TOOLTIP);
+ assert.isTrue(element.classList.contains('wip'));
+ });
+
+ test('WIP flat', () => {
+ element.flat = true;
+ element.status = ChangeStates.WIP;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'WIP'
+ );
+ assert.isDefined(element.tooltipText);
+ assert.isTrue(element.classList.contains('wip'));
+ assert.isTrue(element.hasAttribute('flat'));
+ });
+
+ test('merged', () => {
+ element.status = ChangeStates.MERGED;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Merged'
+ );
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('merged'));
+ assert.isFalse(
+ element.showResolveIcon([{url: 'http://google.com'}], ChangeStates.MERGED)
+ );
+ });
+
+ test('abandoned', () => {
+ element.status = ChangeStates.ABANDONED;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Abandoned'
+ );
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('abandoned'));
+ });
+
+ test('merge conflict', () => {
+ const status = ChangeStates.MERGE_CONFLICT;
+ element.status = status;
+ flush();
+
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Merge Conflict'
+ );
+ assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
+ assert.isTrue(element.classList.contains('merge-conflict'));
+ assert.isFalse(element.hasStatusLink(undefined, [], status));
+ assert.isFalse(element.showResolveIcon([], status));
+ });
+
+ test('merge conflict with resolve link', () => {
+ const status = ChangeStates.MERGE_CONFLICT;
+ const url = 'http://google.com';
+ const weblinks = [{url}];
+
+ assert.isTrue(element.hasStatusLink(undefined, weblinks, status));
+ assert.equal(element.getStatusLink(undefined, weblinks, status), url);
+ assert.isTrue(element.showResolveIcon(weblinks, status));
+ });
+
+ test('reverted change', () => {
+ const url = 'http://google.com';
+ const status = ChangeStates.REVERT_SUBMITTED;
+ const revertedChange = createChange();
+ sinon.stub(GerritNav, 'getUrlForSearchQuery').returns(url);
+
+ assert.isTrue(element.hasStatusLink(revertedChange, [], status));
+ assert.equal(element.getStatusLink(revertedChange, [], status), url);
+ });
+
+ test('private', () => {
+ element.status = ChangeStates.PRIVATE;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Private'
+ );
+ assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
+ assert.isTrue(element.classList.contains('private'));
+ });
+
+ test('active', () => {
+ element.status = ChangeStates.ACTIVE;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Active'
+ );
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('active'));
+ });
+
+ test('ready to submit', () => {
+ element.status = ChangeStates.READY_TO_SUBMIT;
+ flush();
+ assert.equal(
+ element.shadowRoot!.querySelector<HTMLDivElement>('.chip')!.innerText,
+ 'Ready to submit'
+ );
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('ready-to-submit'));
+ });
+
+ test('updating status removes the previous class', () => {
+ element.status = ChangeStates.PRIVATE;
+ flush();
+ assert.isTrue(element.classList.contains('private'));
+ assert.isFalse(element.classList.contains('wip'));
+
+ element.status = ChangeStates.WIP;
+ flush();
+ assert.isFalse(element.classList.contains('private'));
+ assert.isTrue(element.classList.contains('wip'));
+ });
+});
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 cf8a0ed..d6ad030 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,6 +41,7 @@
import {computeDisplayPath} from '../../../utils/path-list-util';
import {computed, customElement, observe, property} from '@polymer/decorators';
import {
+ AccountDetailInfo,
CommentRange,
ConfigInfo,
NumericChangeId,
@@ -62,6 +63,7 @@
import {StorageLocation} from '../../../services/storage/gr-storage';
import {TokenHighlightLayer} from '../../diff/gr-diff-builder/token-highlight-layer';
import {anyLineTooLong} from '../../diff/gr-diff/gr-diff-utils';
+import {getUserName} from '../../../utils/display-name-util';
const UNRESOLVED_EXPAND_COUNT = 5;
const NEWLINE_PATTERN = /\n/g;
@@ -200,6 +202,9 @@
@property({type: Boolean})
showCommentContext = false;
+ @property({type: Object})
+ _selfAccount?: AccountDetailInfo;
+
get keyBindings() {
return {
'e shift+e': '_handleEKey',
@@ -239,6 +244,9 @@
};
this.syntaxLayer.setEnabled(!!prefs.syntax_highlighting);
});
+ this.restApiService.getAccount().then(account => {
+ this._selfAccount = account;
+ });
this._setInitialExpandedState();
}
@@ -404,16 +412,16 @@
return displayPath;
}
- _computeDisplayLine() {
- if (this.lineNum === FILE) {
+ _computeDisplayLine(lineNum?: LineNumber, range?: CommentRange) {
+ if (lineNum === FILE) {
if (this.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
return '';
}
return FILE;
}
- if (this.lineNum) return `#${this.lineNum}`;
+ if (lineNum) return `#${lineNum}`;
// If range is set, then lineNum equals the end line of the range.
- if (this.range) return `#${this.range.end_line}`;
+ if (range) return `#${range.end_line}`;
return '';
}
@@ -698,7 +706,9 @@
const index = this._indexOf(comment, this.comments);
if (index === -1) {
// This should never happen: comment belongs to another thread.
- console.warn('Comment update for another comment thread.');
+ this.reporting.error(
+ new Error(`Comment update for another comment thread: ${comment}`)
+ );
return;
}
this.set(['comments', index], comment);
@@ -743,6 +753,17 @@
this._projectConfig = config;
});
}
+
+ _computeAriaHeading(_orderedComments: UIComment[]) {
+ const firstComment = _orderedComments[0];
+ const author = firstComment?.author ?? this._selfAccount;
+ const lastComment = _orderedComments[_orderedComments.length - 1] || {};
+ const status = [
+ lastComment.unresolved ? 'Unresolved' : '',
+ isDraft(lastComment) ? 'Draft' : '',
+ ].join(' ');
+ return `${status} Comment thread by ${getUserName(undefined, author)}`;
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index 1f0ce3d..6ab82b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -123,12 +123,15 @@
<template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
<a
href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
- >[[_computeDisplayLine()]]</a
+ >[[_computeDisplayLine(lineNum, range)]]</a
>
</template>
</div>
</template>
<div id="container">
+ <h3 class="assistive-tech-only">
+ [[_computeAriaHeading(_orderedComments)]]
+ </h3>
<div class$="[[_computeHostClass(unresolved, isRobotComment)]] comment-box">
<template
id="commentList"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 64f5ad8..2eeb12d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -285,15 +285,24 @@
test('_computeDisplayLine', () => {
element.lineNum = 5;
- assert.equal(element._computeDisplayLine(), '#5');
+ assert.equal(
+ element._computeDisplayLine(element.lineNum, element.range),
+ '#5'
+ );
element.path = SpecialFilePath.COMMIT_MESSAGE;
element.lineNum = 5;
- assert.equal(element._computeDisplayLine(), '#5');
+ assert.equal(
+ element._computeDisplayLine(element.lineNum, element.range),
+ '#5'
+ );
element.lineNum = undefined;
element.path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
- assert.equal(element._computeDisplayLine(), '');
+ assert.equal(
+ element._computeDisplayLine(element.lineNum, element.range),
+ ''
+ );
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index aad8c98..16605b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -269,12 +269,6 @@
>From patchset [[comment.patch_set]]</span
></a
>
- <a
- href="https://bugs.chromium.org/p/gerrit/issues/entry?template=Porting+Comments"
- target="_blank"
- >
- <iron-icon icon="gr-icons:bug" title="report a problem"></iron-icon>
- </a>
</template>
<gr-tooltip-content
class="draftTooltip"
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index a5a5433..017ba50 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -15,31 +15,10 @@
* limitations under the License.
*/
import {BehaviorSubject} from 'rxjs';
+import {AbortStop, CursorMoveResult, Stop} from '../../../api/core';
import {ScrollMode} from '../../../constants/constants';
/**
- * Return type for cursor moves, that indicate whether a move was possible.
- */
-export enum CursorMoveResult {
- /** The cursor was successfully moved. */
- MOVED,
- /** There were no stops - the cursor was reset. */
- NO_STOPS,
- /**
- * There was no more matching stop to move to - the cursor was clipped to the
- * end.
- */
- CLIPPED,
- /** The abort condition would have been fulfilled for the new target. */
- ABORTED,
-}
-
-/** A sentinel that can be inserted to disallow moving across. */
-export class AbortStop {}
-
-export type Stop = HTMLElement | AbortStop;
-
-/**
* Type guard and checker to check if a stop can be targeted.
* Abort stops cannot be targeted.
*/
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index 01b9a74..ba7e4f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -18,7 +18,8 @@
import '../../../test/common-test-setup-karma.js';
import './gr-cursor-manager.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {AbortStop, CursorMoveResult, GrCursorManager} from './gr-cursor-manager.js';
+import {AbortStop, CursorMoveResult} from '../../../api/core.js';
+import {GrCursorManager} from './gr-cursor-manager.js';
const basicTestFixutre = fixtureFromTemplate(html`
<ul>
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 391028a..e93013e 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
@@ -22,6 +22,8 @@
import {customElement, property, observe} from '@polymer/decorators';
import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
import {appContext} from '../../../services/app-context';
+import {queryAndAssert} from '../../../utils/common-util';
+import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
declare global {
interface HTMLElementTagNameMap {
@@ -73,8 +75,7 @@
}
focusOnCopy() {
- // TODO(TS): remove ! assertion later
- this.shadowRoot!.querySelector('gr-shell-command')!.focusOnCopy();
+ queryAndAssert<GrShellCommand>(this, 'gr-shell-command').focusOnCopy();
}
_getLoggedIn() {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index eb44a60..83cd380 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -23,6 +23,9 @@
import {fireAlert, fireEvent} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {queryAndAssert} from '../../../utils/common-util';
+import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import {Interaction} from '../../../constants/reporting';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -131,7 +134,10 @@
}
focusTextarea() {
- this.shadowRoot!.querySelector('iron-autogrow-textarea')!.textarea.focus();
+ queryAndAssert<IronAutogrowTextareaElement>(
+ this,
+ 'iron-autogrow-textarea'
+ ).textarea.focus();
}
_newContentChanged(newContent: string) {
@@ -227,7 +233,7 @@
_toggleCommitCollapsed() {
this._commitCollapsed = !this._commitCollapsed;
- this.reporting.reportInteraction('toggle show all button', {
+ this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
sectionName: 'Commit message',
toState: !this._commitCollapsed ? 'Show all' : 'Show less',
});
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 17b659f..7298af7 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -20,6 +20,7 @@
import {customElement, property} from '@polymer/decorators';
import {htmlTemplate} from './gr-formatted-text_html';
import {CommentLinks} from '../../../types/common';
+import {appContext} from '../../../services/app-context';
const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
@@ -57,6 +58,8 @@
@property({type: Boolean})
noTrailingMargin = false;
+ private readonly reporting = appContext.reportingService;
+
static get observers() {
return ['_contentOrConfigChanged(content, config)'];
}
@@ -304,7 +307,7 @@
return ul;
}
- console.warn('Unrecognized type.');
+ this.reporting.error(new Error(`Unrecognized block type: ${block.type}`));
return document.createElement('span');
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
index d0986de..dac5962 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
@@ -75,7 +75,7 @@
<template is="dom-if" if="[[_isShowing]]">
<div class="top">
<div class="avatar">
- <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+ <gr-avatar account="[[account]]" imageSize="56"></gr-avatar>
</div>
<div class="account">
<h3 class="name heading-3">[[account.name]]</h3>
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 e3a34c8..8968e18 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -148,6 +148,8 @@
<g id="playArrow"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Apause-->
<g id="pause"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></g>
+ <!-- This SVG is a copy from material.io https://material.io/icons/#code-->
+ <g id="code"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
index 3f75970..82c7118 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
@@ -42,7 +42,9 @@
): GrAnnotationActionsInterface {
this.reporting.trackApi(this.plugin, 'annotation', 'setCoverageProvider');
if (this.coverageProvider) {
- console.warn('Overwriting an existing coverage provider.');
+ this.reporting.error(
+ new Error(`Overwriting cov provider: ${this.plugin.getPluginName()}`)
+ );
}
this.coverageProvider = coverageProvider;
return this;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
index 15d4680..b9c4c32 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
@@ -80,7 +80,7 @@
*/
private setEl(el?: GrChangeActionsElement) {
if (!el) {
- console.warn('changeActions() is not ready');
+ this.reporting.error(new Error(`changeActions() API is not ready`));
return;
}
this.el = el;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
index 2135c30..46c7ad6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
@@ -22,6 +22,7 @@
import {windowLocationReload} from '../../../utils/dom-util';
import {PopupPluginApi} from '../../../api/popup';
import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface';
+import {appContext} from '../../../services/app-context';
interface ButtonCallBacks {
onclick: (event: Event) => boolean;
@@ -30,6 +31,8 @@
export class GrPluginActionContext {
private popups: PopupPluginApi[] = [];
+ private readonly reporting = appContext.reportingService;
+
constructor(
public readonly plugin: PluginApi,
public readonly action: UIActionInfo,
@@ -108,7 +111,9 @@
call(payload: RequestPayload, onSuccess: (result: unknown) => void) {
if (!this.action.method) return;
if (!this.action.__url) {
- console.warn(`Unable to ${this.action.method} to ${this.action.__key}!`);
+ this.reporting.error(
+ new Error(`Unable to ${this.action.method} to ${this.action.__key}!`)
+ );
return;
}
this.plugin
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index fe101c5..6fc8f1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -132,7 +132,7 @@
try {
url = new URL(url);
} catch (e) {
- console.warn(e);
+ this._getReporting().error(e);
return false;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index 35acdef..cfa6c04 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -79,9 +79,10 @@
this.domHooks = new GrDomHooksManager(this);
if (!url) {
- console.warn(
- 'Plugin not being loaded from /plugins base path.',
- 'Unable to determine name.'
+ this.report.error(
+ new Error(
+ `Plugin not being loaded from /plugins base path. Unable to determine name.`
+ )
);
return this;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index db22ce5..31a709a 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -81,6 +81,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly reporting = appContext.reportingService;
+
// TODO(TS): not used, remove later
_xhrPromise?: Promise<void>;
@@ -209,7 +211,7 @@
}
})
.catch(err => {
- console.warn(err);
+ this.reporting.error(err);
target.disabled = false;
return;
});
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index 56ab8c6..b6583d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
@@ -88,7 +88,7 @@
<p
class$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]"
>
- No votes.
+ No votes
</p>
<table>
<template
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
index 4bdc1ab..0d44bc8 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
@@ -17,7 +17,7 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
- <style include="shared-styles">
+ <style>
:host {
display: block;
}
@@ -30,6 +30,9 @@
text-decoration: none;
pointer-events: none;
}
+ a {
+ color: var(--link-color);
+ }
</style>
<span id="output"></span>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
similarity index 68%
rename from polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.js
rename to polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
index a67fbc4..b2cdba1 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
@@ -15,27 +15,30 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-linked-text.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import '../../../test/common-test-setup-karma';
+import './gr-linked-text';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {GrLinkedText} from './gr-linked-text';
+import {CommentLinks} from '../../../types/common';
+import {queryAndAssert} from '../../../test/test-utils';
const basicFixture = fixtureFromTemplate(html`
-<gr-linked-text>
- <div id="output"></div>
- </gr-linked-text>
+ <gr-linked-text>
+ <div id="output"></div>
+ </gr-linked-text>
`);
suite('gr-linked-text tests', () => {
- let element;
+ let element: GrLinkedText;
- let originalCanonicalPath;
+ let originalCanonicalPath: string | undefined;
setup(() => {
originalCanonicalPath = window.CANONICAL_PATH;
- element = basicFixture.instantiate();
+ element = basicFixture.instantiate() as GrLinkedText;
- sinon.stub(GerritNav, 'mapCommentlinks').value( x => x);
+ sinon.stub(GerritNav, 'mapCommentlinks').value((x: CommentLinks) => x);
element.config = {
ph: {
match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
@@ -86,7 +89,8 @@
// Regular inline link.
const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
element.content = url;
- const linkEl = element.$.output.childNodes[0];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
assert.equal(linkEl.target, '_blank');
assert.equal(linkEl.rel, 'noopener');
assert.equal(linkEl.href, url);
@@ -97,14 +101,16 @@
// "Issue/Bug" pattern.
element.content = 'Issue 3650';
- let linkEl = element.$.output.childNodes[0];
+ let linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
assert.equal(linkEl.target, '_blank');
assert.equal(linkEl.href, url);
assert.equal(linkEl.textContent, 'Issue 3650');
element.content = 'Bug 3650';
- linkEl = element.$.output.childNodes[0];
+ linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
assert.equal(linkEl.target, '_blank');
assert.equal(linkEl.rel, 'noopener');
assert.equal(linkEl.href, url);
@@ -115,8 +121,9 @@
// Pattern starts with the same prefix (`http`) as the url.
element.content = 'httpexample 3650';
- assert.equal(element.$.output.childNodes.length, 1);
- const linkEl = element.$.output.childNodes[0];
+ assert.equal(queryAndAssert(element, '#output').childNodes.length, 1);
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
assert.equal(linkEl.target, '_blank');
assert.equal(linkEl.href, url);
@@ -129,8 +136,9 @@
const prefix = 'Change-Id: ';
element.content = prefix + changeID;
- const textNode = element.$.output.childNodes[0];
- const linkEl = element.$.output.childNodes[1];
+ const textNode = queryAndAssert(element, '#output').childNodes[0];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[1] as HTMLAnchorElement;
assert.equal(textNode.textContent, prefix);
const url = '/q/' + changeID;
assert.isFalse(linkEl.hasAttribute('target'));
@@ -147,8 +155,9 @@
const prefix = 'Change-Id: ';
element.content = prefix + changeID;
- const textNode = element.$.output.childNodes[0];
- const linkEl = element.$.output.childNodes[1];
+ const textNode = queryAndAssert(element, '#output').childNodes[0];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[1] as HTMLAnchorElement;
assert.equal(textNode.textContent, prefix);
const url = '/r/q/' + changeID;
assert.isFalse(linkEl.hasAttribute('target'));
@@ -159,17 +168,23 @@
test('Multiple matches', () => {
element.content = 'Issue 3650\nIssue 3450';
- const linkEl1 = element.$.output.childNodes[0];
- const linkEl2 = element.$.output.childNodes[2];
+ const linkEl1 = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
+ const linkEl2 = queryAndAssert(element, '#output')
+ .childNodes[2] as HTMLAnchorElement;
assert.equal(linkEl1.target, '_blank');
- assert.equal(linkEl1.href,
- 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650');
+ assert.equal(
+ linkEl1.href,
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'
+ );
assert.equal(linkEl1.textContent, 'Issue 3650');
assert.equal(linkEl2.target, '_blank');
- assert.equal(linkEl2.href,
- 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450');
+ assert.equal(
+ linkEl2.href,
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450'
+ );
assert.equal(linkEl2.textContent, 'Issue 3450');
});
@@ -186,9 +201,11 @@
element.content = prefix + changeID + bug;
- const textNode = element.$.output.childNodes[0];
- const changeLinkEl = element.$.output.childNodes[1];
- const bugLinkEl = element.$.output.childNodes[2];
+ const textNode = queryAndAssert(element, '#output').childNodes[0];
+ const changeLinkEl = queryAndAssert(element, '#output')
+ .childNodes[1] as HTMLAnchorElement;
+ const bugLinkEl = queryAndAssert(element, '#output')
+ .childNodes[2] as HTMLAnchorElement;
assert.equal(textNode.textContent, prefix);
@@ -203,15 +220,19 @@
test('html field in link config', () => {
element.content = 'google:do a barrel roll';
- const linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.getAttribute('href'),
- 'https://google.com/search?q=do a barrel roll');
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
+ assert.equal(
+ linkEl.getAttribute('href'),
+ 'https://google.com/search?q=do a barrel roll'
+ );
assert.equal(linkEl.textContent, 'do a barrel roll');
});
test('removing hash from links', () => {
element.content = 'hash:foo';
- const linkEl = element.$.output.childNodes[0];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
@@ -220,7 +241,8 @@
window.CANONICAL_PATH = '/r';
element.content = 'test foo';
- const linkEl = element.$.output.childNodes[0];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
@@ -229,7 +251,8 @@
window.CANONICAL_PATH = '/r';
element.content = 'a test foo';
- const linkEl = element.$.output.childNodes[1];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[1] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
@@ -238,94 +261,119 @@
window.CANONICAL_PATH = '/r';
element.content = 'hash:foo';
- const linkEl = element.$.output.childNodes[0];
+ const linkEl = queryAndAssert(element, '#output')
+ .childNodes[0] as HTMLAnchorElement;
assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
test('disabled config', () => {
element.content = 'foo:baz';
- assert.equal(element.$.output.innerHTML, 'foo:baz');
+ assert.equal(queryAndAssert(element, '#output').innerHTML, 'foo:baz');
});
test('R=email labels link correctly', () => {
element.removeZeroWidthSpace = true;
element.content = 'R=\u200Btest@google.com';
- assert.equal(element.$.output.textContent, 'R=test@google.com');
- assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1);
+ assert.equal(
+ queryAndAssert(element, '#output').textContent,
+ 'R=test@google.com'
+ );
+ assert.equal(
+ queryAndAssert(element, '#output').innerHTML.match(/(R=<a)/g)!.length,
+ 1
+ );
});
test('CC=email labels link correctly', () => {
element.removeZeroWidthSpace = true;
element.content = 'CC=\u200Btest@google.com';
- assert.equal(element.$.output.textContent, 'CC=test@google.com');
- assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
+ assert.equal(
+ queryAndAssert(element, '#output').textContent,
+ 'CC=test@google.com'
+ );
+ assert.equal(
+ queryAndAssert(element, '#output').innerHTML.match(/(CC=<a)/g)!.length,
+ 1
+ );
});
test('only {http,https,mailto} protocols are linkified', () => {
element.content = 'xx mailto:test@google.com yy';
- let links = element.$.output.querySelectorAll('a');
+ let links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
assert.equal(links[0].innerHTML, 'mailto:test@google.com');
element.content = 'xx http://google.com yy';
- links = element.$.output.querySelectorAll('a');
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'http://google.com');
assert.equal(links[0].innerHTML, 'http://google.com');
element.content = 'xx https://google.com yy';
- links = element.$.output.querySelectorAll('a');
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'https://google.com');
assert.equal(links[0].innerHTML, 'https://google.com');
element.content = 'xx ssh://google.com yy';
- links = element.$.output.querySelectorAll('a');
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
element.content = 'xx ftp://google.com yy';
- links = element.$.output.querySelectorAll('a');
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
});
test('links without leading whitespace are linkified', () => {
element.content = 'xx abcmailto:test@google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
- let links = element.$.output.querySelectorAll('a');
+ assert.equal(
+ queryAndAssert(element, '#output').innerHTML.substr(0, 6),
+ 'xx abc'
+ );
+ let links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
assert.equal(links[0].innerHTML, 'mailto:test@google.com');
element.content = 'xx defhttp://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
- links = element.$.output.querySelectorAll('a');
+ assert.equal(
+ queryAndAssert(element, '#output').innerHTML.substr(0, 6),
+ 'xx def'
+ );
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'http://google.com');
assert.equal(links[0].innerHTML, 'http://google.com');
element.content = 'xx qwehttps://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
- links = element.$.output.querySelectorAll('a');
+ assert.equal(
+ queryAndAssert(element, '#output').innerHTML.substr(0, 6),
+ 'xx qwe'
+ );
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'https://google.com');
assert.equal(links[0].innerHTML, 'https://google.com');
// Non-latin character
element.content = 'xx абвhttps://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
- links = element.$.output.querySelectorAll('a');
+ assert.equal(
+ queryAndAssert(element, '#output').innerHTML.substr(0, 6),
+ 'xx абв'
+ );
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 1);
assert.equal(links[0].getAttribute('href'), 'https://google.com');
assert.equal(links[0].innerHTML, 'https://google.com');
element.content = 'xx ssh://google.com yy';
- links = element.$.output.querySelectorAll('a');
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
element.content = 'xx ftp://google.com yy';
- links = element.$.output.querySelectorAll('a');
+ links = queryAndAssert(element, '#output').querySelectorAll('a');
assert.equal(links.length, 0);
});
@@ -341,11 +389,13 @@
},
};
element.content = '- B: 123, 45';
- const links = element.root.querySelectorAll('a');
+ const links = element.root!.querySelectorAll('a');
assert.equal(links.length, 2);
- assert.equal(element.shadowRoot
- .querySelector('span').textContent, '- B: 123, 45');
+ assert.equal(
+ queryAndAssert<HTMLSpanElement>(element, 'span').textContent,
+ '- B: 123, 45'
+ );
assert.equal(links[0].href, 'ftp://foo/123');
assert.equal(links[0].textContent, '123');
@@ -362,4 +412,3 @@
assert.isTrue(contentConfigStub.called);
});
});
-
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 852f0b6..28bc229 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
@@ -1443,6 +1443,10 @@
this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
}
+ invalidateAccountsDetailCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/self/detail');
+ }
+
getGroups(filter: string, groupsPerPage: number, offset?: number) {
const url = this._getGroupsUrl(filter, groupsPerPage, offset);
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
similarity index 75%
rename from polygerrit-ui/app/elements/shared/gr-select/gr-select_test.js
rename to polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
index c697850..245d7df 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
@@ -15,41 +15,42 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-select.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import '../../../test/common-test-setup-karma';
+import './gr-select';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {GrSelect} from './gr-select';
const basicFixture = fixtureFromTemplate(html`
-<gr-select>
- <select>
- <option value="1">One</option>
- <option value="2">Two</option>
- <option value="3">Three</option>
- </select>
- </gr-select>
+ <gr-select>
+ <select>
+ <option value="1">One</option>
+ <option value="2">Two</option>
+ <option value="3">Three</option>
+ </select>
+ </gr-select>
`);
const noOptionsFixture = fixtureFromTemplate(html`
-<gr-select>
- <select>
- </select>
- </gr-select>
+ <gr-select>
+ <select></select>
+ </gr-select>
`);
suite('gr-select tests', () => {
- let element;
+ let element: GrSelect;
setup(() => {
- element = basicFixture.instantiate();
+ element = basicFixture.instantiate() as GrSelect;
});
test('bindValue must be set to the first option value', () => {
assert.equal(element.bindValue, '1');
+ assert.equal(element.nativeSelect.value, '1');
});
test('value of 0 should still trigger value updates', () => {
- element.bindValue = 0;
- assert.equal(element.nativeSelect.value, 0);
+ element.bindValue = '0';
+ assert.equal(element.nativeSelect.value, '');
});
test('bidirectional binding property-to-attribute', () => {
@@ -82,9 +83,11 @@
// Now change the value.
element.nativeSelect.value = '3';
element.dispatchEvent(
- new CustomEvent('change', {
- composed: true, bubbles: true,
- }));
+ new CustomEvent('change', {
+ composed: true,
+ bubbles: true,
+ })
+ );
// It should be updated.
assert.equal(element.nativeSelect.value, '3');
@@ -93,10 +96,10 @@
});
suite('gr-select no options tests', () => {
- let element;
+ let element: GrSelect;
setup(() => {
- element = noOptionsFixture.instantiate();
+ element = noOptionsFixture.instantiate() as GrSelect;
});
test('bindValue must not be changed', () => {
@@ -104,4 +107,3 @@
});
});
});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
index 56d01d2..b10c43b 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
@@ -27,7 +27,7 @@
}
@customElement('gr-shell-command')
-class GrShellCommand extends PolymerElement {
+export class GrShellCommand extends PolymerElement {
static get template() {
return htmlTemplate;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index b3d1578..a747ac4 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -227,11 +227,13 @@
this._setEmoji(this.$.emojiSuggestions.getCurrentText());
}
- _handleEnterByKey(e: KeyboardEvent) {
+ _handleEnterByKey(e: CustomEvent<{keyboardEvent: KeyboardEvent}>) {
// Enter should have newline behavior if the picker is closed or if the user
- // has only typed ':'.
+ // has only typed ':'. Also make sure that shortcuts aren't clobbered.
if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
- this.indent(e);
+ if (!e.detail.keyboardEvent.metaKey && !e.detail.keyboardEvent.ctrlKey) {
+ this.indent(e);
+ }
return;
}
@@ -402,7 +404,7 @@
);
}
- private indent(e: KeyboardEvent): void {
+ private indent(e: CustomEvent<{keyboardEvent: KeyboardEvent}>): void {
if (!document.queryCommandSupported('insertText')) {
return;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
index 1747fce..7c2f209 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
@@ -230,6 +230,26 @@
assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
});
+ test('ctrl+enter and meta+enter do not indent', async () => {
+ const indentCommand = sinon.stub(document, 'execCommand');
+ element.$.textarea.value = ' a';
+ element._handleEnterByKey(
+ new CustomEvent('keydown', {
+ detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
+ })
+ );
+ await flush();
+ assert.isTrue(indentCommand.notCalled);
+
+ element._handleEnterByKey(
+ new CustomEvent('keydown', {
+ detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
+ })
+ );
+ await flush();
+ assert.isTrue(indentCommand.notCalled);
+ });
+
test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
element.$.emojiSuggestions.dispatchEvent(
diff --git a/polygerrit-ui/app/embed/gr-diff.ts b/polygerrit-ui/app/embed/gr-diff.ts
index ffee627..422667a4 100644
--- a/polygerrit-ui/app/embed/gr-diff.ts
+++ b/polygerrit-ui/app/embed/gr-diff.ts
@@ -25,9 +25,10 @@
import '../scripts/bundled-polymer';
import '../elements/diff/gr-diff/gr-diff';
import '../elements/diff/gr-diff-cursor/gr-diff-cursor';
-import {initDiffAppContext} from './gr-diff-app-context-init';
-import {GrAnnotation} from '../elements/diff/gr-diff-highlight/gr-annotation';
import {TokenHighlightLayer} from '../elements/diff/gr-diff-builder/token-highlight-layer';
+import {GrDiffCursor} from '../elements/diff/gr-diff-cursor/gr-diff-cursor';
+import {GrAnnotation} from '../elements/diff/gr-diff-highlight/gr-annotation';
+import {initDiffAppContext} from './gr-diff-app-context-init';
// Setup appContext for diff.
// TODO (dmfilippov): find a better solution
@@ -35,6 +36,7 @@
// Setup global variables for existing usages of this component
window.grdiff = {
GrAnnotation,
+ GrDiffCursor,
TokenHighlightLayer,
};
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index 9036871..312e78d 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -48,7 +48,7 @@
export function updateState(change?: ParsedChangeInfo) {
const current = privateState$.getValue();
// We want to make it easy for subscribers to react to change changes, so we
- // are explicitly emitting and additional `undefined` when the change number
+ // are explicitly emitting an additional `undefined` when the change number
// changes. So if you are subscribed to the latestPatchsetNumber for example,
// then you can rely on emissions even if the old and the new change have the
// same latestPatchsetNumber.
diff --git a/polygerrit-ui/app/services/change/change-service.ts b/polygerrit-ui/app/services/change/change-service.ts
index 1c6fc4c..0b9a1f2 100644
--- a/polygerrit-ui/app/services/change/change-service.ts
+++ b/polygerrit-ui/app/services/change/change-service.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import {routerChangeNum$} from '../router/router-model';
-import {updateState} from './change-model';
+import {change$, updateState} from './change-model';
import {ParsedChangeInfo} from '../../types/types';
import {appContext} from '../app-context';
import {ChangeInfo} from '../../types/common';
@@ -25,6 +25,8 @@
} from '../../utils/patch-set-util';
export class ChangeService {
+ private change?: ParsedChangeInfo;
+
private readonly restApiService = appContext.restApiService;
constructor() {
@@ -34,6 +36,9 @@
routerChangeNum$.subscribe(changeNum => {
if (!changeNum) updateState(undefined);
});
+ change$.subscribe(change => {
+ this.change = change;
+ });
}
/**
@@ -48,6 +53,15 @@
}
/**
+ * Typically you would just subscribe to change$ yourself to get updates. But
+ * sometimes it is nice to also be able to get the current ChangeInfo on
+ * demand. So here it is for your convenience.
+ */
+ getChange() {
+ return this.change;
+ }
+
+ /**
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
*
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 60cb780..5d3da42 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -21,7 +21,6 @@
Category,
CheckResult as CheckResultApi,
CheckRun as CheckRunApi,
- ChecksApiConfig,
Link,
LinkIcon,
RunStatus,
@@ -32,6 +31,17 @@
import {AttemptDetail, createAttemptMap} from './checks-util';
import {assertIsDefined} from '../../utils/common-util';
+/**
+ * The checks model maintains the state of checks for two patchsets: the latest
+ * and (if different) also for the one selected in the checks tab. So we need
+ * the distinction in a lot of places for checks about whether the code affects
+ * the checks data of the LATEST or the SELECTED patchset.
+ */
+export enum ChecksPatchset {
+ LATEST = 'LATEST',
+ SELECTED = 'SELECTED',
+}
+
export interface CheckResult extends CheckResultApi {
/**
* Internally we want to uniquely identify a run with an id, for example when
@@ -75,21 +85,33 @@
errorMessage?: string;
/** Presence of loginCallback implicitly means that the provider is in NOT_LOGGED_IN state. */
loginCallback?: () => void;
- config?: ChecksApiConfig;
runs: CheckRun[];
actions: Action[];
links: Link[];
}
interface ChecksState {
- patchsetNumber?: PatchSetNumber;
- providerNameToState: {
+ /**
+ * This is the patchset number selected by the user. The *latest* patchset
+ * can be picked up from the change model.
+ */
+ patchsetNumberSelected?: PatchSetNumber;
+ /** Checks data for the latest patchset. */
+ pluginStateLatest: {
+ [name: string]: ChecksProviderState;
+ };
+ /**
+ * Checks data for the selected patchset. Note that `checksSelected$` below
+ * falls back to the data for the latest patchset, if no patchset is selected.
+ */
+ pluginStateSelected: {
[name: string]: ChecksProviderState;
};
}
const initialState: ChecksState = {
- providerNameToState: {},
+ pluginStateLatest: {},
+ pluginStateSelected: {},
};
const privateState$ = new BehaviorSubject(initialState);
@@ -97,29 +119,45 @@
// Re-exporting as Observable so that you can only subscribe, but not emit.
export const checksState$: Observable<ChecksState> = privateState$;
-export const checksPatchsetNumber$ = checksState$.pipe(
- map(state => state.patchsetNumber),
+export const checksSelectedPatchsetNumber$ = checksState$.pipe(
+ map(state => state.patchsetNumberSelected),
distinctUntilChanged()
);
-export const checksProviderState$ = checksState$.pipe(
- map(state => state.providerNameToState),
+export const checksLatest$ = checksState$.pipe(
+ map(state => state.pluginStateLatest),
distinctUntilChanged()
);
-export const aPluginHasRegistered$ = checksProviderState$.pipe(
+export const checksSelected$ = checksState$.pipe(
+ map(state =>
+ state.patchsetNumberSelected
+ ? state.pluginStateSelected
+ : state.pluginStateLatest
+ ),
+ distinctUntilChanged()
+);
+
+export const aPluginHasRegistered$ = checksLatest$.pipe(
map(state => Object.keys(state).length > 0),
distinctUntilChanged()
);
-export const someProvidersAreLoading$ = checksProviderState$.pipe(
+export const someProvidersAreLoadingLatest$ = checksLatest$.pipe(
map(state =>
Object.values(state).some(providerState => providerState.loading)
),
distinctUntilChanged()
);
-export const errorMessage$ = checksProviderState$.pipe(
+export const someProvidersAreLoadingSelected$ = checksSelected$.pipe(
+ map(state =>
+ Object.values(state).some(providerState => providerState.loading)
+ ),
+ distinctUntilChanged()
+);
+
+export const errorMessageLatest$ = checksLatest$.pipe(
map(
state =>
Object.values(state).find(
@@ -129,7 +167,7 @@
distinctUntilChanged()
);
-export const loginCallback$ = checksProviderState$.pipe(
+export const loginCallbackLatest$ = checksLatest$.pipe(
map(
state =>
Object.values(state).find(
@@ -139,7 +177,7 @@
distinctUntilChanged()
);
-export const allActions$ = checksProviderState$.pipe(
+export const topLevelActionsSelected$ = checksSelected$.pipe(
map(state =>
Object.values(state).reduce(
(allActions: Action[], providerState: ChecksProviderState) => [
@@ -151,7 +189,7 @@
)
);
-export const allLinks$ = checksProviderState$.pipe(
+export const topLevelLinksSelected$ = checksSelected$.pipe(
map(state =>
Object.values(state).reduce(
(allActions: Link[], providerState: ChecksProviderState) => [
@@ -163,7 +201,7 @@
)
);
-export const allRuns$ = checksProviderState$.pipe(
+export const allRunsLatestPatchset$ = checksLatest$.pipe(
map(state =>
Object.values(state).reduce(
(allRuns: CheckRun[], providerState: ChecksProviderState) => [
@@ -175,11 +213,23 @@
)
);
-export const allRunsLatest$ = allRuns$.pipe(
+export const allRunsSelectedPatchset$ = checksSelected$.pipe(
+ map(state =>
+ Object.values(state).reduce(
+ (allRuns: CheckRun[], providerState: ChecksProviderState) => [
+ ...allRuns,
+ ...providerState.runs,
+ ],
+ []
+ )
+ )
+);
+
+export const allRunsLatestPatchsetLatestAttempt$ = allRunsLatestPatchset$.pipe(
map(runs => runs.filter(run => run.isLatestAttempt))
);
-export const checkToPluginMap$ = checksProviderState$.pipe(
+export const checkToPluginMap$ = checksLatest$.pipe(
map(state => {
const map = new Map<string, string>();
for (const [pluginName, providerState] of Object.entries(state)) {
@@ -191,7 +241,7 @@
})
);
-export const allResults$ = checksProviderState$.pipe(
+export const allResultsSelected$ = checksSelected$.pipe(
map(state =>
Object.values(state)
.reduce(
@@ -211,16 +261,12 @@
// Must only be used by the checks service or whatever is in control of this
// model.
-export function updateStateSetProvider(
- pluginName: string,
- config?: ChecksApiConfig
-) {
+export function updateStateSetProvider(pluginName: string) {
const nextState = {...privateState$.getValue()};
- nextState.providerNameToState = {...nextState.providerNameToState};
- nextState.providerNameToState[pluginName] = {
+ nextState.pluginStateLatest = {...nextState.pluginStateLatest};
+ nextState.pluginStateLatest[pluginName] = {
pluginName,
loading: false,
- config,
runs: [],
actions: [],
links: [],
@@ -253,6 +299,7 @@
internalResultId: 'f0r1',
category: Category.ERROR,
summary: 'Running the mighty test has failed by crashing.',
+ message: 'Btw, 1 is also not equal to 3. Did you know?',
actions: [
{
name: 'Ignore',
@@ -276,8 +323,16 @@
],
tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}],
links: [
- {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
+ {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
{primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
+ {
+ primary: true,
+ url: 'https://google.com',
+ icon: LinkIcon.DOWNLOAD_MOBILE,
+ },
+ {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE},
{primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG},
{primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE},
{primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY},
@@ -290,6 +345,7 @@
export const fakeRun1: CheckRun = {
internalRunId: 'f1',
checkName: 'FAKE Super Check',
+ patchset: 1,
labelName: 'Verified',
isSingleAttempt: true,
isLatestAttempt: true,
@@ -302,6 +358,26 @@
message: `There is a lot to be said. A lot. I say, a lot.\n
So please keep reading.`,
tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}],
+ codePointers: [
+ {
+ path: '/COMMIT_MSG',
+ range: {
+ start_line: 10,
+ start_character: 0,
+ end_line: 10,
+ end_character: 0,
+ },
+ },
+ {
+ path: 'polygerrit-ui/app/api/checks.ts',
+ range: {
+ start_line: 5,
+ start_character: 0,
+ end_line: 7,
+ end_character: 0,
+ },
+ },
+ ],
links: [
{primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
{primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
@@ -311,6 +387,18 @@
icon: LinkIcon.DOWNLOAD_MOBILE,
},
{primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {
+ primary: false,
+ url: 'https://google.com',
+ tooltip: 'look at this',
+ icon: LinkIcon.IMAGE,
+ },
+ {
+ primary: false,
+ url: 'https://google.com',
+ tooltip: 'not at this',
+ icon: LinkIcon.IMAGE,
+ },
],
},
],
@@ -472,32 +560,82 @@
{
url: 'https://www.google.com',
primary: true,
- tooltip: 'Tooltip for Bug Report Fake Link',
+ tooltip: 'Fake Bug Report 1',
icon: LinkIcon.REPORT_BUG,
},
{
url: 'https://www.google.com',
- primary: false,
- tooltip: 'Tooltip for External Fake Link',
+ primary: true,
+ tooltip: 'Fake Bug Report 2',
+ icon: LinkIcon.REPORT_BUG,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Link 1',
icon: LinkIcon.EXTERNAL,
},
+ {
+ url: 'https://www.google.com',
+ primary: false,
+ tooltip: 'Fake Link 2',
+ icon: LinkIcon.EXTERNAL,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Code Link',
+ icon: LinkIcon.CODE,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Image Link',
+ icon: LinkIcon.IMAGE,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Help Link',
+ icon: LinkIcon.HELP_PAGE,
+ },
];
-export function updateStateSetLoading(pluginName: string) {
+export function getPluginState(
+ state: ChecksState,
+ patchset: ChecksPatchset = ChecksPatchset.LATEST
+) {
+ if (patchset === ChecksPatchset.LATEST) {
+ state.pluginStateLatest = {...state.pluginStateLatest};
+ return state.pluginStateLatest;
+ } else {
+ state.pluginStateSelected = {...state.pluginStateSelected};
+ return state.pluginStateSelected;
+ }
+}
+
+export function updateStateSetLoading(
+ pluginName: string,
+ patchset: ChecksPatchset
+) {
const nextState = {...privateState$.getValue()};
- nextState.providerNameToState = {...nextState.providerNameToState};
- nextState.providerNameToState[pluginName] = {
- ...nextState.providerNameToState[pluginName],
+ const pluginState = getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
loading: true,
};
privateState$.next(nextState);
}
-export function updateStateSetError(pluginName: string, errorMessage: string) {
+export function updateStateSetError(
+ pluginName: string,
+ errorMessage: string,
+ patchset: ChecksPatchset
+) {
const nextState = {...privateState$.getValue()};
- nextState.providerNameToState = {...nextState.providerNameToState};
- nextState.providerNameToState[pluginName] = {
- ...nextState.providerNameToState[pluginName],
+ const pluginState = getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
loading: false,
errorMessage,
loginCallback: undefined,
@@ -509,12 +647,13 @@
export function updateStateSetNotLoggedIn(
pluginName: string,
- loginCallback: () => void
+ loginCallback: () => void,
+ patchset: ChecksPatchset
) {
const nextState = {...privateState$.getValue()};
- nextState.providerNameToState = {...nextState.providerNameToState};
- nextState.providerNameToState[pluginName] = {
- ...nextState.providerNameToState[pluginName],
+ const pluginState = getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
loading: false,
errorMessage: undefined,
loginCallback,
@@ -528,7 +667,8 @@
pluginName: string,
runs: CheckRunApi[],
actions: Action[] = [],
- links: Link[] = []
+ links: Link[] = [],
+ patchset: ChecksPatchset
) {
const attemptMap = createAttemptMap(runs);
for (const attemptInfo of attemptMap.values()) {
@@ -537,9 +677,9 @@
attemptInfo.attempts.sort((a, b) => (b.attempt ?? -1) - (a.attempt ?? -1));
}
const nextState = {...privateState$.getValue()};
- nextState.providerNameToState = {...nextState.providerNameToState};
- nextState.providerNameToState[pluginName] = {
- ...nextState.providerNameToState[pluginName],
+ const pluginState = getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
loading: false,
errorMessage: undefined,
loginCallback: undefined,
@@ -569,6 +709,6 @@
export function updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
const nextState = {...privateState$.getValue()};
- nextState.patchsetNumber = patchsetNumber;
+ nextState.patchsetNumberSelected = patchsetNumber;
privateState$.next(nextState);
}
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
index 48d61e2..e1d435b 100644
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ b/polygerrit-ui/app/services/checks/checks-service.ts
@@ -32,7 +32,8 @@
} from '../../api/checks';
import {change$, changeNum$, latestPatchNum$} from '../change/change-model';
import {
- checksPatchsetNumber$,
+ ChecksPatchset,
+ checksSelectedPatchsetNumber$,
checkToPluginMap$,
updateStateSetError,
updateStateSetLoading,
@@ -55,6 +56,7 @@
import {getShaByPatchNum} from '../../utils/patch-set-util';
import {assertIsDefined} from '../../utils/common-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
+import {routerPatchNum$} from '../router/router-model';
export class ChecksService {
private readonly providers: {[name: string]: ChecksProvider} = {};
@@ -63,15 +65,26 @@
private checkToPluginMap = new Map<string, string>();
+ private latestPatchNum?: PatchSetNumber;
+
private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
constructor(readonly reporting: ReportingService) {
checkToPluginMap$.subscribe(map => {
this.checkToPluginMap = map;
});
- latestPatchNum$.subscribe(num => {
- updateStateSetPatchset(num);
- });
+ combineLatest([routerPatchNum$, latestPatchNum$]).subscribe(
+ ([routerPs, latestPs]) => {
+ this.latestPatchNum = latestPs;
+ if (latestPs === undefined) {
+ this.setPatchset(undefined);
+ } else if (typeof routerPs === 'number') {
+ this.setPatchset(routerPs);
+ } else {
+ this.setPatchset(latestPs);
+ }
+ }
+ );
document.addEventListener('visibilitychange', () => {
this.documentVisibilityChange$.next(undefined);
});
@@ -80,8 +93,8 @@
});
}
- setPatchset(num: PatchSetNumber) {
- updateStateSetPatchset(num);
+ setPatchset(num?: PatchSetNumber) {
+ updateStateSetPatchset(num === this.latestPatchNum ? undefined : num);
}
reload(pluginName: string) {
@@ -105,7 +118,16 @@
) {
this.providers[pluginName] = provider;
this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
- updateStateSetProvider(pluginName, config);
+ updateStateSetProvider(pluginName);
+ this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST);
+ this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED);
+ }
+
+ initFetchingOfData(
+ pluginName: string,
+ config: ChecksApiConfig,
+ patchset: ChecksPatchset
+ ) {
const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000;
// Various events should trigger fetching checks from the provider:
// 1. Change number and patchset number changes.
@@ -114,7 +136,9 @@
// 4. A hidden Gerrit tab becoming visible.
combineLatest([
changeNum$,
- checksPatchsetNumber$,
+ patchset === ChecksPatchset.LATEST
+ ? latestPatchNum$
+ : checksSelectedPatchsetNumber$,
this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
timer(0, pollIntervalMs),
this.documentVisibilityChange$,
@@ -125,27 +149,13 @@
withLatestFrom(change$),
switchMap(
([[changeNum, patchNum], change]): Observable<FetchResponse> => {
- if (
- !change ||
- !changeNum ||
- !patchNum ||
- typeof patchNum !== 'number'
- ) {
- return of({
- responseCode: ResponseCode.OK,
- runs: [],
- });
- }
+ if (!change || !changeNum || !patchNum) return of(this.empty());
+ if (typeof patchNum !== 'number') return of(this.empty());
assertIsDefined(change.revisions, 'change.revisions');
const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
// Sometimes patchNum is updated earlier than change, so change
// revisions don't have patchNum yet
- if (!patchsetSha) {
- return of({
- responseCode: ResponseCode.OK,
- runs: [],
- });
- }
+ if (!patchsetSha) return of(this.empty());
const data: ChangeData = {
changeNumber: changeNum,
patchsetNumber: patchNum,
@@ -154,7 +164,7 @@
commmitMessage: getCurrentRevision(change)?.commit?.message,
changeInfo: change,
};
- return this.fetchResults(pluginName, data);
+ return this.fetchResults(pluginName, data, patchset);
}
),
catchError(e => {
@@ -169,24 +179,36 @@
switch (response.responseCode) {
case ResponseCode.ERROR:
assertIsDefined(response.errorMessage, 'errorMessage');
- updateStateSetError(pluginName, response.errorMessage);
+ updateStateSetError(pluginName, response.errorMessage, patchset);
break;
case ResponseCode.NOT_LOGGED_IN:
assertIsDefined(response.loginCallback, 'loginCallback');
- updateStateSetNotLoggedIn(pluginName, response.loginCallback);
+ updateStateSetNotLoggedIn(
+ pluginName,
+ response.loginCallback,
+ patchset
+ );
break;
case ResponseCode.OK:
updateStateSetResults(
pluginName,
response.runs ?? [],
response.actions ?? [],
- response.links ?? []
+ response.links ?? [],
+ patchset
);
break;
}
});
}
+ private empty(): FetchResponse {
+ return {
+ responseCode: ResponseCode.OK,
+ runs: [],
+ };
+ }
+
private createErrorResponse(
pluginName: string,
message: string
@@ -199,9 +221,10 @@
private fetchResults(
pluginName: string,
- data: ChangeData
+ data: ChangeData,
+ patchset: ChecksPatchset
): Observable<FetchResponse> {
- updateStateSetLoading(pluginName);
+ updateStateSetLoading(pluginName, patchset);
const timer = this.reporting.getTimer('ChecksPluginFetch');
const fetchPromise = this.providers[pluginName]
.fetch(data)
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 981a1f6..9381f30 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -43,6 +43,8 @@
return 'help-outline';
case LinkIcon.REPORT_BUG:
return 'bug';
+ case LinkIcon.CODE:
+ return 'code';
default:
// We don't throw an assertion error here, because plugins don't have to
// be written in TypeScript, so we may encounter arbitrary strings for
@@ -99,7 +101,7 @@
}
}
-enum PRIMARY_STATUS_ACTIONS {
+export enum PRIMARY_STATUS_ACTIONS {
RERUN = 'rerun',
RUN = 'run',
CANCEL = 'cancel',
@@ -129,7 +131,8 @@
}
}
-export function primaryRunAction(run: CheckRun): Action | undefined {
+export function primaryRunAction(run?: CheckRun): Action | undefined {
+ if (!run) return undefined;
return runActions(run).filter(
action => !action.disabled && action.name === primaryActionName(run.status)
)[0];
@@ -238,9 +241,10 @@
export function fireActionTriggered(
target: EventTarget,
- action: Action,
+ action?: Action,
run?: CheckRun
) {
+ if (!action) return;
target.dispatchEvent(
new CustomEvent('action-triggered', {
detail: {action, run},
@@ -310,14 +314,19 @@
};
}
-export function primaryLink(result?: CheckResultApi): Link | undefined {
- const links = result?.links ?? [];
- return links.find(link => link.primary);
+function allPrimaryLinks(result?: CheckResultApi): Link[] {
+ return (result?.links ?? []).filter(link => link.primary);
}
-export function otherLinks(result?: CheckResultApi): Link[] {
- const primary = primaryLink(result);
- const links = result?.links ?? [];
- // Just filter the one primary link, not all primary links.
- return links.filter(link => link !== primary);
+export function firstPrimaryLink(result?: CheckResultApi): Link | undefined {
+ return allPrimaryLinks(result).find(link => link.icon === LinkIcon.EXTERNAL);
+}
+
+export function otherPrimaryLinks(result?: CheckResultApi): Link[] {
+ const first = firstPrimaryLink(result);
+ return allPrimaryLinks(result).filter(link => link !== first);
+}
+
+export function secondaryLinks(result?: CheckResultApi): Link[] {
+ return (result?.links ?? []).filter(link => !link.primary);
}
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 509a140..ac072f3 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -18,7 +18,12 @@
import {NumericChangeId} from '../../types/common';
import {EventDetails} from '../../api/reporting';
import {PluginApi} from '../../api/plugin';
-import {Execution, LifeCycle, Timing} from '../../constants/reporting';
+import {
+ Execution,
+ Interaction,
+ LifeCycle,
+ Timing,
+} from '../../constants/reporting';
export type EventValue = string | number | {error?: Error};
@@ -43,7 +48,7 @@
beforeLocationChanged(): void;
locationChanged(page: string): void;
dashboardDisplayed(): void;
- changeDisplayed(): void;
+ changeDisplayed(eventDetails?: EventDetails): void;
changeFullyLoaded(): void;
diffViewDisplayed(): void;
diffViewFullyLoaded(): void;
@@ -104,7 +109,10 @@
object: string,
method: string
): void;
- reportInteraction(eventName: string, details?: EventDetails): void;
+ reportInteraction(
+ eventName: string | Interaction,
+ details?: EventDetails
+ ): void;
/**
* A draft interaction was started. Update the time-between-draft-actions
* timer.
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 4e6ece2..0b93f07 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -21,7 +21,12 @@
import {NumericChangeId} from '../../types/common';
import {EventDetails} from '../../api/reporting';
import {PluginApi} from '../../api/plugin';
-import {Execution, LifeCycle, Timing} from '../../constants/reporting';
+import {
+ Execution,
+ Interaction,
+ LifeCycle,
+ Timing,
+} from '../../constants/reporting';
// Latency reporting constants.
@@ -508,11 +513,12 @@
}
}
- changeDisplayed() {
+ changeDisplayed(eventDetails?: EventDetails) {
+ eventDetails = {...eventDetails, ...this._pageLoadDetails()};
if (hasOwnProperty(this._baselines, Timing.STARTUP_CHANGE_DISPLAYED)) {
- this.timeEnd(Timing.STARTUP_CHANGE_DISPLAYED, this._pageLoadDetails());
+ this.timeEnd(Timing.STARTUP_CHANGE_DISPLAYED, eventDetails);
} else {
- this.timeEnd(Timing.CHANGE_DISPLAYED, this._pageLoadDetails());
+ this.timeEnd(Timing.CHANGE_DISPLAYED, eventDetails);
}
}
@@ -772,7 +778,7 @@
);
}
- reportInteraction(eventName: string, details: EventDetails) {
+ reportInteraction(eventName: string | Interaction, details: EventDetails) {
this.reporter(
INTERACTION.TYPE,
INTERACTION.CATEGORY.DEFAULT,
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
index c180439..24c27e0 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -17,7 +17,7 @@
import {ReportingService, Timer} from './gr-reporting';
import {EventDetails} from '../../api/reporting';
import {PluginApi} from '../../api/plugin';
-import {Execution} from '../../constants/reporting';
+import {Execution, Interaction} from '../../constants/reporting';
export class MockTimer implements Timer {
end(): this {
@@ -72,7 +72,10 @@
log(`trackApi '${plugin}', ${object}, ${method}`);
},
reportExtension: () => {},
- reportInteraction: (eventName: string, details?: EventDetails) => {
+ reportInteraction: (
+ eventName: string | Interaction,
+ details?: EventDetails
+ ) => {
log(`reportInteraction '${eventName}': ${JSON.stringify(details)}`);
},
reportLifeCycle: () => {},
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 3a890d9..cd3fab3 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -534,6 +534,7 @@
invalidateGroupsCache(): void;
invalidateReposCache(): void;
invalidateAccountsCache(): void;
+ invalidateAccountsDetailCache(): void;
removeFromAttentionSet(
changeNum: NumericChangeId,
user: AccountId,
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.ts b/polygerrit-ui/app/styles/dashboard-header-styles.ts
index 2354f65..0c9f38d 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.ts
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.ts
@@ -55,9 +55,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.ts b/polygerrit-ui/app/styles/gr-change-list-styles.ts
index d1fcdc9..e0a7a28 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.ts
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.ts
@@ -202,9 +202,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
index 3d07d2e..67a6963 100644
--- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
@@ -55,9 +55,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
index 57c8d78..145f0d5 100644
--- a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
+++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
@@ -22,6 +22,15 @@
const $_documentContainer = document.createElement('template');
+/*
+ These are shared styles for change-view-integration endpoints.
+ All plugins that registered that endpoint should include this in
+ the component to have a consistent UX:
+
+ <style include="gr-change-view-integration-shared-styles"></style>
+
+ And use those defined class to apply these styles.
+*/
$_documentContainer.innerHTML = `<dom-module id="gr-change-view-integration-shared-styles">
<template>
<style include="shared-styles">
@@ -61,18 +70,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- This is shared styles for change-view-integration endpoints.
- All plugins that registered that endpoint should include this in
- the component to have a consistent UX:
-
- <style include="gr-change-view-integration-shared-styles"></style>
-
- And use those defined class to apply these styles.
-*/
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-form-styles.ts b/polygerrit-ui/app/styles/gr-form-styles.ts
index 3284ad5..f58a02c 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.ts
+++ b/polygerrit-ui/app/styles/gr-form-styles.ts
@@ -124,9 +124,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.ts b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
index e9a79c1f..d46f136 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.ts
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
@@ -79,9 +79,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.ts b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
index 9010b2d..8c29b85 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.ts
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
@@ -72,9 +72,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.ts b/polygerrit-ui/app/styles/gr-subpage-styles.ts
index 41ee952..5aab0dc 100644
--- a/polygerrit-ui/app/styles/gr-subpage-styles.ts
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.ts
@@ -42,9 +42,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-table-styles.ts b/polygerrit-ui/app/styles/gr-table-styles.ts
index 52fdc67..09d1161 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.ts
+++ b/polygerrit-ui/app/styles/gr-table-styles.ts
@@ -116,9 +116,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.ts b/polygerrit-ui/app/styles/gr-voting-styles.ts
index b50aee6..cb8b0be8 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.ts
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -48,9 +48,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 1bee660..287cf68 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -304,9 +304,3 @@
</dom-module>`;
document.head.appendChild($_documentContainer.content);
-
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 3971fa5..a3df94c 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -411,6 +411,7 @@
invalidateAccountsCache(): void {},
invalidateGroupsCache(): void {},
invalidateReposCache(): void {},
+ invalidateAccountsDetailCache(): void {},
probePath(): Promise<boolean> {
return Promise.resolve(true);
},
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 65c72f9..8e89525 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -18,7 +18,7 @@
import {PatchSetNum, UrlEncodedCommentId} from './common';
import {UIComment} from '../utils/comment-util';
import {FetchRequest} from './types';
-import {MovedLinkClickedEventDetail} from '../api/diff';
+import {LineNumberEventDetail, MovedLinkClickedEventDetail} from '../api/diff';
import {Category, RunStatus} from '../api/checks';
import {ChangeMessage} from '../elements/change/gr-message/gr-message';
@@ -55,6 +55,10 @@
'editable-content-save': EditableContentSaveEvent;
'location-change': LocationChangeEvent;
'iron-announce': IronAnnounceEvent;
+ 'line-number-mouse-enter': LineNumberEvent;
+ 'line-number-mouse-leave': LineNumberEvent;
+ 'line-cursor-moved-in': LineNumberEvent;
+ 'line-cursor-moved-out': LineNumberEvent;
'moved-link-clicked': MovedLinkClickedEvent;
'open-fix-preview': OpenFixPreviewEvent;
'close-fix-preview': CloseFixPreviewEvent;
@@ -124,6 +128,8 @@
export type MovedLinkClickedEvent = CustomEvent<MovedLinkClickedEventDetail>;
+export type LineNumberEvent = CustomEvent<LineNumberEventDetail>;
+
export interface NetworkErrorEventDetail {
error: Error;
}
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index dd91f7b..380d06c 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -24,6 +24,7 @@
RelatedChangeAndCommitInfo,
} from '../types/common';
import {ParsedChangeInfo} from '../types/types';
+import {ChangeStates} from '../elements/shared/gr-change-status/gr-change-status';
// This can be wrong! See WARNING above
interface ChangeStatusesOptions {
@@ -134,28 +135,27 @@
return change?.status === ChangeStatus.NEW;
}
-// TODO(TS): use enum ChangeStates in gr-change-status
export function changeStatuses(
change: ChangeInfo,
opt_options?: ChangeStatusesOptions
-) {
+): ChangeStates[] {
const states = [];
if (change.status === ChangeStatus.MERGED) {
- states.push('Merged');
+ states.push(ChangeStates.MERGED);
} else if (change.status === ChangeStatus.ABANDONED) {
- states.push('Abandoned');
+ states.push(ChangeStates.ABANDONED);
} else if (
change.mergeable === false ||
(opt_options && opt_options.mergeable === false)
) {
// 'mergeable' prop may not always exist (@see Issue 6819)
- states.push('Merge Conflict');
+ states.push(ChangeStates.MERGE_CONFLICT);
}
if (change.work_in_progress) {
- states.push('WIP');
+ states.push(ChangeStates.WIP);
}
if (change.is_private) {
- states.push('Private');
+ states.push(ChangeStates.PRIVATE);
}
// If there are any pre-defined statuses, only return those. Otherwise,
@@ -166,21 +166,24 @@
// If no missing requirements, either active or ready to submit.
if (change.submittable && opt_options.submitEnabled) {
- states.push('Ready to submit');
+ states.push(ChangeStates.READY_TO_SUBMIT);
} else {
// Otherwise it is active.
- states.push('Active');
+ states.push(ChangeStates.ACTIVE);
}
return states;
}
-export function isOwner(change?: ChangeInfo, account?: AccountInfo): boolean {
+export function isOwner(
+ change?: ChangeInfo | ParsedChangeInfo,
+ account?: AccountInfo
+): boolean {
if (!change || !account) return false;
return change.owner?._account_id === account._account_id;
}
export function isReviewer(
- change?: ChangeInfo,
+ change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
if (!change || !account) return false;
diff --git a/polygerrit-ui/app/utils/change-util_test.js b/polygerrit-ui/app/utils/change-util_test.js
deleted file mode 100644
index f348239..0000000
--- a/polygerrit-ui/app/utils/change-util_test.js
+++ /dev/null
@@ -1,202 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {
- changeBaseURL,
- changePath,
- changeStatuses,
- isRemovableReviewer,
-} from './change-util.js';
-
-suite('change-util tests', () => {
- let originalCanonicalPath;
-
- suiteSetup(() => {
- originalCanonicalPath = window.CANONICAL_PATH;
- window.CANONICAL_PATH = '/r';
- });
-
- suiteTeardown(() => {
- window.CANONICAL_PATH = originalCanonicalPath;
- });
-
- test('changeBaseURL', () => {
- assert.deepEqual(
- changeBaseURL('test/project', '1', '2'),
- '/r/changes/test%2Fproject~1/revisions/2'
- );
- });
-
- test('changePath', () => {
- assert.deepEqual(changePath('1'), '/r/c/1');
- });
-
- test('Open status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- let statuses = changeStatuses(change);
- assert.deepEqual(statuses, []);
-
- change.submittable = false;
- statuses = changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Active']);
-
- // With no missing labels but no submitEnabled option.
- change.submittable = true;
- statuses = changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Active']);
-
- // Without missing labels and enabled submit
- statuses = changeStatuses(change,
- {includeDerived: true, submitEnabled: true});
- assert.deepEqual(statuses, ['Ready to submit']);
-
- change.mergeable = false;
- change.submittable = true;
- statuses = changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Merge Conflict']);
-
- delete change.mergeable;
- change.submittable = true;
- statuses = changeStatuses(change,
- {includeDerived: true, mergeable: true, submitEnabled: true});
- assert.deepEqual(statuses, ['Ready to submit']);
-
- change.submittable = true;
- statuses = changeStatuses(change,
- {includeDerived: true, mergeable: false});
- assert.deepEqual(statuses, ['Merge Conflict']);
- });
-
- test('Merge conflict', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: false,
- };
- const statuses = changeStatuses(change);
- assert.deepEqual(statuses, ['Merge Conflict']);
- });
-
- test('mergeable prop undefined', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- };
- const statuses = changeStatuses(change);
- assert.deepEqual(statuses, []);
- });
-
- test('Merged status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'MERGED',
- labels: {},
- };
- const statuses = changeStatuses(change);
- assert.deepEqual(statuses, ['Merged']);
- });
-
- test('Abandoned status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'ABANDONED',
- labels: {},
- };
- const statuses = changeStatuses(change);
- assert.deepEqual(statuses, ['Abandoned']);
- });
-
- test('Open status with private and wip', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- is_private: true,
- work_in_progress: true,
- labels: {},
- mergeable: true,
- };
- const statuses = changeStatuses(change);
- assert.deepEqual(statuses, ['WIP', 'Private']);
- });
-
- test('Merge conflict with private and wip', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- is_private: true,
- work_in_progress: true,
- labels: {},
- mergeable: false,
- };
- const statuses = changeStatuses(change);
- assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
- });
-
- test('isRemovableReviewer', () => {
- let change = {
- removable_reviewers: [{_account_id: 1}],
- };
- const reviewer = {_account_id: 1};
-
- assert.equal(isRemovableReviewer(change, reviewer), true);
-
- change = {
- removable_reviewers: [{_account_id: 2}],
- };
- assert.equal(isRemovableReviewer(change, reviewer), false);
- });
-});
-
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
new file mode 100644
index 0000000..8232e56
--- /dev/null
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -0,0 +1,198 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {ChangeStatus} from '../constants/constants';
+import {ChangeStates} from '../elements/shared/gr-change-status/gr-change-status';
+import '../test/common-test-setup-karma';
+import {createChange, createRevisions} from '../test/test-data-generators';
+import {
+ AccountId,
+ CommitId,
+ NumericChangeId,
+ PatchSetNum,
+} from '../types/common';
+import {
+ changeBaseURL,
+ changePath,
+ changeStatuses,
+ isRemovableReviewer,
+} from './change-util';
+
+suite('change-util tests', () => {
+ let originalCanonicalPath: string | undefined;
+
+ suiteSetup(() => {
+ originalCanonicalPath = window.CANONICAL_PATH;
+ window.CANONICAL_PATH = '/r';
+ });
+
+ suiteTeardown(() => {
+ window.CANONICAL_PATH = originalCanonicalPath;
+ });
+
+ test('changeBaseURL', () => {
+ assert.deepEqual(
+ changeBaseURL('test/project', 1 as NumericChangeId, '2' as PatchSetNum),
+ '/r/changes/test%2Fproject~1/revisions/2'
+ );
+ });
+
+ test('changePath', () => {
+ assert.deepEqual(changePath(1 as NumericChangeId), '/r/c/1');
+ });
+
+ test('Open status', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ mergeable: true,
+ };
+ let statuses = changeStatuses(change);
+ assert.deepEqual(statuses, []);
+
+ change.submittable = false;
+ statuses = changeStatuses(change, {mergeable: true, submitEnabled: false});
+ assert.deepEqual(statuses, [ChangeStates.ACTIVE]);
+
+ // With no missing labels but no submitEnabled option.
+ change.submittable = true;
+ statuses = changeStatuses(change, {mergeable: true, submitEnabled: false});
+ assert.deepEqual(statuses, [ChangeStates.ACTIVE]);
+
+ // Without missing labels and enabled submit
+ statuses = changeStatuses(change, {mergeable: true, submitEnabled: true});
+ assert.deepEqual(statuses, [ChangeStates.READY_TO_SUBMIT]);
+
+ change.mergeable = false;
+ change.submittable = true;
+ statuses = changeStatuses(change, {mergeable: false, submitEnabled: false});
+ assert.deepEqual(statuses, [ChangeStates.MERGE_CONFLICT]);
+
+ change.mergeable = true;
+ statuses = changeStatuses(change, {mergeable: true, submitEnabled: true});
+ assert.deepEqual(statuses, [ChangeStates.READY_TO_SUBMIT]);
+
+ change.submittable = true;
+ statuses = changeStatuses(change, {mergeable: false, submitEnabled: false});
+ assert.deepEqual(statuses, [ChangeStates.MERGE_CONFLICT]);
+ });
+
+ test('Merge conflict', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.NEW,
+ mergeable: false,
+ };
+ const statuses = changeStatuses(change);
+ assert.deepEqual(statuses, [ChangeStates.MERGE_CONFLICT]);
+ });
+
+ test('mergeable prop undefined', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.NEW,
+ };
+ const statuses = changeStatuses(change);
+ assert.deepEqual(statuses, []);
+ });
+
+ test('Merged status', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.MERGED,
+ };
+ const statuses = changeStatuses(change);
+ assert.deepEqual(statuses, [ChangeStates.MERGED]);
+ });
+
+ test('Abandoned status', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.ABANDONED,
+ mergeable: false,
+ };
+ const statuses = changeStatuses(change);
+ assert.deepEqual(statuses, [ChangeStates.ABANDONED]);
+ });
+
+ test('Open status with private and wip', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.NEW,
+ mergeable: true,
+ is_private: true,
+ work_in_progress: true,
+ labels: {},
+ };
+ const statuses = changeStatuses(change);
+ assert.deepEqual(statuses, [ChangeStates.WIP, ChangeStates.PRIVATE]);
+ });
+
+ test('Merge conflict with private and wip', () => {
+ const change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.NEW,
+ mergeable: false,
+ is_private: true,
+ work_in_progress: true,
+ labels: {},
+ };
+ const statuses = changeStatuses(change);
+ assert.deepEqual(statuses, [
+ ChangeStates.MERGE_CONFLICT,
+ ChangeStates.WIP,
+ ChangeStates.PRIVATE,
+ ]);
+ });
+
+ test('isRemovableReviewer', () => {
+ let change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.NEW,
+ mergeable: false,
+ removable_reviewers: [{_account_id: 1 as AccountId}],
+ };
+ const reviewer = {_account_id: 1 as AccountId};
+
+ assert.equal(isRemovableReviewer(change, reviewer), true);
+
+ change = {
+ ...createChange(),
+ revisions: createRevisions(1),
+ current_revision: 'rev1' as CommitId,
+ status: ChangeStatus.NEW,
+ mergeable: false,
+ removable_reviewers: [{_account_id: 2 as AccountId}],
+ };
+ assert.equal(isRemovableReviewer(change, reviewer), false);
+ });
+});
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 08b5e49..36c3657 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -90,6 +90,24 @@
}
}
+function query<E extends Element = Element>(
+ el: Element | undefined,
+ selector: string
+): E | undefined {
+ if (!el) return undefined;
+ const root = el.shadowRoot ?? el;
+ return root.querySelector<E>(selector) ?? undefined;
+}
+
+export function queryAndAssert<E extends Element = Element>(
+ el: Element | undefined,
+ selector: string
+): E {
+ const found = query<E>(el, selector);
+ if (!found) throw new Error(`selector '${selector}' did not match anything'`);
+ return found;
+}
+
/**
* Returns true, if both sets contain the same members.
*/
diff --git a/polygerrit-ui/app/utils/patch-set-util_test.js b/polygerrit-ui/app/utils/patch-set-util_test.js
deleted file mode 100644
index 93073fa..0000000
--- a/polygerrit-ui/app/utils/patch-set-util_test.js
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 {
- _testOnly_computeWipForPatchSets, computeAllPatchSets,
- findEditParentPatchNum, findEditParentRevision,
- getParentIndex, getRevisionByPatchNum,
- isMergeParent,
- sortRevisions,
-} from './patch-set-util.js';
-
-suite('gr-patch-set-util tests', () => {
- test('getRevisionByPatchNum', () => {
- const revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.deepEqual(getRevisionByPatchNum(revisions, 1), revisions[1]);
- assert.deepEqual(getRevisionByPatchNum(revisions, 2), revisions[2]);
- assert.equal(getRevisionByPatchNum(revisions, 3), undefined);
- });
-
- test('_computeWipForPatchSets', () => {
- // Compute patch sets for a given timeline on a change. The initial WIP
- // property of the change can be true or false. The map of tags by
- // revision is keyed by patch set number. Each value is a list of change
- // message tags in the order that they occurred in the timeline. These
- // indicate actions that modify the WIP property of the change and/or
- // create new patch sets.
- //
- // Returns the actual results with an assertWip method that can be used
- // to compare against an expected value for a particular patch set.
- const compute = (initialWip, tagsByRevision) => {
- const change = {
- messages: [],
- work_in_progress: initialWip,
- };
- const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
- for (const rev of revs) {
- for (const tag of tagsByRevision[rev]) {
- change.messages.push({
- tag,
- _revision_number: rev,
- });
- }
- }
- let patchNums = revs.map(rev => { return {num: rev}; });
- patchNums = _testOnly_computeWipForPatchSets(
- change, patchNums);
- const actualWipsByRevision = {};
- for (const patchNum of patchNums) {
- actualWipsByRevision[patchNum.num] = patchNum.wip;
- }
- const verifier = {
- assertWip(revision, expectedWip) {
- const patchNum = patchNums.find(patchNum => patchNum.num == revision);
- if (!patchNum) {
- assert.fail('revision ' + revision + ' not found');
- }
- assert.equal(patchNum.wip, expectedWip,
- 'wip state for ' + revision + ' is ' +
- patchNum.wip + '; expected ' + expectedWip);
- return verifier;
- },
- };
- return verifier;
- };
-
- compute(false, {1: ['upload']}).assertWip(1, false);
- compute(true, {1: ['upload']}).assertWip(1, true);
-
- const setWip = 'autogenerated:gerrit:setWorkInProgress';
- const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
- const clearWip = 'autogenerated:gerrit:setReadyForReview';
-
- compute(false, {
- 1: ['upload', setWip],
- 2: ['upload'],
- 3: ['upload', clearWip],
- 4: ['upload', setWip],
- }).assertWip(1, false) // Change was created with PS1 ready for review
- .assertWip(2, true) // PS2 was uploaded during WIP
- .assertWip(3, false) // PS3 was marked ready for review after upload
- .assertWip(4, false); // PS4 was uploaded ready for review
-
- compute(false, {
- 1: [uploadInWip, null, 'addReviewer'],
- 2: ['upload'],
- 3: ['upload', clearWip, setWip],
- 4: ['upload'],
- 5: ['upload', clearWip],
- 6: [uploadInWip],
- }).assertWip(1, true) // Change was created in WIP
- .assertWip(2, true) // PS2 was uploaded during WIP
- .assertWip(3, false) // PS3 was marked ready for review
- .assertWip(4, true) // PS4 was uploaded during WIP
- .assertWip(5, false) // PS5 was marked ready for review
- .assertWip(6, true); // PS6 was uploaded with WIP option
- });
-
- test('isMergeParent', () => {
- assert.isFalse(isMergeParent(1));
- assert.isFalse(isMergeParent(4321));
- assert.isFalse(isMergeParent('52'));
- assert.isFalse(isMergeParent('edit'));
- assert.isFalse(isMergeParent('PARENT'));
- assert.isFalse(isMergeParent(0));
-
- assert.isTrue(isMergeParent(-23));
- assert.isTrue(isMergeParent(-1));
- assert.isTrue(isMergeParent('-42'));
- });
-
- test('findEditParentRevision', () => {
- let revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.strictEqual(findEditParentRevision(revisions), null);
-
- revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
- assert.strictEqual(findEditParentRevision(revisions), null);
-
- revisions = [...revisions, {_number: 3}];
- assert.deepEqual(findEditParentRevision(revisions), {_number: 3});
- });
-
- test('findEditParentPatchNum', () => {
- let revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.equal(findEditParentPatchNum(revisions), -1);
-
- revisions =
- [...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
- assert.deepEqual(findEditParentPatchNum(revisions), 3);
- });
-
- test('sortRevisions', () => {
- const revisions = [
- {_number: 0},
- {_number: 2},
- {_number: 1},
- ];
- const sorted = [
- {_number: 2},
- {_number: 1},
- {_number: 0},
- ];
-
- assert.deepEqual(sortRevisions(revisions), sorted);
-
- // Edit patchset should follow directly after its basePatchNum.
- revisions.push({_number: 'edit', basePatchNum: 2});
- sorted.unshift({_number: 'edit', basePatchNum: 2});
- assert.deepEqual(sortRevisions(revisions), sorted);
-
- revisions[0].basePatchNum = 0;
- const edit = sorted.shift();
- edit.basePatchNum = 0;
- // Edit patchset should be at index 2.
- sorted.splice(2, 0, edit);
- assert.deepEqual(sortRevisions(revisions), sorted);
- });
-
- test('getParentIndex', () => {
- assert.equal(getParentIndex('-13'), 13);
- assert.equal(getParentIndex(-4), 4);
- });
-
- test('computeAllPatchSets', () => {
- const expected = [
- {num: 4, desc: 'test', sha: 'rev4'},
- {num: 3, desc: 'test', sha: 'rev3'},
- {num: 2, desc: 'test', sha: 'rev2'},
- {num: 1, desc: 'test', sha: 'rev1'},
- ];
- const patchNums = computeAllPatchSets({
- revisions: {
- rev3: {_number: 3, description: 'test', date: 3},
- rev1: {_number: 1, description: 'test', date: 1},
- rev4: {_number: 4, description: 'test', date: 4},
- rev2: {_number: 2, description: 'test', date: 2},
- },
- });
- assert.equal(patchNums.length, expected.length);
- for (let i = 0; i < expected.length; i++) {
- assert.deepEqual(patchNums[i], expected[i]);
- }
- });
-});
-
diff --git a/polygerrit-ui/app/utils/patch-set-util_test.ts b/polygerrit-ui/app/utils/patch-set-util_test.ts
new file mode 100644
index 0000000..a9d9549
--- /dev/null
+++ b/polygerrit-ui/app/utils/patch-set-util_test.ts
@@ -0,0 +1,246 @@
+/**
+ * @license
+ * Copyright (C) 2016 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 {
+ createChange,
+ createChangeMessageInfo,
+ createRevision,
+} from '../test/test-data-generators';
+import {
+ BasePatchSetNum,
+ ChangeInfo,
+ EditPatchSetNum,
+ PatchSetNum,
+ ReviewInputTag,
+} from '../types/common';
+import {
+ _testOnly_computeWipForPatchSets,
+ computeAllPatchSets,
+ findEditParentPatchNum,
+ findEditParentRevision,
+ getParentIndex,
+ getRevisionByPatchNum,
+ isMergeParent,
+ sortRevisions,
+} from './patch-set-util';
+
+suite('gr-patch-set-util tests', () => {
+ test('getRevisionByPatchNum', () => {
+ const revisions = [createRevision(0), createRevision(1), createRevision(2)];
+ assert.deepEqual(
+ getRevisionByPatchNum(revisions, 1 as PatchSetNum),
+ revisions[1]
+ );
+ assert.deepEqual(
+ getRevisionByPatchNum(revisions, 2 as PatchSetNum),
+ revisions[2]
+ );
+ assert.equal(getRevisionByPatchNum(revisions, 3 as PatchSetNum), undefined);
+ });
+
+ test('_computeWipForPatchSets', () => {
+ // Compute patch sets for a given timeline on a change. The initial WIP
+ // property of the change can be true or false. The map of tags by
+ // revision is keyed by patch set number. Each value is a list of change
+ // message tags in the order that they occurred in the timeline. These
+ // indicate actions that modify the WIP property of the change and/or
+ // create new patch sets.
+ //
+ // Returns the actual results with an assertWip method that can be used
+ // to compare against an expected value for a particular patch set.
+ const compute = (
+ initialWip: boolean,
+ tagsByRevision: Map<
+ number | 'edit' | 'PARENT',
+ (ReviewInputTag | undefined)[]
+ >
+ ) => {
+ const change: ChangeInfo = {
+ ...createChange(),
+ messages: [],
+ work_in_progress: initialWip,
+ };
+ for (const rev of tagsByRevision.keys()) {
+ for (const tag of tagsByRevision.get(rev)!) {
+ change.messages!.push({
+ ...createChangeMessageInfo(),
+ tag,
+ _revision_number: rev as PatchSetNum,
+ });
+ }
+ }
+ const patchSets = Array.from(tagsByRevision.keys()).map(rev => {
+ return {num: rev as PatchSetNum, desc: 'test', sha: `rev${rev}`};
+ });
+ const patchNums = _testOnly_computeWipForPatchSets(change, patchSets);
+ const verifier = {
+ assertWip(revision: number, expectedWip: boolean) {
+ const patchNum = patchNums.find(
+ patchNum => patchNum.num === (revision as PatchSetNum)
+ );
+ if (!patchNum) {
+ assert.fail(`revision ${revision} not found`);
+ }
+ assert.equal(
+ patchNum.wip,
+ expectedWip,
+ `wip state for ${revision} ` +
+ `is ${patchNum.wip}; expected ${expectedWip}`
+ );
+ return verifier;
+ },
+ };
+ return verifier;
+ };
+
+ const upload = 'upload' as ReviewInputTag;
+
+ compute(false, new Map([[1, [upload]]])).assertWip(1, false);
+ compute(true, new Map([[1, [upload]]])).assertWip(1, true);
+
+ const setWip = 'autogenerated:gerrit:setWorkInProgress' as ReviewInputTag;
+ const uploadInWip = 'autogenerated:gerrit:newWipPatchSet' as ReviewInputTag;
+ const clearWip = 'autogenerated:gerrit:setReadyForReview' as ReviewInputTag;
+
+ compute(
+ false,
+ new Map([
+ [1, [upload, setWip]],
+ [2, [upload]],
+ [3, [upload, clearWip]],
+ [4, [upload, setWip]],
+ ])
+ )
+ .assertWip(1, false) // Change was created with PS1 ready for review
+ .assertWip(2, true) // PS2 was uploaded during WIP
+ .assertWip(3, false) // PS3 was marked ready for review after upload
+ .assertWip(4, false); // PS4 was uploaded ready for review
+
+ compute(
+ false,
+ new Map([
+ [1, [uploadInWip, undefined, 'addReviewer' as ReviewInputTag]],
+ [2, [upload]],
+ [3, [upload, clearWip, setWip]],
+ [4, [upload]],
+ [5, [upload, clearWip]],
+ [6, [uploadInWip]],
+ ])
+ )
+ .assertWip(1, true) // Change was created in WIP
+ .assertWip(2, true) // PS2 was uploaded during WIP
+ .assertWip(3, false) // PS3 was marked ready for review
+ .assertWip(4, true) // PS4 was uploaded during WIP
+ .assertWip(5, false) // PS5 was marked ready for review
+ .assertWip(6, true); // PS6 was uploaded with WIP option
+ });
+
+ test('isMergeParent', () => {
+ assert.isFalse(isMergeParent(1 as PatchSetNum));
+ assert.isFalse(isMergeParent(4321 as PatchSetNum));
+ assert.isFalse(isMergeParent('edit' as PatchSetNum));
+ assert.isFalse(isMergeParent('PARENT' as PatchSetNum));
+ assert.isFalse(isMergeParent(0 as PatchSetNum));
+
+ assert.isTrue(isMergeParent(-23 as PatchSetNum));
+ assert.isTrue(isMergeParent(-1 as PatchSetNum));
+ });
+
+ test('findEditParentRevision', () => {
+ const revisions = [createRevision(0), createRevision(1), createRevision(2)];
+ assert.strictEqual(findEditParentRevision(revisions), null);
+
+ revisions.push({
+ ...createRevision(),
+ _number: EditPatchSetNum,
+ basePatchNum: 3 as BasePatchSetNum,
+ });
+ assert.strictEqual(findEditParentRevision(revisions), null);
+
+ revisions.push(createRevision(3));
+ assert.deepEqual(findEditParentRevision(revisions), createRevision(3));
+ });
+
+ test('findEditParentPatchNum', () => {
+ const revisions = [createRevision(0), createRevision(1), createRevision(2)];
+ assert.equal(findEditParentPatchNum(revisions), -1);
+
+ revisions.push(
+ {
+ ...createRevision(),
+ _number: EditPatchSetNum,
+ basePatchNum: 3 as BasePatchSetNum,
+ },
+ createRevision(3)
+ );
+ assert.deepEqual(findEditParentPatchNum(revisions), 3);
+ });
+
+ test('sortRevisions', () => {
+ const revisions = [createRevision(0), createRevision(2), createRevision(1)];
+ const sorted = [createRevision(2), createRevision(1), createRevision(0)];
+
+ assert.deepEqual(sortRevisions(revisions), sorted);
+
+ // Edit patchset should follow directly after its basePatchNum.
+ revisions.push({
+ ...createRevision(),
+ _number: EditPatchSetNum,
+ basePatchNum: 2 as BasePatchSetNum,
+ });
+ sorted.unshift({
+ ...createRevision(),
+ _number: EditPatchSetNum,
+ basePatchNum: 2 as BasePatchSetNum,
+ });
+ assert.deepEqual(sortRevisions(revisions), sorted);
+
+ revisions[0].basePatchNum = 0 as BasePatchSetNum;
+ const edit = sorted.shift()!;
+ edit.basePatchNum = 0 as BasePatchSetNum;
+ // Edit patchset should be at index 2.
+ sorted.splice(2, 0, edit);
+ assert.deepEqual(sortRevisions(revisions), sorted);
+ });
+
+ test('getParentIndex', () => {
+ assert.equal(getParentIndex(-4 as PatchSetNum), 4);
+ });
+
+ test('computeAllPatchSets', () => {
+ const expected = [
+ {num: 4 as PatchSetNum, desc: 'test', sha: 'rev4'},
+ {num: 3 as PatchSetNum, desc: 'test', sha: 'rev3'},
+ {num: 2 as PatchSetNum, desc: 'test', sha: 'rev2'},
+ {num: 1 as PatchSetNum, desc: 'test', sha: 'rev1'},
+ ];
+ const patchNums = computeAllPatchSets({
+ ...createChange(),
+ revisions: {
+ rev1: {...createRevision(1), description: 'test'},
+ rev2: {...createRevision(2), description: 'test'},
+ rev3: {...createRevision(3), description: 'test'},
+ rev4: {...createRevision(4), description: 'test'},
+ },
+ });
+ assert.equal(patchNums.length, expected.length);
+ for (let i = 0; i < expected.length; i++) {
+ assert.deepEqual(patchNums[i], expected[i]);
+ }
+ });
+});
diff --git a/proto/cache.proto b/proto/cache.proto
index b1722b4..9d2ba06 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -445,7 +445,7 @@
}
// Serialized form of com.google.gerrit.common.data.LabelType.
-// Next ID: 19
+// Next ID: 21
message LabelTypeProto {
string name = 1;
string function = 2; // ENUM as String
@@ -466,6 +466,7 @@
bool can_override = 17;
repeated string ref_patterns = 18;
bool copy_all_scores_if_list_of_files_did_not_change = 19;
+ string copy_condition = 20;
}
// Serialized form of com.google.gerrit.entities.SubmitRequirement.
@@ -474,7 +475,7 @@
string name = 1;
string description = 2;
string applicability_expression = 3;
- string blocking_expression = 4;
+ string submittability_expression = 4;
string override_expression = 5;
bool allow_override_in_child_projects = 6;
}
@@ -685,3 +686,18 @@
bytes new_commit = 10;
ComparisonType comparison_type = 11;
}
+
+// Serialized form of com.google.gerrit.server.approval.ApprovalCacheImpl.Key.
+// Next ID: 5
+message PatchSetApprovalsKeyProto {
+ string project = 1;
+ int32 change_id = 2;
+ int32 patch_set_id = 3;
+ bytes id = 4;
+}
+
+// Repeated version of PatchSetApprovalProto
+// Next ID: 2
+message AllPatchSetApprovalsProto {
+ repeated devtools.gerritcodereview.PatchSetApproval approval = 1;
+}
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 3a40d22..e1d6f22 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -16,6 +16,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+set -u
+
# avoid [[ which is not POSIX sh.
if test "$#" != 1 ; then
echo "$0 requires an argument."
@@ -28,12 +30,17 @@
fi
# Do not create a change id if requested
-if test "false" = "`git config --bool --get gerrit.createChangeId`" ; then
+if test "false" = "$(git config --bool --get gerrit.createChangeId)" ; then
exit 0
fi
-# $RANDOM will be undefined if not using bash, so don't use set -u
-random=$( (whoami ; hostname ; date; cat $1 ; echo $RANDOM) | git hash-object --stdin)
+if git rev-parse --verify HEAD >/dev/null 2>&1; then
+ refhash="$(git rev-parse HEAD)"
+else
+ refhash="$(git hash-object -t tree /dev/null)"
+fi
+
+random=$({ git var GIT_COMMITTER_IDENT ; echo "$refhash" ; cat "$1"; } | git hash-object --stdin)
dest="$1.tmp.${random}"
trap 'rm -f "${dest}"' EXIT
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 3d04592..434196f 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -6,7 +6,7 @@
GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
-TESTCONTAINERS_VERSION = "1.15.1"
+TESTCONTAINERS_VERSION = "1.15.3"
def declare_nongoogle_deps():
"""loads dependencies that are not used at Google.
@@ -187,21 +187,21 @@
sha1 = "dc13ae4faca6df981fc7aeb5a522d9db446d5d50",
)
- DOCKER_JAVA_VERS = "3.2.7"
+ DOCKER_JAVA_VERS = "3.2.8"
maven_jar(
name = "docker-java-api",
artifact = "com.github.docker-java:docker-java-api:" + DOCKER_JAVA_VERS,
- sha1 = "81408fc988c229ea11354fee9902c47842343f04",
+ sha1 = "4ac22a72d546a9f3523cd4b5fabffa77c4a6ec7c",
)
maven_jar(
name = "docker-java-transport",
artifact = "com.github.docker-java:docker-java-transport:" + DOCKER_JAVA_VERS,
- sha1 = "315903a129f530422747efc163dd255f0fa2555e",
+ sha1 = "c3b5598c67d0a5e2e780bf48f520da26b9915eab",
)
- # https://github.com/docker-java/docker-java/blob/3.2.7/pom.xml#L61
+ # https://github.com/docker-java/docker-java/blob/3.2.8/pom.xml#L61
# <=> DOCKER_JAVA_VERS
maven_jar(
name = "jackson-annotations",
@@ -212,7 +212,7 @@
maven_jar(
name = "testcontainers",
artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
- sha1 = "91e6dfab8f141f77c6a0dd147a94bd186993a22c",
+ sha1 = "95c6cfde71c2209f0c29cb14e432471e0b111880",
)
maven_jar(