Merge "Remove unnessecary Guice injection"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e0bed37..ff12af2 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6921,7 +6921,10 @@
The subject of the commit (header line of the commit message).
|`message` ||The commit message.
|`web_links` |optional|
-Links to the commit in external sites as a list of
+Links to the patch set in external sites as a list of
+link:#web-link-info[WebLinkInfo] entities.
+|`resolve_conflicts_web_links` |optional|
+Links to the commit in external sites for resolving conflicts as a list of
link:#web-link-info[WebLinkInfo] entities.
|===========================
diff --git a/WORKSPACE b/WORKSPACE
index 6c01d03..0caf8c3 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -66,8 +66,8 @@
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "1134ec9b7baee008f1d54f0483049a97e53a57cd3913ec9d6db625549c98395a",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.4.0/rules_nodejs-3.4.0.tar.gz"],
+ sha256 = "10f534e1c80f795cffe1f2822becd4897754d18564612510c59b3c73544ae7c6",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.0/rules_nodejs-3.5.0.tar.gz"],
)
# Golang support for PolyGerrit local dev server.
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 6c6bab0..85c4c13 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -35,6 +35,7 @@
import com.google.gerrit.extensions.webui.EditWebLink;
import com.google.gerrit.extensions.webui.FileHistoryWebLink;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
+import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.change.ChangeETagComputation;
@@ -76,6 +77,7 @@
private final DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners;
private final DynamicSet<FileHistoryWebLink> fileHistoryWebLinks;
private final DynamicSet<PatchSetWebLink> patchSetWebLinks;
+ private final DynamicSet<ResolveConflictsWebLink> resolveConflictsWebLinks;
private final DynamicSet<EditWebLink> editWebLinks;
private final DynamicSet<RevisionCreatedListener> revisionCreatedListeners;
private final DynamicSet<GroupBackend> groupBackends;
@@ -111,6 +113,7 @@
DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners,
DynamicSet<FileHistoryWebLink> fileHistoryWebLinks,
DynamicSet<PatchSetWebLink> patchSetWebLinks,
+ DynamicSet<ResolveConflictsWebLink> resolveConflictsWebLinks,
DynamicSet<EditWebLink> editWebLinks,
DynamicSet<RevisionCreatedListener> revisionCreatedListeners,
DynamicSet<GroupBackend> groupBackends,
@@ -143,6 +146,7 @@
this.fileHistoryWebLinks = fileHistoryWebLinks;
this.patchSetWebLinks = patchSetWebLinks;
this.editWebLinks = editWebLinks;
+ this.resolveConflictsWebLinks = resolveConflictsWebLinks;
this.revisionCreatedListeners = revisionCreatedListeners;
this.groupBackends = groupBackends;
this.accountActivationValidationListeners = accountActivationValidationListeners;
@@ -244,6 +248,10 @@
return add(patchSetWebLinks, patchSetWebLink);
}
+ public Registration add(ResolveConflictsWebLink resolveConflictsWebLink) {
+ return add(resolveConflictsWebLinks, resolveConflictsWebLink);
+ }
+
public Registration add(EditWebLink editWebLink) {
return add(editWebLinks, editWebLink);
}
diff --git a/java/com/google/gerrit/common/PluginData.java b/java/com/google/gerrit/common/PluginData.java
index c440de1..289d93a 100644
--- a/java/com/google/gerrit/common/PluginData.java
+++ b/java/com/google/gerrit/common/PluginData.java
@@ -10,7 +10,7 @@
// 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.common;
+// limitations under the License.
package com.google.gerrit.common;
diff --git a/java/com/google/gerrit/extensions/common/CommitInfo.java b/java/com/google/gerrit/extensions/common/CommitInfo.java
index 1fd8755..202b829 100644
--- a/java/com/google/gerrit/extensions/common/CommitInfo.java
+++ b/java/com/google/gerrit/extensions/common/CommitInfo.java
@@ -29,6 +29,7 @@
public String subject;
public String message;
public List<WebLinkInfo> webLinks;
+ public List<WebLinkInfo> resolveConflictsWebLinks;
@Override
public boolean equals(Object o) {
@@ -42,12 +43,14 @@
&& Objects.equals(committer, c.committer)
&& Objects.equals(subject, c.subject)
&& Objects.equals(message, c.message)
- && Objects.equals(webLinks, c.webLinks);
+ && Objects.equals(webLinks, c.webLinks)
+ && Objects.equals(resolveConflictsWebLinks, c.resolveConflictsWebLinks);
}
@Override
public int hashCode() {
- return Objects.hash(commit, parents, author, committer, subject, message, webLinks);
+ return Objects.hash(
+ commit, parents, author, committer, subject, message, webLinks, resolveConflictsWebLinks);
}
@Override
@@ -64,6 +67,9 @@
if (webLinks != null) {
helper.add("webLinks", webLinks);
}
+ if (resolveConflictsWebLinks != null) {
+ helper.add("resolveConflictsWebLinks", resolveConflictsWebLinks);
+ }
return helper.toString();
}
}
diff --git a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
index d344e18..71fc564 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
@@ -20,6 +20,7 @@
import com.google.common.truth.Correspondence;
import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.gerrit.extensions.common.CommitInfo;
@@ -68,6 +69,16 @@
return check("message").that(commitInfo.message);
}
+ public IterableSubject webLinks() {
+ isNotNull();
+ return check("webLinks").that(commitInfo.webLinks);
+ }
+
+ public IterableSubject resolveConflictsWebLinks() {
+ isNotNull();
+ return check("resolveConflictsWebLinks").that(commitInfo.resolveConflictsWebLinks);
+ }
+
public static Correspondence<CommitInfo, String> hasCommit() {
return NullAwareCorrespondence.transforming(commitInfo -> commitInfo.commit, "hasCommit");
}
diff --git a/java/com/google/gerrit/extensions/webui/ResolveConflictsWebLink.java b/java/com/google/gerrit/extensions/webui/ResolveConflictsWebLink.java
new file mode 100644
index 0000000..19402a9
--- /dev/null
+++ b/java/com/google/gerrit/extensions/webui/ResolveConflictsWebLink.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+@ExtensionPoint
+public interface ResolveConflictsWebLink extends WebLink {
+
+ /**
+ * {@link com.google.gerrit.extensions.common.WebLinkInfo} describing a link from a patch set to
+ * an external service for the purpose of resolving merge conflicts.
+ *
+ * <p>In order for the web link to be visible {@link
+ * com.google.gerrit.extensions.common.WebLinkInfo#url} and {@link
+ * com.google.gerrit.extensions.common.WebLinkInfo#name} must be set.
+ *
+ * @param projectName name of the project
+ * @param commit commit of the patch set
+ * @param commitMessage the commit message of the change
+ * @param branchName target branch of the change
+ * @return WebLinkInfo that links to patch set in external service, {@code null} if there should
+ * be no link.
+ */
+ WebLinkInfo getResolveConflictsWebLink(
+ String projectName, String commit, String commitMessage, String branchName);
+}
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 3b626ea..4acef06 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -33,6 +33,7 @@
import com.google.gerrit.extensions.webui.ParentWebLink;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.extensions.webui.TagWebLink;
import com.google.gerrit.extensions.webui.WebLink;
import com.google.inject.Inject;
@@ -56,6 +57,7 @@
};
private final DynamicSet<PatchSetWebLink> patchSetLinks;
+ private final DynamicSet<ResolveConflictsWebLink> resolveConflictsLinks;
private final DynamicSet<ParentWebLink> parentLinks;
private final DynamicSet<EditWebLink> editLinks;
private final DynamicSet<FileWebLink> fileLinks;
@@ -68,6 +70,7 @@
@Inject
public WebLinks(
DynamicSet<PatchSetWebLink> patchSetLinks,
+ DynamicSet<ResolveConflictsWebLink> resolveConflictsLinks,
DynamicSet<ParentWebLink> parentLinks,
DynamicSet<EditWebLink> editLinks,
DynamicSet<FileWebLink> fileLinks,
@@ -77,6 +80,7 @@
DynamicSet<BranchWebLink> branchLinks,
DynamicSet<TagWebLink> tagLinks) {
this.patchSetLinks = patchSetLinks;
+ this.resolveConflictsLinks = resolveConflictsLinks;
this.parentLinks = parentLinks;
this.editLinks = editLinks;
this.fileLinks = fileLinks;
@@ -103,6 +107,21 @@
/**
* @param project Project name.
+ * @param revision SHA1 of commit.
+ * @param commitMessage the commit message of the commit.
+ * @param branchName branch of the commit.
+ * @return Links for resolving comflicts.
+ */
+ public ImmutableList<WebLinkInfo> getResolveConflictsLinks(
+ Project.NameKey project, String commit, String commitMessage, String branchName) {
+ return filterLinks(
+ resolveConflictsLinks,
+ webLink ->
+ webLink.getResolveConflictsWebLink(project.get(), commit, commitMessage, branchName));
+ }
+
+ /**
+ * @param project Project name.
* @param revision SHA1 of the parent revision.
* @param commitMessage the commit message of the parent revision.
* @param branchName branch of the revision (and parent revision).
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index b46d10d..5a74047 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -428,6 +428,11 @@
* Updates multiple different accounts atomically. This will only store a single new value (aka
* set of all external IDs of the host) in the external ID cache, which is important for storage
* economy. All {@code updates} must be for different accounts.
+ *
+ * <p>NOTE on error handling: Since updates are executed in multiple stages, with some stages
+ * resulting from the union of all individual updates, we cannot point to the update that caused
+ * the error. Callers should be aware that a single "update of death" (or a set of updates that
+ * together have this property) will always prevent the entire batch from being executed.
*/
public ImmutableList<Optional<AccountState>> updateBatch(List<UpdateArguments> updates)
throws IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index b702440..33f3d4f 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -182,9 +182,14 @@
info.message = commit.getFullMessage();
if (addLinks) {
- ImmutableList<WebLinkInfo> links =
+ ImmutableList<WebLinkInfo> patchSetLinks =
webLinks.getPatchSetLinks(project, commit.name(), commit.getFullMessage(), branchName);
- info.webLinks = links.isEmpty() ? null : links;
+ info.webLinks = patchSetLinks.isEmpty() ? null : patchSetLinks;
+ ImmutableList<WebLinkInfo> resolveConflictsLinks =
+ webLinks.getResolveConflictsLinks(
+ project, commit.name(), commit.getFullMessage(), branchName);
+ info.resolveConflictsWebLinks =
+ resolveConflictsLinks.isEmpty() ? null : resolveConflictsLinks;
}
for (RevCommit parent : commit.getParents()) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 339b350..4794858 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -72,6 +72,7 @@
import com.google.gerrit.extensions.webui.ParentWebLink;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.extensions.webui.TagWebLink;
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -392,6 +393,7 @@
DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
DynamicSet.setOf(binder(), PluginPushOption.class);
DynamicSet.setOf(binder(), PatchSetWebLink.class);
+ DynamicSet.setOf(binder(), ResolveConflictsWebLink.class);
DynamicSet.setOf(binder(), ParentWebLink.class);
DynamicSet.setOf(binder(), FileWebLink.class);
DynamicSet.setOf(binder(), FileHistoryWebLink.class);
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index 97cc830..f90a72e 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -33,6 +33,7 @@
import com.google.gerrit.extensions.webui.ParentWebLink;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.extensions.webui.TagWebLink;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
@@ -83,6 +84,7 @@
if (!isNullOrEmpty(type.getRevision())) {
DynamicSet.bind(binder(), PatchSetWebLink.class).to(GitwebLinks.class);
DynamicSet.bind(binder(), ParentWebLink.class).to(GitwebLinks.class);
+ DynamicSet.bind(binder(), ResolveConflictsWebLink.class).to(GitwebLinks.class);
}
if (!isNullOrEmpty(type.getProject())) {
@@ -261,6 +263,7 @@
PatchSetWebLink,
ParentWebLink,
ProjectWebLink,
+ ResolveConflictsWebLink,
TagWebLink {
private final String url;
private final GitwebType type;
@@ -350,6 +353,13 @@
}
@Override
+ public WebLinkInfo getResolveConflictsWebLink(
+ String projectName, String commit, String commitMessage, String branchName) {
+ // For Gitweb treat resolve conflicts links the same as patch set links
+ return getPatchSetWebLink(projectName, commit, commitMessage, branchName);
+ }
+
+ @Override
public WebLinkInfo getParentWebLink(
String projectName, String commit, String commitMessage, String branchName) {
// For Gitweb treat parent revision links the same as patch set links
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index d15a415..3a35d80 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -232,7 +232,14 @@
throws UpdateException, RestApiException {
try (ManualRequestContext ctx = oneOffRequestContext.openAs(sender)) {
List<ChangeData> changeDataList =
- queryProvider.get().byLegacyChangeId(Change.id(metadata.changeNumber));
+ queryProvider
+ .get()
+ .enforceVisibility(true)
+ .byLegacyChangeId(Change.id(metadata.changeNumber));
+ if (changeDataList.isEmpty()) {
+ sendRejectionEmail(message, InboundEmailRejectionSender.Error.CHANGE_NOT_FOUND);
+ return;
+ }
if (changeDataList.size() != 1) {
logger.atSevere().log(
"Message %s references unique change %s,"
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 709bf61..acdeb5a 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -33,7 +33,8 @@
INACTIVE_ACCOUNT,
UNKNOWN_ACCOUNT,
INTERNAL_EXCEPTION,
- COMMENT_REJECTED
+ COMMENT_REJECTED,
+ CHANGE_NOT_FOUND
}
public interface Factory {
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index dd00dca..f800207 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -523,7 +523,10 @@
+ "who also have 'Push' rights on "
+ RefNames.REFS_CONFIG);
} else {
- pde.setAdvice("To push into this reference you need 'Push' rights.");
+ pde.setAdvice(
+ "Push to refs/for/"
+ + RefNames.shortName(refName)
+ + " to create a review, or get 'Push' rights to update the branch.");
}
break;
case DELETE:
diff --git a/java/com/google/gerrit/server/restapi/access/ListAccess.java b/java/com/google/gerrit/server/restapi/access/ListAccess.java
index 1e1bade..dca969d 100644
--- a/java/com/google/gerrit/server/restapi/access/ListAccess.java
+++ b/java/com/google/gerrit/server/restapi/access/ListAccess.java
@@ -14,11 +14,17 @@
package com.google.gerrit.server.restapi.access;
+import com.google.common.base.Strings;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.restapi.project.GetAccess;
import com.google.inject.Inject;
import java.util.ArrayList;
@@ -41,10 +47,15 @@
usage = "projects for which the access rights should be returned")
private List<String> projects = new ArrayList<>();
+ private final PermissionBackend permissionBackend;
+ private final ProjectCache projectCache;
private final GetAccess getAccess;
@Inject
- public ListAccess(GetAccess getAccess) {
+ public ListAccess(
+ PermissionBackend permissionBackend, ProjectCache projectCache, GetAccess getAccess) {
+ this.permissionBackend = permissionBackend;
+ this.projectCache = projectCache;
this.getAccess = getAccess;
}
@@ -53,7 +64,23 @@
throws Exception {
Map<String, ProjectAccessInfo> access = new TreeMap<>();
for (String p : projects) {
- access.put(p, getAccess.apply(Project.nameKey(p)));
+ if (Strings.nullToEmpty(p).isEmpty()) {
+ continue;
+ }
+
+ Project.NameKey projectName = Project.nameKey(p);
+
+ if (!projectCache.get(projectName).isPresent()) {
+ throw new ResourceNotFoundException(projectName.get());
+ }
+
+ try {
+ permissionBackend.currentUser().project(projectName).check(ProjectPermission.ACCESS);
+ } catch (AuthException e) {
+ throw new ResourceNotFoundException(projectName.get(), e);
+ }
+
+ access.put(p, getAccess.apply(projectName));
}
return Response.ok(access);
}
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index edc8fcf..81b6fb3 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -190,7 +190,7 @@
return CommentContextKey.builder()
.project(project)
.changeId(changeId)
- .id(r.id)
+ .id(Url.decode(r.id)) // We reverse the encoding done while filling comment info
.path(r.path)
.patchset(r.patchSet)
.contextPadding(contextPadding)
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 0389f39..20249df 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.change;
import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.server.permissions.ChangePermission.REVERT;
import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
@@ -40,6 +41,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
@@ -64,6 +66,7 @@
import com.google.gerrit.server.project.ContributorAgreementsChecker;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.CherryPickChange.Result;
@@ -98,7 +101,8 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-public class RevertSubmission implements RestModifyView<ChangeResource, RevertInput> {
+public class RevertSubmission
+ implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Provider<InternalChangeQuery> queryProvider;
@@ -513,6 +517,35 @@
}
}
+ @Override
+ public Description getDescription(ChangeResource rsrc) {
+ Change change = rsrc.getChange();
+ boolean projectStatePermitsWrite = false;
+ try {
+ projectStatePermitsWrite =
+ projectCache.get(rsrc.getProject()).map(ProjectState::statePermitsWrite).orElse(false);
+ } catch (StorageException e) {
+ logger.atSevere().withCause(e).log(
+ "Failed to check if project state permits write: %s", rsrc.getProject());
+ }
+ return new UiAction.Description()
+ .setLabel("Revert submission")
+ .setTitle(
+ "Revert this change and all changes that have been submitted together with this change")
+ .setVisible(
+ and(
+ and(
+ change.isMerged()
+ && change.getSubmissionId() != null
+ && isChangePartOfSubmission(change.getSubmissionId())
+ && projectStatePermitsWrite,
+ permissionBackend
+ .user(rsrc.getUser())
+ .ref(change.getDest())
+ .testCond(CREATE_CHANGE)),
+ permissionBackend.user(rsrc.getUser()).change(rsrc.getNotes()).testCond(REVERT)));
+ }
+
/**
* @param submissionId the submission id of the change.
* @return True if the submission has more than one change, false otherwise.
diff --git a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
new file mode 100644
index 0000000..32e8232
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
@@ -0,0 +1,1015 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.schema.AclUtil.grant;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.ConfigSubject.assertThat;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.AccessSection;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.webui.FileHistoryWebLink;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.schema.GrantRevertPermission;
+import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AccessIT extends AbstractDaemonTest {
+
+ private static final String REFS_ALL = Constants.R_REFS + "*";
+ private static final String REFS_HEADS = Constants.R_HEADS + "*";
+ private static final String REFS_META_VERSION = "refs/meta/version";
+ private static final String REFS_DRAFTS = "refs/draft-comments/*";
+ private static final String REFS_STARRED_CHANGES = "refs/starred-changes/*";
+
+ @Inject private ProjectOperations projectOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ExtensionRegistry extensionRegistry;
+ @Inject private GrantRevertPermission grantRevertPermission;
+
+ private Project.NameKey newProjectName;
+
+ @Before
+ public void setUp() throws Exception {
+ newProjectName = projectOperations.newProject().create();
+ }
+
+ @Test
+ public void grantRevertPermission() throws Exception {
+ String ref = "refs/*";
+ String groupId = "global:Registered-Users";
+
+ grantRevertPermission.execute(newProjectName);
+
+ ProjectAccessInfo info = pApi().access();
+ assertThat(info.local.containsKey(ref)).isTrue();
+ AccessSectionInfo accessSectionInfo = info.local.get(ref);
+ assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isTrue();
+ PermissionInfo permissionInfo = accessSectionInfo.permissions.get(Permission.REVERT);
+ assertThat(permissionInfo.rules.containsKey(groupId)).isTrue();
+ PermissionRuleInfo permissionRuleInfo = permissionInfo.rules.get(groupId);
+ assertThat(permissionRuleInfo.action).isEqualTo(PermissionRuleInfo.Action.ALLOW);
+ }
+
+ @Test
+ public void grantRevertPermissionByOnNewRefAndDeletingOnOldRef() throws Exception {
+ String refsHeads = "refs/heads/*";
+ String refsStar = "refs/*";
+ String groupId = "global:Registered-Users";
+ GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
+
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
+ ProjectConfig projectConfig = projectConfigFactory.read(md);
+ projectConfig.upsertAccessSection(
+ AccessSection.HEADS,
+ heads -> {
+ grant(projectConfig, heads, Permission.REVERT, registeredUsers);
+ });
+ md.getCommitBuilder().setAuthor(admin.newIdent());
+ md.getCommitBuilder().setCommitter(admin.newIdent());
+ md.setMessage("Add revert permission for all registered users\n");
+
+ projectConfig.commit(md);
+ }
+ grantRevertPermission.execute(newProjectName);
+
+ ProjectAccessInfo info = pApi().access();
+
+ // Revert permission is removed on refs/heads/*.
+ assertThat(info.local.containsKey(refsHeads)).isTrue();
+ AccessSectionInfo accessSectionInfo = info.local.get(refsHeads);
+ assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isFalse();
+
+ // new permission is added on refs/* with Registered-Users.
+ assertThat(info.local.containsKey(refsStar)).isTrue();
+ accessSectionInfo = info.local.get(refsStar);
+ assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isTrue();
+ PermissionInfo permissionInfo = accessSectionInfo.permissions.get(Permission.REVERT);
+ assertThat(permissionInfo.rules.containsKey(groupId)).isTrue();
+ PermissionRuleInfo permissionRuleInfo = permissionInfo.rules.get(groupId);
+ assertThat(permissionRuleInfo.action).isEqualTo(PermissionRuleInfo.Action.ALLOW);
+ }
+
+ @Test
+ public void grantRevertPermissionDoesntDeleteAdminsPreferences() throws Exception {
+ GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
+ GroupReference otherGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
+
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
+ ProjectConfig projectConfig = projectConfigFactory.read(md);
+ projectConfig.upsertAccessSection(
+ AccessSection.HEADS,
+ heads -> {
+ grant(projectConfig, heads, Permission.REVERT, registeredUsers);
+ grant(projectConfig, heads, Permission.REVERT, otherGroup);
+ });
+ md.getCommitBuilder().setAuthor(admin.newIdent());
+ md.getCommitBuilder().setCommitter(admin.newIdent());
+ md.setMessage("Add revert permission for all registered users\n");
+
+ projectConfig.commit(md);
+ }
+ projectCache.evict(newProjectName);
+ ProjectAccessInfo expected = pApi().access();
+
+ grantRevertPermission.execute(newProjectName);
+ projectCache.evict(newProjectName);
+ ProjectAccessInfo actual = pApi().access();
+ // Permissions don't change
+ assertThat(expected.local).isEqualTo(actual.local);
+ }
+
+ @Test
+ public void grantRevertPermissionOnlyWorksOnce() throws Exception {
+ grantRevertPermission.execute(newProjectName);
+ grantRevertPermission.execute(newProjectName);
+
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
+ ProjectConfig projectConfig = projectConfigFactory.read(md);
+ AccessSection all = projectConfig.getAccessSection(AccessSection.ALL);
+
+ Permission permission = all.getPermission(Permission.REVERT);
+ assertThat(permission.getRules()).hasSize(1);
+ }
+ }
+
+ @Test
+ public void getDefaultInheritance() throws Exception {
+ String inheritedName = pApi().access().inheritsFrom.name;
+ assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
+ }
+
+ private Registration newFileHistoryWebLink() {
+ FileHistoryWebLink weblink =
+ new FileHistoryWebLink() {
+ @Override
+ public WebLinkInfo getFileHistoryWebLink(
+ String projectName, String revision, String fileName) {
+ return new WebLinkInfo(
+ "name", "imageURL", "http://view/" + projectName + "/" + fileName);
+ }
+ };
+ return extensionRegistry.newRegistration().add(weblink);
+ }
+
+ @Test
+ public void webLink() throws Exception {
+ try (Registration registration = newFileHistoryWebLink()) {
+ ProjectAccessInfo info = pApi().access();
+ assertThat(info.configWebLinks).hasSize(1);
+ assertThat(info.configWebLinks.get(0).url)
+ .isEqualTo("http://view/" + newProjectName + "/project.config");
+ }
+ }
+
+ @Test
+ public void webLinkNoRefsMetaConfig() throws Exception {
+ try (Repository repo = repoManager.openRepository(newProjectName);
+ Registration registration = newFileHistoryWebLink()) {
+ RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
+ u.setForceUpdate(true);
+ assertThat(u.delete()).isEqualTo(Result.FORCED);
+
+ // This should not crash.
+ pApi().access();
+ }
+ }
+
+ @Test
+ public void addAccessSection() throws Exception {
+ RevCommit initialHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
+
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ pApi().access(accessInput);
+
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
+
+ RevCommit updatedHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
+ eventRecorder.assertRefUpdatedEvents(
+ newProjectName.get(), RefNames.REFS_CONFIG, null, initialHead, initialHead, updatedHead);
+ }
+
+ @Test
+ public void addAccessSectionForPluginPermission() throws Exception {
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(
+ new PluginProjectPermissionDefinition() {
+ @Override
+ public String getDescription() {
+ return "A Plugin Project Permission";
+ }
+ },
+ "fooPermission")) {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo foo = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ foo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put(
+ "plugin-" + ExtensionRegistry.PLUGIN_NAME + "-fooPermission", foo);
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ ProjectAccessInfo updatedAccessSectionInfo = pApi().access(accessInput);
+ assertThat(updatedAccessSectionInfo.local).isEqualTo(accessInput.add);
+
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
+ }
+ }
+
+ @Test
+ public void addAccessSectionWithInvalidPermission() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put("Invalid Permission", push);
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+ assertThat(ex).hasMessageThat().isEqualTo("Unknown permission: Invalid Permission");
+ }
+
+ @Test
+ public void addAccessSectionWithInvalidLabelPermission() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put("label-Invalid Permission", push);
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+ assertThat(ex).hasMessageThat().isEqualTo("Unknown permission: label-Invalid Permission");
+ }
+
+ @Test
+ public void createAccessChangeNop() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
+ }
+
+ @Test
+ public void createAccessChangeEmptyConfig() throws Exception {
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ RefUpdate ru = repo.updateRef(RefNames.REFS_CONFIG);
+ ru.setForceUpdate(true);
+ assertThat(ru.delete()).isEqualTo(Result.FORCED);
+
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+ PermissionInfo read = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.BLOCK, false);
+ read.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(Permission.READ, read);
+ accessInput.add.put(REFS_HEADS, accessSection);
+
+ ChangeInfo out = pApi().accessChange(accessInput);
+ assertThat(out.status).isEqualTo(ChangeStatus.NEW);
+ }
+ }
+
+ @Test
+ public void createAccessChange() throws Exception {
+ projectOperations
+ .project(newProjectName)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+ // User can see the branch
+ requestScopeOperations.setApiUser(user.id());
+ pApi().branch("refs/heads/master").get();
+
+ ProjectAccessInput accessInput = newProjectAccessInput();
+
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+
+ // Deny read to registered users.
+ PermissionInfo read = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+ read.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ read.exclusive = true;
+ accessSection.permissions.put(Permission.READ, read);
+ accessInput.add.put(REFS_HEADS, accessSection);
+
+ requestScopeOperations.setApiUser(user.id());
+ ChangeInfo out = pApi().accessChange(accessInput);
+
+ assertThat(out.project).isEqualTo(newProjectName.get());
+ assertThat(out.branch).isEqualTo(RefNames.REFS_CONFIG);
+ assertThat(out.status).isEqualTo(ChangeStatus.NEW);
+ assertThat(out.submitted).isNull();
+
+ requestScopeOperations.setApiUser(admin.id());
+
+ ChangeInfo c = gApi.changes().id(out._number).get(MESSAGES);
+ assertThat(c.messages.stream().map(m -> m.message)).containsExactly("Uploaded patch set 1");
+
+ ReviewInput reviewIn = new ReviewInput();
+ reviewIn.label("Code-Review", (short) 2);
+ gApi.changes().id(out._number).current().review(reviewIn);
+ gApi.changes().id(out._number).current().submit();
+
+ // check that the change took effect.
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(ResourceNotFoundException.class, () -> pApi().branch("refs/heads/master").get());
+
+ // Restore.
+ accessInput.add.clear();
+ accessInput.remove.put(REFS_HEADS, accessSection);
+ requestScopeOperations.setApiUser(user.id());
+
+ requestScopeOperations.setApiUser(admin.id());
+ out = pApi().accessChange(accessInput);
+
+ gApi.changes().id(out._number).current().review(reviewIn);
+ gApi.changes().id(out._number).current().submit();
+
+ // Now it works again.
+ requestScopeOperations.setApiUser(user.id());
+ pApi().branch("refs/heads/master").get();
+ }
+
+ @Test
+ public void removePermission() throws Exception {
+ // Add initial permission set
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ pApi().access(accessInput);
+
+ // Remove specific permission
+ AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
+ accessSectionToRemove.permissions.put(
+ Permission.LABEL + LabelId.CODE_REVIEW, newPermissionInfo());
+ ProjectAccessInput removal = newProjectAccessInput();
+ removal.remove.put(REFS_HEADS, accessSectionToRemove);
+ pApi().access(removal);
+
+ // Remove locally
+ accessInput.add.get(REFS_HEADS).permissions.remove(Permission.LABEL + LabelId.CODE_REVIEW);
+
+ // Check
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
+ }
+
+ @Test
+ public void removePermissionRule() throws Exception {
+ // Add initial permission set
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ pApi().access(accessInput);
+
+ // Remove specific permission rule
+ AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
+ PermissionInfo codeReview = newPermissionInfo();
+ codeReview.label = LabelId.CODE_REVIEW;
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+ codeReview.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionToRemove.permissions.put(Permission.LABEL + LabelId.CODE_REVIEW, codeReview);
+ ProjectAccessInput removal = newProjectAccessInput();
+ removal.remove.put(REFS_HEADS, accessSectionToRemove);
+ pApi().access(removal);
+
+ // Remove locally
+ accessInput
+ .add
+ .get(REFS_HEADS)
+ .permissions
+ .get(Permission.LABEL + LabelId.CODE_REVIEW)
+ .rules
+ .remove(SystemGroupBackend.REGISTERED_USERS.get());
+
+ // Check
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
+ }
+
+ @Test
+ public void removePermissionRulesAndCleanupEmptyEntries() throws Exception {
+ // Add initial permission set
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ pApi().access(accessInput);
+
+ // Remove specific permission rules
+ AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
+ PermissionInfo codeReview = newPermissionInfo();
+ codeReview.label = LabelId.CODE_REVIEW;
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+ codeReview.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+ codeReview.rules.put(SystemGroupBackend.PROJECT_OWNERS.get(), pri);
+ accessSectionToRemove.permissions.put(Permission.LABEL + LabelId.CODE_REVIEW, codeReview);
+ ProjectAccessInput removal = newProjectAccessInput();
+ removal.remove.put(REFS_HEADS, accessSectionToRemove);
+ pApi().access(removal);
+
+ // Remove locally
+ accessInput.add.get(REFS_HEADS).permissions.remove(Permission.LABEL + LabelId.CODE_REVIEW);
+
+ // Check
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
+ }
+
+ @Test
+ public void getPermissionsWithDisallowedUser() throws Exception {
+ // Add initial permission set
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createAccessSectionInfoDenyAll();
+
+ // Disallow READ
+ accessInput.add.put(REFS_META_VERSION, accessSectionInfo);
+ accessInput.add.put(REFS_DRAFTS, accessSectionInfo);
+ accessInput.add.put(REFS_STARRED_CHANGES, accessSectionInfo);
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ pApi().access(accessInput);
+
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(ResourceNotFoundException.class, () -> pApi().access());
+ }
+
+ @Test
+ public void setPermissionsWithDisallowedUser() throws Exception {
+ // Add initial permission set
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createAccessSectionInfoDenyAll();
+
+ // Disallow READ
+ accessInput.add.put(REFS_META_VERSION, accessSectionInfo);
+ accessInput.add.put(REFS_DRAFTS, accessSectionInfo);
+ accessInput.add.put(REFS_STARRED_CHANGES, accessSectionInfo);
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ pApi().access(accessInput);
+
+ // Create a change to apply
+ ProjectAccessInput accessInfoToApply = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfoToApply = createDefaultAccessSectionInfo();
+ accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
+
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(ResourceNotFoundException.class, () -> pApi().access());
+ }
+
+ @Test
+ public void permissionsGroupMap() throws Exception {
+ // Add initial permission set
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.PROJECT_OWNERS.get(), pri);
+ accessSection.permissions.put(Permission.PUSH, push);
+
+ PermissionInfo read = newPermissionInfo();
+ pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ read.rules.put(SystemGroupBackend.ANONYMOUS_USERS.get(), pri);
+ accessSection.permissions.put(Permission.READ, read);
+
+ accessInput.add.put(REFS_ALL, accessSection);
+ ProjectAccessInfo result = pApi().access(accessInput);
+ assertThatMap(result.groups)
+ .keys()
+ .containsExactly(
+ SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
+
+ // Check the name, which is what the UI cares about; exhaustive
+ // coverage of GroupInfo should be in groups REST API tests.
+ assertThat(result.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).name)
+ .isEqualTo("Project Owners");
+ // Strip the ID, since it is in the key.
+ assertThat(result.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).id).isNull();
+
+ // Get call returns groups too.
+ ProjectAccessInfo loggedInResult = pApi().access();
+ assertThatMap(loggedInResult.groups)
+ .keys()
+ .containsExactly(
+ SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
+
+ GroupInfo owners = loggedInResult.groups.get(SystemGroupBackend.PROJECT_OWNERS.get());
+ assertThat(owners.name).isEqualTo("Project Owners");
+ assertThat(owners.id).isNull();
+ assertThat(owners.members).isNull();
+ assertThat(owners.includes).isNull();
+
+ // PROJECT_OWNERS is invisible to anonymous user, but GetAccess disregards visibility.
+ requestScopeOperations.setApiUserAnonymous();
+ ProjectAccessInfo anonResult = pApi().access();
+ assertThatMap(anonResult.groups)
+ .keys()
+ .containsExactly(
+ SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
+ }
+
+ @Test
+ public void updateParentAsUser() throws Exception {
+ // Create child
+ String newParentProjectName = projectOperations.newProject().create().get();
+
+ // Set new parent
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ accessInput.parent = newParentProjectName;
+
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown = assertThrows(AuthException.class, () -> pApi().access(accessInput));
+ assertThat(thrown).hasMessageThat().contains("administrate server not permitted");
+ }
+
+ @Test
+ public void updateParentAsAdministrator() throws Exception {
+ // Create parent
+ String newParentProjectName = projectOperations.newProject().create().get();
+
+ // Set new parent
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ accessInput.parent = newParentProjectName;
+
+ pApi().access(accessInput);
+
+ assertThat(pApi().access().inheritsFrom.name).isEqualTo(newParentProjectName);
+ }
+
+ @Test
+ public void addGlobalCapabilityAsUser() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(
+ AuthException.class, () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ }
+
+ @Test
+ public void addGlobalCapabilityAsAdmin() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ ProjectAccessInfo updatedAccessSectionInfo =
+ gApi.projects().name(allProjects.get()).access(accessInput);
+ assertThatMap(updatedAccessSectionInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+ .keys()
+ .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
+ }
+
+ @Test
+ public void addPluginGlobalCapability() throws Exception {
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(
+ new CapabilityDefinition() {
+ @Override
+ public String getDescription() {
+ return "A Plugin Global Capability";
+ }
+ },
+ "fooCapability")) {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo foo = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ foo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put(ExtensionRegistry.PLUGIN_NAME + "-fooCapability", foo);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ ProjectAccessInfo updatedAccessSectionInfo =
+ gApi.projects().name(allProjects.get()).access(accessInput);
+ assertThatMap(
+ updatedAccessSectionInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+ .keys()
+ .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
+ }
+ }
+
+ @Test
+ public void addPermissionAsGlobalCapability() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put(Permission.PUSH, push);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ assertThat(ex).hasMessageThat().isEqualTo("Unknown global capability: " + Permission.PUSH);
+ }
+
+ @Test
+ public void addInvalidGlobalCapability() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+ PermissionInfo permissionInfo = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ permissionInfo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put("Invalid Global Capability", permissionInfo);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ assertThat(ex)
+ .hasMessageThat()
+ .isEqualTo("Unknown global capability: Invalid Global Capability");
+ }
+
+ @Test
+ public void addGlobalCapabilityForNonRootProject() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+ }
+
+ @Test
+ public void addNonGlobalCapabilityToGlobalCapabilities() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo permissionInfo = newPermissionInfo();
+ permissionInfo.rules.put(adminGroupUuid().get(), null);
+ accessSectionInfo.permissions.put(Permission.PUSH, permissionInfo);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ }
+
+ @Test
+ public void removeGlobalCapabilityAsUser() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+ accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(
+ AuthException.class, () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ }
+
+ @Test
+ public void removeGlobalCapabilityAsAdmin() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo permissionInfo = newPermissionInfo();
+ permissionInfo.rules.put(adminGroupUuid().get(), null);
+ accessSectionInfo.permissions.put(GlobalCapability.ACCESS_DATABASE, permissionInfo);
+
+ // Add and validate first as removing existing privileges such as
+ // administrateServer would break upcoming tests
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ ProjectAccessInfo updatedProjectAccessInfo =
+ gApi.projects().name(allProjects.get()).access(accessInput);
+ assertThatMap(updatedProjectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+ .keys()
+ .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
+
+ // Remove
+ accessInput.add.clear();
+ accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ updatedProjectAccessInfo = gApi.projects().name(allProjects.get()).access(accessInput);
+ assertThatMap(updatedProjectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+ .keys()
+ .containsNoneIn(accessSectionInfo.permissions.keySet());
+ }
+
+ @Test
+ public void unknownPermissionRemainsUnchanged() throws Exception {
+ String access = "access";
+ String unknownPermission = "unknownPermission";
+ String registeredUsers = "group Registered Users";
+ String refsFor = "refs/for/*";
+ // Clone repository to forcefully add permission
+ TestRepository<InMemoryRepository> allProjectsRepo = cloneProject(allProjects, admin);
+
+ // Fetch permission ref
+ GitUtil.fetch(allProjectsRepo, "refs/meta/config:cfg");
+ allProjectsRepo.reset("cfg");
+
+ // Load current permissions
+ String config =
+ gApi.projects()
+ .name(allProjects.get())
+ .branch(RefNames.REFS_CONFIG)
+ .file(ProjectConfig.PROJECT_CONFIG)
+ .asString();
+
+ // Append and push unknown permission
+ Config cfg = new Config();
+ cfg.fromText(config);
+ cfg.setString(access, refsFor, unknownPermission, registeredUsers);
+ config = cfg.toText();
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
+ push.to(RefNames.REFS_CONFIG).assertOkStatus();
+
+ // Verify that unknownPermission is present
+ config =
+ gApi.projects()
+ .name(allProjects.get())
+ .branch(RefNames.REFS_CONFIG)
+ .file(ProjectConfig.PROJECT_CONFIG)
+ .asString();
+ cfg.fromText(config);
+ assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
+
+ // Make permission change through API
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+ accessInput.add.put(refsFor, accessSectionInfo);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+ accessInput.add.clear();
+ accessInput.remove.put(refsFor, accessSectionInfo);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Verify that unknownPermission is still present
+ config =
+ gApi.projects()
+ .name(allProjects.get())
+ .branch(RefNames.REFS_CONFIG)
+ .file(ProjectConfig.PROJECT_CONFIG)
+ .asString();
+ cfg.fromText(config);
+ assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
+ }
+
+ @Test
+ public void allUsersCanOnlyInheritFromAllProjects() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ accessInput.parent = project.get();
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allUsers.get()).access(accessInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(allUsers.get() + " must inherit from " + allProjects.get());
+ }
+
+ @Test
+ public void syncCreateGroupPermission_addAndRemoveCreateGroupCapability() throws Exception {
+ // Grant CREATE_GROUP to Registered Users
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+ PermissionInfo createGroup = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ createGroup.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Assert that the permission was synced from All-Projects (global) to All-Users (ref)
+ Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
+ assertThatMap(local).keys().contains(RefNames.REFS_GROUPS + "*");
+ Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
+ // READ is the default permission and should be preserved by the syncer
+ assertThatMap(permissions).keys().containsExactly(Permission.READ, Permission.CREATE);
+ Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
+ assertThatMap(rules).values().containsExactly(pri);
+
+ // Revoke the permission
+ accessInput.add.clear();
+ accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Assert that the permission was synced from All-Projects (global) to All-Users (ref)
+ Map<String, AccessSectionInfo> local2 = gApi.projects().name("All-Users").access().local;
+ assertThatMap(local2).keys().contains(RefNames.REFS_GROUPS + "*");
+ Map<String, PermissionInfo> permissions2 = local2.get(RefNames.REFS_GROUPS + "*").permissions;
+ // READ is the default permission and should be preserved by the syncer
+ assertThatMap(permissions2).keys().containsExactly(Permission.READ);
+ }
+
+ @Test
+ public void syncCreateGroupPermission_addCreateGroupCapabilityToMultipleGroups()
+ throws Exception {
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+
+ // Grant CREATE_GROUP to Registered Users
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+ PermissionInfo createGroup = newPermissionInfo();
+ createGroup.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Grant CREATE_GROUP to Administrators
+ accessInput = newProjectAccessInput();
+ accessSection = newAccessSectionInfo();
+ createGroup = newPermissionInfo();
+ createGroup.rules.put(adminGroupUuid().get(), pri);
+ accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Assert that the permissions were synced from All-Projects (global) to All-Users (ref)
+ Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
+ assertThatMap(local).keys().contains(RefNames.REFS_GROUPS + "*");
+ Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
+ // READ is the default permission and should be preserved by the syncer
+ assertThatMap(permissions).keys().containsExactly(Permission.READ, Permission.CREATE);
+ Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
+ assertThatMap(rules)
+ .keys()
+ .containsExactly(SystemGroupBackend.REGISTERED_USERS.get(), adminGroupUuid().get());
+ assertThat(rules.get(SystemGroupBackend.REGISTERED_USERS.get())).isEqualTo(pri);
+ assertThat(rules.get(adminGroupUuid().get())).isEqualTo(pri);
+ }
+
+ @Test
+ public void addAccessSectionForInvalidRef() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ // 'refs/heads/stable_*' is invalid, correct would be '^refs/heads/stable_.*'
+ String invalidRef = Constants.R_HEADS + "stable_*";
+ accessInput.add.put(invalidRef, accessSectionInfo);
+
+ BadRequestException thrown =
+ assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+ assertThat(thrown).hasMessageThat().contains("Invalid Name: " + invalidRef);
+ }
+
+ @Test
+ public void createAccessChangeWithAccessSectionForInvalidRef() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ // 'refs/heads/stable_*' is invalid, correct would be '^refs/heads/stable_.*'
+ String invalidRef = Constants.R_HEADS + "stable_*";
+ accessInput.add.put(invalidRef, accessSectionInfo);
+
+ BadRequestException thrown =
+ assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
+ assertThat(thrown).hasMessageThat().contains("Invalid Name: " + invalidRef);
+ }
+
+ private ProjectApi pApi() throws Exception {
+ return gApi.projects().name(newProjectName.get());
+ }
+
+ private ProjectAccessInput newProjectAccessInput() {
+ ProjectAccessInput p = new ProjectAccessInput();
+ p.add = new HashMap<>();
+ p.remove = new HashMap<>();
+ return p;
+ }
+
+ private PermissionInfo newPermissionInfo() {
+ PermissionInfo p = new PermissionInfo(null, null);
+ p.rules = new HashMap<>();
+ return p;
+ }
+
+ private AccessSectionInfo newAccessSectionInfo() {
+ AccessSectionInfo a = new AccessSectionInfo();
+ a.permissions = new HashMap<>();
+ return a;
+ }
+
+ private AccessSectionInfo createDefaultAccessSectionInfo() {
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(Permission.PUSH, push);
+
+ PermissionInfo codeReview = newPermissionInfo();
+ codeReview.label = LabelId.CODE_REVIEW;
+ pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+ codeReview.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+
+ pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ pri.max = 1;
+ pri.min = -1;
+ codeReview.rules.put(SystemGroupBackend.PROJECT_OWNERS.get(), pri);
+ accessSection.permissions.put(Permission.LABEL + LabelId.CODE_REVIEW, codeReview);
+
+ return accessSection;
+ }
+
+ private AccessSectionInfo createDefaultGlobalCapabilitiesAccessSectionInfo() {
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+
+ PermissionInfo email = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ email.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(GlobalCapability.EMAIL_REVIEWERS, email);
+
+ return accessSection;
+ }
+
+ private AccessSectionInfo createAccessSectionInfoDenyAll() {
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+
+ PermissionInfo read = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+ read.rules.put(SystemGroupBackend.ANONYMOUS_USERS.get(), pri);
+ accessSection.permissions.put(Permission.READ, read);
+
+ return accessSection;
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 86385da..9d0b1f4 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -93,6 +93,7 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
+import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.FakeEmailSender;
@@ -1633,16 +1634,26 @@
@Test
public void commit() throws Exception {
- WebLinkInfo expectedWebLinkInfo = new WebLinkInfo("foo", "imageUrl", "url");
- PatchSetWebLink link =
+ WebLinkInfo expectedPatchSetLinkInfo = new WebLinkInfo("foo", "imageUrl", "url");
+ PatchSetWebLink patchSetLink =
new PatchSetWebLink() {
@Override
public WebLinkInfo getPatchSetWebLink(
String projectName, String commit, String commitMessage, String branchName) {
- return expectedWebLinkInfo;
+ return expectedPatchSetLinkInfo;
}
};
- try (Registration registration = extensionRegistry.newRegistration().add(link)) {
+ WebLinkInfo expectedResolveConflictsLinkInfo = new WebLinkInfo("bar", "img", "resolve");
+ ResolveConflictsWebLink resolveConflictsLink =
+ new ResolveConflictsWebLink() {
+ @Override
+ public WebLinkInfo getResolveConflictsWebLink(
+ String projectName, String commit, String commitMessage, String branchName) {
+ return expectedResolveConflictsLinkInfo;
+ }
+ };
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(patchSetLink).add(resolveConflictsLink)) {
PushOneCommit.Result r = createChange();
RevCommit c = r.getCommit();
@@ -1659,11 +1670,20 @@
commitInfo = gApi.changes().id(r.getChangeId()).current().commit(true);
assertThat(commitInfo.webLinks).hasSize(1);
- WebLinkInfo webLinkInfo = Iterables.getOnlyElement(commitInfo.webLinks);
- assertThat(webLinkInfo.name).isEqualTo(expectedWebLinkInfo.name);
- assertThat(webLinkInfo.imageUrl).isEqualTo(expectedWebLinkInfo.imageUrl);
- assertThat(webLinkInfo.url).isEqualTo(expectedWebLinkInfo.url);
- assertThat(webLinkInfo.target).isEqualTo(expectedWebLinkInfo.target);
+ WebLinkInfo patchSetLinkInfo = Iterables.getOnlyElement(commitInfo.webLinks);
+ assertThat(patchSetLinkInfo.name).isEqualTo(expectedPatchSetLinkInfo.name);
+ assertThat(patchSetLinkInfo.imageUrl).isEqualTo(expectedPatchSetLinkInfo.imageUrl);
+ assertThat(patchSetLinkInfo.url).isEqualTo(expectedPatchSetLinkInfo.url);
+ assertThat(patchSetLinkInfo.target).isEqualTo(expectedPatchSetLinkInfo.target);
+
+ assertThat(commitInfo.resolveConflictsWebLinks).hasSize(1);
+ WebLinkInfo resolveCommentsLinkInfo =
+ Iterables.getOnlyElement(commitInfo.resolveConflictsWebLinks);
+ assertThat(resolveCommentsLinkInfo.name).isEqualTo(expectedResolveConflictsLinkInfo.name);
+ assertThat(resolveCommentsLinkInfo.imageUrl)
+ .isEqualTo(expectedResolveConflictsLinkInfo.imageUrl);
+ assertThat(resolveCommentsLinkInfo.url).isEqualTo(expectedResolveConflictsLinkInfo.url);
+ assertThat(resolveCommentsLinkInfo.target).isEqualTo(expectedResolveConflictsLinkInfo.target);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 385780b..9d1bdaa 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -103,7 +103,7 @@
assertThat(r)
.hasMessages(
"error: branch refs/heads/master:",
- "To push into this reference you need 'Push' rights.",
+ "Push to refs/for/master to create a review, or get 'Push' rights to update the branch.",
"User: admin",
"Contact an administrator to fix the permissions");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
@@ -183,7 +183,7 @@
"You need 'Delete Reference' rights or 'Push' rights with the ",
"'Force Push' flag set to delete references.",
"error: branch refs/heads/master:",
- "To push into this reference you need 'Push' rights.",
+ "Push to refs/for/master to create a review, or get 'Push' rights to update the branch.",
"User: admin",
"Contact an administrator to fix the permissions");
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 4b33664..e35f758 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -62,12 +62,26 @@
}
@Test
- public void mergedChangeActionHasRevert() throws Exception {
+ public void changeActionOneMergedChangeHasOnlyNormalRevert() throws Exception {
String changeId = createChangeWithTopic().getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
gApi.changes().id(changeId).current().submit();
Map<String, ActionInfo> actions = getChangeActions(changeId);
assertThat(actions).containsKey("revert");
+ assertThat(actions).doesNotContainKey("revert_submission");
+ }
+
+ @Test
+ public void changeActionTwoMergedChangesHaveReverts() throws Exception {
+ String changeId1 = createChangeWithTopic().getChangeId();
+ String changeId2 = createChangeWithTopic().getChangeId();
+ gApi.changes().id(changeId1).current().review(ReviewInput.approve());
+ gApi.changes().id(changeId2).current().review(ReviewInput.approve());
+ gApi.changes().id(changeId2).current().submit();
+ Map<String, ActionInfo> actions1 = getChangeActions(changeId1);
+ assertThatMap(actions1).keys().containsAtLeast("revert", "revert_submission");
+ Map<String, ActionInfo> actions2 = getChangeActions(changeId2);
+ assertThatMap(actions2).keys().containsAtLeast("revert", "revert_submission");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index c08c5b6..d93d3f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index ff4f203..b99c624 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,1004 +11,73 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+
package com.google.gerrit.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
-import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
-import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+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.schema.AclUtil.grant;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.google.gerrit.truth.ConfigSubject.assertThat;
-import static com.google.gerrit.truth.MapSubject.assertThatMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.ExtensionRegistry;
-import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
-import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.entities.AccessSection;
-import com.google.gerrit.entities.GroupReference;
-import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.Permission;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.access.AccessSectionInfo;
-import com.google.gerrit.extensions.api.access.PermissionInfo;
-import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
-import com.google.gerrit.extensions.api.access.ProjectAccessInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.projects.ProjectApi;
-import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.config.CapabilityDefinition;
-import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.webui.FileHistoryWebLink;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.schema.GrantRevertPermission;
+import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
-import java.util.HashMap;
import java.util.Map;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.Before;
import org.junit.Test;
public class AccessIT extends AbstractDaemonTest {
-
- private static final String REFS_ALL = Constants.R_REFS + "*";
- private static final String REFS_HEADS = Constants.R_HEADS + "*";
- private static final String REFS_META_VERSION = "refs/meta/version";
- private static final String REFS_DRAFTS = "refs/draft-comments/*";
- private static final String REFS_STARRED_CHANGES = "refs/starred-changes/*";
-
@Inject private ProjectOperations projectOperations;
- @Inject private RequestScopeOperations requestScopeOperations;
- @Inject private ExtensionRegistry extensionRegistry;
- @Inject private GrantRevertPermission grantRevertPermission;
- private Project.NameKey newProjectName;
-
- @Before
- public void setUp() throws Exception {
- newProjectName = projectOperations.newProject().create();
+ @Test
+ public void listAccessWithoutSpecifyingProject() throws Exception {
+ RestResponse r = adminRestSession.get("/access/");
+ r.assertOK();
+ Map<String, ProjectAccessInfo> infoByProject =
+ newGson()
+ .fromJson(r.getReader(), new TypeToken<Map<String, ProjectAccessInfo>>() {}.getType());
+ assertThat(infoByProject).isEmpty();
}
@Test
- public void grantRevertPermission() throws Exception {
- String ref = "refs/*";
- String groupId = "global:Registered-Users";
-
- grantRevertPermission.execute(newProjectName);
-
- ProjectAccessInfo info = pApi().access();
- assertThat(info.local.containsKey(ref)).isTrue();
- AccessSectionInfo accessSectionInfo = info.local.get(ref);
- assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isTrue();
- PermissionInfo permissionInfo = accessSectionInfo.permissions.get(Permission.REVERT);
- assertThat(permissionInfo.rules.containsKey(groupId)).isTrue();
- PermissionRuleInfo permissionRuleInfo = permissionInfo.rules.get(groupId);
- assertThat(permissionRuleInfo.action).isEqualTo(PermissionRuleInfo.Action.ALLOW);
+ public void listAccessWithoutSpecifyingAnEmptyProjectName() throws Exception {
+ RestResponse r = adminRestSession.get("/access/?p=");
+ r.assertOK();
+ Map<String, ProjectAccessInfo> infoByProject =
+ newGson()
+ .fromJson(r.getReader(), new TypeToken<Map<String, ProjectAccessInfo>>() {}.getType());
+ assertThat(infoByProject).isEmpty();
}
@Test
- public void grantRevertPermissionByOnNewRefAndDeletingOnOldRef() throws Exception {
- String refsHeads = "refs/heads/*";
- String refsStar = "refs/*";
- String groupId = "global:Registered-Users";
- GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
-
- try (Repository repo = repoManager.openRepository(newProjectName)) {
- MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
- ProjectConfig projectConfig = projectConfigFactory.read(md);
- projectConfig.upsertAccessSection(
- AccessSection.HEADS,
- heads -> {
- grant(projectConfig, heads, Permission.REVERT, registeredUsers);
- });
- md.getCommitBuilder().setAuthor(admin.newIdent());
- md.getCommitBuilder().setCommitter(admin.newIdent());
- md.setMessage("Add revert permission for all registered users\n");
-
- projectConfig.commit(md);
- }
- grantRevertPermission.execute(newProjectName);
-
- ProjectAccessInfo info = pApi().access();
-
- // Revert permission is removed on refs/heads/*.
- assertThat(info.local.containsKey(refsHeads)).isTrue();
- AccessSectionInfo accessSectionInfo = info.local.get(refsHeads);
- assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isFalse();
-
- // new permission is added on refs/* with Registered-Users.
- assertThat(info.local.containsKey(refsStar)).isTrue();
- accessSectionInfo = info.local.get(refsStar);
- assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isTrue();
- PermissionInfo permissionInfo = accessSectionInfo.permissions.get(Permission.REVERT);
- assertThat(permissionInfo.rules.containsKey(groupId)).isTrue();
- PermissionRuleInfo permissionRuleInfo = permissionInfo.rules.get(groupId);
- assertThat(permissionRuleInfo.action).isEqualTo(PermissionRuleInfo.Action.ALLOW);
+ public void listAccessForNonExistingProject() throws Exception {
+ RestResponse r = adminRestSession.get("/access/?project=non-existing");
+ r.assertNotFound();
+ assertThat(r.getEntityContent()).isEqualTo("non-existing");
}
@Test
- public void grantRevertPermissionDoesntDeleteAdminsPreferences() throws Exception {
- GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
- GroupReference otherGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
-
- try (Repository repo = repoManager.openRepository(newProjectName)) {
- MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
- ProjectConfig projectConfig = projectConfigFactory.read(md);
- projectConfig.upsertAccessSection(
- AccessSection.HEADS,
- heads -> {
- grant(projectConfig, heads, Permission.REVERT, registeredUsers);
- grant(projectConfig, heads, Permission.REVERT, otherGroup);
- });
- md.getCommitBuilder().setAuthor(admin.newIdent());
- md.getCommitBuilder().setCommitter(admin.newIdent());
- md.setMessage("Add revert permission for all registered users\n");
-
- projectConfig.commit(md);
- }
- projectCache.evict(newProjectName);
- ProjectAccessInfo expected = pApi().access();
-
- grantRevertPermission.execute(newProjectName);
- projectCache.evict(newProjectName);
- ProjectAccessInfo actual = pApi().access();
- // Permissions don't change
- assertThat(expected.local).isEqualTo(actual.local);
- }
-
- @Test
- public void grantRevertPermissionOnlyWorksOnce() throws Exception {
- grantRevertPermission.execute(newProjectName);
- grantRevertPermission.execute(newProjectName);
-
- try (Repository repo = repoManager.openRepository(newProjectName)) {
- MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
- ProjectConfig projectConfig = projectConfigFactory.read(md);
- AccessSection all = projectConfig.getAccessSection(AccessSection.ALL);
-
- Permission permission = all.getPermission(Permission.REVERT);
- assertThat(permission.getRules()).hasSize(1);
- }
- }
-
- @Test
- public void getDefaultInheritance() throws Exception {
- String inheritedName = pApi().access().inheritsFrom.name;
- assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
- }
-
- private Registration newFileHistoryWebLink() {
- FileHistoryWebLink weblink =
- new FileHistoryWebLink() {
- @Override
- public WebLinkInfo getFileHistoryWebLink(
- String projectName, String revision, String fileName) {
- return new WebLinkInfo(
- "name", "imageURL", "http://view/" + projectName + "/" + fileName);
- }
- };
- return extensionRegistry.newRegistration().add(weblink);
- }
-
- @Test
- public void webLink() throws Exception {
- try (Registration registration = newFileHistoryWebLink()) {
- ProjectAccessInfo info = pApi().access();
- assertThat(info.configWebLinks).hasSize(1);
- assertThat(info.configWebLinks.get(0).url)
- .isEqualTo("http://view/" + newProjectName + "/project.config");
- }
- }
-
- @Test
- public void webLinkNoRefsMetaConfig() throws Exception {
- try (Repository repo = repoManager.openRepository(newProjectName);
- Registration registration = newFileHistoryWebLink()) {
- RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
- u.setForceUpdate(true);
- assertThat(u.delete()).isEqualTo(Result.FORCED);
-
- // This should not crash.
- pApi().access();
- }
- }
-
- @Test
- public void addAccessSection() throws Exception {
- RevCommit initialHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
-
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi().access(accessInput);
-
- assertThat(pApi().access().local).isEqualTo(accessInput.add);
-
- RevCommit updatedHead = projectOperations.project(newProjectName).getHead(RefNames.REFS_CONFIG);
- eventRecorder.assertRefUpdatedEvents(
- newProjectName.get(), RefNames.REFS_CONFIG, null, initialHead, initialHead, updatedHead);
- }
-
- @Test
- public void addAccessSectionForPluginPermission() throws Exception {
- try (Registration registration =
- extensionRegistry
- .newRegistration()
- .add(
- new PluginProjectPermissionDefinition() {
- @Override
- public String getDescription() {
- return "A Plugin Project Permission";
- }
- },
- "fooPermission")) {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
-
- PermissionInfo foo = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- foo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions.put(
- "plugin-" + ExtensionRegistry.PLUGIN_NAME + "-fooPermission", foo);
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- ProjectAccessInfo updatedAccessSectionInfo = pApi().access(accessInput);
- assertThat(updatedAccessSectionInfo.local).isEqualTo(accessInput.add);
-
- assertThat(pApi().access().local).isEqualTo(accessInput.add);
- }
- }
-
- @Test
- public void addAccessSectionWithInvalidPermission() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- PermissionInfo push = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions.put("Invalid Permission", push);
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- BadRequestException ex =
- assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
- assertThat(ex).hasMessageThat().isEqualTo("Unknown permission: Invalid Permission");
- }
-
- @Test
- public void addAccessSectionWithInvalidLabelPermission() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- PermissionInfo push = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions.put("label-Invalid Permission", push);
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- BadRequestException ex =
- assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
- assertThat(ex).hasMessageThat().isEqualTo("Unknown permission: label-Invalid Permission");
- }
-
- @Test
- public void createAccessChangeNop() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
- }
-
- @Test
- public void createAccessChangeEmptyConfig() throws Exception {
- try (Repository repo = repoManager.openRepository(newProjectName)) {
- RefUpdate ru = repo.updateRef(RefNames.REFS_CONFIG);
- ru.setForceUpdate(true);
- assertThat(ru.delete()).isEqualTo(Result.FORCED);
-
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSection = newAccessSectionInfo();
- PermissionInfo read = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.BLOCK, false);
- read.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSection.permissions.put(Permission.READ, read);
- accessInput.add.put(REFS_HEADS, accessSection);
-
- ChangeInfo out = pApi().accessChange(accessInput);
- assertThat(out.status).isEqualTo(ChangeStatus.NEW);
- }
- }
-
- @Test
- public void createAccessChange() throws Exception {
+ public void listAccessForNonVisibleProject() throws Exception {
projectOperations
- .project(newProjectName)
+ .project(project)
.forUpdate()
- .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
- // User can see the branch
- requestScopeOperations.setApiUser(user.id());
- pApi().branch("refs/heads/master").get();
- ProjectAccessInput accessInput = newProjectAccessInput();
-
- AccessSectionInfo accessSection = newAccessSectionInfo();
-
- // Deny read to registered users.
- PermissionInfo read = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
- read.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- read.exclusive = true;
- accessSection.permissions.put(Permission.READ, read);
- accessInput.add.put(REFS_HEADS, accessSection);
-
- requestScopeOperations.setApiUser(user.id());
- ChangeInfo out = pApi().accessChange(accessInput);
-
- assertThat(out.project).isEqualTo(newProjectName.get());
- assertThat(out.branch).isEqualTo(RefNames.REFS_CONFIG);
- assertThat(out.status).isEqualTo(ChangeStatus.NEW);
- assertThat(out.submitted).isNull();
-
- requestScopeOperations.setApiUser(admin.id());
-
- ChangeInfo c = gApi.changes().id(out._number).get(MESSAGES);
- assertThat(c.messages.stream().map(m -> m.message)).containsExactly("Uploaded patch set 1");
-
- ReviewInput reviewIn = new ReviewInput();
- reviewIn.label("Code-Review", (short) 2);
- gApi.changes().id(out._number).current().review(reviewIn);
- gApi.changes().id(out._number).current().submit();
-
- // check that the change took effect.
- requestScopeOperations.setApiUser(user.id());
- assertThrows(ResourceNotFoundException.class, () -> pApi().branch("refs/heads/master").get());
-
- // Restore.
- accessInput.add.clear();
- accessInput.remove.put(REFS_HEADS, accessSection);
- requestScopeOperations.setApiUser(user.id());
-
- requestScopeOperations.setApiUser(admin.id());
- out = pApi().accessChange(accessInput);
-
- gApi.changes().id(out._number).current().review(reviewIn);
- gApi.changes().id(out._number).current().submit();
-
- // Now it works again.
- requestScopeOperations.setApiUser(user.id());
- pApi().branch("refs/heads/master").get();
+ RestResponse r = userRestSession.get("/access/?project=" + project.get());
+ r.assertNotFound();
+ assertThat(r.getEntityContent()).isEqualTo(project.get());
}
@Test
- public void removePermission() throws Exception {
- // Add initial permission set
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi().access(accessInput);
-
- // Remove specific permission
- AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
- accessSectionToRemove.permissions.put(
- Permission.LABEL + LabelId.CODE_REVIEW, newPermissionInfo());
- ProjectAccessInput removal = newProjectAccessInput();
- removal.remove.put(REFS_HEADS, accessSectionToRemove);
- pApi().access(removal);
-
- // Remove locally
- accessInput.add.get(REFS_HEADS).permissions.remove(Permission.LABEL + LabelId.CODE_REVIEW);
-
- // Check
- assertThat(pApi().access().local).isEqualTo(accessInput.add);
- }
-
- @Test
- public void removePermissionRule() throws Exception {
- // Add initial permission set
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi().access(accessInput);
-
- // Remove specific permission rule
- AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
- PermissionInfo codeReview = newPermissionInfo();
- codeReview.label = LabelId.CODE_REVIEW;
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
- codeReview.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionToRemove.permissions.put(Permission.LABEL + LabelId.CODE_REVIEW, codeReview);
- ProjectAccessInput removal = newProjectAccessInput();
- removal.remove.put(REFS_HEADS, accessSectionToRemove);
- pApi().access(removal);
-
- // Remove locally
- accessInput
- .add
- .get(REFS_HEADS)
- .permissions
- .get(Permission.LABEL + LabelId.CODE_REVIEW)
- .rules
- .remove(SystemGroupBackend.REGISTERED_USERS.get());
-
- // Check
- assertThat(pApi().access().local).isEqualTo(accessInput.add);
- }
-
- @Test
- public void removePermissionRulesAndCleanupEmptyEntries() throws Exception {
- // Add initial permission set
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi().access(accessInput);
-
- // Remove specific permission rules
- AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
- PermissionInfo codeReview = newPermissionInfo();
- codeReview.label = LabelId.CODE_REVIEW;
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
- codeReview.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
- codeReview.rules.put(SystemGroupBackend.PROJECT_OWNERS.get(), pri);
- accessSectionToRemove.permissions.put(Permission.LABEL + LabelId.CODE_REVIEW, codeReview);
- ProjectAccessInput removal = newProjectAccessInput();
- removal.remove.put(REFS_HEADS, accessSectionToRemove);
- pApi().access(removal);
-
- // Remove locally
- accessInput.add.get(REFS_HEADS).permissions.remove(Permission.LABEL + LabelId.CODE_REVIEW);
-
- // Check
- assertThat(pApi().access().local).isEqualTo(accessInput.add);
- }
-
- @Test
- public void getPermissionsWithDisallowedUser() throws Exception {
- // Add initial permission set
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createAccessSectionInfoDenyAll();
-
- // Disallow READ
- accessInput.add.put(REFS_META_VERSION, accessSectionInfo);
- accessInput.add.put(REFS_DRAFTS, accessSectionInfo);
- accessInput.add.put(REFS_STARRED_CHANGES, accessSectionInfo);
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi().access(accessInput);
-
- requestScopeOperations.setApiUser(user.id());
- assertThrows(ResourceNotFoundException.class, () -> pApi().access());
- }
-
- @Test
- public void setPermissionsWithDisallowedUser() throws Exception {
- // Add initial permission set
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createAccessSectionInfoDenyAll();
-
- // Disallow READ
- accessInput.add.put(REFS_META_VERSION, accessSectionInfo);
- accessInput.add.put(REFS_DRAFTS, accessSectionInfo);
- accessInput.add.put(REFS_STARRED_CHANGES, accessSectionInfo);
- accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi().access(accessInput);
-
- // Create a change to apply
- ProjectAccessInput accessInfoToApply = newProjectAccessInput();
- AccessSectionInfo accessSectionInfoToApply = createDefaultAccessSectionInfo();
- accessInfoToApply.add.put(REFS_HEADS, accessSectionInfoToApply);
-
- requestScopeOperations.setApiUser(user.id());
- assertThrows(ResourceNotFoundException.class, () -> pApi().access());
- }
-
- @Test
- public void permissionsGroupMap() throws Exception {
- // Add initial permission set
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSection = newAccessSectionInfo();
-
- PermissionInfo push = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- push.rules.put(SystemGroupBackend.PROJECT_OWNERS.get(), pri);
- accessSection.permissions.put(Permission.PUSH, push);
-
- PermissionInfo read = newPermissionInfo();
- pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- read.rules.put(SystemGroupBackend.ANONYMOUS_USERS.get(), pri);
- accessSection.permissions.put(Permission.READ, read);
-
- accessInput.add.put(REFS_ALL, accessSection);
- ProjectAccessInfo result = pApi().access(accessInput);
- assertThatMap(result.groups)
- .keys()
- .containsExactly(
- SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
-
- // Check the name, which is what the UI cares about; exhaustive
- // coverage of GroupInfo should be in groups REST API tests.
- assertThat(result.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).name)
- .isEqualTo("Project Owners");
- // Strip the ID, since it is in the key.
- assertThat(result.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).id).isNull();
-
- // Get call returns groups too.
- ProjectAccessInfo loggedInResult = pApi().access();
- assertThatMap(loggedInResult.groups)
- .keys()
- .containsExactly(
- SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
-
- GroupInfo owners = loggedInResult.groups.get(SystemGroupBackend.PROJECT_OWNERS.get());
- assertThat(owners.name).isEqualTo("Project Owners");
- assertThat(owners.id).isNull();
- assertThat(owners.members).isNull();
- assertThat(owners.includes).isNull();
-
- // PROJECT_OWNERS is invisible to anonymous user, but GetAccess disregards visibility.
- requestScopeOperations.setApiUserAnonymous();
- ProjectAccessInfo anonResult = pApi().access();
- assertThatMap(anonResult.groups)
- .keys()
- .containsExactly(
- SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
- }
-
- @Test
- public void updateParentAsUser() throws Exception {
- // Create child
- String newParentProjectName = projectOperations.newProject().create().get();
-
- // Set new parent
- ProjectAccessInput accessInput = newProjectAccessInput();
- accessInput.parent = newParentProjectName;
-
- requestScopeOperations.setApiUser(user.id());
- AuthException thrown = assertThrows(AuthException.class, () -> pApi().access(accessInput));
- assertThat(thrown).hasMessageThat().contains("administrate server not permitted");
- }
-
- @Test
- public void updateParentAsAdministrator() throws Exception {
- // Create parent
- String newParentProjectName = projectOperations.newProject().create().get();
-
- // Set new parent
- ProjectAccessInput accessInput = newProjectAccessInput();
- accessInput.parent = newParentProjectName;
-
- pApi().access(accessInput);
-
- assertThat(pApi().access().inheritsFrom.name).isEqualTo(newParentProjectName);
- }
-
- @Test
- public void addGlobalCapabilityAsUser() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- requestScopeOperations.setApiUser(user.id());
- assertThrows(
- AuthException.class, () -> gApi.projects().name(allProjects.get()).access(accessInput));
- }
-
- @Test
- public void addGlobalCapabilityAsAdmin() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- ProjectAccessInfo updatedAccessSectionInfo =
- gApi.projects().name(allProjects.get()).access(accessInput);
- assertThatMap(updatedAccessSectionInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
- .keys()
- .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
- }
-
- @Test
- public void addPluginGlobalCapability() throws Exception {
- try (Registration registration =
- extensionRegistry
- .newRegistration()
- .add(
- new CapabilityDefinition() {
- @Override
- public String getDescription() {
- return "A Plugin Global Capability";
- }
- },
- "fooCapability")) {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
-
- PermissionInfo foo = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- foo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions.put(ExtensionRegistry.PLUGIN_NAME + "-fooCapability", foo);
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- ProjectAccessInfo updatedAccessSectionInfo =
- gApi.projects().name(allProjects.get()).access(accessInput);
- assertThatMap(
- updatedAccessSectionInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
- .keys()
- .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
- }
- }
-
- @Test
- public void addPermissionAsGlobalCapability() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
-
- PermissionInfo push = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions.put(Permission.PUSH, push);
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
- BadRequestException ex =
- assertThrows(
- BadRequestException.class,
- () -> gApi.projects().name(allProjects.get()).access(accessInput));
- assertThat(ex).hasMessageThat().isEqualTo("Unknown global capability: " + Permission.PUSH);
- }
-
- @Test
- public void addInvalidGlobalCapability() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
-
- PermissionInfo permissionInfo = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- permissionInfo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions.put("Invalid Global Capability", permissionInfo);
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
- BadRequestException ex =
- assertThrows(
- BadRequestException.class,
- () -> gApi.projects().name(allProjects.get()).access(accessInput));
- assertThat(ex)
- .hasMessageThat()
- .isEqualTo("Unknown global capability: Invalid Global Capability");
- }
-
- @Test
- public void addGlobalCapabilityForNonRootProject() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
- }
-
- @Test
- public void addNonGlobalCapabilityToGlobalCapabilities() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
-
- PermissionInfo permissionInfo = newPermissionInfo();
- permissionInfo.rules.put(adminGroupUuid().get(), null);
- accessSectionInfo.permissions.put(Permission.PUSH, permissionInfo);
-
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
- assertThrows(
- BadRequestException.class,
- () -> gApi.projects().name(allProjects.get()).access(accessInput));
- }
-
- @Test
- public void removeGlobalCapabilityAsUser() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
-
- accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- requestScopeOperations.setApiUser(user.id());
- assertThrows(
- AuthException.class, () -> gApi.projects().name(allProjects.get()).access(accessInput));
- }
-
- @Test
- public void removeGlobalCapabilityAsAdmin() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
-
- PermissionInfo permissionInfo = newPermissionInfo();
- permissionInfo.rules.put(adminGroupUuid().get(), null);
- accessSectionInfo.permissions.put(GlobalCapability.ACCESS_DATABASE, permissionInfo);
-
- // Add and validate first as removing existing privileges such as
- // administrateServer would break upcoming tests
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- ProjectAccessInfo updatedProjectAccessInfo =
- gApi.projects().name(allProjects.get()).access(accessInput);
- assertThatMap(updatedProjectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
- .keys()
- .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
-
- // Remove
- accessInput.add.clear();
- accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
-
- updatedProjectAccessInfo = gApi.projects().name(allProjects.get()).access(accessInput);
- assertThatMap(updatedProjectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
- .keys()
- .containsNoneIn(accessSectionInfo.permissions.keySet());
- }
-
- @Test
- public void unknownPermissionRemainsUnchanged() throws Exception {
- String access = "access";
- String unknownPermission = "unknownPermission";
- String registeredUsers = "group Registered Users";
- String refsFor = "refs/for/*";
- // Clone repository to forcefully add permission
- TestRepository<InMemoryRepository> allProjectsRepo = cloneProject(allProjects, admin);
-
- // Fetch permission ref
- GitUtil.fetch(allProjectsRepo, "refs/meta/config:cfg");
- allProjectsRepo.reset("cfg");
-
- // Load current permissions
- String config =
- gApi.projects()
- .name(allProjects.get())
- .branch(RefNames.REFS_CONFIG)
- .file(ProjectConfig.PROJECT_CONFIG)
- .asString();
-
- // Append and push unknown permission
- Config cfg = new Config();
- cfg.fromText(config);
- cfg.setString(access, refsFor, unknownPermission, registeredUsers);
- config = cfg.toText();
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
- push.to(RefNames.REFS_CONFIG).assertOkStatus();
-
- // Verify that unknownPermission is present
- config =
- gApi.projects()
- .name(allProjects.get())
- .branch(RefNames.REFS_CONFIG)
- .file(ProjectConfig.PROJECT_CONFIG)
- .asString();
- cfg.fromText(config);
- assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
-
- // Make permission change through API
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
- accessInput.add.put(refsFor, accessSectionInfo);
- gApi.projects().name(allProjects.get()).access(accessInput);
- accessInput.add.clear();
- accessInput.remove.put(refsFor, accessSectionInfo);
- gApi.projects().name(allProjects.get()).access(accessInput);
-
- // Verify that unknownPermission is still present
- config =
- gApi.projects()
- .name(allProjects.get())
- .branch(RefNames.REFS_CONFIG)
- .file(ProjectConfig.PROJECT_CONFIG)
- .asString();
- cfg.fromText(config);
- assertThat(cfg).stringValue(access, refsFor, unknownPermission).isEqualTo(registeredUsers);
- }
-
- @Test
- public void allUsersCanOnlyInheritFromAllProjects() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- accessInput.parent = project.get();
- BadRequestException thrown =
- assertThrows(
- BadRequestException.class,
- () -> gApi.projects().name(allUsers.get()).access(accessInput));
- assertThat(thrown)
- .hasMessageThat()
- .contains(allUsers.get() + " must inherit from " + allProjects.get());
- }
-
- @Test
- public void syncCreateGroupPermission_addAndRemoveCreateGroupCapability() throws Exception {
- // Grant CREATE_GROUP to Registered Users
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSection = newAccessSectionInfo();
- PermissionInfo createGroup = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- createGroup.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
- gApi.projects().name(allProjects.get()).access(accessInput);
-
- // Assert that the permission was synced from All-Projects (global) to All-Users (ref)
- Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
- assertThatMap(local).keys().contains(RefNames.REFS_GROUPS + "*");
- Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
- // READ is the default permission and should be preserved by the syncer
- assertThatMap(permissions).keys().containsExactly(Permission.READ, Permission.CREATE);
- Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
- assertThatMap(rules).values().containsExactly(pri);
-
- // Revoke the permission
- accessInput.add.clear();
- accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
- gApi.projects().name(allProjects.get()).access(accessInput);
-
- // Assert that the permission was synced from All-Projects (global) to All-Users (ref)
- Map<String, AccessSectionInfo> local2 = gApi.projects().name("All-Users").access().local;
- assertThatMap(local2).keys().contains(RefNames.REFS_GROUPS + "*");
- Map<String, PermissionInfo> permissions2 = local2.get(RefNames.REFS_GROUPS + "*").permissions;
- // READ is the default permission and should be preserved by the syncer
- assertThatMap(permissions2).keys().containsExactly(Permission.READ);
- }
-
- @Test
- public void syncCreateGroupPermission_addCreateGroupCapabilityToMultipleGroups()
- throws Exception {
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
-
- // Grant CREATE_GROUP to Registered Users
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSection = newAccessSectionInfo();
- PermissionInfo createGroup = newPermissionInfo();
- createGroup.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
- gApi.projects().name(allProjects.get()).access(accessInput);
-
- // Grant CREATE_GROUP to Administrators
- accessInput = newProjectAccessInput();
- accessSection = newAccessSectionInfo();
- createGroup = newPermissionInfo();
- createGroup.rules.put(adminGroupUuid().get(), pri);
- accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
- accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
- gApi.projects().name(allProjects.get()).access(accessInput);
-
- // Assert that the permissions were synced from All-Projects (global) to All-Users (ref)
- Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
- assertThatMap(local).keys().contains(RefNames.REFS_GROUPS + "*");
- Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
- // READ is the default permission and should be preserved by the syncer
- assertThatMap(permissions).keys().containsExactly(Permission.READ, Permission.CREATE);
- Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
- assertThatMap(rules)
- .keys()
- .containsExactly(SystemGroupBackend.REGISTERED_USERS.get(), adminGroupUuid().get());
- assertThat(rules.get(SystemGroupBackend.REGISTERED_USERS.get())).isEqualTo(pri);
- assertThat(rules.get(adminGroupUuid().get())).isEqualTo(pri);
- }
-
- @Test
- public void addAccessSectionForInvalidRef() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- // 'refs/heads/stable_*' is invalid, correct would be '^refs/heads/stable_.*'
- String invalidRef = Constants.R_HEADS + "stable_*";
- accessInput.add.put(invalidRef, accessSectionInfo);
-
- BadRequestException thrown =
- assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
- assertThat(thrown).hasMessageThat().contains("Invalid Name: " + invalidRef);
- }
-
- @Test
- public void createAccessChangeWithAccessSectionForInvalidRef() throws Exception {
- ProjectAccessInput accessInput = newProjectAccessInput();
- AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
-
- // 'refs/heads/stable_*' is invalid, correct would be '^refs/heads/stable_.*'
- String invalidRef = Constants.R_HEADS + "stable_*";
- accessInput.add.put(invalidRef, accessSectionInfo);
-
- BadRequestException thrown =
- assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
- assertThat(thrown).hasMessageThat().contains("Invalid Name: " + invalidRef);
- }
-
- private ProjectApi pApi() throws Exception {
- return gApi.projects().name(newProjectName.get());
- }
-
- private ProjectAccessInput newProjectAccessInput() {
- ProjectAccessInput p = new ProjectAccessInput();
- p.add = new HashMap<>();
- p.remove = new HashMap<>();
- return p;
- }
-
- private PermissionInfo newPermissionInfo() {
- PermissionInfo p = new PermissionInfo(null, null);
- p.rules = new HashMap<>();
- return p;
- }
-
- private AccessSectionInfo newAccessSectionInfo() {
- AccessSectionInfo a = new AccessSectionInfo();
- a.permissions = new HashMap<>();
- return a;
- }
-
- private AccessSectionInfo createDefaultAccessSectionInfo() {
- AccessSectionInfo accessSection = newAccessSectionInfo();
-
- PermissionInfo push = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSection.permissions.put(Permission.PUSH, push);
-
- PermissionInfo codeReview = newPermissionInfo();
- codeReview.label = LabelId.CODE_REVIEW;
- pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
- codeReview.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
-
- pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- pri.max = 1;
- pri.min = -1;
- codeReview.rules.put(SystemGroupBackend.PROJECT_OWNERS.get(), pri);
- accessSection.permissions.put(Permission.LABEL + LabelId.CODE_REVIEW, codeReview);
-
- return accessSection;
- }
-
- private AccessSectionInfo createDefaultGlobalCapabilitiesAccessSectionInfo() {
- AccessSectionInfo accessSection = newAccessSectionInfo();
-
- PermissionInfo email = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
- email.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSection.permissions.put(GlobalCapability.EMAIL_REVIEWERS, email);
-
- return accessSection;
- }
-
- private AccessSectionInfo createAccessSectionInfoDenyAll() {
- AccessSectionInfo accessSection = newAccessSectionInfo();
-
- PermissionInfo read = newPermissionInfo();
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
- read.rules.put(SystemGroupBackend.ANONYMOUS_USERS.get(), pri);
- accessSection.permissions.put(Permission.READ, read);
-
- return accessSection;
+ public void listAccess() throws Exception {
+ RestResponse r = adminRestSession.get("/access/?project=" + project.get());
+ r.assertOK();
+ Map<String, ProjectAccessInfo> infoByProject =
+ newGson()
+ .fromJson(r.getReader(), new TypeToken<Map<String, ProjectAccessInfo>>() {}.getType());
+ assertThat(infoByProject.keySet()).containsExactly(project.get());
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 5679c41..85c0212 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -16,6 +16,8 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
@@ -28,7 +30,9 @@
import com.google.common.collect.Streams;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.EmailHeader;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
@@ -44,6 +48,7 @@
import com.google.gerrit.mail.MailMessage;
import com.google.gerrit.mail.MailProcessingUtil;
import com.google.gerrit.server.mail.receive.MailProcessor;
+import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gerrit.testing.TestCommentHelper;
import com.google.inject.Inject;
@@ -61,6 +66,7 @@
@Inject private MailProcessor mailProcessor;
@Inject private AccountOperations accountOperations;
@Inject private TestCommentHelper testCommentHelper;
+ @Inject private ProjectOperations projectOperations;
private static final CommentValidator mockCommentValidator = mock(CommentValidator.class);
@@ -276,6 +282,133 @@
}
@Test
+ public void sendNotificationOnProjectNotFound() throws Exception {
+ String ts =
+ MailProcessingUtil.rfcDateformatter.format(
+ ZonedDateTime.ofInstant(TimeUtil.now(), ZoneId.of("UTC")));
+
+ String changeUrl = canonicalWebUrl.get() + "c/non-existing-project/+/123";
+
+ // Build Message
+ String txt = newPlaintextBody(changeUrl + "/1", "Test Message", null, null);
+ MailMessage.Builder b =
+ messageBuilderWithDefaultFields()
+ .from(user.getNameEmail())
+ .textContent(txt + textFooterForChange(123, ts));
+
+ sender.clear();
+ mailProcessor.process(b.build());
+
+ assertNotifyTo(user);
+ Message message = sender.nextMessage();
+ assertThat(message.body())
+ .contains(
+ "Gerrit Code Review was unable to process your email because the change was not"
+ + " found.");
+ assertThat(message.headers()).containsKey("Subject");
+ }
+
+ @Test
+ public void sendNotificationOnProjectNotVisible() throws Exception {
+ String changeId = createChangeWithReview();
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+
+ String ts =
+ MailProcessingUtil.rfcDateformatter.format(
+ ZonedDateTime.ofInstant(
+ gApi.changes().id(changeId).get().updated.toInstant(), ZoneId.of("UTC")));
+
+ // Block read permissions on the project.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ // Build Message
+ String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null);
+ MailMessage.Builder b =
+ messageBuilderWithDefaultFields()
+ .from(user.getNameEmail())
+ .textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+ sender.clear();
+ mailProcessor.process(b.build());
+
+ assertNotifyTo(user);
+ Message message = sender.nextMessage();
+ assertThat(message.body())
+ .contains(
+ "Gerrit Code Review was unable to process your email because the change was not"
+ + " found.");
+ assertThat(message.headers()).containsKey("Subject");
+ }
+
+ @Test
+ public void sendNotificationOnChangeNotFound() throws Exception {
+ String changeId = createChangeWithReview();
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+
+ String ts =
+ MailProcessingUtil.rfcDateformatter.format(
+ ZonedDateTime.ofInstant(
+ gApi.changes().id(changeId).get().updated.toInstant(), ZoneId.of("UTC")));
+
+ // Delete the change so that it's not found.
+ gApi.changes().id(changeId).delete();
+
+ // Build Message
+ String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null);
+ MailMessage.Builder b =
+ messageBuilderWithDefaultFields()
+ .from(user.getNameEmail())
+ .textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+ sender.clear();
+ mailProcessor.process(b.build());
+
+ assertNotifyTo(user);
+ Message message = sender.nextMessage();
+ assertThat(message.body())
+ .contains(
+ "Gerrit Code Review was unable to process your email because the change was not"
+ + " found.");
+ assertThat(message.headers()).containsKey("Subject");
+ }
+
+ @Test
+ public void sendNotificationOnChangeNotVisible() throws Exception {
+ String changeId = createChangeWithReview();
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+
+ String ts =
+ MailProcessingUtil.rfcDateformatter.format(
+ ZonedDateTime.ofInstant(
+ gApi.changes().id(changeId).get().updated.toInstant(), ZoneId.of("UTC")));
+
+ // Make change private so that it's no visible to user.
+ gApi.changes().id(changeId).setPrivate(true);
+
+ // Build Message
+ String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", "Test Message", null, null);
+ MailMessage.Builder b =
+ messageBuilderWithDefaultFields()
+ .from(user.getNameEmail())
+ .textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+ sender.clear();
+ mailProcessor.process(b.build());
+
+ assertNotifyTo(user);
+ Message message = sender.nextMessage();
+ assertThat(message.body())
+ .contains(
+ "Gerrit Code Review was unable to process your email because the change was not"
+ + " found.");
+ assertThat(message.headers()).containsKey("Subject");
+ }
+
+ @Test
public void validateChangeMessage_rejected() throws Exception {
String changeId = createChangeWithReview();
ChangeInfo changeInfo = gApi.changes().id(changeId).get();
diff --git a/package.json b/package.json
index f5eafee..a3329a1 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
"version": "3.1.0-SNAPSHOT",
"description": "Gerrit Code Review",
"dependencies": {
- "@bazel/rollup": "^3.4.0",
- "@bazel/terser": "^3.4.0",
- "@bazel/typescript": "^3.4.0"
+ "@bazel/rollup": "^3.5.0",
+ "@bazel/terser": "^3.5.0",
+ "@bazel/typescript": "^3.5.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.22.0",
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 6e142d4..f6b3aa0 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -303,12 +303,8 @@
}
export declare type ImageDiffAction =
- | {
- type: 'overview-image-clicked';
- }
- | {
- type: 'overview-frame-dragged';
- }
+ | {type: 'overview-image-clicked'}
+ | {type: 'overview-frame-dragged'}
| {type: 'magnifier-clicked'}
| {type: 'magnifier-dragged'}
| {type: 'version-switcher-clicked'; button: 'base' | 'revision' | 'switch'}
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index df9fa95..eb979a1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -274,6 +274,7 @@
const SKIP_ACTION_KEYS: string[] = [
ChangeActions.REVIEWED,
ChangeActions.UNREVIEWED,
+ ChangeActions.REVERT_SUBMISSION,
];
export function assertUIActionInfo(action?: ActionInfo): UIActionInfo {
@@ -1447,9 +1448,11 @@
);
break;
case RevertType.REVERT_SUBMISSION:
+ // TODO(dhruvsri): replace with this.actions.revert_submission once
+ // BE starts sending it again
this._fireAction(
'/revert_submission',
- assertUIActionInfo(this.actions.revert_submission),
+ {__key: 'revert_submission', method: HttpMethod.POST} as UIActionInfo,
false,
{message}
);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 807d0cd..c6d2ee0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1252,9 +1252,13 @@
this.$.fileList.collapseAllDiffs();
this._patchRange = patchRange;
+ const patchKnown =
+ !patchRange.patchNum ||
+ (this._allPatchSets ?? []).some(ps => ps.num === patchRange.patchNum);
+
// If the change has already been loaded and the parameter change is only
// in the patch range, then don't do a full reload.
- if (!changeChanged && patchChanged) {
+ if (!changeChanged && patchChanged && patchKnown) {
if (!patchRange.patchNum) {
patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
rightPatchNumChanged = true;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 877bdd6..3d51ac6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -1633,6 +1633,13 @@
assert.isTrue(reloadStub.calledOnce);
element._initialLoadComplete = true;
+ element._change = {
+ ...createChangeViewChange(),
+ revisions: {
+ rev1: createRevision(1),
+ rev2: createRevision(2),
+ },
+ };
value.basePatchNum = 1 as BasePatchSetNum;
value.patchNum = 2 as RevisionPatchSetNum;
@@ -1661,6 +1668,13 @@
element._paramsChanged(value);
element._initialLoadComplete = true;
+ element._change = {
+ ...createChangeViewChange(),
+ revisions: {
+ rev1: createRevision(1),
+ rev2: createRevision(2),
+ },
+ };
value.basePatchNum = 1 as BasePatchSetNum;
value.patchNum = 2 as RevisionPatchSetNum;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
index 17647ad..3ec4f2c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
@@ -32,10 +32,10 @@
}
.revertSubmissionLayout {
display: flex;
+ align-items: center;
}
.label {
margin-left: var(--spacing-m);
- margin-bottom: var(--spacing-m);
}
iron-autogrow-textarea {
font-family: var(--monospace-font-family);
@@ -47,6 +47,9 @@
color: var(--error-text-color);
margin-bottom: var(--spacing-m);
}
+ label[for='messageInput'] {
+ margin-top: var(--spacing-m);
+ }
</style>
<gr-dialog
confirm-label="Revert"
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 26af2a2..1711499 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -184,7 +184,7 @@
type: String,
computed:
'_computeMessageContentExpanded(message.message,' +
- ' message.accountsInMessage,' +
+ ' message.accounts_in_message,' +
' message.tag)',
})
_messageContentExpanded = '';
@@ -193,7 +193,7 @@
type: String,
computed:
'_computeMessageContentCollapsed(message.message,' +
- ' message.accountsInMessage,' +
+ ' message.accounts_in_message,' +
' message.tag,' +
' message.commentThreads)',
})
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 8d5bc33..5595d15 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -21,13 +21,7 @@
import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
import {classMap} from 'lit-html/directives/class-map';
import {GrLitElement} from '../../lit/gr-lit-element';
-import {
- customElement,
- property,
- css,
- internalProperty,
- TemplateResult,
-} from 'lit-element';
+import {customElement, property, css, state, TemplateResult} from 'lit-element';
import {sharedStyles} from '../../../styles/shared-styles';
import {
SubmittedTogetherInfo,
@@ -76,22 +70,22 @@
@property()
mergeable?: boolean;
- @internalProperty()
+ @state()
submittedTogether?: SubmittedTogetherInfo = {
changes: [],
non_visible_changes: 0,
};
- @internalProperty()
+ @state()
relatedChanges: RelatedChangeAndCommitInfo[] = [];
- @internalProperty()
+ @state()
conflictingChanges: ChangeInfo[] = [];
- @internalProperty()
+ @state()
cherryPickChanges: ChangeInfo[] = [];
- @internalProperty()
+ @state()
sameTopicChanges: ChangeInfo[] = [];
private readonly restApiService = appContext.restApiService;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index e303dad..5504877 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -40,8 +40,9 @@
} from '../../../constants/constants';
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {
- accountKey,
accountOrGroupKey,
+ isReviewerOrCC,
+ mapReviewer,
removeServiceUsers,
} from '../../../utils/account-util';
import {getDisplayName} from '../../../utils/display-name-util';
@@ -74,7 +75,6 @@
ParsedJSON,
PatchSetNum,
ProjectInfo,
- ReviewerInput,
Reviewers,
ReviewInput,
ReviewResult,
@@ -542,18 +542,6 @@
}
}
- _mapReviewer(addition: AccountAddition): ReviewerInput {
- if (addition.account) {
- return {reviewer: accountKey(addition.account)};
- }
- if (addition.group) {
- const reviewer = decodeURIComponent(addition.group.id) as GroupId;
- const confirmed = addition.group.confirmed;
- return {reviewer, confirmed};
- }
- throw new Error('Reviewer must be either an account or a group.');
- }
-
send(includeComments: boolean, startReview: boolean) {
this.reporting.time(Timing.SEND_REPLY);
const labels = this.$.labelScores.getLabelValues();
@@ -606,16 +594,24 @@
state?: ReviewerState
) => {
additions.forEach(addition => {
- const reviewer = this._mapReviewer(addition);
+ const reviewer = mapReviewer(addition);
if (state) reviewer.state = state;
reviewInput.reviewers?.push(reviewer);
});
};
reviewInput.reviewers = [];
+ assertIsDefined(this.change, 'change');
+ const change = this.change;
addToReviewInput(this.$.reviewers.additions(), ReviewerState.REVIEWER);
addToReviewInput(this.$.ccs.additions(), ReviewerState.CC);
- addToReviewInput(this.$.reviewers.removals(), ReviewerState.REMOVED);
- addToReviewInput(this.$.ccs.removals(), ReviewerState.REMOVED);
+ addToReviewInput(
+ this.$.reviewers.removals().filter(r => isReviewerOrCC(change, r)),
+ ReviewerState.REMOVED
+ );
+ addToReviewInput(
+ this.$.ccs.removals().filter(r => isReviewerOrCC(change, r)),
+ ReviewerState.REMOVED
+ );
this.disabled = true;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index b51eb5f..f5474d8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -1075,6 +1075,11 @@
element._reviewers = [reviewer1, reviewer2];
element._ccs = [cc1, cc2, cc3];
+ element.change.reviewers = {
+ [ReviewerState.CC]: [],
+ [ReviewerState.REVIEWER]: [{_account_id: 33}],
+ };
+
const mutations = [];
stubSaveReview(review => mutations.push(...review.reviewers));
@@ -1128,7 +1133,7 @@
// Send and purge and verify moves, delete cc3.
await element.send();
- expect(mutations).to.have.lengthOf(7);
+ expect(mutations).to.have.lengthOf(5);
expect(mutations[0]).to.deep.equal(mapReviewer(cc1,
ReviewerState.REVIEWER));
expect(mutations[1]).to.deep.equal(mapReviewer(cc2,
@@ -1138,13 +1143,9 @@
expect(mutations[3]).to.deep.equal(mapReviewer(reviewer2,
ReviewerState.CC));
- // 3 remove events stored
+ // Only 1 account was initially part of the change
expect(mutations[4]).to.deep.equal({reviewer: 33, state:
ReviewerState.REMOVED});
- expect(mutations[5]).to.deep.equal({reviewer: 35, state:
- ReviewerState.REMOVED});
- expect(mutations[6]).to.deep.equal({reviewer: 37, state:
- ReviewerState.REMOVED});
});
test('emits cancel on esc key', () => {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
new file mode 100644
index 0000000..6dce9bc
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
@@ -0,0 +1,83 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {html} from 'lit-html';
+import {css, customElement, property} from 'lit-element';
+import {GrLitElement} from '../lit/gr-lit-element';
+import {CheckRun} from '../../services/checks/checks-model';
+
+@customElement('gr-checks-attempt')
+class GrChecksAttempt extends GrLitElement {
+ @property()
+ run?: CheckRun;
+
+ static get styles() {
+ return [
+ css`
+ .attempt {
+ display: inline-block;
+ height: var(--line-height-normal);
+ vertical-align: top;
+ font-size: var(--font-size-small);
+ position: relative;
+ }
+ .attempt .box,
+ .attempt .angle {
+ box-sizing: border-box;
+ height: calc(var(--line-height-normal) - 2px);
+ line-height: calc(var(--line-height-normal) - 2px);
+ border-radius: 2px;
+ }
+ .attempt .box {
+ margin-left: 2px;
+ margin-bottom: 2px;
+ border: 1px solid var(--deemphasized-text-color);
+ padding: 0 var(--spacing-s);
+ }
+ .attempt .angle {
+ position: absolute;
+ top: 2px;
+ /* The text in the .angle div just ensures the correct width. */
+ color: transparent;
+ border-left: 1px solid var(--deemphasized-text-color);
+ border-bottom: 1px solid var(--deemphasized-text-color);
+ /* 1px for the border of the .box div. */
+ padding: 0 calc(var(--spacing-s) + 1px);
+ }
+ `,
+ ];
+ }
+
+ render() {
+ if (!this.run) return undefined;
+ if (this.run.isSingleAttempt) return undefined;
+ if (!this.run.attempt) return undefined;
+ const attempt = this.run.attempt;
+
+ return html`
+ <span class="attempt">
+ <div class="box">${attempt}</div>
+ <div class="angle">${attempt}</div>
+ </span>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-checks-attempt': GrChecksAttempt;
+ }
+}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 1c60161..6f35c49 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -20,13 +20,14 @@
import {
css,
customElement,
- internalProperty,
property,
PropertyValues,
query,
+ state,
TemplateResult,
} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
+import './gr-checks-attempt';
import '@polymer/paper-tooltip/paper-tooltip';
import {
Action,
@@ -113,16 +114,6 @@
overflow: hidden;
text-overflow: ellipsis;
}
- .nameCol .attempt {
- display: inline-block;
- background-color: var(--tag-gray);
- border-radius: var(--line-height-normal);
- height: var(--line-height-normal);
- width: var(--line-height-normal);
- text-align: center;
- vertical-align: top;
- font-size: var(--font-size-small);
- }
.summaryCol {
/* Forces this column to get the remaining space that is left over by
the other columns. */
@@ -263,9 +254,7 @@
<td class="nameCol" @click="${this.toggleExpanded}">
<div>
<span>${this.result.checkName}</span>
- <span class="attempt" ?hidden="${this.result.isSingleAttempt}"
- >${this.result.attempt}</span
- >
+ <gr-checks-attempt .run="${this.result}"></gr-checks-attempt>
</div>
</td>
<td class="summaryCol">
@@ -471,7 +460,7 @@
@query('#filterInput')
filterInput?: HTMLInputElement;
- @internalProperty()
+ @state()
filterRegExp = new RegExp('');
/** All runs. Shown should only the selected/filtered ones. */
@@ -511,7 +500,7 @@
>();
/** Maintains the state of which result sections should show all results. */
- @internalProperty()
+ @state()
isShowAll: Map<Category, boolean> = new Map();
/**
@@ -874,7 +863,8 @@
const filtered = selected.filter(
result =>
this.filterRegExp.test(result.checkName) ||
- this.filterRegExp.test(result.summary)
+ this.filterRegExp.test(result.summary) ||
+ this.filterRegExp.test(result.message ?? '')
);
let expanded = this.isSectionExpanded.get(category);
const expandedByUser = this.isSectionExpandedByUser.get(category) ?? false;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index de9c5fb..1d65546 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -20,12 +20,13 @@
import {
css,
customElement,
- internalProperty,
property,
PropertyValues,
query,
+ state,
} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
+import './gr-checks-attempt';
import {Action, Link, RunStatus} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
import {
@@ -85,16 +86,6 @@
.name {
font-weight: var(--font-weight-bold);
}
- .attempt {
- display: inline-block;
- background-color: var(--tag-gray);
- border-radius: var(--line-height-normal);
- height: var(--line-height-normal);
- width: var(--line-height-normal);
- text-align: center;
- vertical-align: top;
- font-size: var(--font-size-small);
- }
.chip.error {
border-left: var(--thick-border) solid var(--error-foreground);
}
@@ -233,9 +224,7 @@
<iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
${this.renderAdditionalIcon()}
<span class="name">${this.run.checkName}</span>
- <span class="attempt" ?hidden="${this.run.isSingleAttempt}"
- >${this.run.attempt}</span
- >
+ <gr-checks-attempt .run="${this.run}"></gr-checks-attempt>
</div>
<div class="right">
${action
@@ -326,7 +315,7 @@
@query('#filterInput')
filterInput?: HTMLInputElement;
- @internalProperty()
+ @state()
filterRegExp = new RegExp('');
@property()
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 418598b..b28596a 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -15,13 +15,7 @@
* limitations under the License.
*/
import {html} from 'lit-html';
-import {
- css,
- customElement,
- internalProperty,
- property,
- PropertyValues,
-} from 'lit-element';
+import {css, customElement, property, PropertyValues, state} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
import {Action} from '../../api/checks';
import {
@@ -64,11 +58,11 @@
@property()
changeNum: NumericChangeId | undefined = undefined;
- @internalProperty()
+ @state()
selectedRuns: string[] = [];
/** Maps checkName to selected attempt number. `undefined` means `latest`. */
- @internalProperty()
+ @state()
selectedAttempts: Map<string, number | undefined> = new Map<
string,
number | undefined
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
index 1448129..41a448e 100644
--- a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
@@ -22,13 +22,21 @@
import '@polymer/paper-icon-button/paper-icon-button';
import '@polymer/paper-item/paper-item';
import '@polymer/paper-listbox/paper-listbox';
+import '@polymer/paper-tooltip/paper-tooltip.js';
import '../../shared/gr-button/gr-button';
import {pluralize} from '../../../utils/string-util';
import {fire} from '../../../utils/event-util';
import {DiffInfo} from '../../../types/diff';
import {assertIsDefined} from '../../../utils/common-util';
-import {css, customElement, html, LitElement, property} from 'lit-element';
+import {
+ css,
+ customElement,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+} from 'lit-element';
import {
ContextButtonType,
@@ -43,23 +51,27 @@
/**
* Traverses a hierarchical structure of syntax blocks and
* finds the most local/nested block that can be associated line.
- * It finds the closest block that contains the whole line.
+ * It finds the closest block that contains the whole line and
+ * returns the whole path from the syntax layer (blocks) sent as parameter
+ * to the most nested block - the complete path from the top to bottom layer of
+ * a syntax tree. Example: [myNamepace, MyClass, myMethod1, aLocalFunctionInsideMethod1]
*
* @param lineNum line number for the targeted line.
* @param blocks Blocks for a specific syntax level in the file (to allow recursive calls)
- * @returns
*/
-function findMostNestedContainingBlock(
+function findBlockTreePathForLine(
lineNum: number,
blocks?: SyntaxBlock[]
-): SyntaxBlock | undefined {
+): SyntaxBlock[] {
const containingBlock = blocks?.find(
({range}) => range.start_line < lineNum && range.end_line > lineNum
);
- const containingChildBlock = containingBlock
- ? findMostNestedContainingBlock(lineNum, containingBlock?.children)
- : undefined;
- return containingChildBlock || containingBlock;
+ if (!containingBlock) return [];
+ const innerPathInChild = findBlockTreePathForLine(
+ lineNum,
+ containingBlock?.children
+ );
+ return [containingBlock].concat(innerPathInChild);
}
@customElement('gr-context-controls')
@@ -116,6 +128,9 @@
.belowButton {
top: calc(100% + var(--divider-border));
}
+ .breadcrumbTooltip {
+ white-space: nowrap;
+ }
`;
// To pass CSS mixins for @apply to Polymer components, they need to be
@@ -194,7 +209,11 @@
/**
* Creates a specific expansion button (e.g. +X common lines, +10, +Block).
*/
- private createContextButton(type: ContextButtonType, linesToExpand: number) {
+ private createContextButton(
+ type: ContextButtonType,
+ linesToExpand: number,
+ tooltip?: TemplateResult
+ ) {
let text = '';
let groups: GrDiffGroup[] = []; // The groups that replace this one if tapped.
let ariaLabel = '';
@@ -265,6 +284,7 @@
@click="${expandHandler}"
>
<span class="showContext">${text}</span>
+ ${tooltip}
</gr-button>`;
return button;
}
@@ -378,6 +398,24 @@
return undefined;
}
+ private createBlockButtonTooltip(
+ buttonType: ContextButtonType,
+ syntaxPath: SyntaxBlock[],
+ linesToExpand: number
+ ) {
+ // Create breadcrumb string:
+ // myNamepace > MyClass > myMethod1 > aLocalFunctionInsideMethod1 > (anonymous)
+ const tooltipText = syntaxPath.length
+ ? syntaxPath.map(b => b.name || '(anonymous)').join(' > ')
+ : `${linesToExpand} common lines`;
+
+ const position =
+ buttonType === ContextButtonType.BLOCK_ABOVE ? 'top' : 'bottom';
+ return html`<paper-tooltip offset="10" position="${position}"
+ ><div class="breadcrumbTooltip">${tooltipText}</div></paper-tooltip
+ >`;
+ }
+
private createBlockButton(
buttonType: ContextButtonType,
numLines: number,
@@ -385,13 +423,13 @@
) {
assertIsDefined(this.diff, 'diff');
const syntaxTree = this.diff!.meta_b.syntax_tree;
- const containingBlock = findMostNestedContainingBlock(
+ const outlineSyntaxPath = findBlockTreePathForLine(
referenceLine,
syntaxTree
);
let linesToExpand = numLines;
- if (containingBlock) {
- const {range} = containingBlock;
+ if (outlineSyntaxPath.length) {
+ const {range} = outlineSyntaxPath[outlineSyntaxPath.length - 1];
const targetLine =
buttonType === ContextButtonType.BLOCK_ABOVE
? range.end_line
@@ -401,7 +439,12 @@
linesToExpand = distanceToTargetLine;
}
}
- return this.createContextButton(buttonType, linesToExpand);
+ const tooltip = this.createBlockButtonTooltip(
+ buttonType,
+ outlineSyntaxPath,
+ linesToExpand
+ );
+ return this.createContextButton(buttonType, linesToExpand, tooltip);
}
private contextRange() {
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
new file mode 100644
index 0000000..f59b19d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
@@ -0,0 +1,386 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff-group';
+import './gr-context-controls';
+import {GrContextControls} from './gr-context-controls';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
+
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {DiffFileMetaInfo, DiffInfo, SyntaxBlock} from '../../../api/diff';
+
+const blankFixture = fixtureFromElement('div');
+
+suite('gr-context-control tests', () => {
+ let element: GrContextControls;
+
+ setup(async () => {
+ element = document.createElement('gr-context-controls');
+ element.diff = ({content: []} as any) as DiffInfo;
+ element.renderPreferences = {};
+ element.section = document.createElement('div');
+ blankFixture.instantiate().appendChild(element);
+ await flush();
+ });
+
+ function createContextGroups(options: {offset?: number; count?: number}) {
+ const offset = options.offset || 0;
+ const numLines = options.count || 10;
+ const lines = [];
+ for (let i = 0; i < numLines; i++) {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = offset + i + 1;
+ line.afterNumber = offset + i + 1;
+ line.text = 'lorem upsum';
+ lines.push(line);
+ }
+
+ return [new GrDiffGroup(GrDiffGroupType.BOTH, lines)];
+ }
+
+ test('no +10 buttons for 10 or less lines', async () => {
+ element.contextGroups = createContextGroups({count: 10});
+
+ await flush();
+
+ const buttons = element.shadowRoot!.querySelectorAll(
+ 'gr-button.showContext'
+ );
+ assert.equal(buttons.length, 1);
+ assert.equal(buttons[0].textContent!.trim(), '+10 common lines');
+ });
+
+ test('context control at the top', async () => {
+ element.contextGroups = createContextGroups({offset: 0, count: 20});
+ element.showBelow = true;
+
+ await flush();
+
+ const buttons = element.shadowRoot!.querySelectorAll(
+ 'gr-button.showContext'
+ );
+
+ assert.equal(buttons.length, 2);
+ assert.equal(buttons[0].textContent!.trim(), '+20 common lines');
+ assert.equal(buttons[1].textContent!.trim(), '+10');
+
+ assert.include([...buttons[0].classList.values()], 'belowButton');
+ assert.include([...buttons[1].classList.values()], 'belowButton');
+ });
+
+ test('context control in the middle', async () => {
+ element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.showAbove = true;
+ element.showBelow = true;
+
+ await flush();
+
+ const buttons = element.shadowRoot!.querySelectorAll(
+ 'gr-button.showContext'
+ );
+
+ assert.equal(buttons.length, 3);
+ assert.equal(buttons[0].textContent!.trim(), '+20 common lines');
+ assert.equal(buttons[1].textContent!.trim(), '+10');
+ assert.equal(buttons[2].textContent!.trim(), '+10');
+
+ assert.include([...buttons[0].classList.values()], 'centeredButton');
+ assert.include([...buttons[1].classList.values()], 'aboveButton');
+ assert.include([...buttons[2].classList.values()], 'belowButton');
+ });
+
+ test('context control at the bottom', async () => {
+ element.contextGroups = createContextGroups({offset: 30, count: 20});
+ element.showAbove = true;
+
+ await flush();
+
+ const buttons = element.shadowRoot!.querySelectorAll(
+ 'gr-button.showContext'
+ );
+
+ assert.equal(buttons.length, 2);
+ assert.equal(buttons[0].textContent!.trim(), '+20 common lines');
+ assert.equal(buttons[1].textContent!.trim(), '+10');
+
+ assert.include([...buttons[0].classList.values()], 'aboveButton');
+ assert.include([...buttons[1].classList.values()], 'aboveButton');
+ });
+
+ function prepareForBlockExpansion(syntaxTree: SyntaxBlock[]) {
+ element.renderPreferences!.use_block_expansion = true;
+ element.diff!.meta_b = ({
+ syntax_tree: syntaxTree,
+ } as any) as DiffFileMetaInfo;
+ }
+
+ test('context control with block expansion at the top', async () => {
+ prepareForBlockExpansion([]);
+ element.contextGroups = createContextGroups({offset: 0, count: 20});
+ element.showBelow = true;
+
+ await flush();
+
+ const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.fullExpansion gr-button'
+ );
+ const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.partialExpansion gr-button'
+ );
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ assert.equal(fullExpansionButtons.length, 1);
+ assert.equal(partialExpansionButtons.length, 1);
+ assert.equal(blockExpansionButtons.length, 1);
+ assert.equal(
+ blockExpansionButtons[0].querySelector('span')!.textContent!.trim(),
+ '+Block'
+ );
+ assert.include(
+ [...blockExpansionButtons[0].classList.values()],
+ 'belowButton'
+ );
+ });
+
+ test('context control with block expansion in the middle', async () => {
+ prepareForBlockExpansion([]);
+ element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.showAbove = true;
+ element.showBelow = true;
+
+ await flush();
+
+ const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.fullExpansion gr-button'
+ );
+ const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.partialExpansion gr-button'
+ );
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ assert.equal(fullExpansionButtons.length, 1);
+ assert.equal(partialExpansionButtons.length, 2);
+ assert.equal(blockExpansionButtons.length, 2);
+ assert.equal(
+ blockExpansionButtons[0].querySelector('span')!.textContent!.trim(),
+ '+Block'
+ );
+ assert.equal(
+ blockExpansionButtons[1].querySelector('span')!.textContent!.trim(),
+ '+Block'
+ );
+ assert.include(
+ [...blockExpansionButtons[0].classList.values()],
+ 'aboveButton'
+ );
+ assert.include(
+ [...blockExpansionButtons[1].classList.values()],
+ 'belowButton'
+ );
+ });
+
+ test('context control with block expansion at the bottom', async () => {
+ prepareForBlockExpansion([]);
+ element.contextGroups = createContextGroups({offset: 30, count: 20});
+ element.showAbove = true;
+
+ await flush();
+
+ const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.fullExpansion gr-button'
+ );
+ const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.partialExpansion gr-button'
+ );
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ assert.equal(fullExpansionButtons.length, 1);
+ assert.equal(partialExpansionButtons.length, 1);
+ assert.equal(blockExpansionButtons.length, 1);
+ assert.equal(
+ blockExpansionButtons[0].querySelector('span')!.textContent!.trim(),
+ '+Block'
+ );
+ assert.include(
+ [...blockExpansionButtons[0].classList.values()],
+ 'aboveButton'
+ );
+ });
+
+ test('+ Block tooltip tooltip shows syntax block containing the target lines above and below', async () => {
+ prepareForBlockExpansion([
+ {
+ name: 'aSpecificFunction',
+ range: {start_line: 1, start_column: 0, end_line: 25, end_column: 0},
+ children: [],
+ },
+ {
+ name: 'anotherFunction',
+ range: {start_line: 26, start_column: 0, end_line: 50, end_column: 0},
+ children: [],
+ },
+ ]);
+ element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.showAbove = true;
+ element.showBelow = true;
+
+ await flush();
+
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ assert.equal(
+ blockExpansionButtons[0]
+ .querySelector('.breadcrumbTooltip')!
+ .textContent?.trim(),
+ 'aSpecificFunction'
+ );
+ assert.equal(
+ blockExpansionButtons[1]
+ .querySelector('.breadcrumbTooltip')!
+ .textContent?.trim(),
+ 'anotherFunction'
+ );
+ });
+
+ test('+Block tooltip shows nested syntax blocks as breadcrumbs', async () => {
+ prepareForBlockExpansion([
+ {
+ name: 'aSpecificNamespace',
+ range: {start_line: 1, start_column: 0, end_line: 200, end_column: 0},
+ children: [
+ {
+ name: 'MyClass',
+ range: {
+ start_line: 2,
+ start_column: 0,
+ end_line: 100,
+ end_column: 0,
+ },
+ children: [
+ {
+ name: 'aMethod',
+ range: {
+ start_line: 5,
+ start_column: 0,
+ end_line: 80,
+ end_column: 0,
+ },
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.showAbove = true;
+ element.showBelow = true;
+
+ await flush();
+
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ assert.equal(
+ blockExpansionButtons[0]
+ .querySelector('.breadcrumbTooltip')!
+ .textContent?.trim(),
+ 'aSpecificNamespace > MyClass > aMethod'
+ );
+ });
+
+ test('+Block tooltip shows (anonymous) for empty blocks', async () => {
+ prepareForBlockExpansion([
+ {
+ name: 'aSpecificNamespace',
+ range: {start_line: 1, start_column: 0, end_line: 200, end_column: 0},
+ children: [
+ {
+ name: '',
+ range: {
+ start_line: 2,
+ start_column: 0,
+ end_line: 100,
+ end_column: 0,
+ },
+ children: [
+ {
+ name: 'aMethod',
+ range: {
+ start_line: 5,
+ start_column: 0,
+ end_line: 80,
+ end_column: 0,
+ },
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ]);
+ element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.showAbove = true;
+ element.showBelow = true;
+ await flush();
+
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ assert.equal(
+ blockExpansionButtons[0]
+ .querySelector('.breadcrumbTooltip')!
+ .textContent?.trim(),
+ 'aSpecificNamespace > (anonymous) > aMethod'
+ );
+ });
+
+ test('+Block tooltip shows "all common lines" for empty syntax tree', async () => {
+ prepareForBlockExpansion([]);
+
+ element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.showAbove = true;
+ element.showBelow = true;
+ await flush();
+
+ const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
+ '.blockExpansion gr-button'
+ );
+ const tooltipAbove = blockExpansionButtons[0].querySelector(
+ 'paper-tooltip'
+ )!;
+ const tooltipBelow = blockExpansionButtons[1].querySelector(
+ 'paper-tooltip'
+ )!;
+ assert.equal(
+ tooltipAbove.querySelector('.breadcrumbTooltip')!.textContent?.trim(),
+ '20 common lines'
+ );
+ assert.equal(
+ tooltipBelow.querySelector('.breadcrumbTooltip')!.textContent?.trim(),
+ '20 common lines'
+ );
+ assert.equal(tooltipAbove!.getAttribute('position'), 'top');
+ assert.equal(tooltipBelow!.getAttribute('position'), 'bottom');
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 2a4b250..9a98a21 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -29,11 +29,11 @@
css,
customElement,
html,
- internalProperty,
LitElement,
property,
PropertyValues,
query,
+ state,
} from 'lit-element';
import {classMap} from 'lit-html/directives/class-map';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
@@ -66,23 +66,23 @@
// URL for the image to use as revision.
@property({type: String}) revisionUrl = '';
- @internalProperty() protected baseSelected = true;
+ @state() protected baseSelected = true;
- @internalProperty() protected scaledSelected = true;
+ @state() protected scaledSelected = true;
- @internalProperty() protected followMouse = false;
+ @state() protected followMouse = false;
- @internalProperty() protected scale = 1;
+ @state() protected scale = 1;
- @internalProperty() protected checkerboardSelected = true;
+ @state() protected checkerboardSelected = true;
- @internalProperty() protected backgroundColor = '';
+ @state() protected backgroundColor = '';
- @internalProperty() protected automaticBlink = false;
+ @state() protected automaticBlink = false;
- @internalProperty() protected automaticBlinkShown = false;
+ @state() protected automaticBlinkShown = false;
- @internalProperty() protected zoomedImageStyle: StyleInfo = {};
+ @state() protected zoomedImageStyle: StyleInfo = {};
@query('.imageArea') protected imageArea!: HTMLDivElement;
@@ -94,16 +94,16 @@
private imageSize: Dimensions = {width: 0, height: 0};
- @internalProperty()
+ @state()
protected magnifierSize: Dimensions = {width: 0, height: 0};
- @internalProperty()
+ @state()
protected magnifierFrame: Rect = {
origin: {x: 0, y: 0},
dimensions: {width: 0, height: 0},
};
- @internalProperty()
+ @state()
protected overviewFrame: Rect = {
origin: {x: 0, y: 0},
dimensions: {width: 0, height: 0},
@@ -118,7 +118,7 @@
2,
];
- @internalProperty() protected grabbing = false;
+ @state() protected grabbing = false;
private ownsMouseDown = false;
@@ -745,16 +745,20 @@
}
mouseupMagnifier(event: MouseEvent) {
+ if (!this.ownsMouseDown) return;
+ this.grabbing = false;
+ this.ownsMouseDown = false;
const offsetX = event.clientX - this.pointerOnDown.x;
const offsetY = event.clientY - this.pointerOnDown.y;
const distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
// Consider very short drags as clicks. These tend to happen more often on
// external mice.
- if (this.ownsMouseDown && distance < DRAG_DEAD_ZONE_PIXELS) {
+ if (distance < DRAG_DEAD_ZONE_PIXELS) {
this.toggleImage();
+ this.dispatchEvent(createEvent({type: 'magnifier-clicked'}));
+ } else {
+ this.dispatchEvent(createEvent({type: 'magnifier-dragged'}));
}
- this.grabbing = false;
- this.ownsMouseDown = false;
}
mousemoveMagnifier(event: MouseEvent) {
@@ -793,8 +797,10 @@
}
mouseleaveMagnifier() {
+ if (!this.ownsMouseDown) return;
this.grabbing = false;
this.ownsMouseDown = false;
+ this.dispatchEvent(createEvent({type: 'magnifier-dragged'}));
}
dragstartMagnifier(event: DragEvent) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
index d7b6916..9439dca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -18,11 +18,11 @@
css,
customElement,
html,
- internalProperty,
LitElement,
property,
PropertyValues,
query,
+ state,
} from 'lit-element';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
import {ImageDiffAction} from '../../../api/diff';
@@ -45,13 +45,13 @@
@property({type: Object})
frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
- @internalProperty() protected contentStyle: StyleInfo = {};
+ @state() protected contentStyle: StyleInfo = {};
- @internalProperty() protected contentTransformStyle: StyleInfo = {};
+ @state() protected contentTransformStyle: StyleInfo = {};
- @internalProperty() protected frameStyle: StyleInfo = {};
+ @state() protected frameStyle: StyleInfo = {};
- @internalProperty() protected dragging = false;
+ @state() protected dragging = false;
@query('.content-box') protected contentBox!: HTMLDivElement;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
index a14a9cc..4558dda 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
@@ -18,10 +18,10 @@
css,
customElement,
html,
- internalProperty,
LitElement,
property,
PropertyValues,
+ state,
} from 'lit-element';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
import {Rect} from './util';
@@ -41,7 +41,7 @@
@property({type: Object})
frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
- @internalProperty() protected imageStyles: StyleInfo = {};
+ @state() protected imageStyles: StyleInfo = {};
static styles = css`
:host {
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 837a795..62589f8 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -102,7 +102,7 @@
import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin';
import {property} from '@polymer/decorators';
import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
+import {check, Constructor} from '../../utils/common-util';
import {getKeyboardEvent, isModifierPressed} from '../../utils/dom-util';
import {
CustomKeyboardEvent,
@@ -222,11 +222,6 @@
viewMap?: Map<ShortcutSection, SectionView>
) => void;
-interface ShortcutEnabledElement extends PolymerElement {
- // TODO: should replace with Map so we can have proper type here
- keyboardShortcuts(): {[shortcut: string]: string};
-}
-
interface ShortcutHelpItem {
shortcut: Shortcut;
text: string;
@@ -558,14 +553,9 @@
return this.bindings.get(shortcut);
}
- attachHost(host: PolymerElement | ShortcutEnabledElement) {
- if (!('keyboardShortcuts' in host)) {
- return;
- }
- const shortcuts = host.keyboardShortcuts();
- this.activeHosts.set(host, new Map(Object.entries(shortcuts)));
+ attachHost(host: PolymerElement, shortcuts: Map<string, string>) {
+ this.activeHosts.set(host, shortcuts);
this.notifyListeners();
- return shortcuts;
}
detachHost(host: PolymerElement) {
@@ -788,6 +778,12 @@
private readonly restApiService = appContext.restApiService;
+ /** Used to disable shortcuts when the element is not visible. */
+ private observer?: IntersectionObserver;
+
+ /** Are shortcuts currently enabled? True only when element is visible. */
+ private bindingsEnabled = false;
+
modifierPressed(event: CustomKeyboardEvent) {
/* We are checking for g/v as modifiers pressed. There are cases such as
* pressing v and then /, where we want the handler for / to be triggered.
@@ -902,21 +898,62 @@
/** @override */
connectedCallback() {
super.connectedCallback();
-
this.restApiService.getPreferences().then(prefs => {
if (prefs?.disable_keyboard_shortcuts) {
this._disableKeyboardShortcuts = true;
}
});
+ this.createVisibilityObserver();
+ this.enableBindings();
+ }
- const shortcuts = shortcutManager.attachHost(this);
- if (!shortcuts) {
- return;
- }
+ /** @override */
+ disconnectedCallback() {
+ this.destroyVisibilityObserver();
+ this.disableBindings();
+ super.disconnectedCallback();
+ }
- for (const key of Object.keys(shortcuts)) {
- // TODO(TS): not needed if convert shortcuts to Map
- this._addOwnKeyBindings(key as Shortcut, shortcuts[key]);
+ /**
+ * Creates an intersection observer that enables bindings when the
+ * element is visible and disables them when the element is hidden.
+ */
+ private createVisibilityObserver() {
+ if (!this.hasKeyboardShortcuts()) return;
+ if (this.observer) return;
+ this.observer = new IntersectionObserver(entries => {
+ check(entries.length === 1, 'Expected one observer entry.');
+ const isVisible = entries[0].isIntersecting;
+ if (isVisible) {
+ this.enableBindings();
+ } else {
+ this.disableBindings();
+ }
+ });
+ this.observer.observe(this);
+ }
+
+ private destroyVisibilityObserver() {
+ if (this.observer) this.observer.unobserve(this);
+ }
+
+ /**
+ * Enables all the shortcuts returned by keyboardShortcuts().
+ * This is a private method being called when the element becomes
+ * connected or visible.
+ */
+ private enableBindings() {
+ if (!this.hasKeyboardShortcuts()) return;
+ if (this.bindingsEnabled) return;
+ this.bindingsEnabled = true;
+
+ const shortcuts = new Map<string, string>(
+ Object.entries(this.keyboardShortcuts())
+ );
+ shortcutManager.attachHost(this, shortcuts);
+
+ for (const [key, value] of shortcuts.entries()) {
+ this._addOwnKeyBindings(key as Shortcut, value);
}
// each component that uses this behaviour must be aware if go key is
@@ -941,12 +978,21 @@
}
}
- /** @override */
- disconnectedCallback() {
+ /**
+ * Disables all the shortcuts returned by keyboardShortcuts().
+ * This is a private method being called when the element becomes
+ * disconnected or invisible.
+ */
+ private disableBindings() {
+ if (!this.bindingsEnabled) return;
+ this.bindingsEnabled = false;
if (shortcutManager.detachHost(this)) {
this.removeOwnKeyBindings();
}
- super.disconnectedCallback();
+ }
+
+ private hasKeyboardShortcuts() {
+ return Object.entries(this.keyboardShortcuts()).length > 0;
}
keyboardShortcuts() {
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
index b22b8d8..d5e7fe7 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
@@ -181,13 +181,7 @@
mapToObject(mgr.activeShortcutsBySection()),
{});
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [Shortcut.NEXT_FILE]: null,
- };
- },
- });
+ mgr.attachHost({}, new Map([[Shortcut.NEXT_FILE, null]]));
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
@@ -196,13 +190,7 @@
],
});
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [Shortcut.NEXT_LINE]: null,
- };
- },
- });
+ mgr.attachHost({}, new Map([[Shortcut.NEXT_LINE, null]]));
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
@@ -214,14 +202,10 @@
],
});
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [Shortcut.SEARCH]: null,
- [Shortcut.GO_TO_OPENED_CHANGES]: null,
- };
- },
- });
+ mgr.attachHost({}, new Map([
+ [Shortcut.SEARCH, null],
+ [Shortcut.GO_TO_OPENED_CHANGES, null],
+ ]));
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
@@ -254,17 +238,13 @@
assert.deepEqual(mapToObject(mgr.directoryView()), {});
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [Shortcut.GO_TO_OPENED_CHANGES]: null,
- [Shortcut.NEXT_FILE]: null,
- [Shortcut.NEXT_LINE]: null,
- [Shortcut.SAVE_COMMENT]: null,
- [Shortcut.SEARCH]: null,
- };
- },
- });
+ mgr.attachHost({}, new Map([
+ [Shortcut.GO_TO_OPENED_CHANGES, null],
+ [Shortcut.NEXT_FILE, null],
+ [Shortcut.NEXT_LINE, null],
+ [Shortcut.SAVE_COMMENT, null],
+ [Shortcut.SEARCH, null],
+ ]));
assert.deepEqual(
mapToObject(mgr.directoryView()),
{
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index d250537..6d5e475 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -36,7 +36,7 @@
"@webcomponents/webcomponentsjs": "^1.3.3",
"ba-linkify": "file:../../lib/ba-linkify/src/",
"codemirror-minified": "^5.60.0",
- "lit-element": "^2.4.0",
+ "lit-element": "^2.5.1",
"page": "^1.11.6",
"polymer-bridges": "file:../../polymer-bridges/",
"polymer-resin": "^2.0.1",
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 65095dd..30eb658 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -529,7 +529,7 @@
real_author?: AccountInfo;
date: Timestamp;
message: string;
- accountsInMessage?: AccountInfo[];
+ accounts_in_message?: AccountInfo[];
tag?: ReviewInputTag;
_revision_number?: PatchSetNum;
}
@@ -638,6 +638,7 @@
subject: string;
message: string;
web_links?: WebLinkInfo[];
+ resolve_conflicts_web_links?: WebLinkInfo[];
}
export interface CommitInfoWithRequiredCommit extends CommitInfo {
diff --git a/polygerrit-ui/app/utils/account-util.ts b/polygerrit-ui/app/utils/account-util.ts
index ff37608..2a509d3 100644
--- a/polygerrit-ui/app/utils/account-util.ts
+++ b/polygerrit-ui/app/utils/account-util.ts
@@ -18,13 +18,17 @@
import {
AccountId,
AccountInfo,
+ ChangeInfo,
EmailAddress,
+ GroupId,
GroupInfo,
isAccount,
isGroup,
+ ReviewerInput,
} from '../types/common';
-import {AccountTag} from '../constants/constants';
+import {AccountTag, ReviewerState} from '../constants/constants';
import {assertNever} from './common-util';
+import {AccountAddition} from '../elements/shared/gr-account-list/gr-account-list';
export function accountKey(account: AccountInfo): AccountId | EmailAddress {
if (account._account_id) return account._account_id;
@@ -32,6 +36,30 @@
throw new Error('Account has neither _account_id nor email.');
}
+export function mapReviewer(addition: AccountAddition): ReviewerInput {
+ if (addition.account) {
+ return {reviewer: accountKey(addition.account)};
+ }
+ if (addition.group) {
+ const reviewer = decodeURIComponent(addition.group.id) as GroupId;
+ const confirmed = addition.group.confirmed;
+ return {reviewer, confirmed};
+ }
+ throw new Error('Reviewer must be either an account or a group.');
+}
+
+export function isReviewerOrCC(
+ change: ChangeInfo,
+ reviewerAddition: AccountAddition
+): boolean {
+ const reviewers = [
+ ...(change.reviewers[ReviewerState.CC] ?? []),
+ ...(change.reviewers[ReviewerState.REVIEWER] ?? []),
+ ];
+ const reviewer = mapReviewer(reviewerAddition);
+ return reviewers.some(r => accountOrGroupKey(r) === reviewer.reviewer);
+}
+
export function isServiceUser(account?: AccountInfo): boolean {
return !!account?.tags?.includes(AccountTag.SERVICE_USER);
}
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index fa35f288..543017a 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -428,17 +428,17 @@
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-lit-element@^2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-2.4.0.tgz#b22607a037a8fc08f5a80736dddf7f3f5d401452"
- integrity sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==
+lit-element@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-2.5.1.tgz#3fa74b121a6cd22902409ae3859b7847d01aa6b6"
+ integrity sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==
dependencies:
lit-html "^1.1.1"
lit-html@^1.1.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.3.0.tgz#c80f3cc5793a6dea6c07172be90a70ab20e56034"
- integrity sha512-0Q1bwmaFH9O14vycPHw8C/IeHMk/uSDldVLIefu/kfbTBGIc44KGH6A8p1bDfxUfHdc8q6Ct7kQklWoHgr4t1Q==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.4.1.tgz#0c6f3ee4ad4eb610a49831787f0478ad8e9ae5e0"
+ integrity sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==
page@^1.11.6:
version "1.11.6"
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
index 1241665..a9e0a44 100644
--- a/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
@@ -65,6 +65,14 @@
{call InboundEmailRejectionFooter /}
{/template}
+{template InboundEmailRejection_CHANGE_NOT_FOUND kind="text"}
+ Gerrit Code Review was unable to process your email because the change was not found.
+ {\n}
+ Maybe the project doesn't exist or is not visible? Maybe the change is not visible or got
+ deleted?
+ {call InboundEmailRejectionFooter /}
+{/template}
+
{template InboundEmailRejection_COMMENT_REJECTED kind="text"}
Gerrit Code Review rejected one or more comments because they did not pass validation, or
because the maximum number of comments per change would be exceeded.
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
index 6937d13..3444b7f 100644
--- a/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
@@ -82,6 +82,17 @@
{call InboundEmailRejectionFooterHtml /}
{/template}
+{template InboundEmailRejectionHtml_CHANGE_NOT_FOUND}
+ <p>
+ Gerrit Code Review was unable to process your email because the change was not found.
+ </p>
+ <p>
+ Maybe the project doesn't exist or is not visible? Maybe the change is not visible or got
+ deleted?
+ <p>
+ {call InboundEmailRejectionFooterHtml /}
+{/template}
+
{template InboundEmailRejectionHtml_COMMENT_REJECTED}
<p>
Gerrit Code Review rejected one or more comments because they did not pass validation, or
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 6e29299..a0427f2 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -3,8 +3,8 @@
"description": "Gerrit Build Tools",
"browser": false,
"dependencies": {
- "@bazel/rollup": "^3.4.0",
- "@bazel/typescript": "^3.4.0",
+ "@bazel/rollup": "^3.5.0",
+ "@bazel/typescript": "^3.5.0",
"@types/node": "^10.17.12",
"@types/parse5": "^4.0.0",
"@types/parse5-html-rewriting-stream": "^5.1.2",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index b8ac5fb..2825e21 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -492,15 +492,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
-"@bazel/rollup@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.4.0.tgz#cdecb2b90535ef51fb3d56cc8bc19996918bac1a"
- integrity sha512-QKnttbYyEQjRbWrOlkH2JuDnSww+9K7Ppil91zBTtr/qYTGW9XO0v7Ft3cs30s2NIWSGIuKj9/N5as+Uyratrw==
+"@bazel/rollup@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
+ integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
-"@bazel/typescript@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.4.0.tgz#031d989682ff8605ed8745f31448c2f76a1b653a"
- integrity sha512-XlWrlQnsdQHTwsliUAf4mySHOgqRY2S57LKG2rKRjm+a015Lzlmxo6jRQaxjr68UmuhmlklRw0WfCFxdR81AvQ==
+"@bazel/typescript@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.0.tgz#605493f4f0a5297df8a7fcccb86a1a80ea2090bb"
+ integrity sha512-BtGFp4nYFkQTmnONCzomk7dkmOwaINBL3piq+lykBlcc6UxLe9iCAnZpOyPypB1ReN3k3SRNAa53x6oGScQxMg==
dependencies:
protobufjs "6.8.8"
semver "5.6.0"
diff --git a/yarn.lock b/yarn.lock
index ead8dbf..45b4f2c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -500,20 +500,20 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
-"@bazel/rollup@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.4.0.tgz#cdecb2b90535ef51fb3d56cc8bc19996918bac1a"
- integrity sha512-QKnttbYyEQjRbWrOlkH2JuDnSww+9K7Ppil91zBTtr/qYTGW9XO0v7Ft3cs30s2NIWSGIuKj9/N5as+Uyratrw==
+"@bazel/rollup@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
+ integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
-"@bazel/terser@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.4.0.tgz#9a25892977f00974e4195ff6cbe71ec0313a77d5"
- integrity sha512-E26ijh44aXIXcg3EQEZcL2nkGlWZtMka0gwmYo9bDRyGt6rCRhFuSBC0mz9YCifUhKuACKWXLHPz9wvh1CDkEA==
+"@bazel/terser@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b"
+ integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA==
-"@bazel/typescript@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.4.0.tgz#031d989682ff8605ed8745f31448c2f76a1b653a"
- integrity sha512-XlWrlQnsdQHTwsliUAf4mySHOgqRY2S57LKG2rKRjm+a015Lzlmxo6jRQaxjr68UmuhmlklRw0WfCFxdR81AvQ==
+"@bazel/typescript@^3.5.0":
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.0.tgz#605493f4f0a5297df8a7fcccb86a1a80ea2090bb"
+ integrity sha512-BtGFp4nYFkQTmnONCzomk7dkmOwaINBL3piq+lykBlcc6UxLe9iCAnZpOyPypB1ReN3k3SRNAa53x6oGScQxMg==
dependencies:
protobufjs "6.8.8"
semver "5.6.0"