Merge "Add a standard `<gr-weblink>` component"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 348649d..cf89982 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1417,8 +1417,8 @@
Allow to link:cmd-set-account.html[modify accounts over the ssh prompt].
This capability allows the granted group members to modify any user account
-setting. In addition this capability is required to view secondary emails
-of other accounts.
+setting. In addition this capability allows to view secondary emails of other
+accounts.
[[capability_priority]]
=== Priority
@@ -1520,7 +1520,8 @@
This capability allows to view all accounts but not all account data.
E.g. secondary emails of all accounts can only be viewed with the
-link:#capability_modifyAccount[Modify Account] capability.
+link:#capability_viewSecondaryEmails[View Secondary Emails] capability
+or the link:#capability_modifyAccount[Modify Account] capability.
[[capability_viewCaches]]
@@ -1553,6 +1554,15 @@
link:cmd-show-queue.html[look at the Gerrit task queue via ssh].
+[[capability_viewSecondaryEmails]]
+=== View Secondary Emails
+
+Allows viewing secondary emails of other accounts.
+
+Users with the link:#capability_modifyAccount[Modify Account] capability have
+this capbility implicitly.
+
+
[[reference]]
== Permission evaluation reference
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 31008f6..7b1baba 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -457,11 +457,6 @@
NOTE: Rebasing merge commits is not supported. If a change with a merge commit
is submitted the link:#merge_if_necessary[merge if necessary] submit action is
applied.
-+
-When rebasing the patchset, Gerrit automatically appends onto the end of the
-commit message a short summary of the change's approvals, and a URL link back
-to the change in the web UI (see link:#submit-footers[below]). If a fast-forward
-is done no footers are added.
[[rebase_always]]
* 'rebase always':
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index e6494e6..cf5da07 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -204,6 +204,8 @@
Whether the rebase was done on behalf of the uploader.
** `rebase_chain`:
Whether a chain was rebased.
+** `allow_conflicts`:
+ Whether the rebase was done with allowing conflicts.
* `change/submitted_with_rebaser_approval`: Number of rebased changes that were
submitted with a Code-Review approval of the rebaser that would not have been
submittable if the rebase was not done on behalf of the uploader.
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 7e06e4a..7646777 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -2316,6 +2316,9 @@
link:access-control.html#capability_viewPlugins[View Plugins] capability.
|`viewQueue` |not set if `false`|Whether the user has the
link:access-control.html#capability_viewQueue[View Queue] capability.
+|`viewSecondaryEmails`|not set if `false`|Whether the user has the
+link:access-control.html#capability_viewSecondaryEmails[View Secondary
+Emails] capability.
|=================================
[[contributor-agreement-info]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 4293fd6..67b8d75 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -647,7 +647,7 @@
last update (comment or patch set) from the change owner.
[[author]]
-author:'AUTHOR'::
+author:'AUTHOR', a:'AUTHOR'::
+
Changes where 'AUTHOR' is the author of the current patch set. 'AUTHOR' may be
the author's exact email address, or part of the name or email address. The
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index c7a9a63..23151c2 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -129,6 +129,9 @@
/** Can view all pending tasks in the queue (not just the filtered set). */
public static final String VIEW_QUEUE = "viewQueue";
+ /** Can view secondary emails of other accounts. */
+ public static final String VIEW_SECONDARY_EMAILS = "viewSecondaryEmails";
+
private static final List<String> NAMES_ALL;
private static final List<String> NAMES_LC;
private static final String[] RANGE_NAMES = {
@@ -160,6 +163,7 @@
NAMES_ALL.add(VIEW_CONNECTIONS);
NAMES_ALL.add(VIEW_PLUGINS);
NAMES_ALL.add(VIEW_QUEUE);
+ NAMES_ALL.add(VIEW_SECONDARY_EMAILS);
NAMES_LC = new ArrayList<>(NAMES_ALL.size());
for (String name : NAMES_ALL) {
diff --git a/java/com/google/gerrit/entities/BUILD b/java/com/google/gerrit/entities/BUILD
index c0f5de6..dbbcf71 100644
--- a/java/com/google/gerrit/entities/BUILD
+++ b/java/com/google/gerrit/entities/BUILD
@@ -10,6 +10,7 @@
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/server:project_util",
"//lib:gson",
"//lib:guava",
"//lib:jgit",
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index b587b1d..b43650b 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.server.ProjectUtil;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
@@ -61,7 +62,7 @@
/** Parse a Project.NameKey out of a string representation. */
public static NameKey parse(String str) {
- return nameKey(KeyUtil.decode(str));
+ return nameKey(ProjectUtil.sanitizeProjectName(KeyUtil.decode(str)));
}
private final String name;
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 2be3383..3338464 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -14,12 +14,22 @@
"account/externalids/testing/ExternalIdTestUtil.java",
]
+PROJECT_UTIL_SRC = [
+ "ProjectUtil.java",
+]
+
java_library(
name = "constants",
srcs = CONSTANTS_SRC,
visibility = ["//visibility:public"],
)
+java_library(
+ name = "project_util",
+ srcs = PROJECT_UTIL_SRC,
+ visibility = ["//visibility:public"],
+)
+
# Giant kitchen-sink target.
#
# The only reason this hasn't been split up further is because we have too many
@@ -30,13 +40,14 @@
name = "server",
srcs = glob(
["**/*.java"],
- exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC + TESTING_SRC,
+ exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC + TESTING_SRC + PROJECT_UTIL_SRC,
),
resource_strip_prefix = "resources",
resources = ["//resources/com/google/gerrit/server"],
visibility = ["//visibility:public"],
deps = [
":constants",
+ ":project_util",
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/entities",
diff --git a/java/com/google/gerrit/server/BranchUtil.java b/java/com/google/gerrit/server/BranchUtil.java
new file mode 100644
index 0000000..78f693d
--- /dev/null
+++ b/java/com/google/gerrit/server/BranchUtil.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+public class BranchUtil {
+ /**
+ * Checks whether the specified branch exists.
+ *
+ * @param repoManager Git repository manager to open the git repository
+ * @param branch the branch for which it should be checked if it exists
+ * @return {@code true} if the specified branch exists or if {@code HEAD} points to this branch,
+ * otherwise {@code false}
+ * @throws RepositoryNotFoundException the repository of the branch's project does not exist.
+ * @throws IOException error while retrieving the branch from the repository.
+ */
+ public static boolean branchExists(final GitRepositoryManager repoManager, BranchNameKey branch)
+ throws RepositoryNotFoundException, IOException {
+ try (Repository repo = repoManager.openRepository(branch.project())) {
+ boolean exists = repo.getRefDatabase().exactRef(branch.branch()) != null;
+ if (!exists) {
+ exists =
+ repo.getFullBranch().equals(branch.branch())
+ || RefNames.REFS_CONFIG.equals(branch.branch());
+ }
+ return exists;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/ProjectUtil.java b/java/com/google/gerrit/server/ProjectUtil.java
index fa056b3..e87c8bf 100644
--- a/java/com/google/gerrit/server/ProjectUtil.java
+++ b/java/com/google/gerrit/server/ProjectUtil.java
@@ -14,38 +14,7 @@
package com.google.gerrit.server;
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import java.io.IOException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Repository;
-
public class ProjectUtil {
-
- /**
- * Checks whether the specified branch exists.
- *
- * @param repoManager Git repository manager to open the git repository
- * @param branch the branch for which it should be checked if it exists
- * @return {@code true} if the specified branch exists or if {@code HEAD} points to this branch,
- * otherwise {@code false}
- * @throws RepositoryNotFoundException the repository of the branch's project does not exist.
- * @throws IOException error while retrieving the branch from the repository.
- */
- public static boolean branchExists(final GitRepositoryManager repoManager, BranchNameKey branch)
- throws RepositoryNotFoundException, IOException {
- try (Repository repo = repoManager.openRepository(branch.project())) {
- boolean exists = repo.getRefDatabase().exactRef(branch.branch()) != null;
- if (!exists) {
- exists =
- repo.getFullBranch().equals(branch.branch())
- || RefNames.REFS_CONFIG.equals(branch.branch());
- }
- return exists;
- }
- }
-
public static String sanitizeProjectName(String name) {
name = stripGitSuffix(name);
name = stripTrailingSlash(name);
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 1818b1b..2020d2f 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -434,16 +434,16 @@
@Override
public Stream<AccountState> search(String input, CurrentUser asUser) throws IOException {
- boolean canSeeSecondaryEmails = false;
+ boolean canViewSecondaryEmails = false;
try {
- if (permissionBackend.user(asUser).test(GlobalPermission.MODIFY_ACCOUNT)) {
- canSeeSecondaryEmails = true;
+ if (permissionBackend.user(asUser).test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
+ canViewSecondaryEmails = true;
}
} catch (PermissionBackendException e) {
// remains false
}
- if (canSeeSecondaryEmails) {
+ if (canViewSecondaryEmails) {
return toAccountStates(emails.getAccountFor(input));
}
@@ -549,15 +549,15 @@
// up with a reasonable result list.
// TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
// more strict here.
- boolean canSeeSecondaryEmails = false;
+ boolean canViewSecondaryEmails = false;
try {
- if (permissionBackend.user(asUser).test(GlobalPermission.MODIFY_ACCOUNT)) {
- canSeeSecondaryEmails = true;
+ if (permissionBackend.user(asUser).test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
+ canViewSecondaryEmails = true;
}
} catch (PermissionBackendException e) {
// remains false
}
- return accountQueryProvider.get().byDefault(input, canSeeSecondaryEmails).stream();
+ return accountQueryProvider.get().byDefault(input, canViewSecondaryEmails).stream();
}
@Override
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index b895834..64b8ec0 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -96,12 +96,12 @@
return;
}
- boolean canModifyAccount = false;
+ boolean canViewSecondaryEmails = false;
Account.Id currentUserId = null;
if (self.get().isIdentifiedUser()) {
currentUserId = self.get().getAccountId();
- if (permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
- canModifyAccount = true;
+ if (permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
+ canViewSecondaryEmails = true;
}
}
@@ -115,7 +115,7 @@
if (state != null) {
if (!options.contains(FillOptions.SECONDARY_EMAILS)
|| Objects.equals(currentUserId, state.account().id())
- || canModifyAccount) {
+ || canViewSecondaryEmails) {
fill(info, accountStates.get(id), options);
} else {
// user is not allowed to see secondary emails
diff --git a/java/com/google/gerrit/server/config/CapabilityConstants.java b/java/com/google/gerrit/server/config/CapabilityConstants.java
index 59819bb..1f799c6 100644
--- a/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -45,4 +45,5 @@
public String viewConnections;
public String viewPlugins;
public String viewQueue;
+ public String viewSecondaryEmails;
}
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index cc86ffc..e675003 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -66,7 +66,8 @@
* External IDs.
*
* <p>This field includes secondary emails. Use this field only if the current user is allowed to
- * see secondary emails (requires the {@link GlobalCapability#MODIFY_ACCOUNT} capability).
+ * see secondary emails (requires the {@link GlobalCapability#VIEW_SECONDARY_EMAILS} capability or
+ * the {@link GlobalCapability#MODIFY_ACCOUNT} capability).
*/
public static final IndexedField<AccountState, Iterable<String>> EXTERNAL_ID_FIELD =
IndexedField.<AccountState>iterableStringBuilder("ExternalId")
@@ -80,8 +81,9 @@
* Fuzzy prefix match on name and email parts.
*
* <p>This field includes parts from the secondary emails. Use this field only if the current user
- * is allowed to see secondary emails (requires the {@link GlobalCapability#MODIFY_ACCOUNT}
- * capability).
+ * is allowed to see secondary emails (requires requires the {@link
+ * GlobalCapability#VIEW_SECONDARY_EMAILS} capability or the {@link
+ * GlobalCapability#MODIFY_ACCOUNT} capability).
*
* <p>Use the {@link AccountField#NAME_PART_NO_SECONDARY_EMAIL_SPEC} if the current user can't see
* secondary emails.
diff --git a/java/com/google/gerrit/server/mail/send/BranchEmailUtils.java b/java/com/google/gerrit/server/mail/send/BranchEmailUtils.java
new file mode 100644
index 0000000..acba4ea
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/BranchEmailUtils.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.send;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.mail.MailHeader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Contains utils for email notification related to the events on project+branch. */
+class BranchEmailUtils {
+
+ /** Set a reasonable list id so that filters can be used to sort messages. */
+ static void setListIdHeader(OutgoingEmail email, BranchNameKey branch) {
+ email.setHeader(
+ "List-Id",
+ "<gerrit-" + branch.project().get().replace('/', '-') + "." + email.getGerritHost() + ">");
+ if (email.getSettingsUrl() != null) {
+ email.setHeader("List-Unsubscribe", "<" + email.getSettingsUrl() + ">");
+ }
+ }
+
+ /** Add branch information to soy template params. */
+ static void addBranchData(OutgoingEmail email, EmailArguments args, BranchNameKey branch) {
+ Map<String, Object> soyContext = email.getSoyContext();
+ Map<String, Object> soyContextEmailData = email.getSoyContextEmailData();
+
+ String projectName = branch.project().get();
+ soyContext.put("projectName", projectName);
+ // shortProjectName is the project name with the path abbreviated.
+ soyContext.put("shortProjectName", getShortProjectName(projectName));
+
+ // instanceAndProjectName is the instance's name followed by the abbreviated project path
+ soyContext.put(
+ "instanceAndProjectName",
+ getInstanceAndProjectName(args.instanceNameProvider.get(), projectName));
+ soyContext.put("addInstanceNameInSubject", args.addInstanceNameInSubject);
+
+ soyContextEmailData.put("sshHost", getSshHost(email.getGerritHost(), args.sshAddresses));
+
+ Map<String, String> branchData = new HashMap<>();
+ branchData.put("shortName", branch.shortName());
+ soyContext.put("branch", branchData);
+
+ email.addFooter(MailHeader.PROJECT.withDelimiter() + branch.project().get());
+ email.addFooter(MailHeader.BRANCH.withDelimiter() + branch.shortName());
+ }
+
+ @Nullable
+ private static String getSshHost(String gerritHost, List<String> sshAddresses) {
+ String host = Iterables.getFirst(sshAddresses, null);
+ if (host == null) {
+ return null;
+ }
+ if (host.startsWith("*:")) {
+ return gerritHost + host.substring(1);
+ }
+ return host;
+ }
+
+ /** Shortens project/repo name to only show part after the last '/'. */
+ static String getShortProjectName(String projectName) {
+ int lastIndexSlash = projectName.lastIndexOf('/');
+ if (lastIndexSlash == 0) {
+ return projectName.substring(1); // Remove the first slash
+ }
+ if (lastIndexSlash == -1) { // No slash in the project name
+ return projectName;
+ }
+
+ return "..." + projectName.substring(lastIndexSlash + 1);
+ }
+
+ /** Returns a project/repo name that includes instance as prefix. */
+ static String getInstanceAndProjectName(String instanceName, String projectName) {
+ if (instanceName == null || instanceName.isEmpty()) {
+ return getShortProjectName(projectName);
+ }
+ // Extract the project name (everything after the last slash) and prepends it with gerrit's
+ // instance name
+ return instanceName + "/" + projectName.substring(projectName.lastIndexOf('/') + 1);
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index ec81cf6..714ddda 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -26,6 +26,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeSizeBucket;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
@@ -43,6 +44,7 @@
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
+import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOptions;
@@ -78,7 +80,7 @@
import org.eclipse.jgit.util.TemporaryBuffer;
/** Sends an email to one or more interested parties. */
-public abstract class ChangeEmail extends NotificationEmail {
+public abstract class ChangeEmail extends OutgoingEmail {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -100,19 +102,23 @@
protected PatchSetInfo patchSetInfo;
protected String changeMessage;
protected Instant timestamp;
+ protected BranchNameKey branch;
protected ProjectState projectState;
- protected Set<Account.Id> authors;
- protected boolean emailOnlyAuthors;
+ private Set<Account.Id> authors;
+ private boolean emailOnlyAuthors;
protected boolean emailOnlyAttentionSetIfEnabled;
+ // Watchers ignore attention set rules.
+ protected Set<Account.Id> watchers = new HashSet<>();
protected ChangeEmail(EmailArguments args, String messageClass, ChangeData changeData) {
- super(args, messageClass, changeData.change().getDest());
+ super(args, messageClass);
this.changeData = changeData;
change = changeData.change();
emailOnlyAuthors = false;
emailOnlyAttentionSetIfEnabled = true;
currentAttentionSet = getAttentionSet();
+ branch = changeData.change().getDest();
}
@Override
@@ -193,7 +199,6 @@
}
}
}
- authors = getAuthors();
try {
stars = changeData.stars();
@@ -202,6 +207,7 @@
}
super.init();
+ BranchEmailUtils.setListIdHeader(this, branch);
if (timestamp != null) {
setHeader(FieldName.DATE, timestamp);
}
@@ -377,7 +383,7 @@
/** TO or CC all vested parties (change owner, patch set uploader, author). */
protected void addAuthors(RecipientType rt) {
- for (Account.Id id : authors) {
+ for (Account.Id id : getAuthors()) {
addByAccountId(rt, id);
}
}
@@ -395,8 +401,38 @@
}
}
- @Override
- protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
+ /** Include users and groups that want notification of events. */
+ protected void includeWatchers(NotifyType type) {
+ includeWatchers(type, true);
+ }
+
+ /** Include users and groups that want notification of events. */
+ protected void includeWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
+ try {
+ Watchers matching = getWatchers(type, includeWatchersFromNotifyConfig);
+ addWatchers(RecipientType.TO, matching.to);
+ addWatchers(RecipientType.CC, matching.cc);
+ addWatchers(RecipientType.BCC, matching.bcc);
+ } catch (StorageException err) {
+ // Just don't CC everyone. Better to send a partial message to those
+ // we already have queued up then to fail deliver entirely to people
+ // who have a lower interest in the change.
+ logger.atWarning().withCause(err).log("Cannot BCC watchers for %s", type);
+ }
+ }
+
+ /** Add users or email addresses to the TO, CC, or BCC list. */
+ private void addWatchers(RecipientType type, WatcherList watcherList) {
+ watchers.addAll(watcherList.accounts);
+ for (Account.Id user : watcherList.accounts) {
+ addByAccountId(type, user);
+ }
+ for (Address addr : watcherList.emails) {
+ addByEmail(type, addr);
+ }
+ }
+
+ private final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
if (!NotifyHandling.ALL.equals(notify.handling())) {
return new Watchers();
}
@@ -438,38 +474,14 @@
}
@Override
- protected void addByAccountId(RecipientType rt, Account.Id to) {
- addRecipient(rt, to, /* isWatcher= */ false);
- }
-
- /** This bypasses the EmailStrategy.ATTENTION_SET_ONLY strategy when adding the recipient. */
- @Override
- protected void addWatcher(RecipientType rt, Account.Id to) {
- addRecipient(rt, to, /* isWatcher= */ true);
- }
-
- private void addRecipient(RecipientType rt, Account.Id to, boolean isWatcher) {
- if (!isWatcher) {
- Optional<AccountState> accountState = args.accountCache.get(to);
- if (emailOnlyAttentionSetIfEnabled
- && accountState.isPresent()
- && accountState.get().generalPreferences().getEmailStrategy()
- == EmailStrategy.ATTENTION_SET_ONLY
- && !currentAttentionSet.contains(to)) {
- return;
- }
- }
- if (emailOnlyAuthors && !authors.contains(to)) {
- return;
- }
- super.addByAccountId(rt, to);
- }
-
- @Override
protected boolean isRecipientAllowed(Address addr) throws PermissionBackendException {
if (!projectState.statePermitsRead()) {
return false;
}
+ if (emailOnlyAuthors) {
+ return false;
+ }
+
return args.permissionBackend
.user(args.anonymousUser.get())
.change(changeData)
@@ -481,11 +493,28 @@
if (!projectState.statePermitsRead()) {
return false;
}
+ if (emailOnlyAuthors && !authors.contains(to)) {
+ return false;
+ }
+ if (!watchers.contains(to)) {
+ Optional<AccountState> accountState = args.accountCache.get(to);
+ if (emailOnlyAttentionSetIfEnabled
+ && accountState.isPresent()
+ && accountState.get().generalPreferences().getEmailStrategy()
+ == EmailStrategy.ATTENTION_SET_ONLY
+ && !currentAttentionSet.contains(to)) {
+ return false;
+ }
+ }
+
return args.permissionBackend.absentUser(to).change(changeData).test(ChangePermission.READ);
}
- /** Find all users who are authors of any part of this change. */
+ /** Lazily finds all users who are authors of any part of this change. */
protected Set<Account.Id> getAuthors() {
+ if (this.authors != null) {
+ return this.authors;
+ }
Set<Account.Id> authors = new HashSet<>();
switch (notify.handling()) {
@@ -511,12 +540,13 @@
break;
}
- return authors;
+ return this.authors = authors;
}
@Override
protected void setupSoyContext() {
super.setupSoyContext();
+ BranchEmailUtils.addBranchData(this, args, branch);
soyContext.put("changeId", change.getKey().get());
soyContext.put("coverLetter", getCoverLetter());
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index 44b6efa..ce2e3dc 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -56,6 +56,8 @@
super(args, "merged", newChangeData(args, project, changeId));
labelTypes = changeData.getLabelTypes();
this.stickyApprovalDiff = stickyApprovalDiff;
+ // We want to send the submit email even if the "send only when in attention set" is enabled.
+ emailOnlyAttentionSetIfEnabled = false;
}
@Override
@@ -76,9 +78,6 @@
@Override
protected void init() throws EmailException {
- // We want to send the submit email even if the "send only when in attention set" is enabled.
- emailOnlyAttentionSetIfEnabled = false;
-
super.init();
ccAllApprovals();
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
deleted file mode 100644
index c943724..0000000
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ /dev/null
@@ -1,155 +0,0 @@
-// Copyright (C) 2012 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.mail.send;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Iterables;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Address;
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.NotifyConfig.NotifyType;
-import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.mail.MailHeader;
-import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
-import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Common class for notifications that are related to a project and branch */
-public abstract class NotificationEmail extends OutgoingEmail {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- protected BranchNameKey branch;
-
- protected NotificationEmail(EmailArguments args, String messageClass, BranchNameKey branch) {
- super(args, messageClass);
- this.branch = branch;
- }
-
- @Override
- protected void init() throws EmailException {
- super.init();
- setListIdHeader();
- }
-
- private void setListIdHeader() {
- // Set a reasonable list id so that filters can be used to sort messages
- setHeader(
- "List-Id",
- "<gerrit-" + branch.project().get().replace('/', '-') + "." + getGerritHost() + ">");
- if (getSettingsUrl() != null) {
- setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
- }
- }
-
- /** Include users and groups that want notification of events. */
- protected void includeWatchers(NotifyType type) {
- includeWatchers(type, true);
- }
-
- /** Include users and groups that want notification of events. */
- protected void includeWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
- try {
- Watchers matching = getWatchers(type, includeWatchersFromNotifyConfig);
- addWatchers(RecipientType.TO, matching.to);
- addWatchers(RecipientType.CC, matching.cc);
- addWatchers(RecipientType.BCC, matching.bcc);
- } catch (StorageException err) {
- // Just don't CC everyone. Better to send a partial message to those
- // we already have queued up then to fail deliver entirely to people
- // who have a lower interest in the change.
- logger.atWarning().withCause(err).log("Cannot BCC watchers for %s", type);
- }
- }
-
- /** Returns all watchers that are relevant */
- protected abstract Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig);
-
- /** Add users or email addresses to the TO, CC, or BCC list. */
- protected void addWatchers(RecipientType type, WatcherList watcherList) {
- for (Account.Id user : watcherList.accounts) {
- addWatcher(type, user);
- }
- for (Address addr : watcherList.emails) {
- addByEmail(type, addr);
- }
- }
-
- protected abstract void addWatcher(RecipientType type, Account.Id to);
-
- @Nullable
- public String getSshHost() {
- String host = Iterables.getFirst(args.sshAddresses, null);
- if (host == null) {
- return null;
- }
- if (host.startsWith("*:")) {
- return getGerritHost() + host.substring(1);
- }
- return host;
- }
-
- @Override
- protected void setupSoyContext() {
- super.setupSoyContext();
-
- String projectName = branch.project().get();
- soyContext.put("projectName", projectName);
- // shortProjectName is the project name with the path abbreviated.
- soyContext.put("shortProjectName", getShortProjectName(projectName));
-
- // instanceAndProjectName is the instance's name followed by the abbreviated project path
- soyContext.put(
- "instanceAndProjectName",
- getInstanceAndProjectName(args.instanceNameProvider.get(), projectName));
- soyContext.put("addInstanceNameInSubject", args.addInstanceNameInSubject);
-
- soyContextEmailData.put("sshHost", getSshHost());
-
- Map<String, String> branchData = new HashMap<>();
- branchData.put("shortName", branch.shortName());
- soyContext.put("branch", branchData);
-
- footers.add(MailHeader.PROJECT.withDelimiter() + branch.project().get());
- footers.add(MailHeader.BRANCH.withDelimiter() + branch.shortName());
- }
-
- @VisibleForTesting
- protected static String getShortProjectName(String projectName) {
- int lastIndexSlash = projectName.lastIndexOf('/');
- if (lastIndexSlash == 0) {
- return projectName.substring(1); // Remove the first slash
- }
- if (lastIndexSlash == -1) { // No slash in the project name
- return projectName;
- }
-
- return "..." + projectName.substring(lastIndexSlash + 1);
- }
-
- @VisibleForTesting
- protected static String getInstanceAndProjectName(String instanceName, String projectName) {
- if (instanceName == null || instanceName.isEmpty()) {
- return getShortProjectName(projectName);
- }
- // Extract the project name (everything after the last slash) and prepends it with gerrit's
- // instance name
- return instanceName + "/" + projectName.substring(projectName.lastIndexOf('/') + 1);
- }
-}
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index d00c874..aba8f62 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -661,6 +661,26 @@
soyContext.put("email", soyContextEmailData);
}
+ /** Mutable map of parameters passed into email templates when rendering. */
+ public Map<String, Object> getSoyContext() {
+ return this.soyContext;
+ }
+
+ // TODO: It's not clear why we need this explicit separation. Probably worth
+ // simplifying.
+ /** Mutable content of `email` parameter in the templates. */
+ public Map<String, Object> getSoyContextEmailData() {
+ return this.soyContextEmailData;
+ }
+
+ /**
+ * Add a line to email footer with additional information. Typically, in the form of {@literal
+ * <key>: <value>}.
+ */
+ public void addFooter(String footer) {
+ footers.add(footer);
+ }
+
private String getInstanceName() {
return args.instanceNameProvider.get();
}
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index bf4d05a..a4ee052 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -195,12 +195,17 @@
case MODIFY_ACCOUNT:
case READ_AS:
case STREAM_EVENTS:
+ case VIEW_ACCESS:
case VIEW_ALL_ACCOUNTS:
case VIEW_CONNECTIONS:
case VIEW_PLUGINS:
- case VIEW_ACCESS:
return has(globalPermissionName(perm)) || isAdmin();
+ case VIEW_SECONDARY_EMAILS:
+ return has(globalPermissionName(perm))
+ || has(globalPermissionName(GlobalPermission.MODIFY_ACCOUNT))
+ || isAdmin();
+
case ACCESS_DATABASE:
case RUN_AS:
return has(globalPermissionName(perm));
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index da24dcd..958de1b 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -61,6 +61,7 @@
.put(GlobalPermission.VIEW_CONNECTIONS, GlobalCapability.VIEW_CONNECTIONS)
.put(GlobalPermission.VIEW_PLUGINS, GlobalCapability.VIEW_PLUGINS)
.put(GlobalPermission.VIEW_QUEUE, GlobalCapability.VIEW_QUEUE)
+ .put(GlobalPermission.VIEW_SECONDARY_EMAILS, GlobalCapability.VIEW_SECONDARY_EMAILS)
.build();
static {
diff --git a/java/com/google/gerrit/server/permissions/GlobalPermission.java b/java/com/google/gerrit/server/permissions/GlobalPermission.java
index c0b44e5..3429978 100644
--- a/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -58,7 +58,8 @@
VIEW_CACHES,
VIEW_CONNECTIONS,
VIEW_PLUGINS,
- VIEW_QUEUE;
+ VIEW_QUEUE,
+ VIEW_SECONDARY_EMAILS;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index d5c4a97..2f4a923 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -151,7 +151,7 @@
@Operator
public Predicate<AccountState> email(String email)
throws PermissionBackendException, QueryParseException {
- if (canSeeSecondaryEmails()) {
+ if (canViewSecondaryEmails()) {
return AccountPredicates.emailIncludingSecondaryEmails(email);
}
@@ -185,7 +185,7 @@
@Operator
public Predicate<AccountState> name(String name)
throws PermissionBackendException, QueryParseException {
- if (canSeeSecondaryEmails()) {
+ if (canViewSecondaryEmails()) {
return AccountPredicates.equalsNameIncludingSecondaryEmails(name);
}
@@ -210,7 +210,7 @@
@Override
protected Predicate<AccountState> defaultField(String query) {
Predicate<AccountState> defaultPredicate =
- AccountPredicates.defaultPredicate(args.schema(), checkedCanSeeSecondaryEmails(), query);
+ AccountPredicates.defaultPredicate(args.schema(), checkedCanViewSecondaryEmails(), query);
if (query.startsWith("cansee:")) {
try {
return cansee(query.substring(7));
@@ -233,13 +233,13 @@
return args.getIdentifiedUser().getAccountId();
}
- private boolean canSeeSecondaryEmails() throws PermissionBackendException, QueryParseException {
- return args.permissionBackend.user(args.getUser()).test(GlobalPermission.MODIFY_ACCOUNT);
+ private boolean canViewSecondaryEmails() throws PermissionBackendException, QueryParseException {
+ return args.permissionBackend.user(args.getUser()).test(GlobalPermission.VIEW_SECONDARY_EMAILS);
}
- private boolean checkedCanSeeSecondaryEmails() {
+ private boolean checkedCanViewSecondaryEmails() {
try {
- return canSeeSecondaryEmails();
+ return canViewSecondaryEmails();
} catch (PermissionBackendException e) {
logger.atSevere().withCause(e).log("Permission check failed");
return false;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index ef067a1..57b59ef 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -1539,6 +1539,11 @@
}
@Operator
+ public Predicate<ChangeData> a(String who) throws QueryParseException {
+ return author(who);
+ }
+
+ @Operator
public Predicate<ChangeData> author(String who) throws QueryParseException {
return getAuthorOrCommitterPredicate(
who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
index 82d8717..9ee4852 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.query.change.EqualsLabelPredicates.type;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
@@ -30,6 +31,8 @@
import java.util.Optional;
public class MagicLabelPredicates {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static class PostFilterMagicLabelPredicate extends PostFilterPredicate<ChangeData> {
private static class PostFilterMatcher extends Matcher {
public PostFilterMatcher(
@@ -177,6 +180,7 @@
}
public boolean ignoresUploaderApprovals() {
+ logger.atFine().log("account = %d", account.get());
return account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
|| account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID);
}
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index dd0ec78d..4ce8c42 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -21,6 +21,7 @@
"//java/com/google/gerrit/json",
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server:project_util",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
"//java/com/google/gerrit/server/util/time",
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 4d70eb9..a518532 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -52,7 +52,7 @@
public Response<List<EmailInfo>> apply(AccountResource rsrc)
throws AuthException, PermissionBackendException {
if (!self.get().hasSameAccountId(rsrc.getUser())) {
- permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
+ permissionBackend.currentUser().check(GlobalPermission.VIEW_SECONDARY_EMAILS);
}
return Response.ok(
rsrc.getUser().getEmailAddresses().stream()
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 79737f3..9fc0c42 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -173,7 +173,7 @@
}
boolean modifyAccountCapabilityChecked = false;
if (options.contains(ListAccountsOption.ALL_EMAILS)) {
- permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
+ permissionBackend.currentUser().check(GlobalPermission.VIEW_SECONDARY_EMAILS);
modifyAccountCapabilityChecked = true;
fillOptions.add(FillOptions.EMAIL);
fillOptions.add(FillOptions.SECONDARY_EMAILS);
@@ -185,7 +185,7 @@
if (modifyAccountCapabilityChecked) {
fillOptions.add(FillOptions.SECONDARY_EMAILS);
} else {
- if (permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
+ if (permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
index 74033c1..78c4fbe 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -31,8 +31,7 @@
import java.util.Optional;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jgit.api.errors.PatchApplyException;
-import org.eclipse.jgit.api.errors.PatchFormatException;
+import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -67,10 +66,9 @@
PatchApplier applier = new PatchApplier(repo, tip, oi);
PatchApplier.Result applyResult = applier.applyPatch(patchStream);
return applyResult.getTreeId();
- } catch (PatchFormatException e) {
- throw new BadRequestException("Invalid patch format: " + input.patch, e);
- } catch (PatchApplyException e) {
- throw RestApiException.wrap("Cannot apply patch: " + input.patch, e);
+ } catch (GitAPIException e) {
+ // TODO(nitzan) - write better error handling here.
+ throw new BadRequestException("Cannot apply patch: " + input.patch, e);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 3cb1870..e47515a 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -127,7 +127,7 @@
bu.addOp(change.getId(), rebaseOp);
bu.execute();
- rebaseMetrics.countRebase(input.onBehalfOfUploader);
+ rebaseMetrics.countRebase(input.onBehalfOfUploader, input.allowConflicts);
ChangeInfo changeInfo = json.create(OPTIONS).format(change.getProject(), change.getId());
changeInfo.containsGitConflicts =
@@ -142,9 +142,7 @@
UiAction.Description description =
new UiAction.Description()
.setLabel("Rebase")
- .setTitle(
- "Rebase onto tip of branch or parent change. Makes you the uploader of this "
- + "change which can affect validity of approvals.")
+ .setTitle("Rebase onto tip of branch or parent change.")
.setVisible(false);
Change change = rsrc.getChange();
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index 32b5b11..bbac318 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -208,7 +208,7 @@
}
}
- rebaseMetrics.countRebaseChain(input.onBehalfOfUploader);
+ rebaseMetrics.countRebaseChain(input.onBehalfOfUploader, input.allowConflicts);
RebaseChainInfo res = new RebaseChainInfo();
res.rebasedChanges = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java b/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java
index 114a112..d6577ea 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.restapi.change;
-import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
@@ -24,7 +24,7 @@
/** Metrics for the rebase REST endpoints ({@link Rebase} and {@link RebaseChain}). */
@Singleton
public class RebaseMetrics {
- private final Counter2<Boolean, Boolean> countRebases;
+ private final Counter3<Boolean, Boolean, Boolean> countRebases;
@Inject
public RebaseMetrics(MetricMaker metricMaker) {
@@ -37,18 +37,25 @@
.build(),
Field.ofBoolean("rebase_chain", (metadataBuilder, isRebaseChain) -> {})
.description("Whether a chain was rebased.")
+ .build(),
+ Field.ofBoolean("allow_conflicts", (metadataBuilder, allow_conflicts) -> {})
+ .description("Whether the rebase was done with allowing conflicts.")
.build());
}
- public void countRebase(boolean isOnBehalfOfUploader) {
- countRebase(isOnBehalfOfUploader, /* isRebaseChain= */ false);
+ public void countRebase(boolean isOnBehalfOfUploader, boolean allowConflicts) {
+ countRebase(isOnBehalfOfUploader, /* isRebaseChain= */ false, allowConflicts);
}
- public void countRebaseChain(boolean isOnBehalfOfUploader) {
- countRebase(isOnBehalfOfUploader, /* isRebaseChain= */ true);
+ public void countRebaseChain(boolean isOnBehalfOfUploader, boolean allowConflicts) {
+ countRebase(isOnBehalfOfUploader, /* isRebaseChain= */ true, allowConflicts);
}
- private void countRebase(boolean isOnBehalfOfUploader, boolean isRebaseChain) {
- countRebases.increment(/* field1= */ isOnBehalfOfUploader, /* field2= */ isRebaseChain);
+ private void countRebase(
+ boolean isOnBehalfOfUploader, boolean isRebaseChain, boolean allowConflicts) {
+ countRebases.increment(
+ /* field1= */ isOnBehalfOfUploader,
+ /* field2= */ isRebaseChain,
+ /* field3= */ allowConflicts);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 5fc4f41..b1f1da5 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -43,11 +43,11 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.BranchUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
@@ -198,7 +198,7 @@
Change change = rsrc.getChange();
if (!change.isNew()) {
throw new ResourceConflictException("change is " + ChangeUtil.status(change));
- } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
+ } else if (!BranchUtil.branchExists(repoManager, change.getDest())) {
throw new ResourceConflictException(
String.format("destination branch \"%s\" not found.", change.getDest().branch()));
} else if (!rsrc.getPatchSet().id().equals(change.currentPatchSetId())) {
diff --git a/java/com/google/gerrit/server/submit/MergeMetrics.java b/java/com/google/gerrit/server/submit/MergeMetrics.java
index e9832fd..4c11925 100644
--- a/java/com/google/gerrit/server/submit/MergeMetrics.java
+++ b/java/com/google/gerrit/server/submit/MergeMetrics.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.submit;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
@@ -28,6 +29,8 @@
/** Metrics are recorded when a change is merged (aka submitted). */
public class MergeMetrics {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final Provider<SubmitRequirementChangeQueryBuilder> submitRequirementChangequeryBuilder;
// TODO: This metric is for measuring the impact of allowing users to rebase changes on behalf of
@@ -81,12 +84,19 @@
// impersonated. Impersonating the uploader is only allowed on rebase by rebasing on behalf of
// the uploader. Hence if the current patch set has different accounts as uploader and real
// uploader we can assume that it was created by rebase on behalf of the uploader.
- return !cd.currentPatchSet().uploader().equals(cd.currentPatchSet().realUploader());
+ boolean isRebaseOnBehalfOfUploader =
+ !cd.currentPatchSet().uploader().equals(cd.currentPatchSet().realUploader());
+ logger.atFine().log("isRebaseOnBehalfOfUploader = %s", isRebaseOnBehalfOfUploader);
+ return isRebaseOnBehalfOfUploader;
}
private boolean hasCodeReviewApprovalOfRealUploader(ChangeData cd) {
- return cd.currentApprovals().stream()
- .anyMatch(psa -> psa.accountId().equals(cd.currentPatchSet().realUploader()));
+ boolean hasCodeReviewApprovalOfRealUploader =
+ cd.currentApprovals().stream()
+ .anyMatch(psa -> psa.accountId().equals(cd.currentPatchSet().realUploader()));
+ logger.atFine().log(
+ "hasCodeReviewApprovalOfRealUploader = %s", hasCodeReviewApprovalOfRealUploader);
+ return hasCodeReviewApprovalOfRealUploader;
}
private boolean ignoresCodeReviewApprovalsOfUploader(ChangeData cd) {
@@ -96,15 +106,27 @@
submitRequirementChangequeryBuilder
.get()
.parse(submitRequirement.submittabilityExpression().expressionString());
- return ignoresCodeReviewApprovalsOfUploader(predicate);
+ boolean ignoresCodeReviewApprovalsOfUploader =
+ ignoresCodeReviewApprovalsOfUploader(predicate);
+ logger.atFine().log(
+ "ignoresCodeReviewApprovalsOfUploader = %s", ignoresCodeReviewApprovalsOfUploader);
+ if (ignoresCodeReviewApprovalsOfUploader) {
+ return true;
+ }
} catch (QueryParseException e) {
- return false;
+ logger.atFine().log(
+ "Failed to parse submit requirement expression %s: %s",
+ submitRequirement.submittabilityExpression().expressionString(), e.getMessage());
+ // ignore and inspect the next submit requirement
}
}
return false;
}
private boolean ignoresCodeReviewApprovalsOfUploader(Predicate<ChangeData> predicate) {
+ logger.atFine().log(
+ "predicate = (%s) %s (child count = %d)",
+ predicate.getClass().getName(), predicate, predicate.getChildCount());
if (predicate.getChildCount() == 0) {
// Submit requirements may require a Code-Review approval but ignore approvals by the
// uploader. This is done by using a label predicate with 'user=non_uploader' or
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index a9a0b70..dd04200 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -991,14 +991,15 @@
}
@Test
- public void cannotGetEmailsOfOtherAccountWithoutModifyAccount() throws Exception {
+ public void cannotGetEmailsOfOtherAccountWithoutViewSecondaryEmailsAndWithoutModifyAccount()
+ throws Exception {
String email = "preferred2@example.com";
TestAccount foo = accountCreator.create(name("foo"), email, "Foo", null);
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.accounts().id(foo.id().get()).getEmails());
- assertThat(thrown).hasMessageThat().contains("modify account not permitted");
+ assertThat(thrown).hasMessageThat().contains("view secondary emails not permitted");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index 7b7e145a..fc95425 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -49,7 +49,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
-import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.Base64;
import org.junit.Test;
@@ -243,18 +242,6 @@
}
@Test
- public void applyPatchWithConflict_fails() throws Exception {
- initBaseWithFile(MODIFIED_FILE_NAME, "Unexpected base content");
- ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
-
- Throwable error = assertThrows(RestApiException.class, () -> applyPatch(in));
-
- assertThat(error).hasMessageThat().contains("Cannot apply patch");
- assertThat(error).hasCauseThat().isInstanceOf(PatchApplyException.class);
- assertThat(error).hasCauseThat().hasMessageThat().contains("Cannot apply: HunkHeader");
- }
-
- @Test
public void applyPatchWithoutAddPatchSetPermissions_fails() throws Exception {
initDestBranch();
ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
index de73c00..1790133 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -54,6 +54,28 @@
}
@Test
+ public void projectChangeNumberReturnsChangeWhenProjectEndsWithSlash() throws Exception {
+ Project.NameKey p = projectOperations.newProject().create();
+ ChangeInfo ci = gApi.changes().create(new ChangeInput(p.get(), "master", "msg")).get();
+
+ ChangeInfo changeInfo = gApi.changes().id(p.get() + "/", ci._number).get();
+
+ assertThat(changeInfo.changeId).isEqualTo(ci.changeId);
+ assertThat(changeInfo.project).isEqualTo(p.get());
+ }
+
+ @Test
+ public void projectChangeNumberReturnsChangeWhenProjectEndsWithDotGit() throws Exception {
+ Project.NameKey p = projectOperations.newProject().create();
+ ChangeInfo ci = gApi.changes().create(new ChangeInput(p.get(), "master", "msg")).get();
+
+ ChangeInfo changeInfo = gApi.changes().id(p.get() + ".git", ci._number).get();
+
+ assertThat(changeInfo.changeId).isEqualTo(ci.changeId);
+ assertThat(changeInfo.project).isEqualTo(p.get());
+ }
+
+ @Test
public void wrongProjectInProjectChangeNumberReturnsNotFound() throws Exception {
ResourceNotFoundException thrown =
assertThrows(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
index 43c8684..3f3ad37 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
@@ -549,6 +549,19 @@
@Test
public void changesFoundWhenQueryingBySecondaryEmailWithModifyAccountCapability()
throws Exception {
+ testCangesFoundWhenQueryingBySecondaryEmailWithModifyAccountCapability(
+ GlobalCapability.MODIFY_ACCOUNT);
+ }
+
+ @Test
+ public void changesFoundWhenQueryingBySecondaryEmailWithViewSecondaryEmailsCapability()
+ throws Exception {
+ testCangesFoundWhenQueryingBySecondaryEmailWithModifyAccountCapability(
+ GlobalCapability.VIEW_SECONDARY_EMAILS);
+ }
+
+ private void testCangesFoundWhenQueryingBySecondaryEmailWithModifyAccountCapability(
+ String globalCapability) throws Exception {
String secondaryOwnerEmail = "owner-secondary@example.com";
Account.Id owner =
accountOperations
@@ -574,7 +587,7 @@
projectOperations
.allProjectsForUpdate()
- .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
+ .add(allowCapability(globalCapability).group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
index 13f9904..e4a6247 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
@@ -1234,11 +1234,11 @@
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
- // field1 is on_behalf_of_uploader, field2 is rebase_chain
- assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(1);
- assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(0);
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(0);
}
private void assertRebase(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index dee8d1f..4e95032 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -665,10 +665,15 @@
extensionRegistry.newRegistration().add(wipStateChangedListener)) {
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.allowConflicts = true;
+ testMetricMaker.reset();
ChangeInfo changeInfo =
gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput);
assertThat(changeInfo.containsGitConflicts).isTrue();
assertThat(changeInfo.workInProgress).isTrue();
+
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false, true))
+ .isEqualTo(1);
}
assertThat(wipStateChangedListener.invoked).isTrue();
assertThat(wipStateChangedListener.wip).isTrue();
@@ -790,11 +795,12 @@
// Rebase the second change
testMetricMaker.reset();
rebaseCallWithInput.call(r2.getChangeId(), new RebaseInput());
- // field1 is on_behalf_of_uploader, field2 is rebase_chain
- assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(1);
- assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(0);
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false))
+ .isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(0);
}
@Test
@@ -1249,11 +1255,12 @@
testMetricMaker.reset();
verifyRebaseChainResponse(
gApi.changes().id(r4.getChangeId()).rebaseChain(), false, r2, r3, r4);
- // field1 is on_behalf_of_uploader, field2 is rebase_chain
- assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(1);
- assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(0);
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false))
+ .isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(0);
}
private void verifyRebaseChainResponse(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index 8565ced..82d22b5 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -21,6 +21,8 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
+import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.Iterables;
@@ -37,6 +39,8 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
@@ -1025,11 +1029,25 @@
@Test
public void testSubmittedWithRebaserApprovalMetric() throws Exception {
+ allowVotingOnCodeReviewToAllUsers();
+
+ createVerifiedLabel();
+ allowVotingOnVerifiedToAllUsers();
+
// Require a Code-Review approval from a non-uploader for submit.
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.upsertSubmitRequirement(
SubmitRequirement.builder()
+ .setName(TestLabels.verified().getName())
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(
+ String.format("label:%s=MAX", TestLabels.verified().getName())))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ u.getConfig()
+ .upsertSubmitRequirement(
+ SubmitRequirement.builder()
.setName(TestLabels.codeReview().getName())
.setSubmittabilityExpression(
SubmitRequirementExpression.create(
@@ -1056,7 +1074,10 @@
// Approve and submit the change that will be the new base for the change that will be rebased.
requestScopeOperations.setApiUser(approver);
- gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ gApi.changes()
+ .id(changeToBeTheNewBase.get())
+ .current()
+ .review(ReviewInput.approve().label(TestLabels.verified().getName(), 1));
testMetricMaker.reset();
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(0);
@@ -1068,8 +1089,10 @@
gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
// Approve the change as the rebaser.
- allowVotingOnCodeReviewToAllUsers();
- gApi.changes().id(changeToBeRebased.get()).current().review(ReviewInput.approve());
+ gApi.changes()
+ .id(changeToBeRebased.get())
+ .current()
+ .review(ReviewInput.approve().label(TestLabels.verified().getName(), 1));
// The change is submittable because the approval is from a user (the rebaser) that is not the
// uploader.
@@ -1105,11 +1128,11 @@
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
- // field1 is on_behalf_of_uploader, field2 is rebase_chain
- assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(1);
- assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(0);
- assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(0);
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(0);
}
private void allowPermissionToAllUsers(String permission) {
@@ -1136,6 +1159,29 @@
.update();
}
+ private void createVerifiedLabel() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder verified =
+ labelBuilder(
+ LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"))
+ .setCopyCondition("is:MIN");
+ u.getConfig().upsertLabelType(verified.build());
+ u.save();
+ }
+ }
+
+ private void allowVotingOnVerifiedToAllUsers() {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(TestLabels.verified().getName())
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+ }
+
private void blockPermissionForAllUsers(String permission) {
blockPermission(permission, REGISTERED_USERS);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java b/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
index 7598062..3ce6d8d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
@@ -38,6 +38,7 @@
public boolean viewConnections;
public boolean viewPlugins;
public boolean viewQueue;
+ public boolean viewSecondaryEmails;
static class QueryLimit {
short min;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 0360622..a1ec41c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -95,7 +95,6 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.api.errors.PatchFormatException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -1003,15 +1002,6 @@
+ "@@ -0,0 +1,2 @@\n"
+ "+First added line\n"
+ "+Second added line\n";
- private static final String MODIFICATION_PATCH_INPUT =
- "diff --git a/a_file.txt b/a_file.txt\n"
- + "new file mode 100644\n"
- + "--- a/a_file.txt\n"
- + "+++ b/a_file.txt.txt\n"
- + "@@ -1,2 +1 @@\n"
- + "-First original line\n"
- + "-Second original line\n"
- + "+Modified line\n";
@Test
public void createPatchApplyingChange_success() throws Exception {
@@ -1130,24 +1120,6 @@
}
@Test
- public void createPatchApplyingChange_withInfeasiblePatch_fails() throws Exception {
- createBranch(BranchNameKey.create(project, "other"));
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(),
- testRepo,
- "Adding unexpected base content, which will cause the patch to fail",
- PATCH_FILE_NAME,
- "unexpected base content");
- Result conflictingChange = push.to("refs/heads/other");
- conflictingChange.assertOkStatus();
- ChangeInput input = newPatchApplyingChangeInput("other", MODIFICATION_PATCH_INPUT);
-
- assertCreateFailsWithCause(
- input, RestApiException.class, PatchApplyException.class, "Cannot apply: HunkHeader");
- }
-
- @Test
@UseSystemTime
public void sha1sOfTwoNewChangesDiffer() throws Exception {
ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
diff --git a/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java b/javatests/com/google/gerrit/server/mail/send/BranchEmailUtilsTest.java
similarity index 88%
rename from javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
rename to javatests/com/google/gerrit/server/mail/send/BranchEmailUtilsTest.java
index b87c4a1..3c60b79 100644
--- a/javatests/com/google/gerrit/server/mail/send/NotificationEmailTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/BranchEmailUtilsTest.java
@@ -15,12 +15,12 @@
package com.google.gerrit.server.mail.send;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.mail.send.NotificationEmail.getInstanceAndProjectName;
-import static com.google.gerrit.server.mail.send.NotificationEmail.getShortProjectName;
+import static com.google.gerrit.server.mail.send.BranchEmailUtils.getInstanceAndProjectName;
+import static com.google.gerrit.server.mail.send.BranchEmailUtils.getShortProjectName;
import org.junit.Test;
-public class NotificationEmailTest {
+public class BranchEmailUtilsTest {
@Test
public void instanceAndProjectName() throws Exception {
assertThat(getInstanceAndProjectName("test", "/my/api")).isEqualTo("test/api");
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 0f6245f..a188251 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -676,11 +676,21 @@
}
@Test
+ public void byAuthorExact_byAlias() throws Exception {
+ byAuthorOrCommitterExact("a:");
+ }
+
+ @Test
public void byAuthorFullText() throws Exception {
byAuthorOrCommitterFullText("author:");
}
@Test
+ public void byAuthorFullText_byAlias() throws Exception {
+ byAuthorOrCommitterFullText("a:");
+ }
+
+ @Test
public void byCommitterExact() throws Exception {
byAuthorOrCommitterExact("committer:");
}
diff --git a/modules/jgit b/modules/jgit
index 0687c73..5166ded 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 0687c73a12b5157c350de430f62ea8243d813e19
+Subproject commit 5166ded0986df7a99bbc9ae6bc057a27a1e7d974
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 597573b..1a7835b 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 597573b5d1ed44c75844d2b8047aae08f7ef8619
+Subproject commit 1a7835bdfa55d60c717bad20b943e34499da0016
diff --git a/plugins/package.json b/plugins/package.json
index 2299a93..612062b 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -32,7 +32,7 @@
"@codemirror/theme-one-dark": "^6.1.1",
"@codemirror/view": "^6.9.1",
"lit": "^2.2.3",
- "rxjs": "^7.8.0",
+ "rxjs": "^6.6.7",
"sinon": "^13.0.0"
},
"license": "Apache-2.0",
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index bad5fcc..3156f4c 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -2480,12 +2480,12 @@
dependencies:
queue-microtask "^1.2.2"
-rxjs@^7.8.0:
- version "7.8.0"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
- integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
+rxjs@^6.6.7:
+ version "6.6.7"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
dependencies:
- tslib "^2.1.0"
+ tslib "^1.9.0"
safe-buffer@5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
@@ -2681,10 +2681,10 @@
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
-tslib@^2.1.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
- integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
+tslib@^1.9.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tsscmp@1.0.6:
version "1.0.6"
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index d8f4942..4bf253d 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -243,12 +243,6 @@
image_diff_prefs?: ImageDiffPreferences;
responsive_mode?: DiffResponsiveMode;
num_lines_rendered_at_once?: number;
- /**
- * If enabled, then a new (experimental) diff rendering is used that is
- * based on Lit components and multiple rendering passes. This is planned to
- * be a temporary setting until the experiment is concluded.
- */
- use_lit_components?: boolean;
show_sign_col?: boolean;
/**
* The default view mode is SIDE_BY_SIDE.
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts
index 146ab61..5588f40 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts
@@ -66,7 +66,7 @@
border-color: var(--info-foreground);
background: var(--info-background);
}
- .summaryChip.info:hover {
+ button.summaryChip.info:hover {
background: var(--info-background-hover);
box-shadow: var(--elevation-level-1);
}
@@ -80,7 +80,7 @@
border-color: var(--warning-foreground);
background: var(--warning-background);
}
- .summaryChip.warning:hover {
+ button.summaryChip.warning:hover {
background: var(--warning-background-hover);
box-shadow: var(--elevation-level-1);
}
@@ -94,7 +94,7 @@
border-color: var(--gray-foreground);
background: var(--gray-background);
}
- .summaryChip.check:hover {
+ button.summaryChip.check:hover {
background: var(--gray-background-hover);
box-shadow: var(--elevation-level-1);
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 3982666..166d895 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -3,6 +3,7 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../../shared/gr-account-chip/gr-account-chip';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {when} from 'lit/directives/when.js';
@@ -10,6 +11,8 @@
NumericChangeId,
BranchName,
ChangeActionDialog,
+ AccountDetailInfo,
+ AccountInfo,
} from '../../../types/common';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-autocomplete/gr-autocomplete';
@@ -23,10 +26,10 @@
import {ValueChangedEvent} from '../../../types/events';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {fireNoBubbleNoCompose} from '../../../utils/event-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {resolve} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
import {subscribe} from '../../lit/subscription-controller';
+import {userModelToken} from '../../../models/user/user-model';
export interface RebaseChange {
name: string;
@@ -87,9 +90,6 @@
@state()
allowConflicts = false;
- @state()
- isOwner = false;
-
@query('#rebaseOnParentInput')
private rebaseOnParentInput!: HTMLInputElement;
@@ -108,19 +108,30 @@
@query('#parentInput')
parentInput!: GrAutocomplete;
+ @state()
+ account?: AccountDetailInfo;
+
+ @state()
+ uploader?: AccountInfo;
+
private readonly restApiService = getAppContext().restApiService;
- private readonly flagsService = getAppContext().flagsService;
-
private readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getUserModel = resolve(this, userModelToken);
+
constructor() {
super();
this.query = input => this.getChangeSuggestions(input);
subscribe(
this,
- () => this.getChangeModel().isOwner$,
- x => (this.isOwner = x)
+ () => this.getUserModel().account$,
+ x => (this.account = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().latestUploader$,
+ x => (this.uploader = x)
);
}
@@ -155,12 +166,15 @@
display: block;
width: 100%;
}
- .rebaseCheckbox:first-of-type {
+ .rebaseCheckbox {
margin-top: var(--spacing-m);
}
.rebaseOption {
margin: var(--spacing-m) 0;
}
+ .rebaseOnBehalfMsg {
+ margin-top: var(--spacing-m);
+ }
`,
];
@@ -258,11 +272,7 @@
>
</div>
${when(
- this.flagsService.isEnabled(
- KnownExperimentId.REBASE_ON_BEHALF_OF_UPLOADER
- ) &&
- !this.isOwner &&
- this.allowConflicts,
+ !this.isCurrentUserEqualToLatestUploader() && this.allowConflicts,
() =>
html`<span class="message"
>Rebase cannot be done on behalf of the uploader when allowing
@@ -283,6 +293,16 @@
<label for="rebaseChain">Rebase all ancestors</label>
</div>`
)}
+ ${when(
+ !this.isCurrentUserEqualToLatestUploader(),
+ () => html`<div class="rebaseOnBehalfMsg">Rebase will be done on behalf of${
+ !this.allowConflicts ? ' the uploader:' : ''
+ } <gr-account-chip
+ .account=${this.allowConflicts ? this.account : this.uploader}
+ .hideHovercard=${true}
+ ></gr-account-chip
+ ><span></div>`
+ )}
</div>
</gr-dialog>
`;
@@ -317,6 +337,11 @@
});
}
+ isCurrentUserEqualToLatestUploader() {
+ if (!this.account || !this.uploader) return true;
+ return this.account._account_id === this.uploader._account_id;
+ }
+
getRecentChanges() {
if (this.recentChanges) {
return Promise.resolve(this.recentChanges);
@@ -396,13 +421,6 @@
}
private rebaseOnBehalfOfUploader() {
- if (
- !this.flagsService.isEnabled(
- KnownExperimentId.REBASE_ON_BEHALF_OF_UPLOADER
- )
- ) {
- return false;
- }
if (this.allowConflicts) return false;
return true;
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index 3064014..8d907c9 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -9,21 +9,34 @@
import {
pressKey,
queryAndAssert,
- stubFlags,
stubRestApi,
waitUntil,
} from '../../../test/test-utils';
-import {NumericChangeId, BranchName} from '../../../types/common';
-import {createChangeViewChange} from '../../../test/test-data-generators';
+import {NumericChangeId, BranchName, Timestamp} from '../../../types/common';
+import {
+ createAccountWithEmail,
+ createChangeViewChange,
+} from '../../../test/test-data-generators';
import {fixture, html, assert} from '@open-wc/testing';
import {Key} from '../../../utils/dom-util';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken} from '../../../models/user/user-model';
+import {
+ changeModelToken,
+ LoadingStatus,
+} from '../../../models/change/change-model';
+import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
suite('gr-confirm-rebase-dialog tests', () => {
let element: GrConfirmRebaseDialog;
setup(async () => {
- stubFlags('isEnabled').returns(true);
+ const userModel = testResolver(userModelToken);
+ userModel.setAccount({
+ ...createAccountWithEmail('abc@def.com'),
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ });
element = await fixture(
html`<gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>`
);
@@ -91,6 +104,57 @@
);
});
+ suite('on behalf of uploader', () => {
+ let changeModel;
+ const change = {
+ ...createChangeViewChange(),
+ };
+ setup(async () => {
+ element.branch = 'test' as BranchName;
+ await element.updateComplete;
+ changeModel = testResolver(changeModelToken);
+ changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change,
+ });
+ });
+ test('for reviewer it shows message about on behalf', () => {
+ const rebaseOnBehalfMsg = queryAndAssert(element, '.rebaseOnBehalfMsg');
+ assert.dom.equal(
+ rebaseOnBehalfMsg,
+ /* HTML */ `<div class="rebaseOnBehalfMsg">
+ Rebase will be done on behalf of the uploader:
+ <gr-account-chip> </gr-account-chip> <span> </span>
+ </div>`
+ );
+ const accountChip: GrAccountChip = queryAndAssert(
+ rebaseOnBehalfMsg,
+ 'gr-account-chip'
+ );
+ assert.equal(
+ accountChip.account!,
+ change?.revisions[change.current_revision]?.uploader
+ );
+ });
+ test('allowConflicts', async () => {
+ element.allowConflicts = true;
+ await element.updateComplete;
+ const rebaseOnBehalfMsg = queryAndAssert(element, '.rebaseOnBehalfMsg');
+ assert.dom.equal(
+ rebaseOnBehalfMsg,
+ /* HTML */ `<div class="rebaseOnBehalfMsg">
+ Rebase will be done on behalf of
+ <gr-account-chip> </gr-account-chip> <span> </span>
+ </div>`
+ );
+ const accountChip: GrAccountChip = queryAndAssert(
+ rebaseOnBehalfMsg,
+ 'gr-account-chip'
+ );
+ assert.equal(accountChip.account, element.account);
+ });
+ });
+
test('disableActions property disables dialog confirm', async () => {
element.disableActions = false;
await element.updateComplete;
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 63a2ac3..345feb2 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
@@ -21,6 +21,8 @@
PatchSetNum,
VotingRangeInfo,
isRobot,
+ EDIT,
+ PARENT,
} from '../../../types/common';
import {GrMessage, MessageAnchorTapDetail} from '../gr-message/gr-message';
import {getVotingRange} from '../../../utils/label-util';
@@ -154,13 +156,25 @@
message: CombinedMessage,
allMessages: CombinedMessage[]
): PatchSetNum | undefined {
- if (message._revision_number && message._revision_number > 0)
+ if (
+ message._revision_number !== undefined &&
+ message._revision_number !== 0 &&
+ message._revision_number !== PARENT &&
+ message._revision_number !== EDIT
+ ) {
return message._revision_number;
+ }
let revision: PatchSetNum = 0 as PatchSetNum;
for (const m of allMessages) {
if (m.date > message.date) break;
- if (m._revision_number && m._revision_number > revision)
+ if (
+ m._revision_number !== undefined &&
+ m._revision_number !== 0 &&
+ m._revision_number !== PARENT &&
+ m._revision_number !== EDIT
+ ) {
revision = m._revision_number;
+ }
}
return revision > 0 ? revision : undefined;
}
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 9783306..9bead95 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
@@ -342,10 +342,6 @@
resolve(this, highlightServiceToken),
() => getAppContext().reportingService
);
- this.renderPrefs = {
- ...this.renderPrefs,
- use_lit_components: true,
- };
this.addEventListener(
// These are named inconsistently for a reason:
// The create-comment event is fired to indicate that we should
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index c38e788..de6a39c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -237,7 +237,7 @@
* This is triggered when the user types into the editing textarea. We then
* debounce it and call autoSave().
*/
- private autoSaveTrigger$ = new Subject<void>();
+ private autoSaveTrigger$ = new Subject();
/**
* Set to the content of DraftInfo when entering editing mode.
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 782c055..7779fff 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -475,14 +475,19 @@
}
private async computeSuggestions() {
+ this.suggestions = [];
if (this.currentSearchString === undefined) {
- this.suggestions = [];
return;
}
+ const searchString = this.currentSearchString;
+ let suggestions: (Item | EmojiSuggestion)[] = [];
if (this.isEmojiDropdownActive()) {
- this.computeEmojiSuggestions(this.currentSearchString);
+ suggestions = this.computeEmojiSuggestions(this.currentSearchString);
} else if (this.isMentionsDropdownActive()) {
- await this.computeReviewerSuggestions();
+ suggestions = await this.computeReviewerSuggestions();
+ }
+ if (searchString === this.currentSearchString) {
+ this.suggestions = suggestions;
}
}
@@ -570,7 +575,7 @@
}
// private but used in test
- formatSuggestions(matchedSuggestions: EmojiSuggestion[]) {
+ formatSuggestions(matchedSuggestions: EmojiSuggestion[]): EmojiSuggestion[] {
const suggestions = [];
for (const suggestion of matchedSuggestions) {
assert(isEmojiSuggestion(suggestion), 'malformed suggestion');
@@ -578,28 +583,27 @@
suggestion.text = `${suggestion.value} ${suggestion.match}`;
suggestions.push(suggestion);
}
- this.suggestions = suggestions;
+ return suggestions;
}
// private but used in test
- computeEmojiSuggestions(suggestionsText?: string) {
+ computeEmojiSuggestions(suggestionsText?: string): EmojiSuggestion[] {
if (suggestionsText === undefined) {
- this.suggestions = [];
- return;
+ return [];
}
if (!suggestionsText.length) {
- this.formatSuggestions(ALL_SUGGESTIONS);
+ return this.formatSuggestions(ALL_SUGGESTIONS);
} else {
const matches = ALL_SUGGESTIONS.filter(suggestion =>
suggestion.match.includes(suggestionsText)
).slice(0, MAX_ITEMS_DROPDOWN);
- this.formatSuggestions(matches);
+ return this.formatSuggestions(matches);
}
}
// TODO(dhruvsri): merge with getAccountSuggestions in account-util
- async computeReviewerSuggestions() {
- this.suggestions = (
+ async computeReviewerSuggestions(): Promise<Item[]> {
+ return (
(await this.restApiService.getSuggestedAccounts(
this.currentSearchString ?? '',
/* number= */ 15,
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 154ea0a..4dcaa80 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -6,8 +6,16 @@
import '../../../test/common-test-setup';
import './gr-textarea';
import {GrTextarea} from './gr-textarea';
-import {ItemSelectedEventDetail} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {pressKey, stubRestApi, waitUntil} from '../../../test/test-utils';
+import {
+ Item,
+ ItemSelectedEventDetail,
+} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {
+ mockPromise,
+ pressKey,
+ stubRestApi,
+ waitUntil,
+} from '../../../test/test-utils';
import {fixture, html, assert} from '@open-wc/testing';
import {createAccountWithEmail} from '../../../test/test-data-generators';
import {Key} from '../../../utils/dom-util';
@@ -120,6 +128,98 @@
assert.isFalse(element.mentionsSuggestions!.isHidden);
});
+ test('mention suggestions cleared before request returns', async () => {
+ const promise = mockPromise<Item[]>();
+ stubRestApi('getSuggestedAccounts').returns(promise);
+ element.textarea!.focus();
+ await waitUntil(() => element.textarea!.focused === true);
+
+ element.suggestions = [
+ {dataValue: 'prior@google.com', text: 'Prior suggestion'},
+ ];
+ element.textarea!.selectionStart = 1;
+ element.textarea!.selectionEnd = 1;
+ element.text = '@';
+
+ await element.updateComplete;
+ assert.equal(element.suggestions.length, 0);
+
+ promise.resolve([
+ createAccountWithEmail('abc@google.com'),
+ createAccountWithEmail('abcdef@google.com'),
+ ]);
+ await waitUntil(() => element.suggestions.length !== 0);
+ assert.deepEqual(element.suggestions, [
+ {
+ dataValue: 'abc@google.com',
+ text: 'abc@google.com <abc@google.com>',
+ },
+ {
+ dataValue: 'abcdef@google.com',
+ text: 'abcdef@google.com <abcdef@google.com>',
+ },
+ ]);
+ });
+
+ test('mention dropdown shows suggestion for latest text', async () => {
+ const promise1 = mockPromise<Item[]>();
+ const promise2 = mockPromise<Item[]>();
+ const suggestionStub = stubRestApi('getSuggestedAccounts');
+ suggestionStub.returns(promise1);
+ element.textarea!.focus();
+ await waitUntil(() => element.textarea!.focused === true);
+
+ element.textarea!.selectionStart = 1;
+ element.textarea!.selectionEnd = 1;
+ element.text = '@';
+ await element.updateComplete;
+ assert.equal(element.currentSearchString, '');
+
+ suggestionStub.returns(promise2);
+ element.text = '@abc@google.com';
+ // None of suggestions returned yet.
+ assert.equal(element.suggestions.length, 0);
+ await element.updateComplete;
+ assert.equal(element.currentSearchString, 'abc@google.com');
+
+ promise2.resolve([
+ createAccountWithEmail('abc@google.com'),
+ createAccountWithEmail('abcdef@google.com'),
+ ]);
+
+ await waitUntil(() => element.suggestions.length !== 0);
+ assert.deepEqual(element.suggestions, [
+ {
+ dataValue: 'abc@google.com',
+ text: 'abc@google.com <abc@google.com>',
+ },
+ {
+ dataValue: 'abcdef@google.com',
+ text: 'abcdef@google.com <abcdef@google.com>',
+ },
+ ]);
+
+ promise1.resolve([
+ createAccountWithEmail('dce@google.com'),
+ createAccountWithEmail('defcba@google.com'),
+ ]);
+ // Empty the event queue.
+ await new Promise<void>(resolve => {
+ setTimeout(() => resolve());
+ });
+ // Suggestions didn't change
+ assert.deepEqual(element.suggestions, [
+ {
+ dataValue: 'abc@google.com',
+ text: 'abc@google.com <abc@google.com>',
+ },
+ {
+ dataValue: 'abcdef@google.com',
+ text: 'abcdef@google.com <abcdef@google.com>',
+ },
+ ]);
+ });
+
test('emoji selector does not open when previous char is \n', async () => {
element.textarea!.focus();
await waitUntil(() => element.textarea!.focused === true);
@@ -195,21 +295,25 @@
assert.isFalse(element.mentionsSuggestions!.isHidden);
element.text = '@h';
+ await waitUntil(() => element.suggestions.length > 0);
await element.updateComplete;
assert.isTrue(element.emojiSuggestions!.isHidden);
assert.isFalse(element.mentionsSuggestions!.isHidden);
element.text = '@h ';
+ await waitUntil(() => element.suggestions.length > 0);
await element.updateComplete;
assert.isTrue(element.emojiSuggestions!.isHidden);
assert.isFalse(element.mentionsSuggestions!.isHidden);
element.text = '@h :';
+ await waitUntil(() => element.suggestions.length > 0);
await element.updateComplete;
assert.isTrue(element.emojiSuggestions!.isHidden);
assert.isFalse(element.mentionsSuggestions!.isHidden);
element.text = '@h :D';
+ await waitUntil(() => element.suggestions.length > 0);
await element.updateComplete;
assert.isTrue(element.emojiSuggestions!.isHidden);
assert.isFalse(element.mentionsSuggestions!.isHidden);
@@ -465,13 +569,13 @@
{value: '😢', match: 'tear'},
{value: '😂', match: 'tears'},
];
- element.formatSuggestions(matchedSuggestions);
+ const suggestions = element.formatSuggestions(matchedSuggestions);
assert.deepEqual(
[
{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
{value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'},
],
- element.suggestions
+ suggestions
);
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 71a0637..7fa6d72 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -13,9 +13,7 @@
DiffContextExpandedEventDetail,
isImageDiffBuilder,
} from './gr-diff-builder';
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
import {GrDiffBuilderImage} from './gr-diff-builder-image';
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
import {GrDiffBuilderLit} from './gr-diff-builder-lit';
import {CancelablePromise, makeCancelable} from '../../../utils/async-util';
@@ -428,7 +426,6 @@
}
let builder = null;
- const useLit = this.renderPrefs?.use_lit_components ?? false;
if (this.isImageDiff) {
builder = new GrDiffBuilderImage(
this.diff,
@@ -447,45 +444,25 @@
...this.renderPrefs,
view_mode: DiffViewMode.SIDE_BY_SIDE,
};
- if (useLit) {
- builder = new GrDiffBuilderLit(
- this.diff,
- localPrefs,
- this.diffElement,
- this.layersInternal,
- this.renderPrefs
- );
- } else {
- builder = new GrDiffBuilderSideBySide(
- this.diff,
- localPrefs,
- this.diffElement,
- this.layersInternal,
- this.renderPrefs
- );
- }
+ builder = new GrDiffBuilderLit(
+ this.diff,
+ localPrefs,
+ this.diffElement,
+ this.layersInternal,
+ this.renderPrefs
+ );
} else if (this.viewMode === DiffViewMode.UNIFIED) {
this.renderPrefs = {
...this.renderPrefs,
view_mode: DiffViewMode.UNIFIED,
};
- if (useLit) {
- builder = new GrDiffBuilderLit(
- this.diff,
- localPrefs,
- this.diffElement,
- this.layersInternal,
- this.renderPrefs
- );
- } else {
- builder = new GrDiffBuilderUnified(
- this.diff,
- localPrefs,
- this.diffElement,
- this.layersInternal,
- this.renderPrefs
- );
- }
+ builder = new GrDiffBuilderLit(
+ this.diff,
+ localPrefs,
+ this.diffElement,
+ this.layersInternal,
+ this.renderPrefs
+ );
}
if (!builder) {
throw Error(`Unsupported diff view mode: ${this.viewMode}`);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
index 9cf9bae..ba6acfd 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -6,17 +6,15 @@
import '../../../test/common-test-setup';
import {
createConfig,
- createDiff,
createEmptyDiff,
} from '../../../test/test-data-generators';
import './gr-diff-builder-element';
-import {queryAndAssert, stubBaseUrl, waitUntil} from '../../../test/test-utils';
+import {stubBaseUrl, waitUntil} from '../../../test/test-utils';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
import {
DiffContent,
- DiffInfo,
DiffLayer,
DiffPreferencesInfo,
DiffViewMode,
@@ -30,6 +28,7 @@
import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
import {BlameInfo} from '../../../types/common';
import {fixture, html, assert} from '@open-wc/testing';
+import {GrDiffRow} from './gr-diff-row';
const DEFAULT_PREFS = createDefaultDiffPrefs();
@@ -39,7 +38,6 @@
let diffTable: HTMLTableElement;
const LINE_BREAK_HTML = '<span class="gr-diff br"></span>';
- const WBR_HTML = '<wbr class="gr-diff">';
const setBuilderPrefs = (prefs: Partial<DiffPreferencesInfo>) => {
builder = new GrDiffBuilderSideBySide(
@@ -65,15 +63,6 @@
setBuilderPrefs({});
});
- test('line_length applied with <wbr> if line_wrapping is true', () => {
- setBuilderPrefs({line_wrapping: true, tab_size: 4, line_length: 50});
- const text = 'a'.repeat(51);
- const expected = 'a'.repeat(50) + WBR_HTML + 'a';
- const result = builder.createTextEl(null, line(text)).firstElementChild
- ?.firstElementChild?.innerHTML;
- assert.equal(result, expected);
- });
-
test('line_length applied with line break if line_wrapping is false', () => {
setBuilderPrefs({line_wrapping: false, tab_size: 4, line_length: 50});
const text = 'a'.repeat(51);
@@ -691,7 +680,7 @@
assert.include(diffRows[4].textContent, 'unchanged 11');
});
- test('clicking +x common lines expands those lines', () => {
+ test('clicking +x common lines expands those lines', async () => {
const contextControls = diffTable.querySelectorAll('gr-context-controls');
const topExpandCommonButton =
contextControls[0].shadowRoot?.querySelectorAll<HTMLElement>(
@@ -699,10 +688,19 @@
)[0];
assert.isOk(topExpandCommonButton);
assert.include(topExpandCommonButton!.textContent, '+9 common lines');
+ let diffRows = diffTable.querySelectorAll('.diff-row');
+ // 5 lines:
+ // FILE, LOST, the changed line plus one line of context in each direction
+ assert.equal(diffRows.length, 5);
+
topExpandCommonButton!.click();
- const diffRows = diffTable.querySelectorAll('.diff-row');
- // The first two are LOST and FILE line
- assert.equal(diffRows.length, 2 + 10 + 1 + 1);
+
+ await waitUntil(() => {
+ diffRows = diffTable.querySelectorAll<GrDiffRow>('.diff-row');
+ return diffRows.length === 14;
+ });
+ // 14 lines: The 5 above plus the 9 unchanged lines that were expanded
+ assert.equal(diffRows.length, 14);
assert.include(diffRows[2].textContent, 'unchanged 1');
assert.include(diffRows[3].textContent, 'unchanged 2');
assert.include(diffRows[4].textContent, 'unchanged 3');
@@ -722,6 +720,11 @@
dispatchStub.reset();
element.unhideLine(4, Side.LEFT);
+ await waitUntil(() => {
+ const rows = diffTable.querySelectorAll<GrDiffRow>('.diff-row');
+ return rows.length === 2 + 5 + 1 + 1 + 1;
+ });
+
const diffRows = diffTable.querySelectorAll('.diff-row');
// The first two are LOST and FILE line
// Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded
@@ -745,332 +748,6 @@
});
});
- [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE].forEach(mode => {
- suite(`mock-diff mode:${mode}`, () => {
- let builder: GrDiffBuilderSideBySide;
- let diff: DiffInfo;
- let keyLocations: KeyLocations;
-
- setup(() => {
- element.viewMode = mode;
- diff = createDiff();
- element.diff = diff;
-
- keyLocations = {left: {}, right: {}};
-
- element.prefs = {
- ...createDefaultDiffPrefs(),
- line_length: 80,
- show_tabs: true,
- tab_size: 4,
- };
- element.render(keyLocations);
- builder = element.builder as GrDiffBuilderSideBySide;
- });
-
- test('aria-labels on added line numbers', () => {
- const deltaLineNumberButton = diffTable.querySelectorAll(
- '.lineNumButton.right'
- )[5];
-
- assert.isOk(deltaLineNumberButton);
- assert.equal(
- deltaLineNumberButton.getAttribute('aria-label'),
- '5 added'
- );
- });
-
- test('aria-labels on removed line numbers', () => {
- const deltaLineNumberButton = diffTable.querySelectorAll(
- '.lineNumButton.left'
- )[10];
-
- assert.isOk(deltaLineNumberButton);
- assert.equal(
- deltaLineNumberButton.getAttribute('aria-label'),
- '10 removed'
- );
- });
-
- test('getContentByLine', () => {
- let actual: HTMLElement | null;
-
- actual = builder.getContentByLine(2, Side.LEFT);
- assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
-
- actual = builder.getContentByLine(2, Side.RIGHT);
- assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
-
- actual = builder.getContentByLine(5, Side.LEFT);
- assert.equal(actual?.textContent, diff.content[2].ab?.[0]);
-
- actual = builder.getContentByLine(5, Side.RIGHT);
- assert.equal(actual?.textContent, diff.content[1].b?.[0]);
- });
-
- test('getContentTdByLineEl works both with button and td', () => {
- const diffRow = diffTable.querySelectorAll('tr.diff-row')[2];
-
- const lineNumTdLeft = queryAndAssert(diffRow, 'td.lineNum.left');
- const lineNumButtonLeft = queryAndAssert(lineNumTdLeft, 'button');
- const contentTdLeft = diffRow.querySelectorAll('.content')[0];
-
- const lineNumTdRight = queryAndAssert(diffRow, 'td.lineNum.right');
- const lineNumButtonRight = queryAndAssert(lineNumTdRight, 'button');
- const contentTdRight =
- mode === DiffViewMode.SIDE_BY_SIDE
- ? diffRow.querySelectorAll('.content')[1]
- : contentTdLeft;
-
- assert.equal(
- element.getContentTdByLineEl(lineNumTdLeft),
- contentTdLeft
- );
- assert.equal(
- element.getContentTdByLineEl(lineNumButtonLeft),
- contentTdLeft
- );
- assert.equal(
- element.getContentTdByLineEl(lineNumTdRight),
- contentTdRight
- );
- assert.equal(
- element.getContentTdByLineEl(lineNumButtonRight),
- contentTdRight
- );
- });
-
- test('findLinesByRange LEFT', () => {
- const lines: GrDiffLine[] = [];
- const elems: HTMLElement[] = [];
- const start = 1;
- const end = 44;
-
- // lines 26-29 are collapsed, so minus 4
- let count = end - start + 1 - 4;
- // Lines 14+15 are part of a 'common' chunk. And we have a bug in
- // unified diff that results in not rendering these lines for the LEFT
- // side. TODO: Fix that bug!
- if (mode === DiffViewMode.UNIFIED) count -= 2;
-
- builder.findLinesByRange(start, end, Side.LEFT, lines, elems);
-
- assert.equal(lines.length, count);
- assert.equal(elems.length, count);
-
- for (let i = 0; i < count; i++) {
- assert.instanceOf(lines[i], GrDiffLine);
- assert.instanceOf(elems[i], HTMLElement);
- assert.equal(lines[i].text, elems[i].textContent);
- }
- });
-
- test('findLinesByRange RIGHT', () => {
- const lines: GrDiffLine[] = [];
- const elems: HTMLElement[] = [];
- const start = 1;
- const end = 48;
-
- // lines 26-29 are collapsed, so minus 4
- const count = end - start + 1 - 4;
-
- builder.findLinesByRange(start, end, Side.RIGHT, lines, elems);
-
- assert.equal(lines.length, count);
- assert.equal(elems.length, count);
-
- for (let i = 0; i < count; i++) {
- assert.instanceOf(lines[i], GrDiffLine);
- assert.instanceOf(elems[i], HTMLElement);
- assert.equal(lines[i].text, elems[i].textContent);
- }
- });
-
- test('renderContentByRange', () => {
- const spy = sinon.spy(builder, 'createTextEl');
- const start = 9;
- const end = 14;
- let count = end - start + 1;
- // Lines 14+15 are part of a 'common' chunk. And we have a bug in
- // unified diff that results in not rendering these lines for the LEFT
- // side. TODO: Fix that bug!
- if (mode === DiffViewMode.UNIFIED) count -= 1;
-
- builder.renderContentByRange(start, end, Side.LEFT);
-
- assert.equal(spy.callCount, count);
- spy.getCalls().forEach((call, i: number) => {
- assert.equal(call.args[1].beforeNumber, start + i);
- });
- });
-
- test('renderContentByRange non-existent elements', () => {
- const spy = sinon.spy(builder, 'createTextEl');
-
- sinon
- .stub(builder, 'getLineNumberEl')
- .returns(document.createElement('div'));
- sinon
- .stub(builder, 'findLinesByRange')
- .callsFake((_1, _2, _3, lines, elements) => {
- // Add a line and a corresponding element.
- lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
- const tr = document.createElement('tr');
- const td = document.createElement('td');
- const el = document.createElement('div');
- tr.appendChild(td);
- td.appendChild(el);
- elements?.push(el);
-
- // Add 2 lines without corresponding elements.
- lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
- lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
- });
-
- builder.renderContentByRange(1, 10, Side.LEFT);
- // Should be called only once because only one line had a corresponding
- // element.
- assert.equal(spy.callCount, 1);
- });
-
- test('getLineNumberEl side-by-side left', () => {
- const contentEl = builder.getContentByLine(
- 5,
- Side.LEFT,
- element.diffElement as HTMLTableElement
- );
- assert.isOk(contentEl);
- const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
- assert.isOk(lineNumberEl);
- assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
- });
-
- test('getLineNumberEl side-by-side right', () => {
- const contentEl = builder.getContentByLine(
- 5,
- Side.RIGHT,
- element.diffElement as HTMLTableElement
- );
- assert.isOk(contentEl);
- const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
- assert.isOk(lineNumberEl);
- assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
- });
-
- test('getLineNumberEl unified left', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations);
- builder = element.builder as GrDiffBuilderSideBySide;
-
- const contentEl = builder.getContentByLine(
- 5,
- Side.LEFT,
- element.diffElement as HTMLTableElement
- );
- assert.isOk(contentEl);
- const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
- assert.isOk(lineNumberEl);
- assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
- });
-
- test('getLineNumberEl unified right', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations);
- builder = element.builder as GrDiffBuilderSideBySide;
-
- const contentEl = builder.getContentByLine(
- 5,
- Side.RIGHT,
- element.diffElement as HTMLTableElement
- );
- assert.isOk(contentEl);
- const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
- assert.isOk(lineNumberEl);
- assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
- });
-
- test('getNextContentOnSide side-by-side left', () => {
- const startElem = builder.getContentByLine(
- 5,
- Side.LEFT,
- element.diffElement as HTMLTableElement
- );
- assert.isOk(startElem);
- const expectedStartString = diff.content[2].ab?.[0];
- const expectedNextString = diff.content[2].ab?.[1];
- assert.equal(startElem!.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
- assert.isOk(nextElem);
- assert.equal(nextElem!.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide side-by-side right', () => {
- const startElem = builder.getContentByLine(
- 5,
- Side.RIGHT,
- element.diffElement as HTMLTableElement
- );
- const expectedStartString = diff.content[1].b?.[0];
- const expectedNextString = diff.content[1].b?.[1];
- assert.isOk(startElem);
- assert.equal(startElem!.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
- assert.isOk(nextElem);
- assert.equal(nextElem!.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide unified left', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations);
- builder = element.builder as GrDiffBuilderSideBySide;
-
- const startElem = builder.getContentByLine(
- 5,
- Side.LEFT,
- element.diffElement as HTMLTableElement
- );
- const expectedStartString = diff.content[2].ab?.[0];
- const expectedNextString = diff.content[2].ab?.[1];
- assert.isOk(startElem);
- assert.equal(startElem!.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
- assert.isOk(nextElem);
- assert.equal(nextElem!.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide unified right', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations);
- builder = element.builder as GrDiffBuilderSideBySide;
-
- const startElem = builder.getContentByLine(
- 5,
- Side.RIGHT,
- element.diffElement as HTMLTableElement
- );
- const expectedStartString = diff.content[1].b?.[0];
- const expectedNextString = diff.content[1].b?.[1];
- assert.isOk(startElem);
- assert.equal(startElem!.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
- assert.isOk(nextElem);
- assert.equal(nextElem!.textContent, expectedNextString);
- });
- });
- });
-
suite('blame', () => {
let mockBlame: BlameInfo[];
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index b3f3714..e786cf4 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -313,7 +313,7 @@
// For unified diff, this method will be called with number set to 0 for
// the empty line number column for added/removed lines. This should not
// be announced to the screenreader.
- if (number > 0) {
+ if (number !== 'FILE' && number > 0) {
if (line.type === GrDiffLineType.REMOVE) {
button.setAttribute('aria-label', `${number} removed`);
} else if (line.type === GrDiffLineType.ADD) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index b97ae59..9acda81 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -309,7 +309,7 @@
// For unified diff, this method will be called with number set to 0 for
// the empty line number column for added/removed lines. This should not
// be announced to the screenreader.
- if (lineNumber <= 0) return undefined;
+ if (lineNumber === 'LOST' || lineNumber <= 0) return undefined;
switch (line.type) {
case GrDiffLineType.REMOVE:
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
index a0e7840..3858bed 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -10,6 +10,8 @@
const LINE_BREAK = '<span class="gr-diff br"></span>';
+const LINE_BREAK_WBR = '<wbr class="gr-diff"></wbr>';
+
const TAB = '<span class="" style=""></span>';
const TAB_IGNORE = ['class', 'style'];
@@ -39,6 +41,12 @@
await check('a'.repeat(20), `aaaaaaaaaa${LINE_BREAK}aaaaaaaaaa`);
});
+ test('renderText newlines 1 responsive', async () => {
+ element.isResponsive = true;
+ await check('abcdef', 'abcdef');
+ await check('a'.repeat(20), `aaaaaaaaaa${LINE_BREAK_WBR}aaaaaaaaaa`);
+ });
+
test('renderText newlines 2', async () => {
await check(
'<span class="thumbsup">👍</span>',
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
index b9db280..61f8551 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
@@ -46,7 +46,6 @@
diff = createDiff();
diffElement.prefs = createDefaultDiffPrefs();
- diffElement.renderPrefs = {use_lit_components: true};
diffElement.diff = diff;
await promise;
});
@@ -661,7 +660,7 @@
// Goto second last line of the first diff
cursor.moveToLineNumber(lastLine - 1, Side.RIGHT);
assert.equal(
- cursor.getTargetLineElement()!.textContent,
+ cursor.getTargetLineElement()!.textContent?.trim(),
`${lastLine - 1}`
);
@@ -669,7 +668,7 @@
cursor.moveDown();
assert.equal(getTargetDiffIndex(), 0);
assert.equal(
- cursor.getTargetLineElement()!.textContent,
+ cursor.getTargetLineElement()!.textContent?.trim(),
lastLine.toString()
);
@@ -677,7 +676,7 @@
cursor.moveDown();
assert.equal(getTargetDiffIndex(), 0);
assert.equal(
- cursor.getTargetLineElement()!.textContent,
+ cursor.getTargetLineElement()!.textContent?.trim(),
lastLine.toString()
);
@@ -686,9 +685,10 @@
await waitForEventOnce(diffElements[1], 'render');
// Now we can go down
- cursor.moveDown();
+ cursor.moveDown(); // LOST
+ cursor.moveDown(); // FILE
assert.equal(getTargetDiffIndex(), 1);
- assert.equal(cursor.getTargetLineElement()!.textContent, 'File');
+ assert.equal(cursor.getTargetLineElement()!.textContent?.trim(), 'File');
});
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
index 9e3d288..f216e04 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
@@ -62,7 +62,6 @@
],
};
grDiff.prefs = createDefaultDiffPrefs();
- grDiff.renderPrefs = {use_lit_components: true};
grDiff.diff = diff;
await waitForEventOnce(grDiff, 'render');
assert.isOk(element.diffTable);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index 59da20d..5a1ded2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -132,8 +132,14 @@
const after = [];
for (const line of group.lines) {
if (
- (line.beforeNumber && line.beforeNumber < leftSplit) ||
- (line.afterNumber && line.afterNumber < rightSplit)
+ (line.beforeNumber &&
+ line.beforeNumber !== 'FILE' &&
+ line.beforeNumber !== 'LOST' &&
+ line.beforeNumber < leftSplit) ||
+ (line.afterNumber &&
+ line.afterNumber !== 'FILE' &&
+ line.afterNumber !== 'LOST' &&
+ line.afterNumber < rightSplit)
) {
before.push(line);
} else {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index 4adb1cf..227ac2d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -68,18 +68,8 @@
);
});
- test('a unified diff legacy', async () => {
- element.viewMode = DiffViewMode.UNIFIED;
- await testUnified();
- });
-
test('a unified diff lit', async () => {
element.viewMode = DiffViewMode.UNIFIED;
- element.renderPrefs = {...element.renderPrefs, use_lit_components: true};
- await testUnified();
- });
-
- const testUnified = async () => {
element.prefs = {...MINIMAL_PREFS};
element.diff = createDiff();
await element.updateComplete;
@@ -1343,18 +1333,9 @@
],
}
);
- };
-
- test('a normal diff legacy', async () => {
- await testNormal();
});
test('a normal diff lit', async () => {
- element.renderPrefs = {...element.renderPrefs, use_lit_components: true};
- await testNormal();
- });
-
- const testNormal = async () => {
element.prefs = {...MINIMAL_PREFS};
element.diff = createDiff();
await element.updateComplete;
@@ -3008,7 +2989,7 @@
],
}
);
- };
+ });
});
suite('selectionchange event handling', () => {
@@ -3548,7 +3529,11 @@
await element.updateComplete;
const ROWS = 48;
const FILE_ROW = 1;
- assert.equal(element.getCursorStops().length, ROWS + FILE_ROW);
+ const LOST_ROW = 1;
+ assert.equal(
+ element.getCursorStops().length,
+ ROWS + FILE_ROW + LOST_ROW
+ );
});
test('returns an additional AbortStop when still loading', async () => {
@@ -3557,8 +3542,9 @@
await element.updateComplete;
const ROWS = 48;
const FILE_ROW = 1;
+ const LOST_ROW = 1;
const actual = element.getCursorStops();
- assert.equal(actual.length, ROWS + FILE_ROW + 1);
+ assert.equal(actual.length, ROWS + FILE_ROW + LOST_ROW + 1);
assert.isTrue(actual[actual.length - 1] instanceof AbortStop);
});
});
@@ -4050,13 +4036,13 @@
b: ['Non eram nescius, Brute, cum, quae summis ingeniis '],
},
];
- function assertDiffTableWithContent() {
+ function diffTableHasContent() {
assertIsDefined(element.diffTable);
const diffTable = element.diffTable;
- assert.isTrue(diffTable.innerText.includes(content[0].a?.[0] ?? ''));
+ return diffTable.innerText.includes(content[0].a?.[0] ?? '');
}
await setupSampleDiff({content});
- assertDiffTableWithContent();
+ await waitUntil(diffTableHasContent);
element.diff = {...element.diff!};
await element.updateComplete;
// immediately cleaned up
@@ -4066,7 +4052,7 @@
element.renderDiffTable();
await element.updateComplete;
// rendered again
- assertDiffTableWithContent();
+ await waitUntil(diffTableHasContent);
});
suite('selection test', () => {
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index ad5b217..b6b9de7 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -196,6 +196,11 @@
computeLatestPatchNumWithEdit(patchsets)
);
+ public readonly latestUploader$ = select(
+ this.change$,
+ change => change?.revisions[change.current_revision]?.uploader
+ );
+
/**
* Emits the current patchset number. If the route does not define the current
* patchset num, then this selector waits for the change to be defined and
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 0683af0..b7ff16f 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -155,7 +155,8 @@
),
this.subscribeToFiles(
(psLeft, _) => {
- if (psLeft === PARENT || psLeft <= 0) return undefined;
+ if (psLeft === PARENT || (psLeft as PatchSetNumber) <= 0)
+ return undefined;
return {basePatchNum: PARENT, patchNum: psLeft as PatchSetNumber};
},
files => {
@@ -164,7 +165,8 @@
),
this.subscribeToFiles(
(psLeft, psRight) => {
- if (psLeft === PARENT || psLeft <= 0) return undefined;
+ if (psLeft === PARENT || (psLeft as PatchSetNumber) <= 0)
+ return undefined;
return {basePatchNum: PARENT, patchNum: psRight as PatchSetNumber};
},
files => {
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index 35a8465..da4e3f1 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -139,9 +139,6 @@
// emits at 'trailing' of throttle interval
assert.equal(fetchSpy.callCount, 1);
- // 600 ms is greater than the 500 ms throttle time.
- clock.tick(600);
-
model.reload('test-plugin');
model.reload('test-plugin');
model.reload('test-plugin');
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 8c642c1..e8d715e 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -472,7 +472,11 @@
);
}
value.isSingleAttempt = false;
- if (run.attempt > value.latestAttempt) {
+ if (
+ value.latestAttempt !== 'all' &&
+ value.latestAttempt !== 'latest' &&
+ run.attempt > value.latestAttempt
+ ) {
value.latestAttempt = run.attempt;
}
value.attempts.push(detail);
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 32aff91..edf0206 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -42,7 +42,7 @@
"polymer-bridges": "file:../../polymer-bridges/",
"polymer-resin": "^2.0.1",
"resemblejs": "^4.0.0",
- "rxjs": "^7.8.0",
+ "rxjs": "^6.6.7",
"safevalues": "^0.3.1",
"web-vitals": "^3.0.0"
},
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 53f589a..973960e 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -19,6 +19,5 @@
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
SUGGEST_EDIT = 'UiFeature__suggest_edit',
- REBASE_ON_BEHALF_OF_UPLOADER = 'UiFeature__rebase_on_behalf_of_uploader',
COMMENTS_CHIPS_IN_FILE_LIST = 'UiFeature__comments_chips_in_file_list',
}
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index c021046..e056a35 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -846,12 +846,12 @@
dependencies:
glob "^7.1.3"
-rxjs@^7.8.0:
- version "7.8.0"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
- integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
+rxjs@^6.6.7:
+ version "6.6.7"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
dependencies:
- tslib "^2.1.0"
+ tslib "^1.9.0"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
@@ -954,10 +954,10 @@
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
-tslib@^2.1.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
- integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
+tslib@^1.9.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
util-deprecate@~1.0.1:
version "1.0.2"
diff --git a/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 1a355eb..b6aca6f 100644
--- a/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ b/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -21,3 +21,4 @@
viewConnections = View Connections
viewPlugins = View Plugins
viewQueue = View Queue
+viewSecondaryEmails = View Secondary Emails