Merge "Reset font weight for label columns"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 4cece72..be50d3b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1218,15 +1218,6 @@
+
Default is true.
-[[change.enableParallelFormatting]]change.enableParallelFormatting::
-+
-Whether or not changes can be formatted in parallel when requesting
-multiple changes at once. An example for this is Dashboards.
-+
-This setting is experimental.
-+
-Default is `false`.
-
[[change.showAssigneeInChangesTable]]change.showAssigneeInChangesTable::
+
Show assignee field in changes table. If set to false, assignees will
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index b4a4a57..37d6d01 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -20,6 +20,15 @@
* `action/retry_timeout_count`: Number of action executions of RetryHelper
that ultimately timed out
+=== Pushes
+
+* `receivecommits/changes`: histogram of number of changes processed
+in a single upload, split up by update type (new change created,
+existing changed updated, change autoclosed).
+* `receivecommits/latency`: latency per change for processing a push,
+split up by update type (create+replace, and autoclose)
+* `receivecommits/timeout`: number of timeouts during push processing.
+
=== Process
* `proc/birth_timestamp`: Time at which the Gerrit process started.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 13c10b2..a340163 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5318,8 +5318,8 @@
}
----
-As response a link:#change-info[ChangeInfo] entity is returned that
-describes the resulting cherry picked change.
+As response a link:#cherry-pick-change-info[CherryPickChangeInfo]
+entity is returned that describes the resulting cherry-pick change.
.Response
----
@@ -5921,6 +5921,23 @@
Which patchset (if any) generated this message.
|==================================
+[[cherry-pick-change-info]]
+=== CherryPickChangeInfo
+The `CherryPickChangeInfo` entity contains information about a
+cherry-pick change.
+
+`CherryPickChangeInfo` has the same fields as link:#change-info[
+ChangeInfo]. In addition `CherryPickChangeInfo` has the following
+fields:
+
+[options="header",cols="1,^1,5"]
+|======================================
+|Field Name ||Description
+|`contains_git_conflicts` |optional, not set if `false`|
+Whether any file in the change contains Git conflict markers.
+|======================================
+
+
[[cherrypick-input]]
=== CherryPickInput
The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
@@ -5928,7 +5945,7 @@
[options="header",cols="1,^1,5"]
|===========================
|Field Name ||Description
-|`message` ||Commit message for the cherry-picked change
+|`message` ||Commit message for the cherry-pick change
|`destination` ||Destination branch
|`base` |optional|
40-hex digit SHA-1 of the commit which will be the parent commit of the newly created change.
@@ -5944,7 +5961,15 @@
Additional information about whom to notify about the update as a map
of recipient type to link:#notify-info[NotifyInfo] entity.
|`keep_reviewers` |optional, defaults to false|
-If true, carries reviewers and ccs over from original change to newly created one.
+If `true`, carries reviewers and ccs over from original change to newly created one.
+|`allow_conflicts` |optional, defaults to false|
+If `true`, the cherry-pick uses content merge and succeeds also if
+there are conflicts. If there are conflicts the file contents of the
+created change contain git conflict markers to indicate the conflicts.
+Callers can find out if there were conflicts by checking the
+`contains_git_conflicts` field in the link:#cherry-pick-change-info[
+CherryPickChangeInfo] that is returned by the cherry-pick REST
+endpoints.
|===========================
[[comment-info]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 944e7b0..58e67e8 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2596,8 +2596,9 @@
}
----
-As response a link:rest-api-changes.html#change-info[ChangeInfo] entity is returned that
-describes the resulting cherry-picked change.
+As response a link:rest-api-changes.html#cherry-pick-change-info[
+CherryPickChangeInfo] entity is returned that describes the resulting
+cherry-picked change.
.Response
----
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index ada2560..f64c449 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -183,7 +183,7 @@
those changes that contain edits, see link:user-search.html#has[has:edit].
-[change-edit-actions]
+[[change-edit-actions]]
== Modifying Changes
@@ -233,4 +233,4 @@
Part of link:index.html[Gerrit Code Review]
-SEARCHBOX
\ No newline at end of file
+SEARCHBOX
diff --git a/WORKSPACE b/WORKSPACE
index d8fd339..62331cd 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1085,12 +1085,12 @@
sha1 = "a2baf2d4fdf03f31fbd39351a32bee25fcdfa1cf",
)
-JACKSON_VERSION = "2.8.9"
+JACKSON_VERSION = "2.9.7"
maven_jar(
name = "jackson-core",
artifact = "com.fasterxml.jackson.core:jackson-core:" + JACKSON_VERSION,
- sha1 = "569b1752705da98f49aabe2911cc956ff7d8ed9d",
+ sha1 = "4b7f0e0dc527fab032e9800ed231080fdc3ac015",
)
maven_jar(
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index df57c27..a4945c7 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -195,8 +195,8 @@
if (recipients.get(type).contains(email) != expected) {
failWithoutActual(
fact(
- expected ? "notifies" : "doesn't notify",
- "[\n" + type + ": " + users.emailToName(email) + "\n]"));
+ expected ? "should notify" : "shouldn't notify",
+ type + ": " + users.emailToName(email)));
}
if (expected) {
accountedFor.add(email);
diff --git a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
index 694e06b..5ac67e7 100644
--- a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -28,4 +28,5 @@
public Map<RecipientType, NotifyInfo> notifyDetails;
public boolean keepReviewers;
+ public boolean allowConflicts;
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java b/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
index 3a33de9..3c32d29 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
@@ -38,7 +38,7 @@
@Override
public String toString() {
- return username;
+ return username != null ? username : email;
}
private ReviewerInfo() {}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 72e762c..3665706 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.EditInfo;
@@ -53,6 +54,8 @@
ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
+ CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
+
ChangeApi rebase() throws RestApiException;
ChangeApi rebase(RebaseInput in) throws RestApiException;
@@ -190,6 +193,11 @@
}
@Override
+ public CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public ChangeApi rebase() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/common/CherryPickChangeInfo.java b/java/com/google/gerrit/extensions/common/CherryPickChangeInfo.java
new file mode 100644
index 0000000..5e2b902
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/CherryPickChangeInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2018 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.common;
+
+public class CherryPickChangeInfo extends ChangeInfo {
+ public Boolean containsGitConflicts;
+}
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index e835e2b..48bd1c1 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -19,7 +19,13 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
@@ -38,22 +44,31 @@
@Singleton
public class AccountResolver {
+ private final Provider<CurrentUser> self;
private final Realm realm;
private final Accounts accounts;
private final AccountCache byId;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final AccountControl.Factory accountControlFactory;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final Emails emails;
@Inject
AccountResolver(
+ Provider<CurrentUser> self,
Realm realm,
Accounts accounts,
AccountCache byId,
+ IdentifiedUser.GenericFactory userFactory,
+ AccountControl.Factory accountControlFactory,
Provider<InternalAccountQuery> accountQueryProvider,
Emails emails) {
+ this.self = self;
this.realm = realm;
this.accounts = accounts;
this.byId = byId;
+ this.userFactory = userFactory;
+ this.accountControlFactory = accountControlFactory;
this.accountQueryProvider = accountQueryProvider;
this.emails = emails;
}
@@ -62,11 +77,11 @@
* Locate exactly one account matching the input string.
*
* @param input a string of the format "Full Name <email@example>", just the email address
- * ("email@example"), a full name ("Full Name"), an account id ("18419") or an user name
+ * ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
* ("username").
* @return the single account that matches; null if no account matches or there are multiple
* candidates. If {@code input} is a numeric string, returns an account if and only if that
- * number corresponds to an actual account.
+ * number corresponds to an actual account ID.
*/
public Account find(String input) throws OrmException, IOException, ConfigInvalidException {
Set<Account.Id> r = findAll(input);
@@ -92,11 +107,11 @@
* Find all accounts matching the input string.
*
* @param input a string of the format "Full Name <email@example>", just the email address
- * ("email@example"), a full name ("Full Name"), an account id ("18419") or an user name
+ * ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
* ("username").
* @return the accounts that match, empty set if none. Never null. If {@code input} is a numeric
- * string, returns a singleton set if that number corresponds to a real account, and an empty
- * set otherwise if it does not.
+ * string, returns a singleton set if that number corresponds to a real account ID, and an
+ * empty set otherwise if it does not.
*/
public Set<Account.Id> findAll(String input)
throws OrmException, IOException, ConfigInvalidException {
@@ -197,4 +212,74 @@
.map(a -> a.getAccount().getId())
.collect(toSet());
}
+
+ /**
+ * Parses a account ID from a request body and returns the user.
+ *
+ * @param id ID of the account, can be a string of the format "{@code Full Name
+ * <email@example.com>}", just the email address, a full name if it is unique, an account ID,
+ * a user name or "{@code self}" for the calling user
+ * @return the user, never null.
+ * @throws UnprocessableEntityException thrown if the account ID cannot be resolved or if the
+ * account is not visible to the calling user
+ */
+ public IdentifiedUser parse(String id)
+ throws AuthException, UnprocessableEntityException, OrmException, IOException,
+ ConfigInvalidException {
+ return parseOnBehalfOf(null, id);
+ }
+
+ /**
+ * Parses an account ID and returns the user without making any permission check whether the
+ * current user can see the account.
+ *
+ * @param id ID of the account, can be a string of the format "{@code Full Name
+ * <email@example.com>}", just the email address, a full name if it is unique, an account ID,
+ * a user name or "{@code self}" for the calling user
+ * @return the user, null if no user is found for the given account ID
+ * @throws AuthException thrown if 'self' is used as account ID and the current user is not
+ * authenticated
+ * @throws OrmException
+ * @throws ConfigInvalidException
+ * @throws IOException
+ */
+ public IdentifiedUser parseId(String id)
+ throws AuthException, OrmException, IOException, ConfigInvalidException {
+ return parseIdOnBehalfOf(null, id);
+ }
+
+ /**
+ * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
+ */
+ public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
+ throws AuthException, UnprocessableEntityException, OrmException, IOException,
+ ConfigInvalidException {
+ IdentifiedUser user = parseIdOnBehalfOf(caller, id);
+ if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
+ throw new UnprocessableEntityException(
+ String.format("Account '%s' is not found or ambiguous", id));
+ }
+ return user;
+ }
+
+ private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
+ throws AuthException, OrmException, IOException, ConfigInvalidException {
+ if (id.equals("self")) {
+ CurrentUser user = self.get();
+ if (user.isIdentifiedUser()) {
+ return user.asIdentifiedUser();
+ } else if (user instanceof AnonymousUser) {
+ throw new AuthException("Authentication required");
+ } else {
+ return null;
+ }
+ }
+
+ Account match = find(id);
+ if (match == null) {
+ return null;
+ }
+ CurrentUser realUser = caller != null ? caller.getRealUser() : null;
+ return userFactory.runAs(null, match.getId(), realUser);
+ }
}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 8357568..bd602a8 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -34,6 +34,7 @@
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.DescriptionInput;
@@ -290,6 +291,15 @@
}
@Override
+ public CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
+ try {
+ return cherryPick.apply(revision, in);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot cherry pick", e);
+ }
+ }
+
+ @Override
public RevisionReviewerApi reviewer(String id) throws RestApiException {
try {
return revisionReviewerApi.create(
diff --git a/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index bac6649..1fdeac1 100644
--- a/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -26,10 +26,11 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.account.AccountsCollection;
import com.google.gerrit.server.restapi.group.CreateGroup;
import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.gerrit.server.restapi.group.ListGroups;
@@ -43,8 +44,9 @@
@Singleton
class GroupsImpl implements Groups {
- private final AccountsCollection accounts;
+ private final AccountResolver accountResolver;
private final GroupsCollection groups;
+ private final GroupResolver groupResolver;
private final ProjectsCollection projects;
private final Provider<ListGroups> listGroups;
private final Provider<QueryGroups> queryGroups;
@@ -54,16 +56,18 @@
@Inject
GroupsImpl(
- AccountsCollection accounts,
+ AccountResolver accountResolver,
GroupsCollection groups,
+ GroupResolver groupResolver,
ProjectsCollection projects,
Provider<ListGroups> listGroups,
Provider<QueryGroups> queryGroups,
PermissionBackend permissionBackend,
CreateGroup createGroup,
GroupApiImpl.Factory api) {
- this.accounts = accounts;
+ this.accountResolver = accountResolver;
this.groups = groups;
+ this.groupResolver = groupResolver;
this.projects = projects;
this.listGroups = listGroups;
this.queryGroups = queryGroups;
@@ -126,7 +130,7 @@
}
for (String group : req.getGroups()) {
- list.addGroup(groups.parse(group).getGroupUUID());
+ list.addGroup(groupResolver.parse(group).getGroupUUID());
}
list.setVisibleToAll(req.getVisibleToAll());
@@ -137,7 +141,7 @@
if (req.getUser() != null) {
try {
- list.setUser(accounts.parse(req.getUser()).getAccountId());
+ list.setUser(accountResolver.parse(req.getUser()).getAccountId());
} catch (Exception e) {
throw asRestApiException("Error looking up user " + req.getUser(), e);
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
similarity index 94%
rename from java/com/google/gerrit/server/restapi/change/PostReviewersEmail.java
rename to java/com/google/gerrit/server/change/AddReviewersEmail.java
index 7540222..4173950 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.restapi.change;
+package com.google.gerrit.server.change;
import static com.google.common.collect.ImmutableList.toImmutableList;
@@ -32,17 +32,17 @@
import java.util.Collection;
@Singleton
-class PostReviewersEmail {
+public class AddReviewersEmail {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final AddReviewerSender.Factory addReviewerSenderFactory;
@Inject
- PostReviewersEmail(AddReviewerSender.Factory addReviewerSenderFactory) {
+ AddReviewersEmail(AddReviewerSender.Factory addReviewerSenderFactory) {
this.addReviewerSenderFactory = addReviewerSenderFactory;
}
- void emailReviewers(
+ public void emailReviewers(
IdentifiedUser user,
Change change,
Collection<Account.Id> added,
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
new file mode 100644
index 0000000..e558d00
--- /dev/null
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -0,0 +1,272 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static java.util.stream.Collectors.toList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.extensions.events.ReviewerAdded;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public class AddReviewersOp implements BatchUpdateOp {
+ public interface Factory {
+
+ /**
+ * Create a new op.
+ *
+ * <p>Users may be added by account or by email addresses, as determined by {@code accountIds}
+ * and {@code addresses}. The reviewer state for both accounts and email addresses is determined
+ * by {@code state}.
+ *
+ * @param accountIds account IDs to add.
+ * @param addresses email addresses to add.
+ * @param state resulting reviewer state.
+ * @param notify notification handling.
+ * @param accountsToNotify additional accounts to notify.
+ * @return batch update operation.
+ */
+ AddReviewersOp create(
+ Set<Account.Id> accountIds,
+ Collection<Address> addresses,
+ ReviewerState state,
+ @Nullable NotifyHandling notify,
+ ListMultimap<RecipientType, Account.Id> accountsToNotify);
+ }
+
+ @AutoValue
+ public abstract static class Result {
+ public abstract ImmutableList<PatchSetApproval> addedReviewers();
+
+ public abstract ImmutableList<Address> addedReviewersByEmail();
+
+ public abstract ImmutableList<Account.Id> addedCCs();
+
+ public abstract ImmutableList<Address> addedCCsByEmail();
+
+ static Builder builder() {
+ return new AutoValue_AddReviewersOp_Result.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setAddedReviewers(Iterable<PatchSetApproval> addedReviewers);
+
+ abstract Builder setAddedReviewersByEmail(Iterable<Address> addedReviewersByEmail);
+
+ abstract Builder setAddedCCs(Iterable<Account.Id> addedCCs);
+
+ abstract Builder setAddedCCsByEmail(Iterable<Address> addedCCsByEmail);
+
+ abstract Result build();
+ }
+ }
+
+ private final ApprovalsUtil approvalsUtil;
+ private final PatchSetUtil psUtil;
+ private final ReviewerAdded reviewerAdded;
+ private final AccountCache accountCache;
+ private final ProjectCache projectCache;
+ private final AddReviewersEmail addReviewersEmail;
+ private final NotesMigration migration;
+ private final Set<Account.Id> accountIds;
+ private final Collection<Address> addresses;
+ private final ReviewerState state;
+ private final NotifyHandling notify;
+ private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
+
+ // Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned
+ // via the REST API is supposed to include vote information.
+ private List<PatchSetApproval> addedReviewers = ImmutableList.of();
+ private Collection<Address> addedReviewersByEmail = ImmutableList.of();
+ private Collection<Account.Id> addedCCs = ImmutableList.of();
+ private Collection<Address> addedCCsByEmail = ImmutableList.of();
+
+ private Change change;
+ private PatchSet patchSet;
+ private Result opResult;
+
+ @Inject
+ AddReviewersOp(
+ ApprovalsUtil approvalsUtil,
+ PatchSetUtil psUtil,
+ ReviewerAdded reviewerAdded,
+ AccountCache accountCache,
+ ProjectCache projectCache,
+ AddReviewersEmail addReviewersEmail,
+ NotesMigration migration,
+ @Assisted Set<Account.Id> accountIds,
+ @Assisted Collection<Address> addresses,
+ @Assisted ReviewerState state,
+ @Assisted @Nullable NotifyHandling notify,
+ @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
+ this.approvalsUtil = approvalsUtil;
+ this.psUtil = psUtil;
+ this.reviewerAdded = reviewerAdded;
+ this.accountCache = accountCache;
+ this.projectCache = projectCache;
+ this.addReviewersEmail = addReviewersEmail;
+ this.migration = migration;
+
+ this.accountIds = accountIds;
+ this.addresses = addresses;
+ this.state = state;
+ this.notify = notify;
+ this.accountsToNotify = accountsToNotify;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws RestApiException, OrmException, IOException {
+ change = ctx.getChange();
+ if (!accountIds.isEmpty()) {
+ if (migration.readChanges() && state == CC) {
+ addedCCs =
+ approvalsUtil.addCcs(
+ ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds);
+ } else {
+ addedReviewers =
+ approvalsUtil.addReviewers(
+ ctx.getDb(),
+ ctx.getNotes(),
+ ctx.getUpdate(change.currentPatchSetId()),
+ projectCache.checkedGet(change.getProject()).getLabelTypes(change.getDest()),
+ change,
+ accountIds);
+ }
+ }
+
+ ImmutableList<Address> addressesToAdd = ImmutableList.of();
+ ReviewerStateInternal internalState = ReviewerStateInternal.fromReviewerState(state);
+ if (migration.readChanges()) {
+ // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does.
+ ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState);
+ addressesToAdd =
+ addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList());
+
+ if (state == CC) {
+ addedCCsByEmail = addressesToAdd;
+ } else {
+ addedReviewersByEmail = addressesToAdd;
+ }
+ for (Address a : addressesToAdd) {
+ ctx.getUpdate(change.currentPatchSetId()).putReviewerByEmail(a, internalState);
+ }
+ }
+ if (addedCCs.isEmpty() && addedReviewers.isEmpty() && addressesToAdd.isEmpty()) {
+ return false;
+ }
+
+ checkAdded();
+
+ patchSet = psUtil.current(ctx.getDb(), ctx.getNotes());
+ return true;
+ }
+
+ private void checkAdded() {
+ // Should only affect either reviewers or CCs, not both. But the logic in updateChange is
+ // complex, so programmer error is conceivable.
+ boolean addedAnyReviewers = !addedReviewers.isEmpty() || !addedReviewersByEmail.isEmpty();
+ boolean addedAnyCCs = !addedCCs.isEmpty() || !addedCCsByEmail.isEmpty();
+ checkState(
+ !(addedAnyReviewers && addedAnyCCs),
+ "should not have added both reviewers and CCs:\n"
+ + "Arguments:\n"
+ + " accountIds=%s\n"
+ + " addresses=%s\n"
+ + "Results:\n"
+ + " addedReviewers=%s\n"
+ + " addedReviewersByEmail=%s\n"
+ + " addedCCs=%s\n"
+ + " addedCCsByEmail=%s",
+ accountIds,
+ addresses,
+ addedReviewers,
+ addedReviewersByEmail,
+ addedCCs,
+ addedCCsByEmail);
+ }
+
+ @Override
+ public void postUpdate(Context ctx) throws Exception {
+ opResult =
+ Result.builder()
+ .setAddedReviewers(addedReviewers)
+ .setAddedReviewersByEmail(addedReviewersByEmail)
+ .setAddedCCs(addedCCs)
+ .setAddedCCsByEmail(addedCCsByEmail)
+ .build();
+ addReviewersEmail.emailReviewers(
+ ctx.getUser().asIdentifiedUser(),
+ change,
+ Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
+ addedCCs,
+ addedReviewersByEmail,
+ addedCCsByEmail,
+ notify,
+ accountsToNotify,
+ !change.isWorkInProgress());
+ if (!addedReviewers.isEmpty()) {
+ List<AccountState> reviewers =
+ addedReviewers
+ .stream()
+ .map(r -> accountCache.get(r.getAccountId()))
+ .flatMap(Streams::stream)
+ .collect(toList());
+ reviewerAdded.fire(change, patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
+ }
+ }
+
+ public Result getResult() {
+ checkState(opResult != null, "Batch update wasn't executed yet");
+ return opResult;
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 5de50f6..ad41920 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -14,55 +14,36 @@
package com.google.gerrit.server.change;
-import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
-import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
-import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
-import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_MERGEABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
-import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
-import static com.google.gerrit.server.CommonConverters.toGitPerson;
import static java.util.stream.Collectors.toList;
-import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
-import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
-import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRecord.Status;
import com.google.gerrit.common.data.SubmitRequirement;
@@ -74,22 +55,13 @@
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.FetchInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.ProblemInfo;
-import com.google.gerrit.extensions.common.PushCertificateInfo;
import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.common.VotingRangeInfo;
-import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.config.DownloadCommand;
-import com.google.gerrit.extensions.config.DownloadScheme;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.Extension;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.mail.Address;
@@ -100,40 +72,27 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.GpgException;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.GpgApiAdapter;
-import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
-import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
@@ -150,23 +109,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
+import java.util.function.Supplier;
/**
* Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
@@ -183,7 +130,7 @@
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
- public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
+ static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
ImmutableSet.of(
ALL_COMMITS,
ALL_REVISIONS,
@@ -251,34 +198,21 @@
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> userProvider;
- private final AnonymousUser anonymous;
private final PermissionBackend permissionBackend;
- private final GitRepositoryManager repoManager;
- private final ProjectCache projectCache;
- private final MergeUtil.Factory mergeUtilFactory;
- private final IdentifiedUser.GenericFactory userFactory;
private final ChangeData.Factory changeDataFactory;
- private final FileInfoJson fileInfoJson;
private final AccountLoader.Factory accountLoaderFactory;
- private final DynamicMap<DownloadScheme> downloadSchemes;
- private final DynamicMap<DownloadCommand> downloadCommands;
- private final WebLinks webLinks;
private final ImmutableSet<ListChangesOption> options;
private final ChangeMessagesUtil cmUtil;
private final Provider<ConsistencyChecker> checkerProvider;
private final ActionJson actionJson;
- private final GpgApiAdapter gpgApi;
private final ChangeNotes.Factory notesFactory;
- private final ChangeResource.Factory changeResourceFactory;
- private final ChangeKindCache changeKindCache;
- private final ApprovalsUtil approvalsUtil;
+ private final LabelsJson labelsJson;
private final RemoveReviewerControl removeReviewerControl;
private final TrackingFooters trackingFooters;
private final Metrics metrics;
- private final boolean enableParallelFormatting;
- private final ExecutorService fanOutExecutor;
+ private final RevisionJson revisionJson;
+ private final boolean lazyLoad;
- private boolean lazyLoad = true;
private AccountLoader accountLoader;
private FixInput fix;
private PluginDefinedAttributesFactory pluginDefinedAttributesFactory;
@@ -287,70 +221,50 @@
ChangeJson(
Provider<ReviewDb> db,
Provider<CurrentUser> user,
- AnonymousUser au,
PermissionBackend permissionBackend,
- GitRepositoryManager repoManager,
- ProjectCache projectCache,
- MergeUtil.Factory mergeUtilFactory,
- IdentifiedUser.GenericFactory uf,
ChangeData.Factory cdf,
- FileInfoJson fileInfoJson,
AccountLoader.Factory ailf,
- DynamicMap<DownloadScheme> downloadSchemes,
- DynamicMap<DownloadCommand> downloadCommands,
- WebLinks webLinks,
ChangeMessagesUtil cmUtil,
Provider<ConsistencyChecker> checkerProvider,
ActionJson actionJson,
- GpgApiAdapter gpgApi,
ChangeNotes.Factory notesFactory,
- ChangeResource.Factory changeResourceFactory,
- ChangeKindCache changeKindCache,
- ApprovalsUtil approvalsUtil,
+ LabelsJson.Factory labelsJsonFactory,
RemoveReviewerControl removeReviewerControl,
TrackingFooters trackingFooters,
Metrics metrics,
- @GerritServerConfig Config config,
- @FanOutExecutor ExecutorService fanOutExecutor,
+ RevisionJson.Factory revisionJsonFactory,
@Assisted Iterable<ListChangesOption> options) {
this.db = db;
this.userProvider = user;
- this.anonymous = au;
this.changeDataFactory = cdf;
this.permissionBackend = permissionBackend;
- this.repoManager = repoManager;
- this.userFactory = uf;
- this.projectCache = projectCache;
- this.mergeUtilFactory = mergeUtilFactory;
- this.fileInfoJson = fileInfoJson;
this.accountLoaderFactory = ailf;
- this.downloadSchemes = downloadSchemes;
- this.downloadCommands = downloadCommands;
- this.webLinks = webLinks;
this.cmUtil = cmUtil;
this.checkerProvider = checkerProvider;
this.actionJson = actionJson;
- this.gpgApi = gpgApi;
this.notesFactory = notesFactory;
- this.changeResourceFactory = changeResourceFactory;
- this.changeKindCache = changeKindCache;
- this.approvalsUtil = approvalsUtil;
+ this.labelsJson = labelsJsonFactory.create(options);
this.removeReviewerControl = removeReviewerControl;
this.trackingFooters = trackingFooters;
this.metrics = metrics;
- this.enableParallelFormatting = config.getBoolean("change", "enableParallelFormatting", false);
- this.fanOutExecutor = fanOutExecutor;
+ this.revisionJson = revisionJsonFactory.create(options);
this.options = Sets.immutableEnumSet(options);
+ this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
logger.atFine().log("options = %s", options);
}
- /**
- * See {@link ChangeData#setLazyLoad(boolean)}. If lazyLoad is set, converting data from
- * index-backed {@link ChangeData} will fail with an exception.
- */
- public ChangeJson lazyLoad(boolean load) {
- lazyLoad = load;
- return this;
+ public static ApprovalInfo getApprovalInfo(
+ Account.Id id,
+ Integer value,
+ VotingRangeInfo permittedVotingRange,
+ String tag,
+ Timestamp date) {
+ ApprovalInfo ai = new ApprovalInfo(id.get());
+ ai.value = value;
+ ai.permittedVotingRange = permittedVotingRange;
+ ai.date = date;
+ ai.tag = tag;
+ return ai;
}
public ChangeJson fix(FixInput fix) {
@@ -371,6 +285,11 @@
}
public ChangeInfo format(Project.NameKey project, Change.Id id) throws OrmException {
+ return format(project, id, ChangeInfo::new);
+ }
+
+ public <I extends ChangeInfo> I format(
+ Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) throws OrmException {
ChangeNotes notes;
try {
notes = notesFactory.createChecked(db.get(), project, id);
@@ -378,43 +297,23 @@
if (!has(CHECK)) {
throw e;
}
- return checkOnly(changeDataFactory.create(db.get(), project, id));
+ return checkOnly(changeDataFactory.create(db.get(), project, id), changeInfoSupplier);
}
- return format(changeDataFactory.create(db.get(), notes));
+ return format(changeDataFactory.create(db.get(), notes), changeInfoSupplier);
}
public ChangeInfo format(ChangeData cd) throws OrmException {
- return format(cd, Optional.empty(), true);
+ return format(cd, ChangeInfo::new);
}
- private ChangeInfo format(
- ChangeData cd, Optional<PatchSet.Id> limitToPsId, boolean fillAccountLoader)
+ public <I extends ChangeInfo> I format(ChangeData cd, Supplier<I> changeInfoSupplier)
throws OrmException {
- try {
- if (fillAccountLoader) {
- accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
- ChangeInfo res = toChangeInfo(cd, limitToPsId);
- accountLoader.fill();
- return res;
- }
- return toChangeInfo(cd, limitToPsId);
- } catch (PatchListNotAvailableException
- | GpgException
- | OrmException
- | IOException
- | PermissionBackendException
- | RuntimeException e) {
- if (!has(CHECK)) {
- Throwables.throwIfInstanceOf(e, OrmException.class);
- throw new OrmException(e);
- }
- return checkOnly(cd);
- }
+ return format(cd, Optional.empty(), true, changeInfoSupplier);
}
public ChangeInfo format(RevisionResource rsrc) throws OrmException {
ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
- return format(cd, Optional.of(rsrc.getPatchSet().getId()), true);
+ return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new);
}
public List<List<ChangeInfo>> formatQueryResults(List<QueryResult<ChangeData>> in)
@@ -442,7 +341,7 @@
ensureLoaded(in);
List<ChangeInfo> out = new ArrayList<>(in.size());
for (ChangeData cd : in) {
- out.add(format(cd, Optional.empty(), false));
+ out.add(format(cd, Optional.empty(), false, ChangeInfo::new));
}
accountLoader.fill();
return out;
@@ -465,6 +364,45 @@
return new SubmitRequirementInfo(status.name(), req.fallbackText(), req.type(), req.data());
}
+ private static void finish(ChangeInfo info) {
+ info.id =
+ Joiner.on('~')
+ .join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId));
+ }
+
+ private static boolean containsAnyOf(
+ ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
+ return !Sets.intersection(toFind, set).isEmpty();
+ }
+
+ private <I extends ChangeInfo> I format(
+ ChangeData cd,
+ Optional<PatchSet.Id> limitToPsId,
+ boolean fillAccountLoader,
+ Supplier<I> changeInfoSupplier)
+ throws OrmException {
+ try {
+ if (fillAccountLoader) {
+ accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+ I res = toChangeInfo(cd, limitToPsId, changeInfoSupplier);
+ accountLoader.fill();
+ return res;
+ }
+ return toChangeInfo(cd, limitToPsId, changeInfoSupplier);
+ } catch (PatchListNotAvailableException
+ | GpgException
+ | OrmException
+ | IOException
+ | PermissionBackendException
+ | RuntimeException e) {
+ if (!has(CHECK)) {
+ Throwables.throwIfInstanceOf(e, OrmException.class);
+ throw new OrmException(e);
+ }
+ return checkOnly(cd, changeInfoSupplier);
+ }
+ }
+
private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
if (lazyLoad) {
ChangeData.ensureChangeLoaded(all);
@@ -491,62 +429,32 @@
private List<ChangeInfo> toChangeInfos(
List<ChangeData> changes, Map<Change.Id, ChangeInfo> cache) {
try (Timer0.Context ignored = metrics.toChangeInfosLatency.start()) {
- // Create a list of formatting calls that can be called sequentially or in parallel
- List<Callable<Optional<ChangeInfo>>> formattingCalls = new ArrayList<>(changes.size());
+ List<ChangeInfo> changeInfos = new ArrayList<>(changes.size());
for (ChangeData cd : changes) {
- formattingCalls.add(
- () -> {
- ChangeInfo i = cache.get(cd.getId());
- if (i != null) {
- return Optional.of(i);
- }
- try {
- ensureLoaded(Collections.singleton(cd));
- return Optional.of(format(cd, Optional.empty(), false));
- } catch (OrmException | RuntimeException e) {
- logger.atWarning().withCause(e).log(
- "Omitting corrupt change %s from results", cd.getId());
- return Optional.empty();
- }
- });
- }
-
- long numProjects = changes.stream().map(ChangeData::project).distinct().count();
- if (!enableParallelFormatting || !lazyLoad || changes.size() < 3 || numProjects < 2) {
- // Format these changes in the request thread as the multithreading overhead would be too
- // high.
- List<ChangeInfo> result = new ArrayList<>(changes.size());
- for (Callable<Optional<ChangeInfo>> c : formattingCalls) {
- try {
- c.call().ifPresent(result::add);
- } catch (Exception e) {
- logger.atWarning().withCause(e).log("Omitting change due to exception");
- }
+ ChangeInfo i = cache.get(cd.getId());
+ if (i != null) {
+ continue;
}
- return result;
- }
-
- // Format the changes in parallel on the executor
- List<ChangeInfo> result = new ArrayList<>(changes.size());
- try {
- for (Future<Optional<ChangeInfo>> f : fanOutExecutor.invokeAll(formattingCalls)) {
- f.get().ifPresent(result::add);
+ try {
+ ensureLoaded(Collections.singleton(cd));
+ changeInfos.add(format(cd, Optional.empty(), false, ChangeInfo::new));
+ } catch (OrmException | RuntimeException e) {
+ logger.atWarning().withCause(e).log(
+ "Omitting corrupt change %s from results", cd.getId());
}
- } catch (InterruptedException | ExecutionException e) {
- throw new IllegalStateException(e);
}
- return result;
+ return changeInfos;
}
}
- private ChangeInfo checkOnly(ChangeData cd) {
+ private <I extends ChangeInfo> I checkOnly(ChangeData cd, Supplier<I> changeInfoSupplier) {
ChangeNotes notes;
try {
notes = cd.notes();
} catch (OrmException e) {
String msg = "Error loading change";
logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
- ChangeInfo info = new ChangeInfo();
+ I info = changeInfoSupplier.get();
info._number = cd.getId().get();
ProblemInfo p = new ProblemInfo();
p.message = msg;
@@ -555,10 +463,9 @@
}
ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix);
- ChangeInfo info;
+ I info = changeInfoSupplier.get();
Change c = result.change();
if (c != null) {
- info = new ChangeInfo();
info.project = c.getProject().get();
info.branch = c.getDest().getShortName();
info.topic = c.getTopic();
@@ -575,25 +482,26 @@
info.hasReviewStarted = c.hasReviewStarted();
finish(info);
} else {
- info = new ChangeInfo();
info._number = result.id().get();
info.problems = result.problems();
}
return info;
}
- private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
+ private <I extends ChangeInfo> I toChangeInfo(
+ ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
IOException {
try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
- return toChangeInfoImpl(cd, limitToPsId);
+ return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier);
}
}
- private ChangeInfo toChangeInfoImpl(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
+ private <I extends ChangeInfo> I toChangeInfoImpl(
+ ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
IOException {
- ChangeInfo out = new ChangeInfo();
+ I out = changeInfoSupplier.get();
CurrentUser user = userProvider.get();
if (has(CHECK)) {
@@ -654,7 +562,7 @@
out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
}
- out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
+ out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
out.requirements = requirementsFor(cd);
if (out.labels != null && has(DETAILED_LABELS)) {
@@ -664,7 +572,7 @@
&& (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
out.permittedLabels =
cd.change().getStatus() != Change.Status.ABANDONED
- ? permittedLabels(user.getAccountId(), cd)
+ ? labelsJson.permittedLabels(user.getAccountId(), cd)
: ImmutableMap.of();
}
@@ -699,7 +607,7 @@
// This block must come after the ChangeInfo is mostly populated, since
// it will be passed to ActionVisitors as-is.
if (needRevisions) {
- out.revisions = revisions(cd, src, limitToPsId, out);
+ out.revisions = revisionJson.getRevisions(accountLoader, cd, src, limitToPsId, out);
if (out.revisions != null) {
for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
if (entry.getValue().isCurrent) {
@@ -760,210 +668,6 @@
return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
}
- private List<SubmitRecord> submitRecords(ChangeData cd) {
- return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
- }
-
- private Map<String, LabelInfo> labelsFor(ChangeData cd, boolean standard, boolean detailed)
- throws OrmException, PermissionBackendException {
- if (!standard && !detailed) {
- return null;
- }
-
- LabelTypes labelTypes = cd.getLabelTypes();
- Map<String, LabelWithStatus> withStatus =
- cd.change().getStatus() == Change.Status.MERGED
- ? labelsForSubmittedChange(cd, labelTypes, standard, detailed)
- : labelsForUnsubmittedChange(cd, labelTypes, standard, detailed);
- return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
- }
-
- private Map<String, LabelWithStatus> labelsForUnsubmittedChange(
- ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
- throws OrmException, PermissionBackendException {
- Map<String, LabelWithStatus> labels = initLabels(cd, labelTypes, standard);
- if (detailed) {
- setAllApprovals(cd, labels);
- }
- for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
- LabelType type = labelTypes.byLabel(e.getKey());
- if (type == null) {
- continue;
- }
- if (standard) {
- for (PatchSetApproval psa : cd.currentApprovals()) {
- if (type.matches(psa)) {
- short val = psa.getValue();
- Account.Id accountId = psa.getAccountId();
- setLabelScores(type, e.getValue(), val, accountId);
- }
- }
- }
- if (detailed) {
- setLabelValues(type, e.getValue());
- }
- }
- return labels;
- }
-
- private Map<String, LabelWithStatus> initLabels(
- ChangeData cd, LabelTypes labelTypes, boolean standard) {
- Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
- for (SubmitRecord rec : submitRecords(cd)) {
- if (rec.labels == null) {
- continue;
- }
- for (SubmitRecord.Label r : rec.labels) {
- LabelWithStatus p = labels.get(r.label);
- if (p == null || p.status().compareTo(r.status) < 0) {
- LabelInfo n = new LabelInfo();
- if (standard) {
- switch (r.status) {
- case OK:
- n.approved = accountLoader.get(r.appliedBy);
- break;
- case REJECT:
- n.rejected = accountLoader.get(r.appliedBy);
- n.blocking = true;
- break;
- case IMPOSSIBLE:
- case MAY:
- case NEED:
- default:
- break;
- }
- }
-
- n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null;
- labels.put(r.label, LabelWithStatus.create(n, r.status));
- }
- }
- }
- return labels;
- }
-
- private void setLabelScores(
- LabelType type, LabelWithStatus l, short score, Account.Id accountId) {
- if (l.label().approved != null || l.label().rejected != null) {
- return;
- }
-
- if (type.getMin() == null || type.getMax() == null) {
- // Can't set score for unknown or misconfigured type.
- return;
- }
-
- if (score != 0) {
- if (score == type.getMin().getValue()) {
- l.label().rejected = accountLoader.get(accountId);
- } else if (score == type.getMax().getValue()) {
- l.label().approved = accountLoader.get(accountId);
- } else if (score < 0) {
- l.label().disliked = accountLoader.get(accountId);
- l.label().value = score;
- } else if (score > 0 && l.label().disliked == null) {
- l.label().recommended = accountLoader.get(accountId);
- l.label().value = score;
- }
- }
- }
-
- private void setAllApprovals(ChangeData cd, Map<String, LabelWithStatus> labels)
- throws OrmException, PermissionBackendException {
- Change.Status status = cd.change().getStatus();
- checkState(
- status != Change.Status.MERGED, "should not call setAllApprovals on %s change", status);
-
- // Include a user in the output for this label if either:
- // - They are an explicit reviewer.
- // - They ever voted on this change.
- Set<Account.Id> allUsers = new HashSet<>();
- allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
- for (PatchSetApproval psa : cd.approvals().values()) {
- allUsers.add(psa.getAccountId());
- }
-
- Table<Account.Id, String, PatchSetApproval> current =
- HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
- for (PatchSetApproval psa : cd.currentApprovals()) {
- current.put(psa.getAccountId(), psa.getLabel(), psa);
- }
-
- LabelTypes labelTypes = cd.getLabelTypes();
- for (Account.Id accountId : allUsers) {
- PermissionBackend.ForChange perm = permissionBackendForChange(accountId, cd);
- Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
- for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
- LabelType lt = labelTypes.byLabel(e.getKey());
- if (lt == null) {
- // Ignore submit record for undefined label; likely the submit rule
- // author didn't intend for the label to show up in the table.
- continue;
- }
- Integer value;
- VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.getName(), null);
- String tag = null;
- Timestamp date = null;
- PatchSetApproval psa = current.get(accountId, lt.getName());
- if (psa != null) {
- value = Integer.valueOf(psa.getValue());
- if (value == 0) {
- // This may be a dummy approval that was inserted when the reviewer
- // was added. Explicitly check whether the user can vote on this
- // label.
- value = perm.test(new LabelPermission(lt)) ? 0 : null;
- }
- tag = psa.getTag();
- date = psa.getGranted();
- if (psa.isPostSubmit()) {
- logger.atWarning().log("unexpected post-submit approval on open change: %s", psa);
- }
- } else {
- // Either the user cannot vote on this label, or they were added as a
- // reviewer but have not responded yet. Explicitly check whether the
- // user can vote on this label.
- value = perm.test(new LabelPermission(lt)) ? 0 : null;
- }
- addApproval(
- e.getValue().label(), approvalInfo(accountId, value, permittedVotingRange, tag, date));
- }
- }
- }
-
- private Map<String, VotingRangeInfo> getPermittedVotingRanges(
- Map<String, Collection<String>> permittedLabels) {
- Map<String, VotingRangeInfo> permittedVotingRanges =
- Maps.newHashMapWithExpectedSize(permittedLabels.size());
- for (String label : permittedLabels.keySet()) {
- List<Integer> permittedVotingRange =
- permittedLabels
- .get(label)
- .stream()
- .map(this::parseRangeValue)
- .filter(java.util.Objects::nonNull)
- .sorted()
- .collect(toList());
-
- if (permittedVotingRange.isEmpty()) {
- permittedVotingRanges.put(label, null);
- } else {
- int minPermittedValue = permittedVotingRange.get(0);
- int maxPermittedValue = Iterables.getLast(permittedVotingRange);
- permittedVotingRanges.put(label, new VotingRangeInfo(minPermittedValue, maxPermittedValue));
- }
- }
- return permittedVotingRanges;
- }
-
- private Integer parseRangeValue(String value) {
- if (value.startsWith("+")) {
- value = value.substring(1);
- } else if (value.startsWith(" ")) {
- value = value.trim();
- }
- return Ints.tryParse(value);
- }
-
private void setSubmitter(ChangeData cd, ChangeInfo out) throws OrmException {
Optional<PatchSetApproval> s = cd.getSubmitApproval();
if (!s.isPresent()) {
@@ -973,214 +677,6 @@
out.submitter = accountLoader.get(s.get().getAccountId());
}
- private Map<String, LabelWithStatus> labelsForSubmittedChange(
- ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
- throws OrmException, PermissionBackendException {
- Set<Account.Id> allUsers = new HashSet<>();
- if (detailed) {
- // Users expect to see all reviewers on closed changes, even if they
- // didn't vote on the latest patch set. If we don't need detailed labels,
- // we aren't including 0 votes for all users below, so we can just look at
- // the latest patch set (in the next loop).
- for (PatchSetApproval psa : cd.approvals().values()) {
- allUsers.add(psa.getAccountId());
- }
- }
-
- Set<String> labelNames = new HashSet<>();
- SetMultimap<Account.Id, PatchSetApproval> current =
- MultimapBuilder.hashKeys().hashSetValues().build();
- for (PatchSetApproval a : cd.currentApprovals()) {
- allUsers.add(a.getAccountId());
- LabelType type = labelTypes.byLabel(a.getLabelId());
- if (type != null) {
- labelNames.add(type.getName());
- // Not worth the effort to distinguish between votable/non-votable for 0
- // values on closed changes, since they can't vote anyway.
- current.put(a.getAccountId(), a);
- }
- }
-
- // Since voting on merged changes is allowed all labels which apply to
- // the change must be returned. All applying labels can be retrieved from
- // the submit records, which is what initLabels does.
- // It's not possible to only compute the labels based on the approvals
- // since merged changes may not have approvals for all labels (e.g. if not
- // all labels are required for submit or if the change was auto-closed due
- // to direct push or if new labels were defined after the change was
- // merged).
- Map<String, LabelWithStatus> labels;
- labels = initLabels(cd, labelTypes, standard);
-
- // Also include all labels for which approvals exists. E.g. there can be
- // approvals for labels that are ignored by a Prolog submit rule and hence
- // it wouldn't be included in the submit records.
- for (String name : labelNames) {
- if (!labels.containsKey(name)) {
- labels.put(name, LabelWithStatus.create(new LabelInfo(), null));
- }
- }
-
- if (detailed) {
- labels
- .entrySet()
- .stream()
- .filter(e -> labelTypes.byLabel(e.getKey()) != null)
- .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), e.getValue()));
- }
-
- for (Account.Id accountId : allUsers) {
- Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size());
- Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
- if (detailed) {
- pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
- for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
- ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null);
- byLabel.put(entry.getKey(), ai);
- addApproval(entry.getValue().label(), ai);
- }
- }
- for (PatchSetApproval psa : current.get(accountId)) {
- LabelType type = labelTypes.byLabel(psa.getLabelId());
- if (type == null) {
- continue;
- }
-
- short val = psa.getValue();
- ApprovalInfo info = byLabel.get(type.getName());
- if (info != null) {
- info.value = Integer.valueOf(val);
- info.permittedVotingRange = pvr.getOrDefault(type.getName(), null);
- info.date = psa.getGranted();
- info.tag = psa.getTag();
- if (psa.isPostSubmit()) {
- info.postSubmit = true;
- }
- }
- if (!standard) {
- continue;
- }
-
- setLabelScores(type, labels.get(type.getName()), val, accountId);
- }
- }
- return labels;
- }
-
- private ApprovalInfo approvalInfo(
- Account.Id id,
- Integer value,
- VotingRangeInfo permittedVotingRange,
- String tag,
- Timestamp date) {
- ApprovalInfo ai = getApprovalInfo(id, value, permittedVotingRange, tag, date);
- accountLoader.put(ai);
- return ai;
- }
-
- public static ApprovalInfo getApprovalInfo(
- Account.Id id,
- Integer value,
- VotingRangeInfo permittedVotingRange,
- String tag,
- Timestamp date) {
- ApprovalInfo ai = new ApprovalInfo(id.get());
- ai.value = value;
- ai.permittedVotingRange = permittedVotingRange;
- ai.date = date;
- ai.tag = tag;
- return ai;
- }
-
- private static boolean isOnlyZero(Collection<String> values) {
- return values.isEmpty() || (values.size() == 1 && values.contains(" 0"));
- }
-
- private void setLabelValues(LabelType type, LabelWithStatus l) {
- l.label().defaultValue = type.getDefaultValue();
- l.label().values = new LinkedHashMap<>();
- for (LabelValue v : type.getValues()) {
- l.label().values.put(v.formatValue(), v.getText());
- }
- if (isOnlyZero(l.label().values.keySet())) {
- l.label().values = null;
- }
- }
-
- private Map<String, Collection<String>> permittedLabels(
- Account.Id filterApprovalsBy, ChangeData cd) throws OrmException, PermissionBackendException {
- boolean isMerged = cd.change().getStatus() == Change.Status.MERGED;
- LabelTypes labelTypes = cd.getLabelTypes();
- Map<String, LabelType> toCheck = new HashMap<>();
- for (SubmitRecord rec : submitRecords(cd)) {
- if (rec.labels != null) {
- for (SubmitRecord.Label r : rec.labels) {
- LabelType type = labelTypes.byLabel(r.label);
- if (type != null && (!isMerged || type.allowPostSubmit())) {
- toCheck.put(type.getName(), type);
- }
- }
- }
- }
-
- Map<String, Short> labels = null;
- Set<LabelPermission.WithValue> can =
- permissionBackendForChange(filterApprovalsBy, cd).testLabels(toCheck.values());
- SetMultimap<String, String> permitted = LinkedHashMultimap.create();
- for (SubmitRecord rec : submitRecords(cd)) {
- if (rec.labels == null) {
- continue;
- }
- for (SubmitRecord.Label r : rec.labels) {
- LabelType type = labelTypes.byLabel(r.label);
- if (type == null || (isMerged && !type.allowPostSubmit())) {
- continue;
- }
-
- for (LabelValue v : type.getValues()) {
- boolean ok = can.contains(new LabelPermission.WithValue(type, v));
- if (isMerged) {
- if (labels == null) {
- labels = currentLabels(filterApprovalsBy, cd);
- }
- short prev = labels.getOrDefault(type.getName(), (short) 0);
- ok &= v.getValue() >= prev;
- }
- if (ok) {
- permitted.put(r.label, v.formatValue());
- }
- }
- }
- }
-
- List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
- for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
- if (isOnlyZero(e.getValue())) {
- toClear.add(e.getKey());
- }
- }
- for (String label : toClear) {
- permitted.removeAll(label);
- }
- return permitted.asMap();
- }
-
- private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd)
- throws OrmException {
- Map<String, Short> result = new HashMap<>();
- for (PatchSetApproval psa :
- approvalsUtil.byPatchSetUser(
- db.get(),
- lazyLoad ? cd.notes() : notesFactory.createFromIndexedChange(cd.change()),
- cd.change().currentPatchSetId(),
- accountId,
- null,
- null)) {
- result.put(psa.getLabel(), psa.getValue());
- }
- return result;
- }
-
private Collection<ChangeMessageInfo> messages(ChangeData cd) throws OrmException {
List<ChangeMessage> messages = cmUtil.byChange(db.get(), cd.notes());
if (messages.isEmpty()) {
@@ -1285,47 +781,6 @@
.collect(toList());
}
- @Nullable
- private Repository openRepoIfNecessary(Project.NameKey project) throws IOException {
- if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
- return repoManager.openRepository(project);
- }
- return null;
- }
-
- @Nullable
- private RevWalk newRevWalk(@Nullable Repository repo) {
- return repo != null ? new RevWalk(repo) : null;
- }
-
- private Map<String, RevisionInfo> revisions(
- ChangeData cd,
- Map<PatchSet.Id, PatchSet> map,
- Optional<PatchSet.Id> limitToPsId,
- ChangeInfo changeInfo)
- throws PatchListNotAvailableException, GpgException, OrmException, IOException,
- PermissionBackendException {
- Map<String, RevisionInfo> res = new LinkedHashMap<>();
- try (Repository repo = openRepoIfNecessary(cd.project());
- RevWalk rw = newRevWalk(repo)) {
- for (PatchSet in : map.values()) {
- PatchSet.Id id = in.getId();
- boolean want;
- if (has(ALL_REVISIONS)) {
- want = true;
- } else if (limitToPsId.isPresent()) {
- want = id.equals(limitToPsId.get());
- } else {
- want = id.equals(cd.change().currentPatchSetId());
- }
- if (want) {
- res.put(in.getRevision().get(), toRevisionInfo(cd, in, repo, rw, false, changeInfo));
- }
- }
- return res;
- }
- }
-
private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
throws OrmException {
Collection<PatchSet> src;
@@ -1353,197 +808,11 @@
return map;
}
- public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
- throws PatchListNotAvailableException, GpgException, OrmException, IOException,
- PermissionBackendException {
- accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
- try (Repository repo = openRepoIfNecessary(cd.project());
- RevWalk rw = newRevWalk(repo)) {
- RevisionInfo rev = toRevisionInfo(cd, in, repo, rw, true, null);
- accountLoader.fill();
- return rev;
- }
- }
-
- private RevisionInfo toRevisionInfo(
- ChangeData cd,
- PatchSet in,
- @Nullable Repository repo,
- @Nullable RevWalk rw,
- boolean fillCommit,
- @Nullable ChangeInfo changeInfo)
- throws PatchListNotAvailableException, GpgException, OrmException, IOException,
- PermissionBackendException {
- Change c = cd.change();
- RevisionInfo out = new RevisionInfo();
- out.isCurrent = in.getId().equals(c.currentPatchSetId());
- out._number = in.getId().get();
- out.ref = in.getRefName();
- out.created = in.getCreatedOn();
- out.uploader = accountLoader.get(in.getUploader());
- out.fetch = makeFetchMap(cd, in);
- out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
- out.description = in.getDescription();
-
- boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
- boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
- if (setCommit || addFooters) {
- checkState(rw != null);
- checkState(repo != null);
- Project.NameKey project = c.getProject();
- String rev = in.getRevision().get();
- RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
- rw.parseBody(commit);
- if (setCommit) {
- out.commit = toCommit(project, rw, commit, has(WEB_LINKS), fillCommit);
- }
- if (addFooters) {
- Ref ref = repo.exactRef(cd.change().getDest().get());
- RevCommit mergeTip = null;
- if (ref != null) {
- mergeTip = rw.parseCommit(ref.getObjectId());
- rw.parseBody(mergeTip);
- }
- out.commitWithFooters =
- mergeUtilFactory
- .create(projectCache.get(project))
- .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.getId());
- }
- }
-
- if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
- out.files = fileInfoJson.toFileInfoMap(c, in);
- out.files.remove(Patch.COMMIT_MSG);
- out.files.remove(Patch.MERGE_LIST);
- }
-
- if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
-
- actionJson.addRevisionActions(
- changeInfo,
- out,
- new RevisionResource(changeResourceFactory.create(cd.notes(), userProvider.get()), in));
- }
-
- if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
- if (in.getPushCertificate() != null) {
- out.pushCertificate =
- gpgApi.checkPushCertificate(
- in.getPushCertificate(), userFactory.create(in.getUploader()));
- } else {
- out.pushCertificate = new PushCertificateInfo();
- }
- }
-
- return out;
- }
-
- public CommitInfo toCommit(
- Project.NameKey project, RevWalk rw, RevCommit commit, boolean addLinks, boolean fillCommit)
- throws IOException {
- CommitInfo info = new CommitInfo();
- if (fillCommit) {
- info.commit = commit.name();
- }
- info.parents = new ArrayList<>(commit.getParentCount());
- info.author = toGitPerson(commit.getAuthorIdent());
- info.committer = toGitPerson(commit.getCommitterIdent());
- info.subject = commit.getShortMessage();
- info.message = commit.getFullMessage();
-
- if (addLinks) {
- List<WebLinkInfo> links = webLinks.getPatchSetLinks(project, commit.name());
- info.webLinks = links.isEmpty() ? null : links;
- }
-
- for (RevCommit parent : commit.getParents()) {
- rw.parseBody(parent);
- CommitInfo i = new CommitInfo();
- i.commit = parent.name();
- i.subject = parent.getShortMessage();
- if (addLinks) {
- List<WebLinkInfo> parentLinks = webLinks.getParentLinks(project, parent.name());
- i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
- }
- info.parents.add(i);
- }
- return info;
- }
-
- private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
- throws PermissionBackendException, OrmException, IOException {
- Map<String, FetchInfo> r = new LinkedHashMap<>();
- for (Extension<DownloadScheme> e : downloadSchemes) {
- String schemeName = e.getExportName();
- DownloadScheme scheme = e.getProvider().get();
- if (!scheme.isEnabled()
- || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
- continue;
- }
- if (!scheme.isAuthSupported() && !isWorldReadable(cd)) {
- continue;
- }
-
- String projectName = cd.project().get();
- String url = scheme.getUrl(projectName);
- String refName = in.getRefName();
- FetchInfo fetchInfo = new FetchInfo(url, refName);
- r.put(schemeName, fetchInfo);
-
- if (has(DOWNLOAD_COMMANDS)) {
- populateFetchMap(scheme, downloadCommands, projectName, refName, fetchInfo);
- }
- }
-
- return r;
- }
-
- public static void populateFetchMap(
- DownloadScheme scheme,
- DynamicMap<DownloadCommand> commands,
- String projectName,
- String refName,
- FetchInfo fetchInfo) {
- for (Extension<DownloadCommand> e2 : commands) {
- String commandName = e2.getExportName();
- DownloadCommand command = e2.getProvider().get();
- String c = command.getCommand(scheme, projectName, refName);
- if (c != null) {
- addCommand(fetchInfo, commandName, c);
- }
- }
- }
-
- private static void addCommand(FetchInfo fetchInfo, String commandName, String c) {
- if (fetchInfo.commands == null) {
- fetchInfo.commands = new TreeMap<>();
- }
- fetchInfo.commands.put(commandName, c);
- }
-
- static void finish(ChangeInfo info) {
- info.id =
- Joiner.on('~')
- .join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId));
- }
-
- private static void addApproval(LabelInfo label, ApprovalInfo approval) {
- if (label.all == null) {
- label.all = new ArrayList<>();
- }
- label.all.add(approval);
- }
-
private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
throws OrmException {
return permissionBackendForChange(permissionBackend.user(user).database(db), cd);
}
- private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd)
- throws OrmException {
- return permissionBackendForChange(permissionBackend.absentUser(user).database(db), cd);
- }
-
/**
* @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
* from either an index-backed or a database-backed {@link ChangeData} depending on {@code
@@ -1555,31 +824,4 @@
? withUser.change(cd)
: withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
}
-
- private boolean isWorldReadable(ChangeData cd)
- throws OrmException, PermissionBackendException, IOException {
- try {
- permissionBackendForChange(anonymous, cd).check(ChangePermission.READ);
- } catch (AuthException ae) {
- return false;
- }
- ProjectState projectState = projectCache.checkedGet(cd.project());
- if (projectState == null) {
- logger.atSevere().log("project state for project %s is null", cd.project());
- return false;
- }
- return projectState.statePermitsRead();
- }
-
- @AutoValue
- abstract static class LabelWithStatus {
- private static LabelWithStatus create(LabelInfo label, SubmitRecord.Label.Status status) {
- return new AutoValue_ChangeJson_LabelWithStatus(label, status);
- }
-
- abstract LabelInfo label();
-
- @Nullable
- abstract SubmitRecord.Label.Status status();
- }
}
diff --git a/java/com/google/gerrit/server/change/DownloadCommandsJson.java b/java/com/google/gerrit/server/change/DownloadCommandsJson.java
new file mode 100644
index 0000000..f56a16c
--- /dev/null
+++ b/java/com/google/gerrit/server/change/DownloadCommandsJson.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.FetchInfo;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
+import java.util.TreeMap;
+
+/** Populates the {@link FetchInfo} which is serialized to JSON afterwards. */
+public class DownloadCommandsJson {
+
+ private DownloadCommandsJson() {}
+
+ /**
+ * Populates the provided {@link FetchInfo} by calling all {@link DownloadCommand} extensions.
+ * Will mutate {@link FetchInfo#commands}.
+ */
+ public static void populateFetchMap(
+ DownloadScheme scheme,
+ DynamicMap<DownloadCommand> commands,
+ String projectName,
+ String refName,
+ FetchInfo fetchInfo) {
+ for (Extension<DownloadCommand> ext : commands) {
+ String commandName = ext.getExportName();
+ DownloadCommand command = ext.getProvider().get();
+ String c = command.getCommand(scheme, projectName, refName);
+ if (c != null) {
+ if (fetchInfo.commands == null) {
+ fetchInfo.commands = new TreeMap<>();
+ }
+ fetchInfo.commands.put(commandName, c);
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
new file mode 100644
index 0000000..17e45a9
--- /dev/null
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -0,0 +1,557 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Collectors.toList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.VotingRangeInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import javax.inject.Inject;
+
+/**
+ * Produces label-related entities, like {@link LabelInfo}s, which is serialized to JSON afterwards.
+ */
+public class LabelsJson {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ LabelsJson create(Iterable<ListChangesOption> options);
+ }
+
+ private final Provider<ReviewDb> db;
+ private final ApprovalsUtil approvalsUtil;
+ private final ChangeNotes.Factory notesFactory;
+ private final PermissionBackend permissionBackend;
+ private final boolean lazyLoad;
+
+ @Inject
+ LabelsJson(
+ Provider<ReviewDb> db,
+ ApprovalsUtil approvalsUtil,
+ ChangeNotes.Factory notesFactory,
+ PermissionBackend permissionBackend,
+ @Assisted Iterable<ListChangesOption> options) {
+ this.db = db;
+ this.approvalsUtil = approvalsUtil;
+ this.notesFactory = notesFactory;
+ this.permissionBackend = permissionBackend;
+ this.lazyLoad = containsAnyOf(Sets.immutableEnumSet(options), ChangeJson.REQUIRE_LAZY_LOAD);
+ }
+
+ /**
+ * Returns all {@link LabelInfo}s for a single change. Uses the provided {@link AccountLoader} to
+ * lazily populate accounts. Callers have to call {@link AccountLoader#fill()} afterwards to
+ * populate all accounts in the returned {@link LabelInfo}s.
+ */
+ Map<String, LabelInfo> labelsFor(
+ AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
+ throws OrmException, PermissionBackendException {
+ if (!standard && !detailed) {
+ return null;
+ }
+
+ LabelTypes labelTypes = cd.getLabelTypes();
+ Map<String, LabelWithStatus> withStatus =
+ cd.change().getStatus() == Change.Status.MERGED
+ ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
+ : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
+ return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
+ }
+
+ /** Returns all labels that the provided user has permission to vote on. */
+ Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
+ throws OrmException, PermissionBackendException {
+ boolean isMerged = cd.change().getStatus() == Change.Status.MERGED;
+ LabelTypes labelTypes = cd.getLabelTypes();
+ Map<String, LabelType> toCheck = new HashMap<>();
+ for (SubmitRecord rec : submitRecords(cd)) {
+ if (rec.labels != null) {
+ for (SubmitRecord.Label r : rec.labels) {
+ LabelType type = labelTypes.byLabel(r.label);
+ if (type != null && (!isMerged || type.allowPostSubmit())) {
+ toCheck.put(type.getName(), type);
+ }
+ }
+ }
+ }
+
+ Map<String, Short> labels = null;
+ Set<LabelPermission.WithValue> can =
+ permissionBackendForChange(filterApprovalsBy, cd).testLabels(toCheck.values());
+ SetMultimap<String, String> permitted = LinkedHashMultimap.create();
+ for (SubmitRecord rec : submitRecords(cd)) {
+ if (rec.labels == null) {
+ continue;
+ }
+ for (SubmitRecord.Label r : rec.labels) {
+ LabelType type = labelTypes.byLabel(r.label);
+ if (type == null || (isMerged && !type.allowPostSubmit())) {
+ continue;
+ }
+
+ for (LabelValue v : type.getValues()) {
+ boolean ok = can.contains(new LabelPermission.WithValue(type, v));
+ if (isMerged) {
+ if (labels == null) {
+ labels = currentLabels(filterApprovalsBy, cd);
+ }
+ short prev = labels.getOrDefault(type.getName(), (short) 0);
+ ok &= v.getValue() >= prev;
+ }
+ if (ok) {
+ permitted.put(r.label, v.formatValue());
+ }
+ }
+ }
+ }
+
+ List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
+ for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
+ if (isOnlyZero(e.getValue())) {
+ toClear.add(e.getKey());
+ }
+ }
+ for (String label : toClear) {
+ permitted.removeAll(label);
+ }
+ return permitted.asMap();
+ }
+
+ private static boolean containsAnyOf(
+ ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
+ return !Sets.intersection(toFind, set).isEmpty();
+ }
+
+ private static boolean isOnlyZero(Collection<String> values) {
+ return values.isEmpty() || (values.size() == 1 && values.contains(" 0"));
+ }
+
+ private static void addApproval(LabelInfo label, ApprovalInfo approval) {
+ if (label.all == null) {
+ label.all = new ArrayList<>();
+ }
+ label.all.add(approval);
+ }
+
+ private Map<String, LabelWithStatus> labelsForUnsubmittedChange(
+ AccountLoader accountLoader,
+ ChangeData cd,
+ LabelTypes labelTypes,
+ boolean standard,
+ boolean detailed)
+ throws OrmException, PermissionBackendException {
+ Map<String, LabelWithStatus> labels = initLabels(accountLoader, cd, labelTypes, standard);
+ if (detailed) {
+ setAllApprovals(accountLoader, cd, labels);
+ }
+ for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
+ LabelType type = labelTypes.byLabel(e.getKey());
+ if (type == null) {
+ continue;
+ }
+ if (standard) {
+ for (PatchSetApproval psa : cd.currentApprovals()) {
+ if (type.matches(psa)) {
+ short val = psa.getValue();
+ Account.Id accountId = psa.getAccountId();
+ setLabelScores(accountLoader, type, e.getValue(), val, accountId);
+ }
+ }
+ }
+ if (detailed) {
+ setLabelValues(type, e.getValue());
+ }
+ }
+ return labels;
+ }
+
+ private Integer parseRangeValue(String value) {
+ if (value.startsWith("+")) {
+ value = value.substring(1);
+ } else if (value.startsWith(" ")) {
+ value = value.trim();
+ }
+ return Ints.tryParse(value);
+ }
+
+ private ApprovalInfo approvalInfo(
+ AccountLoader accountLoader,
+ Account.Id id,
+ Integer value,
+ VotingRangeInfo permittedVotingRange,
+ String tag,
+ Timestamp date) {
+ ApprovalInfo ai = ChangeJson.getApprovalInfo(id, value, permittedVotingRange, tag, date);
+ accountLoader.put(ai);
+ return ai;
+ }
+
+ private void setLabelValues(LabelType type, LabelWithStatus l) {
+ l.label().defaultValue = type.getDefaultValue();
+ l.label().values = new LinkedHashMap<>();
+ for (LabelValue v : type.getValues()) {
+ l.label().values.put(v.formatValue(), v.getText());
+ }
+ if (isOnlyZero(l.label().values.keySet())) {
+ l.label().values = null;
+ }
+ }
+
+ private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd)
+ throws OrmException {
+ Map<String, Short> result = new HashMap<>();
+ for (PatchSetApproval psa :
+ approvalsUtil.byPatchSetUser(
+ db.get(),
+ lazyLoad ? cd.notes() : notesFactory.createFromIndexedChange(cd.change()),
+ cd.change().currentPatchSetId(),
+ accountId,
+ null,
+ null)) {
+ result.put(psa.getLabel(), psa.getValue());
+ }
+ return result;
+ }
+
+ private Map<String, LabelWithStatus> labelsForSubmittedChange(
+ AccountLoader accountLoader,
+ ChangeData cd,
+ LabelTypes labelTypes,
+ boolean standard,
+ boolean detailed)
+ throws OrmException, PermissionBackendException {
+ Set<Account.Id> allUsers = new HashSet<>();
+ if (detailed) {
+ // Users expect to see all reviewers on closed changes, even if they
+ // didn't vote on the latest patch set. If we don't need detailed labels,
+ // we aren't including 0 votes for all users below, so we can just look at
+ // the latest patch set (in the next loop).
+ for (PatchSetApproval psa : cd.approvals().values()) {
+ allUsers.add(psa.getAccountId());
+ }
+ }
+
+ Set<String> labelNames = new HashSet<>();
+ SetMultimap<Id, PatchSetApproval> current = MultimapBuilder.hashKeys().hashSetValues().build();
+ for (PatchSetApproval a : cd.currentApprovals()) {
+ allUsers.add(a.getAccountId());
+ LabelType type = labelTypes.byLabel(a.getLabelId());
+ if (type != null) {
+ labelNames.add(type.getName());
+ // Not worth the effort to distinguish between votable/non-votable for 0
+ // values on closed changes, since they can't vote anyway.
+ current.put(a.getAccountId(), a);
+ }
+ }
+
+ // Since voting on merged changes is allowed all labels which apply to
+ // the change must be returned. All applying labels can be retrieved from
+ // the submit records, which is what initLabels does.
+ // It's not possible to only compute the labels based on the approvals
+ // since merged changes may not have approvals for all labels (e.g. if not
+ // all labels are required for submit or if the change was auto-closed due
+ // to direct push or if new labels were defined after the change was
+ // merged).
+ Map<String, LabelWithStatus> labels;
+ labels = initLabels(accountLoader, cd, labelTypes, standard);
+
+ // Also include all labels for which approvals exists. E.g. there can be
+ // approvals for labels that are ignored by a Prolog submit rule and hence
+ // it wouldn't be included in the submit records.
+ for (String name : labelNames) {
+ if (!labels.containsKey(name)) {
+ labels.put(name, LabelWithStatus.create(new LabelInfo(), null));
+ }
+ }
+
+ if (detailed) {
+ labels
+ .entrySet()
+ .stream()
+ .filter(e -> labelTypes.byLabel(e.getKey()) != null)
+ .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), e.getValue()));
+ }
+
+ for (Account.Id accountId : allUsers) {
+ Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size());
+ Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
+ if (detailed) {
+ pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
+ for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
+ ApprovalInfo ai = approvalInfo(accountLoader, accountId, 0, null, null, null);
+ byLabel.put(entry.getKey(), ai);
+ addApproval(entry.getValue().label(), ai);
+ }
+ }
+ for (PatchSetApproval psa : current.get(accountId)) {
+ LabelType type = labelTypes.byLabel(psa.getLabelId());
+ if (type == null) {
+ continue;
+ }
+
+ short val = psa.getValue();
+ ApprovalInfo info = byLabel.get(type.getName());
+ if (info != null) {
+ info.value = Integer.valueOf(val);
+ info.permittedVotingRange = pvr.getOrDefault(type.getName(), null);
+ info.date = psa.getGranted();
+ info.tag = psa.getTag();
+ if (psa.isPostSubmit()) {
+ info.postSubmit = true;
+ }
+ }
+ if (!standard) {
+ continue;
+ }
+
+ setLabelScores(accountLoader, type, labels.get(type.getName()), val, accountId);
+ }
+ }
+ return labels;
+ }
+
+ private Map<String, LabelWithStatus> initLabels(
+ AccountLoader accountLoader, ChangeData cd, LabelTypes labelTypes, boolean standard) {
+ Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
+ for (SubmitRecord rec : submitRecords(cd)) {
+ if (rec.labels == null) {
+ continue;
+ }
+ for (SubmitRecord.Label r : rec.labels) {
+ LabelWithStatus p = labels.get(r.label);
+ if (p == null || p.status().compareTo(r.status) < 0) {
+ LabelInfo n = new LabelInfo();
+ if (standard) {
+ switch (r.status) {
+ case OK:
+ n.approved = accountLoader.get(r.appliedBy);
+ break;
+ case REJECT:
+ n.rejected = accountLoader.get(r.appliedBy);
+ n.blocking = true;
+ break;
+ case IMPOSSIBLE:
+ case MAY:
+ case NEED:
+ default:
+ break;
+ }
+ }
+
+ n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null;
+ labels.put(r.label, LabelWithStatus.create(n, r.status));
+ }
+ }
+ }
+ return labels;
+ }
+
+ private void setLabelScores(
+ AccountLoader accountLoader,
+ LabelType type,
+ LabelWithStatus l,
+ short score,
+ Account.Id accountId) {
+ if (l.label().approved != null || l.label().rejected != null) {
+ return;
+ }
+
+ if (type.getMin() == null || type.getMax() == null) {
+ // Can't set score for unknown or misconfigured type.
+ return;
+ }
+
+ if (score != 0) {
+ if (score == type.getMin().getValue()) {
+ l.label().rejected = accountLoader.get(accountId);
+ } else if (score == type.getMax().getValue()) {
+ l.label().approved = accountLoader.get(accountId);
+ } else if (score < 0) {
+ l.label().disliked = accountLoader.get(accountId);
+ l.label().value = score;
+ } else if (score > 0 && l.label().disliked == null) {
+ l.label().recommended = accountLoader.get(accountId);
+ l.label().value = score;
+ }
+ }
+ }
+
+ private void setAllApprovals(
+ AccountLoader accountLoader, ChangeData cd, Map<String, LabelWithStatus> labels)
+ throws OrmException, PermissionBackendException {
+ Change.Status status = cd.change().getStatus();
+ checkState(
+ status != Change.Status.MERGED, "should not call setAllApprovals on %s change", status);
+
+ // Include a user in the output for this label if either:
+ // - They are an explicit reviewer.
+ // - They ever voted on this change.
+ Set<Id> allUsers = new HashSet<>();
+ allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
+ for (PatchSetApproval psa : cd.approvals().values()) {
+ allUsers.add(psa.getAccountId());
+ }
+
+ Table<Id, String, PatchSetApproval> current =
+ HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
+ for (PatchSetApproval psa : cd.currentApprovals()) {
+ current.put(psa.getAccountId(), psa.getLabel(), psa);
+ }
+
+ LabelTypes labelTypes = cd.getLabelTypes();
+ for (Account.Id accountId : allUsers) {
+ PermissionBackend.ForChange perm = permissionBackendForChange(accountId, cd);
+ Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
+ for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
+ LabelType lt = labelTypes.byLabel(e.getKey());
+ if (lt == null) {
+ // Ignore submit record for undefined label; likely the submit rule
+ // author didn't intend for the label to show up in the table.
+ continue;
+ }
+ Integer value;
+ VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.getName(), null);
+ String tag = null;
+ Timestamp date = null;
+ PatchSetApproval psa = current.get(accountId, lt.getName());
+ if (psa != null) {
+ value = Integer.valueOf(psa.getValue());
+ if (value == 0) {
+ // This may be a dummy approval that was inserted when the reviewer
+ // was added. Explicitly check whether the user can vote on this
+ // label.
+ value = perm.test(new LabelPermission(lt)) ? 0 : null;
+ }
+ tag = psa.getTag();
+ date = psa.getGranted();
+ if (psa.isPostSubmit()) {
+ logger.atWarning().log("unexpected post-submit approval on open change: %s", psa);
+ }
+ } else {
+ // Either the user cannot vote on this label, or they were added as a
+ // reviewer but have not responded yet. Explicitly check whether the
+ // user can vote on this label.
+ value = perm.test(new LabelPermission(lt)) ? 0 : null;
+ }
+ addApproval(
+ e.getValue().label(),
+ approvalInfo(accountLoader, accountId, value, permittedVotingRange, tag, date));
+ }
+ }
+ }
+
+ /**
+ * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
+ * from either an index-backed or a database-backed {@link ChangeData} depending on {@code
+ * lazyload}.
+ */
+ private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd)
+ throws OrmException {
+ PermissionBackend.WithUser withUser = permissionBackend.absentUser(user).database(db);
+ return lazyLoad
+ ? withUser.change(cd)
+ : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
+ }
+
+ private List<SubmitRecord> submitRecords(ChangeData cd) {
+ return cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
+ }
+
+ private Map<String, VotingRangeInfo> getPermittedVotingRanges(
+ Map<String, Collection<String>> permittedLabels) {
+ Map<String, VotingRangeInfo> permittedVotingRanges =
+ Maps.newHashMapWithExpectedSize(permittedLabels.size());
+ for (String label : permittedLabels.keySet()) {
+ List<Integer> permittedVotingRange =
+ permittedLabels
+ .get(label)
+ .stream()
+ .map(this::parseRangeValue)
+ .filter(java.util.Objects::nonNull)
+ .sorted()
+ .collect(toList());
+
+ if (permittedVotingRange.isEmpty()) {
+ permittedVotingRanges.put(label, null);
+ } else {
+ int minPermittedValue = permittedVotingRange.get(0);
+ int maxPermittedValue = Iterables.getLast(permittedVotingRange);
+ permittedVotingRanges.put(label, new VotingRangeInfo(minPermittedValue, maxPermittedValue));
+ }
+ }
+ return permittedVotingRanges;
+ }
+
+ @AutoValue
+ abstract static class LabelWithStatus {
+ private static LabelWithStatus create(LabelInfo label, SubmitRecord.Label.Status status) {
+ return new AutoValue_LabelsJson_LabelWithStatus(label, status);
+ }
+
+ abstract LabelInfo label();
+
+ @Nullable
+ abstract SubmitRecord.Label.Status status();
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
new file mode 100644
index 0000000..f2be90d
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -0,0 +1,473 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public class ReviewerAdder {
+ public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
+ public static final int DEFAULT_MAX_REVIEWERS = 20;
+
+ private final AccountResolver accountResolver;
+ private final PermissionBackend permissionBackend;
+ private final GroupResolver groupResolver;
+ private final GroupMembers groupMembers;
+ private final AccountLoader.Factory accountLoaderFactory;
+ private final Provider<ReviewDb> dbProvider;
+ private final Config cfg;
+ private final ReviewerJson json;
+ private final NotesMigration migration;
+ private final NotifyUtil notifyUtil;
+ private final ProjectCache projectCache;
+ private final Provider<AnonymousUser> anonymousProvider;
+ private final AddReviewersOp.Factory addReviewersOpFactory;
+ private final OutgoingEmailValidator validator;
+
+ @Inject
+ ReviewerAdder(
+ AccountResolver accountResolver,
+ PermissionBackend permissionBackend,
+ GroupResolver groupResolver,
+ GroupMembers groupMembers,
+ AccountLoader.Factory accountLoaderFactory,
+ Provider<ReviewDb> db,
+ @GerritServerConfig Config cfg,
+ ReviewerJson json,
+ NotesMigration migration,
+ NotifyUtil notifyUtil,
+ ProjectCache projectCache,
+ Provider<AnonymousUser> anonymousProvider,
+ AddReviewersOp.Factory addReviewersOpFactory,
+ OutgoingEmailValidator validator) {
+ this.accountResolver = accountResolver;
+ this.permissionBackend = permissionBackend;
+ this.groupResolver = groupResolver;
+ this.groupMembers = groupMembers;
+ this.accountLoaderFactory = accountLoaderFactory;
+ this.dbProvider = db;
+ this.cfg = cfg;
+ this.json = json;
+ this.migration = migration;
+ this.notifyUtil = notifyUtil;
+ this.projectCache = projectCache;
+ this.anonymousProvider = anonymousProvider;
+ this.addReviewersOpFactory = addReviewersOpFactory;
+ this.validator = validator;
+ }
+
+ /**
+ * Prepare application of a single {@link AddReviewerInput}.
+ *
+ * @param notes change notes.
+ * @param user user performing the reviewer addition.
+ * @param input input describing user or group to add as a reviewer.
+ * @param allowGroup whether to allow
+ * @return handle describing the addition operation. If the {@code op} field is present, this
+ * operation may be added to a {@code BatchUpdate}. Otherwise, the {@code error} field
+ * contains information about an error that occurred
+ * @throws OrmException
+ * @throws IOException
+ * @throws PermissionBackendException
+ * @throws ConfigInvalidException
+ */
+ public ReviewerAddition prepare(
+ ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
+ throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+ Branch.NameKey dest = notes.getChange().getDest();
+ String reviewer = checkNotNull(input.reviewer);
+ ReviewerState state = input.state();
+ NotifyHandling notify = input.notify;
+ ListMultimap<RecipientType, Account.Id> accountsToNotify;
+ try {
+ accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails);
+ } catch (BadRequestException e) {
+ return fail(reviewer, e.getMessage());
+ }
+ boolean confirmed = input.confirmed();
+ boolean allowByEmail =
+ projectCache
+ .checkedGet(dest.getParentKey())
+ .is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
+
+ ReviewerAddition byAccountId =
+ addByAccountId(
+ reviewer, dest, user, state, notify, accountsToNotify, allowGroup, allowByEmail);
+
+ ReviewerAddition wholeGroup = null;
+ if (byAccountId == null || !byAccountId.exactMatchFound) {
+ wholeGroup =
+ addWholeGroup(
+ reviewer,
+ dest,
+ user,
+ state,
+ notify,
+ accountsToNotify,
+ confirmed,
+ allowGroup,
+ allowByEmail);
+ if (wholeGroup != null && wholeGroup.exactMatchFound) {
+ return wholeGroup;
+ }
+ }
+
+ if (byAccountId != null) {
+ return byAccountId;
+ }
+ if (wholeGroup != null) {
+ return wholeGroup;
+ }
+
+ return addByEmail(reviewer, notes, user, state, notify, accountsToNotify);
+ }
+
+ public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) {
+ return new ReviewerAddition(
+ user.getUserName().orElse(null),
+ revision.getUser(),
+ ImmutableSet.of(user.getAccountId()),
+ null,
+ CC,
+ NotifyHandling.NONE,
+ ImmutableListMultimap.of(),
+ true);
+ }
+
+ @Nullable
+ private ReviewerAddition addByAccountId(
+ String reviewer,
+ Branch.NameKey dest,
+ CurrentUser user,
+ ReviewerState state,
+ NotifyHandling notify,
+ ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ boolean allowGroup,
+ boolean allowByEmail)
+ throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
+ IdentifiedUser reviewerUser;
+ boolean exactMatchFound = false;
+ try {
+ reviewerUser = accountResolver.parse(reviewer);
+ if (reviewer.equalsIgnoreCase(reviewerUser.getName())
+ || reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
+ exactMatchFound = true;
+ }
+ } catch (UnprocessableEntityException | AuthException e) {
+ // AuthException won't occur since the user is authenticated at this point.
+ if (!allowGroup && !allowByEmail) {
+ // Only return failure if we aren't going to try other interpretations.
+ return fail(
+ reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer));
+ }
+ return null;
+ }
+
+ if (isValidReviewer(dest, reviewerUser.getAccount())) {
+ return new ReviewerAddition(
+ reviewer,
+ user,
+ ImmutableSet.of(reviewerUser.getAccountId()),
+ null,
+ state,
+ notify,
+ accountsToNotify,
+ exactMatchFound);
+ }
+ if (!reviewerUser.getAccount().isActive()) {
+ if (allowByEmail && state == CC) {
+ return null;
+ }
+ return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInactive, reviewer));
+ }
+ return fail(
+ reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
+ }
+
+ @Nullable
+ private ReviewerAddition addWholeGroup(
+ String reviewer,
+ Branch.NameKey dest,
+ CurrentUser user,
+ ReviewerState state,
+ NotifyHandling notify,
+ ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ boolean confirmed,
+ boolean allowGroup,
+ boolean allowByEmail)
+ throws IOException, PermissionBackendException {
+ if (!allowGroup) {
+ return null;
+ }
+
+ GroupDescription.Basic group;
+ try {
+ group = groupResolver.parseInternal(reviewer);
+ } catch (UnprocessableEntityException e) {
+ if (!allowByEmail) {
+ return fail(
+ reviewer,
+ MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, reviewer));
+ }
+ return null;
+ }
+
+ if (!isLegalReviewerGroup(group.getGroupUUID())) {
+ return fail(
+ reviewer, MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName()));
+ }
+
+ Set<Account.Id> reviewers = new HashSet<>();
+ Set<Account> members;
+ try {
+ members = groupMembers.listAccounts(group.getGroupUUID(), dest.getParentKey());
+ } catch (NoSuchProjectException e) {
+ return fail(reviewer, e.getMessage());
+ }
+
+ // if maxAllowed is set to 0, it is allowed to add any number of
+ // reviewers
+ int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
+ if (maxAllowed > 0 && members.size() > maxAllowed) {
+ return fail(
+ reviewer,
+ MessageFormat.format(ChangeMessages.get().groupHasTooManyMembers, group.getName()));
+ }
+
+ // if maxWithoutCheck is set to 0, we never ask for confirmation
+ int maxWithoutConfirmation =
+ cfg.getInt("addreviewer", "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+ if (!confirmed && maxWithoutConfirmation > 0 && members.size() > maxWithoutConfirmation) {
+ return fail(
+ reviewer,
+ true,
+ MessageFormat.format(
+ ChangeMessages.get().groupManyMembersConfirmation, group.getName(), members.size()));
+ }
+
+ for (Account member : members) {
+ if (isValidReviewer(dest, member)) {
+ reviewers.add(member.getId());
+ }
+ }
+
+ return new ReviewerAddition(
+ reviewer, user, reviewers, null, state, notify, accountsToNotify, true);
+ }
+
+ @Nullable
+ private ReviewerAddition addByEmail(
+ String reviewer,
+ ChangeNotes notes,
+ CurrentUser user,
+ ReviewerState state,
+ NotifyHandling notify,
+ ListMultimap<RecipientType, Account.Id> accountsToNotify)
+ throws PermissionBackendException {
+ try {
+ permissionBackend
+ .user(anonymousProvider.get())
+ .database(dbProvider)
+ .change(notes)
+ .check(ChangePermission.READ);
+ } catch (AuthException e) {
+ return fail(
+ reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
+ }
+
+ if (!migration.readChanges()) {
+ // addByEmail depends on NoteDb.
+ return fail(
+ reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer));
+ }
+ Address adr = Address.tryParse(reviewer);
+ if (adr == null || !validator.isValid(adr.getEmail())) {
+ return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInvalid, reviewer));
+ }
+ return new ReviewerAddition(
+ reviewer, user, null, ImmutableList.of(adr), state, notify, accountsToNotify, true);
+ }
+
+ private boolean isValidReviewer(Branch.NameKey branch, Account member)
+ throws PermissionBackendException {
+ if (!member.isActive()) {
+ return false;
+ }
+
+ try {
+ // Check ref permission instead of change permission, since change permissions take into
+ // account the private bit, whereas adding a user as a reviewer is explicitly allowing them to
+ // see private changes.
+ permissionBackend
+ .absentUser(member.getId())
+ .database(dbProvider)
+ .ref(branch)
+ .check(RefPermission.READ);
+ return true;
+ } catch (AuthException e) {
+ return false;
+ }
+ }
+
+ private ReviewerAddition fail(String reviewer, String error) {
+ return fail(reviewer, false, error);
+ }
+
+ private ReviewerAddition fail(String reviewer, boolean confirm, String error) {
+ ReviewerAddition addition = new ReviewerAddition(reviewer);
+ addition.result.confirm = confirm ? true : null;
+ addition.result.error = error;
+ return addition;
+ }
+
+ public class ReviewerAddition {
+ public final AddReviewerResult result;
+ @Nullable public final AddReviewersOp op;
+ public final ImmutableSet<Account.Id> reviewers;
+ public final ImmutableSet<Address> reviewersByEmail;
+ public final ReviewerState state;
+ @Nullable final IdentifiedUser caller;
+ final boolean exactMatchFound;
+
+ private ReviewerAddition(String reviewer) {
+ result = new AddReviewerResult(reviewer);
+ op = null;
+ reviewers = ImmutableSet.of();
+ reviewersByEmail = ImmutableSet.of();
+ state = REVIEWER;
+ caller = null;
+ exactMatchFound = false;
+ }
+
+ private ReviewerAddition(
+ String reviewer,
+ CurrentUser caller,
+ @Nullable Iterable<Account.Id> reviewers,
+ @Nullable Iterable<Address> reviewersByEmail,
+ ReviewerState state,
+ @Nullable NotifyHandling notify,
+ ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ boolean exactMatchFound) {
+ checkArgument(
+ reviewers != null || reviewersByEmail != null,
+ "must have either reviewers or reviewersByEmail");
+
+ result = new AddReviewerResult(reviewer);
+ this.reviewers = reviewers == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewers);
+ this.reviewersByEmail =
+ reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
+ this.state = state;
+ this.caller = caller.asIdentifiedUser();
+ op =
+ addReviewersOpFactory.create(
+ this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
+ this.exactMatchFound = exactMatchFound;
+ }
+
+ public void gatherResults(ChangeData cd) throws OrmException, PermissionBackendException {
+ checkState(op != null, "addition did not result in an update op");
+ checkState(op.getResult() != null, "op did not return a result");
+
+ // Generate result details and fill AccountLoader. This occurs outside
+ // the Op because the accounts are in a different table.
+ AddReviewersOp.Result opResult = op.getResult();
+ if (migration.readChanges() && state == CC) {
+ result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size());
+ for (Account.Id accountId : opResult.addedCCs()) {
+ result.ccs.add(json.format(new ReviewerInfo(accountId.get()), accountId, cd));
+ }
+ accountLoaderFactory.create(true).fill(result.ccs);
+ for (Address a : opResult.addedCCsByEmail()) {
+ result.ccs.add(new AccountInfo(a.getName(), a.getEmail()));
+ }
+ } else {
+ result.reviewers = Lists.newArrayListWithCapacity(opResult.addedReviewers().size());
+ for (PatchSetApproval psa : opResult.addedReviewers()) {
+ // New reviewers have value 0, don't bother normalizing.
+ result.reviewers.add(
+ json.format(
+ new ReviewerInfo(psa.getAccountId().get()),
+ psa.getAccountId(),
+ cd,
+ ImmutableList.of(psa)));
+ }
+ accountLoaderFactory.create(true).fill(result.reviewers);
+ for (Address a : opResult.addedReviewersByEmail()) {
+ result.reviewers.add(ReviewerInfo.byEmail(a.getName(), a.getEmail()));
+ }
+ }
+ }
+ }
+
+ public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) {
+ return !SystemGroupBackend.isSystemGroup(groupUUID);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
similarity index 97%
rename from java/com/google/gerrit/server/restapi/change/ReviewerJson.java
rename to java/com/google/gerrit/server/change/ReviewerJson.java
index 29c5649..6502569 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.restapi.change;
+package com.google.gerrit.server.change;
import static com.google.gerrit.common.data.LabelValue.formatValue;
@@ -29,7 +29,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
new file mode 100644
index 0000000..b67028d
--- /dev/null
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -0,0 +1,393 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
+import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
+import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
+import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
+import static com.google.gerrit.server.CommonConverters.toGitPerson;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.FetchInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.GpgApiAdapter;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Produces {@link RevisionInfo} and {@link CommitInfo} which are serialized to JSON afterwards. */
+public class RevisionJson {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ RevisionJson create(Iterable<ListChangesOption> options);
+ }
+
+ private final MergeUtil.Factory mergeUtilFactory;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final FileInfoJson fileInfoJson;
+ private final GpgApiAdapter gpgApi;
+ private final ChangeResource.Factory changeResourceFactory;
+ private final ChangeKindCache changeKindCache;
+ private final ActionJson actionJson;
+ private final DynamicMap<DownloadScheme> downloadSchemes;
+ private final DynamicMap<DownloadCommand> downloadCommands;
+ private final WebLinks webLinks;
+ private final Provider<CurrentUser> userProvider;
+ private final ProjectCache projectCache;
+ private final ImmutableSet<ListChangesOption> options;
+ private final AccountLoader.Factory accountLoaderFactory;
+ private final AnonymousUser anonymous;
+ private final GitRepositoryManager repoManager;
+ private final PermissionBackend permissionBackend;
+ private final ChangeNotes.Factory notesFactory;
+ private final boolean lazyLoad;
+
+ @Inject
+ RevisionJson(
+ Provider<CurrentUser> userProvider,
+ AnonymousUser anonymous,
+ ProjectCache projectCache,
+ IdentifiedUser.GenericFactory userFactory,
+ MergeUtil.Factory mergeUtilFactory,
+ FileInfoJson fileInfoJson,
+ AccountLoader.Factory accountLoaderFactory,
+ DynamicMap<DownloadScheme> downloadSchemes,
+ DynamicMap<DownloadCommand> downloadCommands,
+ WebLinks webLinks,
+ ActionJson actionJson,
+ GpgApiAdapter gpgApi,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeKindCache changeKindCache,
+ GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ ChangeNotes.Factory notesFactory,
+ @Assisted Iterable<ListChangesOption> options) {
+ this.userProvider = userProvider;
+ this.anonymous = anonymous;
+ this.projectCache = projectCache;
+ this.userFactory = userFactory;
+ this.mergeUtilFactory = mergeUtilFactory;
+ this.fileInfoJson = fileInfoJson;
+ this.accountLoaderFactory = accountLoaderFactory;
+ this.downloadSchemes = downloadSchemes;
+ this.downloadCommands = downloadCommands;
+ this.webLinks = webLinks;
+ this.actionJson = actionJson;
+ this.gpgApi = gpgApi;
+ this.changeResourceFactory = changeResourceFactory;
+ this.changeKindCache = changeKindCache;
+ this.permissionBackend = permissionBackend;
+ this.notesFactory = notesFactory;
+ this.repoManager = repoManager;
+ this.options = ImmutableSet.copyOf(options);
+ this.lazyLoad = containsAnyOf(this.options, ChangeJson.REQUIRE_LAZY_LOAD);
+ }
+
+ /**
+ * Returns a {@link RevisionInfo} based on a change and patch set. Reads from the repository
+ * depending on the options provided when constructing this instance.
+ */
+ public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
+ throws PatchListNotAvailableException, GpgException, OrmException, IOException,
+ PermissionBackendException {
+ AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+ try (Repository repo = openRepoIfNecessary(cd.project());
+ RevWalk rw = newRevWalk(repo)) {
+ RevisionInfo rev = toRevisionInfo(accountLoader, cd, in, repo, rw, true, null);
+ accountLoader.fill();
+ return rev;
+ }
+ }
+
+ /**
+ * Returns a {@link CommitInfo} based on a commit and formatting options. Uses the provided
+ * RevWalk and assumes it is backed by an open repository.
+ */
+ public CommitInfo getCommitInfo(
+ Project.NameKey project, RevWalk rw, RevCommit commit, boolean addLinks, boolean fillCommit)
+ throws IOException {
+ CommitInfo info = new CommitInfo();
+ if (fillCommit) {
+ info.commit = commit.name();
+ }
+ info.parents = new ArrayList<>(commit.getParentCount());
+ info.author = toGitPerson(commit.getAuthorIdent());
+ info.committer = toGitPerson(commit.getCommitterIdent());
+ info.subject = commit.getShortMessage();
+ info.message = commit.getFullMessage();
+
+ if (addLinks) {
+ List<WebLinkInfo> links = webLinks.getPatchSetLinks(project, commit.name());
+ info.webLinks = links.isEmpty() ? null : links;
+ }
+
+ for (RevCommit parent : commit.getParents()) {
+ rw.parseBody(parent);
+ CommitInfo i = new CommitInfo();
+ i.commit = parent.name();
+ i.subject = parent.getShortMessage();
+ if (addLinks) {
+ List<WebLinkInfo> parentLinks = webLinks.getParentLinks(project, parent.name());
+ i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
+ }
+ info.parents.add(i);
+ }
+ return info;
+ }
+
+ /**
+ * Returns multiple {@link RevisionInfo}s for a single change. Uses the provided {@link
+ * AccountLoader} to lazily populate accounts. Callers have to call {@link AccountLoader#fill()}
+ * afterwards to populate all accounts in the returned {@link RevisionInfo}s.
+ */
+ Map<String, RevisionInfo> getRevisions(
+ AccountLoader accountLoader,
+ ChangeData cd,
+ Map<PatchSet.Id, PatchSet> map,
+ Optional<Id> limitToPsId,
+ ChangeInfo changeInfo)
+ throws PatchListNotAvailableException, GpgException, OrmException, IOException,
+ PermissionBackendException {
+ Map<String, RevisionInfo> res = new LinkedHashMap<>();
+ try (Repository repo = openRepoIfNecessary(cd.project());
+ RevWalk rw = newRevWalk(repo)) {
+ for (PatchSet in : map.values()) {
+ PatchSet.Id id = in.getId();
+ boolean want;
+ if (has(ALL_REVISIONS)) {
+ want = true;
+ } else if (limitToPsId.isPresent()) {
+ want = id.equals(limitToPsId.get());
+ } else {
+ want = id.equals(cd.change().currentPatchSetId());
+ }
+ if (want) {
+ res.put(
+ in.getRevision().get(),
+ toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
+ }
+ }
+ return res;
+ }
+ }
+
+ private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
+ throws PermissionBackendException, OrmException, IOException {
+ Map<String, FetchInfo> r = new LinkedHashMap<>();
+ for (Extension<DownloadScheme> e : downloadSchemes) {
+ String schemeName = e.getExportName();
+ DownloadScheme scheme = e.getProvider().get();
+ if (!scheme.isEnabled()
+ || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
+ continue;
+ }
+ if (!scheme.isAuthSupported() && !isWorldReadable(cd)) {
+ continue;
+ }
+
+ String projectName = cd.project().get();
+ String url = scheme.getUrl(projectName);
+ String refName = in.getRefName();
+ FetchInfo fetchInfo = new FetchInfo(url, refName);
+ r.put(schemeName, fetchInfo);
+
+ if (has(DOWNLOAD_COMMANDS)) {
+ DownloadCommandsJson.populateFetchMap(
+ scheme, downloadCommands, projectName, refName, fetchInfo);
+ }
+ }
+
+ return r;
+ }
+
+ private RevisionInfo toRevisionInfo(
+ AccountLoader accountLoader,
+ ChangeData cd,
+ PatchSet in,
+ @Nullable Repository repo,
+ @Nullable RevWalk rw,
+ boolean fillCommit,
+ @Nullable ChangeInfo changeInfo)
+ throws PatchListNotAvailableException, GpgException, OrmException, IOException,
+ PermissionBackendException {
+ Change c = cd.change();
+ RevisionInfo out = new RevisionInfo();
+ out.isCurrent = in.getId().equals(c.currentPatchSetId());
+ out._number = in.getId().get();
+ out.ref = in.getRefName();
+ out.created = in.getCreatedOn();
+ out.uploader = accountLoader.get(in.getUploader());
+ out.fetch = makeFetchMap(cd, in);
+ out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
+ out.description = in.getDescription();
+
+ boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
+ boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
+ if (setCommit || addFooters) {
+ checkState(rw != null);
+ checkState(repo != null);
+ Project.NameKey project = c.getProject();
+ String rev = in.getRevision().get();
+ RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+ rw.parseBody(commit);
+ if (setCommit) {
+ out.commit = getCommitInfo(project, rw, commit, has(WEB_LINKS), fillCommit);
+ }
+ if (addFooters) {
+ Ref ref = repo.exactRef(cd.change().getDest().get());
+ RevCommit mergeTip = null;
+ if (ref != null) {
+ mergeTip = rw.parseCommit(ref.getObjectId());
+ rw.parseBody(mergeTip);
+ }
+ out.commitWithFooters =
+ mergeUtilFactory
+ .create(projectCache.get(project))
+ .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.getId());
+ }
+ }
+
+ if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
+ out.files = fileInfoJson.toFileInfoMap(c, in);
+ out.files.remove(Patch.COMMIT_MSG);
+ out.files.remove(Patch.MERGE_LIST);
+ }
+
+ if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
+ actionJson.addRevisionActions(
+ changeInfo,
+ out,
+ new RevisionResource(changeResourceFactory.create(cd.notes(), userProvider.get()), in));
+ }
+
+ if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
+ if (in.getPushCertificate() != null) {
+ out.pushCertificate =
+ gpgApi.checkPushCertificate(
+ in.getPushCertificate(), userFactory.create(in.getUploader()));
+ } else {
+ out.pushCertificate = new PushCertificateInfo();
+ }
+ }
+
+ return out;
+ }
+
+ private boolean has(ListChangesOption option) {
+ return options.contains(option);
+ }
+
+ /**
+ * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
+ * from either an index-backed or a database-backed {@link ChangeData} depending on {@code
+ * lazyload}.
+ */
+ private PermissionBackend.ForChange permissionBackendForChange(
+ PermissionBackend.WithUser withUser, ChangeData cd) throws OrmException {
+ return lazyLoad
+ ? withUser.change(cd)
+ : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
+ }
+
+ private boolean isWorldReadable(ChangeData cd)
+ throws OrmException, PermissionBackendException, IOException {
+ try {
+ permissionBackendForChange(permissionBackend.user(anonymous), cd)
+ .check(ChangePermission.READ);
+ } catch (AuthException ae) {
+ return false;
+ }
+ ProjectState projectState = projectCache.checkedGet(cd.project());
+ if (projectState == null) {
+ logger.atSevere().log("project state for project %s is null", cd.project());
+ return false;
+ }
+ return projectState.statePermitsRead();
+ }
+
+ @Nullable
+ private Repository openRepoIfNecessary(Project.NameKey project) throws IOException {
+ if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
+ return repoManager.openRepository(project);
+ }
+ return null;
+ }
+
+ @Nullable
+ private RevWalk newRevWalk(@Nullable Repository repo) {
+ return repo != null ? new RevWalk(repo) : null;
+ }
+
+ private static boolean containsAnyOf(
+ ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
+ return !Sets.intersection(toFind, set).isEmpty();
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 367fdb6..b4f9cc7 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -101,8 +101,10 @@
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
+import com.google.gerrit.server.change.LabelsJson;
import com.google.gerrit.server.change.MergeabilityCacheImpl;
import com.google.gerrit.server.change.ReviewerSuggestion;
+import com.google.gerrit.server.change.RevisionJson;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.events.EventListener;
import com.google.gerrit.server.events.EventsMetrics;
@@ -260,12 +262,14 @@
factory(ChangeData.AssistedFactory.class);
factory(ChangeJson.AssistedFactory.class);
factory(CreateChangeSender.Factory.class);
+ factory(LabelsJson.Factory.class);
factory(MergedSender.Factory.class);
factory(MergeUtil.Factory.class);
factory(PatchScriptFactory.Factory.class);
factory(ProjectState.Factory.class);
factory(RegisterNewEmailSender.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
+ factory(RevisionJson.Factory.class);
factory(SetAssigneeSender.Factory.class);
factory(InboundEmailRejectionSender.Factory.class);
bind(PermissionCollection.Factory.class);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditJson.java b/java/com/google/gerrit/server/edit/ChangeEditJson.java
index bd9c3a6..bf20404 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditJson.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -23,7 +23,7 @@
import com.google.gerrit.extensions.registration.Extension;
import com.google.gerrit.server.CommonConverters;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.DownloadCommandsJson;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -97,7 +97,8 @@
FetchInfo fetchInfo = new FetchInfo(scheme.getUrl(projectName), refName);
r.put(schemeName, fetchInfo);
- ChangeJson.populateFetchMap(scheme, downloadCommands, projectName, refName, fetchInfo);
+ DownloadCommandsJson.populateFetchMap(
+ scheme, downloadCommands, projectName, refName, fetchInfo);
}
return r;
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 3a10fc5..79876df 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.RevisionJson;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
@@ -64,15 +65,18 @@
private final ChangeData.Factory changeDataFactory;
private final Provider<ReviewDb> db;
private final ChangeJson.Factory changeJsonFactory;
+ private final RevisionJson.Factory revisionJsonFactory;
@Inject
EventUtil(
ChangeJson.Factory changeJsonFactory,
+ RevisionJson.Factory revisionJsonFactory,
ChangeData.Factory changeDataFactory,
Provider<ReviewDb> db) {
this.changeDataFactory = changeDataFactory;
this.db = db;
this.changeJsonFactory = changeJsonFactory;
+ this.revisionJsonFactory = revisionJsonFactory;
}
public ChangeInfo changeInfo(Change change) throws OrmException {
@@ -89,7 +93,7 @@
throws OrmException, PatchListNotAvailableException, GpgException, IOException,
PermissionBackendException {
ChangeData cd = changeDataFactory.create(db.get(), project, ps.getId().getParentKey());
- return changeJsonFactory.create(CHANGE_OPTIONS).getRevisionInfo(cd, ps);
+ return revisionJsonFactory.create(CHANGE_OPTIONS).getRevisionInfo(cd, ps);
}
public AccountInfo accountInfo(AccountState accountState) {
diff --git a/java/com/google/gerrit/server/git/CodeReviewCommit.java b/java/com/google/gerrit/server/git/CodeReviewCommit.java
index e7ccd331..c210dcd 100644
--- a/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Change;
@@ -24,6 +25,7 @@
import com.google.gerrit.server.submit.CommitMergeStatus;
import java.io.IOException;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
@@ -126,6 +128,9 @@
*/
private Optional<String> statusMessage = Optional.empty();
+ /** List of files in this commit that contain Git conflict markers. */
+ private ImmutableSet<String> filesWithGitConflicts;
+
public CodeReviewCommit(AnyObjectId id) {
super(id);
}
@@ -150,6 +155,17 @@
this.statusMessage = Optional.ofNullable(statusMessage);
}
+ public ImmutableSet<String> getFilesWithGitConflicts() {
+ return filesWithGitConflicts != null ? filesWithGitConflicts : ImmutableSet.of();
+ }
+
+ public void setFilesWithGitConflicts(@Nullable Set<String> filesWithGitConflicts) {
+ this.filesWithGitConflicts =
+ filesWithGitConflicts != null && !filesWithGitConflicts.isEmpty()
+ ? ImmutableSet.copyOf(filesWithGitConflicts)
+ : null;
+ }
+
public PatchSet.Id getPatchsetId() {
return patchsetId;
}
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 016415c..d5e2a41 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -16,11 +16,15 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
@@ -29,6 +33,7 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
@@ -57,15 +62,22 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import org.eclipse.jgit.diff.Sequence;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -81,6 +93,8 @@
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeFormatter;
+import org.eclipse.jgit.merge.MergeResult;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ResolveMerger;
@@ -92,6 +106,7 @@
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.TemporaryBuffer;
/**
* Utility methods used during the merge process.
@@ -232,29 +247,158 @@
String commitMsg,
CodeReviewRevWalk rw,
int parentIndex,
- boolean ignoreIdenticalTree)
+ boolean ignoreIdenticalTree,
+ boolean allowConflicts)
throws MissingObjectException, IncorrectObjectTypeException, IOException,
- MergeIdenticalTreeException, MergeConflictException {
+ MergeIdenticalTreeException, MergeConflictException, MethodNotAllowedException {
- final ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
-
+ ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
m.setBase(originalCommit.getParent(parentIndex));
+
+ DirCache dc = DirCache.newInCore();
+ if (allowConflicts && m instanceof ResolveMerger) {
+ // The DirCache must be set on ResolveMerger before calling
+ // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
+ ((ResolveMerger) m).setDirCache(dc);
+ }
+
+ ObjectId tree;
+ ImmutableSet<String> filesWithGitConflicts;
if (m.merge(mergeTip, originalCommit)) {
- ObjectId tree = m.getResultTreeId();
+ filesWithGitConflicts = null;
+ tree = m.getResultTreeId();
if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) {
throw new MergeIdenticalTreeException("identical tree");
}
+ } else {
+ if (!allowConflicts) {
+ throw new MergeConflictException("merge conflict");
+ }
- CommitBuilder mergeCommit = new CommitBuilder();
- mergeCommit.setTreeId(tree);
- mergeCommit.setParentId(mergeTip);
- mergeCommit.setAuthor(originalCommit.getAuthorIdent());
- mergeCommit.setCommitter(cherryPickCommitterIdent);
- mergeCommit.setMessage(commitMsg);
- matchAuthorToCommitterDate(project, mergeCommit);
- return rw.parseCommit(inserter.insert(mergeCommit));
+ if (!useContentMerge) {
+ // If content merge is disabled we don't have a ResolveMerger and hence cannot merge with
+ // conflict markers.
+ throw new MethodNotAllowedException(
+ "Cherry-pick with allow conflicts requires that content merge is enabled.");
+ }
+
+ // For merging with conflict markers we need a ResolveMerger, double-check that we have one.
+ checkState(m instanceof ResolveMerger, "allow conflicts is not supported");
+ Map<String, MergeResult<? extends Sequence>> mergeResults =
+ ((ResolveMerger) m).getMergeResults();
+
+ filesWithGitConflicts =
+ mergeResults
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue().containsConflicts())
+ .map(Map.Entry::getKey)
+ .collect(toImmutableSet());
+
+ tree =
+ mergeWithConflicts(
+ rw, inserter, dc, "HEAD", mergeTip, "CHANGE", originalCommit, mergeResults);
}
- throw new MergeConflictException("merge conflict");
+
+ CommitBuilder cherryPickCommit = new CommitBuilder();
+ cherryPickCommit.setTreeId(tree);
+ cherryPickCommit.setParentId(mergeTip);
+ cherryPickCommit.setAuthor(originalCommit.getAuthorIdent());
+ cherryPickCommit.setCommitter(cherryPickCommitterIdent);
+ cherryPickCommit.setMessage(commitMsg);
+ matchAuthorToCommitterDate(project, cherryPickCommit);
+ CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit));
+ commit.setFilesWithGitConflicts(filesWithGitConflicts);
+ return commit;
+ }
+
+ public static ObjectId mergeWithConflicts(
+ RevWalk rw,
+ ObjectInserter ins,
+ DirCache dc,
+ String oursName,
+ RevCommit ours,
+ String theirsName,
+ RevCommit theirs,
+ Map<String, MergeResult<? extends Sequence>> mergeResults)
+ throws IOException {
+ rw.parseBody(ours);
+ rw.parseBody(theirs);
+ String oursMsg = ours.getShortMessage();
+ String theirsMsg = theirs.getShortMessage();
+
+ int nameLength = Math.max(oursName.length(), theirsName.length());
+ String oursNameFormatted =
+ String.format(
+ "%0$-" + nameLength + "s (%s %s)",
+ oursName,
+ ours.abbreviate(6).name(),
+ oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
+ String theirsNameFormatted =
+ String.format(
+ "%0$-" + nameLength + "s (%s %s)",
+ theirsName,
+ theirs.abbreviate(6).name(),
+ theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
+
+ MergeFormatter fmt = new MergeFormatter();
+ Map<String, ObjectId> resolved = new HashMap<>();
+ for (Map.Entry<String, MergeResult<? extends Sequence>> entry : mergeResults.entrySet()) {
+ MergeResult<? extends Sequence> p = entry.getValue();
+ try (TemporaryBuffer buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
+ fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8.name());
+ buf.close();
+
+ try (InputStream in = buf.openInputStream()) {
+ resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
+ }
+ }
+ }
+
+ DirCacheBuilder builder = dc.builder();
+ int cnt = dc.getEntryCount();
+ for (int i = 0; i < cnt; ) {
+ DirCacheEntry entry = dc.getEntry(i);
+ if (entry.getStage() == 0) {
+ builder.add(entry);
+ i++;
+ continue;
+ }
+
+ int next = dc.nextEntry(i);
+ String path = entry.getPathString();
+ DirCacheEntry res = new DirCacheEntry(path);
+ if (resolved.containsKey(path)) {
+ // For a file with content merge conflict that we produced a result
+ // above on, collapse the file down to a single stage 0 with just
+ // the blob content, and a randomly selected mode (the lowest stage,
+ // which should be the merge base, or ours).
+ res.setFileMode(entry.getFileMode());
+ res.setObjectId(resolved.get(path));
+
+ } else if (next == i + 1) {
+ // If there is exactly one stage present, shouldn't be a conflict...
+ res.setFileMode(entry.getFileMode());
+ res.setObjectId(entry.getObjectId());
+
+ } else if (next == i + 2) {
+ // Two stages suggests a delete/modify conflict. Pick the higher
+ // stage as the automatic result.
+ entry = dc.getEntry(i + 1);
+ res.setFileMode(entry.getFileMode());
+ res.setObjectId(entry.getObjectId());
+
+ } else {
+ // 3 stage conflict, no resolve above
+ // Punt on the 3-stage conflict and show the base, for now.
+ res.setFileMode(entry.getFileMode());
+ res.setObjectId(entry.getObjectId());
+ }
+ builder.add(res);
+ i = next;
+ }
+ builder.finish();
+ return dc.writeTree(ins);
}
public static RevCommit createMergeCommit(
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 114107d..52ebd35 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -14,11 +14,21 @@
package com.google.gerrit.server.git.receive;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram1;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.ConfigUtil;
@@ -164,7 +174,39 @@
}
}
+ @Singleton
+ private static class Metrics {
+ private final Histogram1<ResultChangeIds.Key> changes;
+ private final Timer1<String> latencyPerChange;
+ private final Counter0 timeouts;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ changes =
+ metricMaker.newHistogram(
+ "receivecommits/changes",
+ new Description("number of changes uploaded in a single push.").setCumulative(),
+ Field.ofEnum(
+ ResultChangeIds.Key.class,
+ "type",
+ "type of update (replace, create, autoclose)"));
+ latencyPerChange =
+ metricMaker.newTimer(
+ "receivecommits/latency",
+ new Description("average delay per updated change")
+ .setUnit(Units.MILLISECONDS)
+ .setCumulative(),
+ Field.ofString("type", "type of update (create/replace, autoclose)"));
+
+ timeouts =
+ metricMaker.newCounter(
+ "receivecommits/timeout", new Description("rate of push timeouts").setRate());
+ }
+ }
+
+ private final Metrics metrics;
private final ReceiveCommits receiveCommits;
+ private final ResultChangeIds resultChangeIds;
private final PermissionBackend.ForProject perm;
private final ReceivePack receivePack;
private final ExecutorService executor;
@@ -188,6 +230,7 @@
TransferConfig transferConfig,
Provider<LazyPostReceiveHookChain> lazyPostReceive,
ContributorAgreementsChecker contributorAgreements,
+ Metrics metrics,
@Named(TIMEOUT_NAME) long timeoutMillis,
@Assisted ProjectState projectState,
@Assisted IdentifiedUser user,
@@ -202,6 +245,7 @@
this.projectState = projectState;
this.user = user;
this.repo = repo;
+ this.metrics = metrics;
Project.NameKey projectName = projectState.getNameKey();
receivePack = new ReceivePack(repo);
@@ -237,7 +281,10 @@
advHooks.add(new HackPushNegotiateHook());
receivePack.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
- receiveCommits = factory.create(projectState, user, receivePack, allRefsWatcher, messageSender);
+ resultChangeIds = new ResultChangeIds();
+ receiveCommits =
+ factory.create(
+ projectState, user, receivePack, allRefsWatcher, messageSender, resultChangeIds);
receiveCommits.init();
}
@@ -268,11 +315,14 @@
// pre-receive hooks
return;
}
+
+ long startNanos = System.nanoTime();
Worker w = new Worker(commands);
try {
w.progress.waitFor(
executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
+ metrics.timeouts.increment();
logger.atWarning().withCause(e).log(
"Error in ReceiveCommits while processing changes for project %s",
projectState.getName());
@@ -287,6 +337,23 @@
} finally {
w.sendMessages();
}
+
+ long deltaNanos = System.nanoTime() - startNanos;
+ int totalChanges = 0;
+ for (ResultChangeIds.Key key : ResultChangeIds.Key.values()) {
+ List<Change.Id> ids = resultChangeIds.get(key);
+ metrics.changes.record(key, ids.size());
+ totalChanges += ids.size();
+ }
+
+ if (totalChanges > 0) {
+ metrics.latencyPerChange.record(
+ resultChangeIds.get(ResultChangeIds.Key.AUTOCLOSED).isEmpty()
+ ? "CREATE_REPLACE"
+ : ResultChangeIds.Key.AUTOCLOSED.name(),
+ deltaNanos / totalChanges,
+ NANOSECONDS);
+ }
}
public ReceivePack getReceivePack() {
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 0724215..f762611 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -6,6 +6,7 @@
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/logging",
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 1a626e0..2e51c17 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -107,6 +107,7 @@
import com.google.gerrit.server.git.ReceivePackInitializer;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.receive.ResultChangeIds.Key;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.RefOperationValidationException;
import com.google.gerrit.server.git.validators.RefOperationValidators;
@@ -228,7 +229,8 @@
IdentifiedUser user,
ReceivePack receivePack,
AllRefsWatcher allRefsWatcher,
- MessageSender messageSender);
+ MessageSender messageSender,
+ ResultChangeIds resultChangeIds);
}
private class ReceivePackMessageSender implements MessageSender {
@@ -352,6 +354,7 @@
private Optional<String> tracePushOption;
private MessageSender messageSender;
+ private ResultChangeIds resultChangeIds;
@Inject
ReceiveCommits(
@@ -393,7 +396,8 @@
@Assisted IdentifiedUser user,
@Assisted ReceivePack rp,
@Assisted AllRefsWatcher allRefsWatcher,
- @Nullable @Assisted MessageSender messageSender)
+ @Nullable @Assisted MessageSender messageSender,
+ @Assisted ResultChangeIds resultChangeIds)
throws IOException {
// Injected fields.
this.accountResolver = accountResolver;
@@ -460,6 +464,7 @@
// Handles for outputting back over the wire to the end user.
this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender();
+ this.resultChangeIds = resultChangeIds;
}
void init() {
@@ -810,6 +815,13 @@
} catch (UpdateException e) {
throw INSERT_EXCEPTION.apply(e);
}
+
+ replaceByChange
+ .values()
+ .stream()
+ .forEach(req -> resultChangeIds.add(Key.REPLACED, req.ontoChange));
+ newChanges.stream().forEach(req -> resultChangeIds.add(Key.CREATED, req.changeId));
+
if (magicBranchCmd != null) {
magicBranchCmd.setResult(OK);
}
@@ -3031,6 +3043,7 @@
private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
logger.atFine().log("Starting auto-closing of changes");
String refName = cmd.getRefName();
+ Set<Change.Id> ids = new HashSet<>();
// TODO(dborowitz): Combine this BatchUpdate with the main one in
// handleRegularCommands
@@ -3107,6 +3120,7 @@
.create(requestScopePropagator, req.psId, refName)
.setPatchSetProvider(req.replaceOp::getPatchSet));
bu.addOp(id, new ChangeProgressOp(progress));
+ ids.add(id);
}
logger.atFine().log(
@@ -3115,7 +3129,14 @@
bu.execute();
} catch (IOException | OrmException | PermissionBackendException e) {
logger.atSevere().withCause(e).log("Failed to auto-close changes");
+ return null;
}
+
+ // If we are here, we didn't throw UpdateException. Record the result.
+ // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id doesn't
+ // fit into TreeSet.
+ ids.stream().forEach(id -> resultChangeIds.add(Key.AUTOCLOSED, id));
+
return null;
},
// Use a multiple of the default timeout to account for inner retries that may otherwise
diff --git a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
new file mode 100644
index 0000000..4099e14
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Change;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Keeps track of the change IDs thus far updated by ReceiveCommit.
+ *
+ * <p>This class is thread-safe.
+ */
+public class ResultChangeIds {
+ enum Key {
+ CREATED,
+ REPLACED,
+ AUTOCLOSED,
+ }
+
+ private final Map<Key, List<Change.Id>> ids;
+
+ ResultChangeIds() {
+ ids = new EnumMap<>(Key.class);
+ for (Key k : Key.values()) {
+ ids.put(k, new ArrayList<>());
+ }
+ }
+
+ /** Record a change ID update as having completed. Thread-safe. */
+ public void add(Key key, Change.Id id) {
+ synchronized (this) {
+ ids.get(key).add(id);
+ }
+ }
+
+ /** Returns change IDs of the given type for which the BatchUpdate succeeded. Thread-safe. */
+ public List<Change.Id> get(Key key) {
+ synchronized (this) {
+ return ImmutableList.copyOf(ids.get(key));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
new file mode 100644
index 0000000..5fe3e8e
--- /dev/null
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Optional;
+
+@Singleton
+public class GroupResolver {
+ private final GroupBackend groupBackend;
+ private final GroupCache groupCache;
+ private final GroupControl.Factory groupControlFactory;
+
+ @Inject
+ GroupResolver(
+ GroupBackend groupBackend, GroupCache groupCache, GroupControl.Factory groupControlFactory) {
+ this.groupBackend = groupBackend;
+ this.groupCache = groupCache;
+ this.groupControlFactory = groupControlFactory;
+ }
+
+ /**
+ * Parses a group ID from a request body and returns the group.
+ *
+ * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
+ * @return the group
+ * @throws UnprocessableEntityException thrown if the group ID cannot be resolved or if the group
+ * is not visible to the calling user
+ */
+ public GroupDescription.Basic parse(String id) throws UnprocessableEntityException {
+ GroupDescription.Basic group = parseId(id);
+ if (group == null || !groupControlFactory.controlFor(group).isVisible()) {
+ throw new UnprocessableEntityException(String.format("Group Not Found: %s", id));
+ }
+ return group;
+ }
+
+ /**
+ * Parses a group ID from a request body and returns the group if it is a Gerrit internal group.
+ *
+ * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
+ * @return the group
+ * @throws UnprocessableEntityException thrown if the group ID cannot be resolved, if the group is
+ * not visible to the calling user or if it's an external group
+ */
+ public GroupDescription.Internal parseInternal(String id) throws UnprocessableEntityException {
+ GroupDescription.Basic group = parse(id);
+ if (group instanceof GroupDescription.Internal) {
+ return (GroupDescription.Internal) group;
+ }
+
+ throw new UnprocessableEntityException(String.format("External Group Not Allowed: %s", id));
+ }
+
+ /**
+ * Parses a group ID and returns the group without making any permission check whether the current
+ * user can see the group.
+ *
+ * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
+ * @return the group, null if no group is found for the given group ID
+ */
+ public GroupDescription.Basic parseId(String id) {
+ AccountGroup.UUID uuid = new AccountGroup.UUID(id);
+ if (groupBackend.handles(uuid)) {
+ GroupDescription.Basic d = groupBackend.get(uuid);
+ if (d != null) {
+ return d;
+ }
+ }
+
+ // Might be a numeric AccountGroup.Id. -> Internal group.
+ if (id.matches("^[1-9][0-9]*$")) {
+ try {
+ AccountGroup.Id groupId = AccountGroup.Id.parse(id);
+ Optional<InternalGroup> group = groupCache.get(groupId);
+ if (group.isPresent()) {
+ return new InternalGroupDescription(group.get());
+ }
+ } catch (IllegalArgumentException e) {
+ // Ignored
+ }
+ }
+
+ // Might be a group name, be nice and accept unique names.
+ GroupReference ref = GroupBackends.findExactSuggestion(groupBackend, id);
+ if (ref != null) {
+ GroupDescription.Basic d = groupBackend.get(ref.getUUID());
+ if (d != null) {
+ return d;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index ab2203e..9fa1edc 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -230,8 +230,9 @@
String newNoteDbStateStr = change.getNoteDbState();
if (newNoteDbStateStr == null) {
throw new OrmException(
- "Rebuilding change %s produced no writes to NoteDb: "
- + bundleReader.fromReviewDb(db, changeId));
+ String.format(
+ "Rebuilding change %s produced no writes to NoteDb: %s",
+ changeId, bundleReader.fromReviewDb(db, changeId)));
}
NoteDbChangeState newNoteDbState =
checkNotNull(NoteDbChangeState.parse(changeId, newNoteDbStateStr));
@@ -290,8 +291,7 @@
// Can only rebuild a change if its primary storage is ReviewDb.
NoteDbChangeState s = NoteDbChangeState.parse(c);
if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) {
- throw new OrmException(
- String.format("cannot rebuild change " + c.getId() + " with state " + s));
+ throw new OrmException(String.format("cannot rebuild change %s with state %s", c.getId(), s));
}
return c;
}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 15fedd7..285c37d 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.patch;
import static com.google.common.base.Preconditions.checkArgument;
-import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -24,18 +23,12 @@
import com.google.gerrit.server.UsedAt;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.git.MergeUtil;
import com.google.inject.Inject;
import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.dircache.DirCache;
-import org.eclipse.jgit.dircache.DirCacheBuilder;
-import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -43,14 +36,11 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.MergeFormatter;
-import org.eclipse.jgit.merge.MergeResult;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.TemporaryBuffer;
public class AutoMerger {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -127,84 +117,17 @@
ObjectId treeId;
if (couldMerge) {
treeId = m.getResultTreeId();
-
} else {
- RevCommit ours = merge.getParent(0);
- RevCommit theirs = merge.getParent(1);
- rw.parseBody(ours);
- rw.parseBody(theirs);
- String oursMsg = ours.getShortMessage();
- String theirsMsg = theirs.getShortMessage();
-
- String oursName =
- String.format(
- "HEAD (%s %s)",
- ours.abbreviate(6).name(), oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
- String theirsName =
- String.format(
- "BRANCH (%s %s)",
- theirs.abbreviate(6).name(),
- theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
-
- MergeFormatter fmt = new MergeFormatter();
- Map<String, MergeResult<? extends Sequence>> r = m.getMergeResults();
- Map<String, ObjectId> resolved = new HashMap<>();
- for (Map.Entry<String, MergeResult<? extends Sequence>> entry : r.entrySet()) {
- MergeResult<? extends Sequence> p = entry.getValue();
- try (TemporaryBuffer buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
- fmt.formatMerge(buf, p, "BASE", oursName, theirsName, UTF_8.name());
- buf.close();
-
- try (InputStream in = buf.openInputStream()) {
- resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
- }
- }
- }
-
- DirCacheBuilder builder = dc.builder();
- int cnt = dc.getEntryCount();
- for (int i = 0; i < cnt; ) {
- DirCacheEntry entry = dc.getEntry(i);
- if (entry.getStage() == 0) {
- builder.add(entry);
- i++;
- continue;
- }
-
- int next = dc.nextEntry(i);
- String path = entry.getPathString();
- DirCacheEntry res = new DirCacheEntry(path);
- if (resolved.containsKey(path)) {
- // For a file with content merge conflict that we produced a result
- // above on, collapse the file down to a single stage 0 with just
- // the blob content, and a randomly selected mode (the lowest stage,
- // which should be the merge base, or ours).
- res.setFileMode(entry.getFileMode());
- res.setObjectId(resolved.get(path));
-
- } else if (next == i + 1) {
- // If there is exactly one stage present, shouldn't be a conflict...
- res.setFileMode(entry.getFileMode());
- res.setObjectId(entry.getObjectId());
-
- } else if (next == i + 2) {
- // Two stages suggests a delete/modify conflict. Pick the higher
- // stage as the automatic result.
- entry = dc.getEntry(i + 1);
- res.setFileMode(entry.getFileMode());
- res.setObjectId(entry.getObjectId());
-
- } else {
- // 3 stage conflict, no resolve above
- // Punt on the 3-stage conflict and show the base, for now.
- res.setFileMode(entry.getFileMode());
- res.setObjectId(entry.getObjectId());
- }
- builder.add(res);
- i = next;
- }
- builder.finish();
- treeId = dc.writeTree(ins);
+ treeId =
+ MergeUtil.mergeWithConflicts(
+ rw,
+ ins,
+ dc,
+ "HEAD",
+ merge.getParent(0),
+ "BRANCH",
+ merge.getParent(1),
+ m.getMergeResults());
}
return commit(repo, rw, tmpIns, ins, refName, treeId, merge);
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index a0091a3..df98f5e 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -104,7 +104,7 @@
Account.Id reviewer,
int value)
throws PermissionBackendException {
- if (!change.getStatus().isOpen()) {
+ if (change.getStatus().equals(Change.Status.MERGED)) {
return false;
}
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
index 370833c..c301ab2 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.restapi.account;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
@@ -22,10 +21,6 @@
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountResolver;
@@ -39,25 +34,19 @@
@Singleton
public class AccountsCollection implements RestCollection<TopLevelResource, AccountResource> {
- private final Provider<CurrentUser> self;
- private final AccountResolver resolver;
+ private final AccountResolver accountResolver;
private final AccountControl.Factory accountControlFactory;
- private final IdentifiedUser.GenericFactory userFactory;
private final Provider<QueryAccounts> list;
private final DynamicMap<RestView<AccountResource>> views;
@Inject
public AccountsCollection(
- Provider<CurrentUser> self,
- AccountResolver resolver,
+ AccountResolver accountResolver,
AccountControl.Factory accountControlFactory,
- IdentifiedUser.GenericFactory userFactory,
Provider<QueryAccounts> list,
DynamicMap<RestView<AccountResource>> views) {
- this.self = self;
- this.resolver = resolver;
+ this.accountResolver = accountResolver;
this.accountControlFactory = accountControlFactory;
- this.userFactory = userFactory;
this.list = list;
this.views = views;
}
@@ -66,7 +55,7 @@
public AccountResource parse(TopLevelResource root, IdString id)
throws ResourceNotFoundException, AuthException, OrmException, IOException,
ConfigInvalidException {
- IdentifiedUser user = parseId(id.get());
+ IdentifiedUser user = accountResolver.parseId(id.get());
if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
throw new ResourceNotFoundException(
String.format("Account '%s' is not found or ambiguous", id));
@@ -74,76 +63,6 @@
return new AccountResource(user);
}
- /**
- * Parses a account ID from a request body and returns the user.
- *
- * @param id ID of the account, can be a string of the format "{@code Full Name
- * <email@example.com>}", just the email address, a full name if it is unique, an account ID,
- * a user name or "{@code self}" for the calling user
- * @return the user, never null.
- * @throws UnprocessableEntityException thrown if the account ID cannot be resolved or if the
- * account is not visible to the calling user
- */
- public IdentifiedUser parse(String id)
- throws AuthException, UnprocessableEntityException, OrmException, IOException,
- ConfigInvalidException {
- return parseOnBehalfOf(null, id);
- }
-
- /**
- * Parses an account ID and returns the user without making any permission check whether the
- * current user can see the account.
- *
- * @param id ID of the account, can be a string of the format "{@code Full Name
- * <email@example.com>}", just the email address, a full name if it is unique, an account ID,
- * a user name or "{@code self}" for the calling user
- * @return the user, null if no user is found for the given account ID
- * @throws AuthException thrown if 'self' is used as account ID and the current user is not
- * authenticated
- * @throws OrmException
- * @throws ConfigInvalidException
- * @throws IOException
- */
- public IdentifiedUser parseId(String id)
- throws AuthException, OrmException, IOException, ConfigInvalidException {
- return parseIdOnBehalfOf(null, id);
- }
-
- /**
- * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
- */
- public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
- throws AuthException, UnprocessableEntityException, OrmException, IOException,
- ConfigInvalidException {
- IdentifiedUser user = parseIdOnBehalfOf(caller, id);
- if (user == null || !accountControlFactory.get().canSee(user.getAccount())) {
- throw new UnprocessableEntityException(
- String.format("Account '%s' is not found or ambiguous", id));
- }
- return user;
- }
-
- private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
- throws AuthException, OrmException, IOException, ConfigInvalidException {
- if (id.equals("self")) {
- CurrentUser user = self.get();
- if (user.isIdentifiedUser()) {
- return user.asIdentifiedUser();
- } else if (user instanceof AnonymousUser) {
- throw new AuthException("Authentication required");
- } else {
- return null;
- }
- }
-
- Account match = resolver.find(id);
- if (match == null) {
- return null;
- }
- CurrentUser realUser = caller != null ? caller.getRealUser() : null;
- return userFactory.runAs(null, match.getId(), realUser);
- }
-
@Override
public RestView<TopLevelResource> list() throws ResourceNotFoundException {
return list.get();
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 0e8eb70..4185f36 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -46,11 +46,11 @@
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -68,7 +68,7 @@
public class CreateAccount
implements RestCollectionCreateView<TopLevelResource, AccountResource, AccountInput> {
private final Sequences seq;
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final SshKeyCache sshKeyCache;
private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -80,7 +80,7 @@
@Inject
CreateAccount(
Sequences seq,
- GroupsCollection groupsCollection,
+ GroupResolver groupResolver,
VersionedAuthorizedKeys.Accessor authorizedKeys,
SshKeyCache sshKeyCache,
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
@@ -89,7 +89,7 @@
@UserInitiated Provider<GroupsUpdate> groupsUpdate,
OutgoingEmailValidator validator) {
this.seq = seq;
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
this.authorizedKeys = authorizedKeys;
this.sshKeyCache = sshKeyCache;
this.accountsUpdateProvider = accountsUpdateProvider;
@@ -183,7 +183,7 @@
Set<AccountGroup.UUID> groupUuids = new HashSet<>();
if (groups != null) {
for (String g : groups) {
- GroupDescription.Internal internalGroup = groupsCollection.parseInternal(g);
+ GroupDescription.Internal internalGroup = groupResolver.parseInternal(g);
groupUuids.add(internalGroup.getGroupUUID());
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index 53d81d7..b68122e 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -18,13 +18,12 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
-import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.RevisionResource;
@@ -50,7 +49,7 @@
@Singleton
public class CherryPick
- extends RetryingRestModifyView<RevisionResource, CherryPickInput, ChangeInfo>
+ extends RetryingRestModifyView<RevisionResource, CherryPickInput, CherryPickChangeInfo>
implements UiAction<RevisionResource> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -77,7 +76,7 @@
}
@Override
- public ChangeInfo applyImpl(
+ public CherryPickChangeInfo applyImpl(
BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input)
throws OrmException, IOException, UpdateException, RestApiException,
PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
@@ -99,14 +98,19 @@
projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
try {
- Change.Id cherryPickedChangeId =
+ CherryPickChange.Result cherryPickResult =
cherryPickChange.cherryPick(
updateFactory,
rsrc.getChange(),
rsrc.getPatchSet(),
input,
new Branch.NameKey(rsrc.getProject(), refName));
- return json.noOptions().format(rsrc.getProject(), cherryPickedChangeId);
+ CherryPickChangeInfo changeInfo =
+ json.noOptions()
+ .format(rsrc.getProject(), cherryPickResult.changeId(), CherryPickChangeInfo::new);
+ changeInfo.containsGitConflicts =
+ !cherryPickResult.filesWithGitConflicts().isEmpty() ? true : null;
+ return changeInfo;
} catch (InvalidChangeOperationException e) {
throw new BadRequestException(e.getMessage());
} catch (IntegrationException | NoSuchChangeException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 0de84053..65c652d 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.restapi.change;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -84,6 +86,16 @@
@Singleton
public class CherryPickChange {
+ @AutoValue
+ abstract static class Result {
+ static Result create(Change.Id changeId, ImmutableSet<String> filesWithGitConflicts) {
+ return new AutoValue_CherryPickChange_Result(changeId, filesWithGitConflicts);
+ }
+
+ abstract Change.Id changeId();
+
+ abstract ImmutableSet<String> filesWithGitConflicts();
+ }
private final Provider<ReviewDb> dbProvider;
private final Sequences seq;
@@ -132,7 +144,7 @@
this.notifyUtil = notifyUtil;
}
- public Change.Id cherryPick(
+ public Result cherryPick(
BatchUpdate.Factory batchUpdateFactory,
Change change,
PatchSet patch,
@@ -150,7 +162,7 @@
dest);
}
- public Change.Id cherryPick(
+ public Result cherryPick(
BatchUpdate.Factory batchUpdateFactory,
@Nullable Change sourceChange,
@Nullable PatchSet.Id sourcePatchId,
@@ -205,19 +217,26 @@
throw new NoSuchProjectException(dest.getParentKey());
}
try {
+ MergeUtil mergeUtil;
+ if (input.allowConflicts) {
+ // allowConflicts requires to use content merge
+ mergeUtil = mergeUtilFactory.create(projectState, true);
+ } else {
+ // use content merge only if it's configured on the project
+ mergeUtil = mergeUtilFactory.create(projectState);
+ }
cherryPickCommit =
- mergeUtilFactory
- .create(projectState)
- .createCherryPickFromCommit(
- oi,
- git.getConfig(),
- baseCommit,
- commitToCherryPick,
- committerIdent,
- commitMessage,
- revWalk,
- input.parent - 1,
- false);
+ mergeUtil.createCherryPickFromCommit(
+ oi,
+ git.getConfig(),
+ baseCommit,
+ commitToCherryPick,
+ committerIdent,
+ commitMessage,
+ revWalk,
+ input.parent - 1,
+ false,
+ input.allowConflicts);
Change.Key changeKey;
final List<String> idList = cherryPickCommit.getFooterLines(FooterConstants.CHANGE_ID);
@@ -241,11 +260,11 @@
try (BatchUpdate bu =
batchUpdateFactory.create(dbProvider.get(), project, identifiedUser, now)) {
bu.setRepository(git, revWalk, oi);
- Change.Id result;
+ Change.Id changeId;
if (destChanges.size() == 1) {
// The change key exists on the destination branch. The cherry pick
// will be added as a new patch set.
- result = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input);
+ changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input);
} else {
// Change key not found on destination branch. We can create a new
// change.
@@ -253,7 +272,7 @@
if (sourceChange != null && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
newTopic = sourceChange.getTopic() + "-" + newDest.getShortName();
}
- result =
+ changeId =
createNewChange(
bu, cherryPickCommit, dest.get(), newTopic, sourceChange, sourceCommit, input);
@@ -265,7 +284,7 @@
}
}
bu.execute();
- return result;
+ return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
}
} catch (MergeIdenticalTreeException | MergeConflictException e) {
throw new IntegrationException("Cherry pick failed: " + e.getMessage());
@@ -346,7 +365,9 @@
Change.Id changeId = new Change.Id(seq.nextChangeId());
ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
- ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch, sourceCommit))
+ ins.setMessage(
+ messageForDestinationChange(
+ ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit))
.setTopic(topic)
.setNotify(input.notify)
.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
@@ -404,15 +425,27 @@
}
private String messageForDestinationChange(
- PatchSet.Id patchSetId, Branch.NameKey sourceBranch, ObjectId sourceCommit) {
+ PatchSet.Id patchSetId,
+ Branch.NameKey sourceBranch,
+ ObjectId sourceCommit,
+ CodeReviewCommit cherryPickCommit) {
StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get());
-
if (sourceBranch != null) {
stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.getShortName());
} else {
stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName());
}
+ stringBuilder.append(".");
- return stringBuilder.append(".").toString();
+ if (!cherryPickCommit.getFilesWithGitConflicts().isEmpty()) {
+ stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
+ cherryPickCommit
+ .getFilesWithGitConflicts()
+ .stream()
+ .sorted()
+ .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
+ }
+
+ return stringBuilder.toString();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index 11aa256..d18b172 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -16,12 +16,11 @@
import com.google.common.base.Strings;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
-import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.CurrentUser;
@@ -48,7 +47,7 @@
@Singleton
public class CherryPickCommit
- extends RetryingRestModifyView<CommitResource, CherryPickInput, ChangeInfo> {
+ extends RetryingRestModifyView<CommitResource, CherryPickInput, CherryPickChangeInfo> {
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> user;
private final CherryPickChange cherryPickChange;
@@ -72,7 +71,7 @@
}
@Override
- public ChangeInfo applyImpl(
+ public CherryPickChangeInfo applyImpl(
BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
throws OrmException, IOException, UpdateException, RestApiException,
PermissionBackendException, ConfigInvalidException, NoSuchProjectException {
@@ -97,7 +96,7 @@
rsrc.getProjectState().checkStatePermitsWrite();
try {
- Change.Id cherryPickedChangeId =
+ CherryPickChange.Result cherryPickResult =
cherryPickChange.cherryPick(
updateFactory,
null,
@@ -106,7 +105,12 @@
commit,
input,
new Branch.NameKey(rsrc.getProjectState().getNameKey(), refName));
- return json.noOptions().format(projectName, cherryPickedChangeId);
+ CherryPickChangeInfo changeInfo =
+ json.noOptions()
+ .format(projectName, cherryPickResult.changeId(), CherryPickChangeInfo::new);
+ changeInfo.containsGitConflicts =
+ !cherryPickResult.filesWithGitConflicts().isEmpty() ? true : null;
+ return changeInfo;
} catch (InvalidChangeOperationException e) {
throw new BadRequestException(e.getMessage());
} catch (IntegrationException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetCommit.java b/java/com/google/gerrit/server/restapi/change/GetCommit.java
index 645d7d1..29286cb 100644
--- a/java/com/google/gerrit/server/restapi/change/GetCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/GetCommit.java
@@ -14,12 +14,13 @@
package com.google.gerrit.server.restapi.change;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.RevisionJson;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
@@ -33,12 +34,12 @@
public class GetCommit implements RestReadView<RevisionResource> {
private final GitRepositoryManager repoManager;
- private final ChangeJson.Factory json;
+ private final RevisionJson.Factory json;
private boolean addLinks;
@Inject
- GetCommit(GitRepositoryManager repoManager, ChangeJson.Factory json) {
+ GetCommit(GitRepositoryManager repoManager, RevisionJson.Factory json) {
this.repoManager = repoManager;
this.json = json;
}
@@ -57,7 +58,9 @@
String rev = rsrc.getPatchSet().getRevision().get();
RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
rw.parseBody(commit);
- CommitInfo info = json.noOptions().toCommit(rsrc.getProject(), rw, commit, addLinks, true);
+ CommitInfo info =
+ json.create(ImmutableSet.of())
+ .getCommitInfo(rsrc.getProject(), rw, commit, addLinks, true);
Response<CommitInfo> r = Response.ok(info);
if (rsrc.isCacheable()) {
r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
diff --git a/java/com/google/gerrit/server/restapi/change/GetMergeList.java b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
index 2f3b536..8e7e693 100644
--- a/java/com/google/gerrit/server/restapi/change/GetMergeList.java
+++ b/java/com/google/gerrit/server/restapi/change/GetMergeList.java
@@ -15,13 +15,14 @@
package com.google.gerrit.server.restapi.change;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.RevisionJson;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.MergeListBuilder;
@@ -38,7 +39,7 @@
public class GetMergeList implements RestReadView<RevisionResource> {
private final GitRepositoryManager repoManager;
- private final ChangeJson.Factory json;
+ private final RevisionJson.Factory json;
@Option(name = "--parent", usage = "Uninteresting parent (1-based, default = 1)")
private int uninterestingParent = 1;
@@ -47,7 +48,7 @@
private boolean addLinks;
@Inject
- GetMergeList(GitRepositoryManager repoManager, ChangeJson.Factory json) {
+ GetMergeList(GitRepositoryManager repoManager, RevisionJson.Factory json) {
this.repoManager = repoManager;
this.json = json;
}
@@ -80,9 +81,9 @@
List<RevCommit> commits = MergeListBuilder.build(rw, commit, uninterestingParent);
List<CommitInfo> result = new ArrayList<>(commits.size());
- ChangeJson changeJson = json.noOptions();
+ RevisionJson changeJson = json.create(ImmutableSet.of());
for (RevCommit c : commits) {
- result.add(changeJson.toCommit(rsrc.getProject(), rw, c, addLinks, true));
+ result.add(changeJson.getCommitInfo(rsrc.getProject(), rw, c, addLinks, true));
}
return createResponse(rsrc, result);
}
diff --git a/java/com/google/gerrit/server/restapi/change/GetReviewer.java b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
index b9b6b09..a11380b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/GetReviewer.java
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ReviewerJson;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
diff --git a/java/com/google/gerrit/server/restapi/change/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 46bb33f..99d8746 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ReviewerJson;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index 32c4ea3..7add548 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.change.ReviewerJson;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index f8b0cc4..a45a6d8 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -29,6 +29,7 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.AddReviewersOp;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.EmailReviewComments;
@@ -190,7 +191,7 @@
factory(DeleteReviewerOp.Factory.class);
factory(EmailReviewComments.Factory.class);
factory(PatchSetInserter.Factory.class);
- factory(PostReviewersOp.Factory.class);
+ factory(AddReviewersOp.Factory.class);
factory(RebaseChangeOp.Factory.class);
factory(ReviewerResource.Factory.class);
factory(SetAssigneeOp.Factory.class);
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index c7028ab..e47c582 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -88,10 +88,13 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.AddReviewersEmail;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.ChangeResource.Factory;
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.ReviewerAdder;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -111,7 +114,6 @@
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.restapi.account.AccountsCollection;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -167,11 +169,11 @@
private final PublishCommentUtil publishCommentUtil;
private final PatchSetUtil psUtil;
private final PatchListCache patchListCache;
- private final AccountsCollection accounts;
+ private final AccountResolver accountResolver;
private final EmailReviewComments.Factory email;
private final CommentAdded commentAdded;
- private final PostReviewers postReviewers;
- private final PostReviewersEmail postReviewersEmail;
+ private final ReviewerAdder reviewerAdder;
+ private final AddReviewersEmail addReviewersEmail;
private final NotesMigration migration;
private final NotifyUtil notifyUtil;
private final Config gerritConfig;
@@ -184,7 +186,7 @@
PostReview(
Provider<ReviewDb> db,
RetryHelper retryHelper,
- Factory changeResourceFactory,
+ ChangeResource.Factory changeResourceFactory,
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
@@ -192,11 +194,11 @@
PublishCommentUtil publishCommentUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
- AccountsCollection accounts,
+ AccountResolver accountResolver,
EmailReviewComments.Factory email,
CommentAdded commentAdded,
- PostReviewers postReviewers,
- PostReviewersEmail postReviewersEmail,
+ ReviewerAdder reviewerAdder,
+ AddReviewersEmail addReviewersEmail,
NotesMigration migration,
NotifyUtil notifyUtil,
@GerritServerConfig Config gerritConfig,
@@ -213,11 +215,11 @@
this.patchListCache = patchListCache;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
- this.accounts = accounts;
+ this.accountResolver = accountResolver;
this.email = email;
this.commentAdded = commentAdded;
- this.postReviewers = postReviewers;
- this.postReviewersEmail = postReviewersEmail;
+ this.reviewerAdder = reviewerAdder;
+ this.addReviewersEmail = addReviewersEmail;
this.migration = migration;
this.notifyUtil = notifyUtil;
this.gerritConfig = gerritConfig;
@@ -273,21 +275,20 @@
notifyUtil.resolveAccounts(input.notifyDetails);
Map<String, AddReviewerResult> reviewerJsonResults = null;
- List<PostReviewers.Addition> reviewerResults = Lists.newArrayList();
+ List<ReviewerAddition> reviewerResults = Lists.newArrayList();
boolean hasError = false;
boolean confirm = false;
if (input.reviewers != null) {
reviewerJsonResults = Maps.newHashMap();
for (AddReviewerInput reviewerInput : input.reviewers) {
- // Prevent individual PostReviewersOps from sending one email each. Instead, we call
+ // Prevent individual AddReviewersOps from sending one email each. Instead, we call
// batchEmailReviewers at the very end to send out a single email.
// TODO(dborowitz): I think this still sends out separate emails if any of input.reviewers
// specifies explicit accountsToNotify. Unclear whether that's a good thing.
reviewerInput.notify = NotifyHandling.NONE;
- PostReviewers.Addition result =
- postReviewers.prepareApplication(
- revision.getNotes(), revision.getUser(), reviewerInput, true);
+ ReviewerAddition result =
+ reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
reviewerJsonResults.put(reviewerInput.reviewer, result.result);
if (result.result.error != null) {
hasError = true;
@@ -327,7 +328,7 @@
// Apply reviewer changes first. Revision emails should be sent to the
// updated set of reviewers. Also keep track of whether the user added
// themselves as a reviewer or to the CC list.
- for (PostReviewers.Addition reviewerResult : reviewerResults) {
+ for (ReviewerAddition reviewerResult : reviewerResults) {
bu.addOp(revision.getChange().getId(), reviewerResult.op);
if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
@@ -351,8 +352,7 @@
// User posting this review isn't currently in the reviewer or CC list,
// isn't being explicitly added, and isn't voting on any label.
// Automatically CC them on this change so they receive replies.
- PostReviewers.Addition selfAddition =
- postReviewers.ccCurrentUser(revision.getUser(), revision);
+ ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision);
bu.addOp(revision.getChange().getId(), selfAddition.op);
}
@@ -389,13 +389,13 @@
// Re-read change to take into account results of the update.
ChangeData cd =
changeDataFactory.create(db.get(), revision.getProject(), revision.getChange().getId());
- for (PostReviewers.Addition reviewerResult : reviewerResults) {
+ for (ReviewerAddition reviewerResult : reviewerResults) {
reviewerResult.gatherResults(cd);
}
boolean readyForReview =
(output.ready != null && output.ready) || !revision.getChange().isWorkInProgress();
- // Sending from PostReviewersOp was suppressed so we can send a single batch email here.
+ // Sending from AddReviewersOp was suppressed so we can send a single batch email here.
batchEmailReviewers(
revision.getUser(),
revision.getChange(),
@@ -434,7 +434,7 @@
private void batchEmailReviewers(
CurrentUser user,
Change change,
- List<PostReviewers.Addition> reviewerAdditions,
+ List<ReviewerAddition> reviewerAdditions,
@Nullable NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean readyForReview) {
@@ -442,7 +442,7 @@
List<Account.Id> cc = new ArrayList<>();
List<Address> toByEmail = new ArrayList<>();
List<Address> ccByEmail = new ArrayList<>();
- for (PostReviewers.Addition addition : reviewerAdditions) {
+ for (ReviewerAddition addition : reviewerAdditions) {
if (addition.state == ReviewerState.REVIEWER) {
to.addAll(addition.reviewers);
toByEmail.addAll(addition.reviewersByEmail);
@@ -451,7 +451,7 @@
ccByEmail.addAll(addition.reviewersByEmail);
}
}
- postReviewersEmail.emailReviewers(
+ addReviewersEmail.emailReviewers(
user.asIdentifiedUser(),
change,
to,
@@ -505,7 +505,7 @@
String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
}
- IdentifiedUser reviewer = accounts.parseOnBehalfOf(caller, in.onBehalfOf);
+ IdentifiedUser reviewer = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
try {
permissionBackend
.user(reviewer)
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index dd6e457..9147d44 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -14,61 +14,18 @@
package com.google.gerrit.server.restapi.change;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.extensions.client.ReviewerState.CC;
-import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.api.changes.ReviewerInfo;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.ReviewerAdder;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
import com.google.gerrit.server.change.ReviewerResource;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.permissions.ChangePermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.restapi.account.AccountsCollection;
-import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryingRestCollectionModifyView;
@@ -79,72 +36,27 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
@Singleton
public class PostReviewers
extends RetryingRestCollectionModifyView<
ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> {
- public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
- public static final int DEFAULT_MAX_REVIEWERS = 20;
-
- private final AccountsCollection accounts;
- private final PermissionBackend permissionBackend;
-
- private final GroupsCollection groupsCollection;
- private final GroupMembers groupMembers;
- private final AccountLoader.Factory accountLoaderFactory;
private final Provider<ReviewDb> dbProvider;
private final ChangeData.Factory changeDataFactory;
- private final Config cfg;
- private final ReviewerJson json;
- private final NotesMigration migration;
- private final NotifyUtil notifyUtil;
- private final ProjectCache projectCache;
- private final Provider<AnonymousUser> anonymousProvider;
- private final PostReviewersOp.Factory postReviewersOpFactory;
- private final OutgoingEmailValidator validator;
+ private final ReviewerAdder reviewerAdder;
@Inject
PostReviewers(
- AccountsCollection accounts,
- PermissionBackend permissionBackend,
- GroupsCollection groupsCollection,
- GroupMembers groupMembers,
- AccountLoader.Factory accountLoaderFactory,
Provider<ReviewDb> db,
ChangeData.Factory changeDataFactory,
RetryHelper retryHelper,
- @GerritServerConfig Config cfg,
- ReviewerJson json,
- NotesMigration migration,
- NotifyUtil notifyUtil,
- ProjectCache projectCache,
- Provider<AnonymousUser> anonymousProvider,
- PostReviewersOp.Factory postReviewersOpFactory,
- OutgoingEmailValidator validator) {
+ ReviewerAdder reviewerAdder) {
super(retryHelper);
- this.accounts = accounts;
- this.permissionBackend = permissionBackend;
- this.groupsCollection = groupsCollection;
- this.groupMembers = groupMembers;
- this.accountLoaderFactory = accountLoaderFactory;
this.dbProvider = db;
this.changeDataFactory = changeDataFactory;
- this.cfg = cfg;
- this.json = json;
- this.migration = migration;
- this.notifyUtil = notifyUtil;
- this.projectCache = projectCache;
- this.anonymousProvider = anonymousProvider;
- this.postReviewersOpFactory = postReviewersOpFactory;
- this.validator = validator;
+ this.reviewerAdder = reviewerAdder;
}
@Override
@@ -156,7 +68,7 @@
throw new BadRequestException("missing reviewer field");
}
- Addition addition = prepareApplication(rsrc.getNotes(), rsrc.getUser(), input, true);
+ ReviewerAddition addition = reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
if (addition.op == null) {
return addition.result;
}
@@ -173,349 +85,4 @@
changeDataFactory.create(dbProvider.get(), rsrc.getProject(), rsrc.getId()));
return addition.result;
}
-
- /**
- * Prepare application of a single {@link AddReviewerInput}.
- *
- * @param notes change notes.
- * @param user user performing the reviewer addition.
- * @param input input describing user or group to add as a reviewer.
- * @param allowGroup whether to allow
- * @return handle describing the addition operation. If the {@code op} field is present, this
- * operation may be added to a {@code BatchUpdate}. Otherwise, the {@code error} field
- * contains information about an error that occurred
- * @throws OrmException
- * @throws IOException
- * @throws PermissionBackendException
- * @throws ConfigInvalidException
- */
- public Addition prepareApplication(
- ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
- throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
- Branch.NameKey dest = notes.getChange().getDest();
- String reviewer = input.reviewer;
- ReviewerState state = input.state();
- NotifyHandling notify = input.notify;
- ListMultimap<RecipientType, Account.Id> accountsToNotify;
- try {
- accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails);
- } catch (BadRequestException e) {
- return fail(reviewer, e.getMessage());
- }
- boolean confirmed = input.confirmed();
- boolean allowByEmail =
- projectCache
- .checkedGet(dest.getParentKey())
- .is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
-
- Addition byAccountId =
- addByAccountId(
- reviewer, dest, user, state, notify, accountsToNotify, allowGroup, allowByEmail);
-
- Addition wholeGroup = null;
- if (byAccountId == null || !byAccountId.exactMatchFound) {
- wholeGroup =
- addWholeGroup(
- reviewer,
- dest,
- user,
- state,
- notify,
- accountsToNotify,
- confirmed,
- allowGroup,
- allowByEmail);
- if (wholeGroup != null && wholeGroup.exactMatchFound) {
- return wholeGroup;
- }
- }
-
- if (byAccountId != null) {
- return byAccountId;
- }
- if (wholeGroup != null) {
- return wholeGroup;
- }
-
- return addByEmail(reviewer, notes, user, state, notify, accountsToNotify);
- }
-
- Addition ccCurrentUser(CurrentUser user, RevisionResource revision) {
- return new Addition(
- user.getUserName().orElse(null),
- revision.getUser(),
- ImmutableSet.of(user.getAccountId()),
- null,
- CC,
- NotifyHandling.NONE,
- ImmutableListMultimap.of(),
- true);
- }
-
- @Nullable
- private Addition addByAccountId(
- String reviewer,
- Branch.NameKey dest,
- CurrentUser user,
- ReviewerState state,
- NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
- boolean allowGroup,
- boolean allowByEmail)
- throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
- IdentifiedUser reviewerUser;
- boolean exactMatchFound = false;
- try {
- reviewerUser = accounts.parse(reviewer);
- if (reviewer.equalsIgnoreCase(reviewerUser.getName())
- || reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
- exactMatchFound = true;
- }
- } catch (UnprocessableEntityException | AuthException e) {
- // AuthException won't occur since the user is authenticated at this point.
- if (!allowGroup && !allowByEmail) {
- // Only return failure if we aren't going to try other interpretations.
- return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer));
- }
- return null;
- }
-
- if (isValidReviewer(dest, reviewerUser.getAccount())) {
- return new Addition(
- reviewer,
- user,
- ImmutableSet.of(reviewerUser.getAccountId()),
- null,
- state,
- notify,
- accountsToNotify,
- exactMatchFound);
- }
- if (!reviewerUser.getAccount().isActive()) {
- if (allowByEmail && state == CC) {
- return null;
- }
- return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInactive, reviewer));
- }
- return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
- }
-
- @Nullable
- private Addition addWholeGroup(
- String reviewer,
- Branch.NameKey dest,
- CurrentUser user,
- ReviewerState state,
- NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
- boolean confirmed,
- boolean allowGroup,
- boolean allowByEmail)
- throws IOException, PermissionBackendException {
- if (!allowGroup) {
- return null;
- }
-
- GroupDescription.Basic group;
- try {
- group = groupsCollection.parseInternal(reviewer);
- } catch (UnprocessableEntityException e) {
- if (!allowByEmail) {
- return fail(
- reviewer,
- MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, reviewer));
- }
- return null;
- }
-
- if (!isLegalReviewerGroup(group.getGroupUUID())) {
- return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName()));
- }
-
- Set<Account.Id> reviewers = new HashSet<>();
- Set<Account> members;
- try {
- members = groupMembers.listAccounts(group.getGroupUUID(), dest.getParentKey());
- } catch (NoSuchProjectException e) {
- return fail(reviewer, e.getMessage());
- }
-
- // if maxAllowed is set to 0, it is allowed to add any number of
- // reviewers
- int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
- if (maxAllowed > 0 && members.size() > maxAllowed) {
- return fail(
- reviewer,
- MessageFormat.format(ChangeMessages.get().groupHasTooManyMembers, group.getName()));
- }
-
- // if maxWithoutCheck is set to 0, we never ask for confirmation
- int maxWithoutConfirmation =
- cfg.getInt("addreviewer", "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
- if (!confirmed && maxWithoutConfirmation > 0 && members.size() > maxWithoutConfirmation) {
- return fail(
- reviewer,
- true,
- MessageFormat.format(
- ChangeMessages.get().groupManyMembersConfirmation, group.getName(), members.size()));
- }
-
- for (Account member : members) {
- if (isValidReviewer(dest, member)) {
- reviewers.add(member.getId());
- }
- }
-
- return new Addition(reviewer, user, reviewers, null, state, notify, accountsToNotify, true);
- }
-
- @Nullable
- private Addition addByEmail(
- String reviewer,
- ChangeNotes notes,
- CurrentUser user,
- ReviewerState state,
- NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify)
- throws PermissionBackendException {
- try {
- permissionBackend
- .user(anonymousProvider.get())
- .database(dbProvider)
- .change(notes)
- .check(ChangePermission.READ);
- } catch (AuthException e) {
- return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
- }
-
- if (!migration.readChanges()) {
- // addByEmail depends on NoteDb.
- return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer));
- }
- Address adr = Address.tryParse(reviewer);
- if (adr == null || !validator.isValid(adr.getEmail())) {
- return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInvalid, reviewer));
- }
- return new Addition(
- reviewer, user, null, ImmutableList.of(adr), state, notify, accountsToNotify, true);
- }
-
- private boolean isValidReviewer(Branch.NameKey branch, Account member)
- throws PermissionBackendException {
- if (!member.isActive()) {
- return false;
- }
-
- try {
- // Check ref permission instead of change permission, since change permissions take into
- // account the private bit, whereas adding a user as a reviewer is explicitly allowing them to
- // see private changes.
- permissionBackend
- .absentUser(member.getId())
- .database(dbProvider)
- .ref(branch)
- .check(RefPermission.READ);
- return true;
- } catch (AuthException e) {
- return false;
- }
- }
-
- private Addition fail(String reviewer, String error) {
- return fail(reviewer, false, error);
- }
-
- private Addition fail(String reviewer, boolean confirm, String error) {
- Addition addition = new Addition(reviewer);
- addition.result.confirm = confirm ? true : null;
- addition.result.error = error;
- return addition;
- }
-
- public class Addition {
- public final AddReviewerResult result;
- @Nullable public final PostReviewersOp op;
- final Set<Account.Id> reviewers;
- final Collection<Address> reviewersByEmail;
- final ReviewerState state;
- @Nullable final IdentifiedUser caller;
- final boolean exactMatchFound;
-
- Addition(String reviewer) {
- result = new AddReviewerResult(reviewer);
- op = null;
- reviewers = ImmutableSet.of();
- reviewersByEmail = ImmutableSet.of();
- state = REVIEWER;
- caller = null;
- exactMatchFound = false;
- }
-
- Addition(
- String reviewer,
- CurrentUser caller,
- @Nullable Set<Account.Id> reviewers,
- @Nullable Collection<Address> reviewersByEmail,
- ReviewerState state,
- @Nullable NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
- boolean exactMatchFound) {
- checkArgument(
- reviewers != null || reviewersByEmail != null,
- "must have either reviewers or reviewersByEmail");
-
- result = new AddReviewerResult(reviewer);
- this.reviewers = reviewers == null ? ImmutableSet.of() : reviewers;
- this.reviewersByEmail = reviewersByEmail == null ? ImmutableList.of() : reviewersByEmail;
- this.state = state;
- this.caller = caller.asIdentifiedUser();
- op =
- postReviewersOpFactory.create(
- this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
- this.exactMatchFound = exactMatchFound;
- }
-
- void gatherResults(ChangeData cd) throws OrmException, PermissionBackendException {
- checkState(op != null, "addition did not result in an update op");
- checkState(op.getResult() != null, "op did not return a result");
-
- // Generate result details and fill AccountLoader. This occurs outside
- // the Op because the accounts are in a different table.
- PostReviewersOp.Result opResult = op.getResult();
- if (migration.readChanges() && state == CC) {
- result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size());
- for (Account.Id accountId : opResult.addedCCs()) {
- result.ccs.add(json.format(new ReviewerInfo(accountId.get()), accountId, cd));
- }
- accountLoaderFactory.create(true).fill(result.ccs);
- for (Address a : reviewersByEmail) {
- result.ccs.add(new AccountInfo(a.getName(), a.getEmail()));
- }
- } else {
- result.reviewers = Lists.newArrayListWithCapacity(opResult.addedReviewers().size());
- for (PatchSetApproval psa : opResult.addedReviewers()) {
- // New reviewers have value 0, don't bother normalizing.
- result.reviewers.add(
- json.format(
- new ReviewerInfo(psa.getAccountId().get()),
- psa.getAccountId(),
- cd,
- ImmutableList.of(psa)));
- }
- accountLoaderFactory.create(true).fill(result.reviewers);
- for (Address a : reviewersByEmail) {
- result.reviewers.add(ReviewerInfo.byEmail(a.getName(), a.getEmail()));
- }
- }
- }
- }
-
- public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) {
- return !SystemGroupBackend.isSystemGroup(groupUUID);
- }
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
deleted file mode 100644
index 6379393..0000000
--- a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.extensions.client.ReviewerState.CC;
-import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
-import static java.util.stream.Collectors.toList;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.extensions.events.ReviewerAdded;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.Context;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-public class PostReviewersOp implements BatchUpdateOp {
- public interface Factory {
- PostReviewersOp create(
- Set<Account.Id> reviewers,
- Collection<Address> reviewersByEmail,
- ReviewerState state,
- @Nullable NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify);
- }
-
- @AutoValue
- public abstract static class Result {
- public abstract ImmutableList<PatchSetApproval> addedReviewers();
-
- public abstract ImmutableList<Account.Id> addedCCs();
-
- static Builder builder() {
- return new AutoValue_PostReviewersOp_Result.Builder();
- }
-
- @AutoValue.Builder
- abstract static class Builder {
- abstract Builder setAddedReviewers(ImmutableList<PatchSetApproval> addedReviewers);
-
- abstract Builder setAddedCCs(ImmutableList<Account.Id> addedCCs);
-
- abstract Result build();
- }
- }
-
- private final ApprovalsUtil approvalsUtil;
- private final PatchSetUtil psUtil;
- private final ReviewerAdded reviewerAdded;
- private final AccountCache accountCache;
- private final ProjectCache projectCache;
- private final PostReviewersEmail postReviewersEmail;
- private final NotesMigration migration;
- private final Provider<IdentifiedUser> user;
- private final Provider<ReviewDb> dbProvider;
- private final Set<Account.Id> reviewers;
- private final Collection<Address> reviewersByEmail;
- private final ReviewerState state;
- private final NotifyHandling notify;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
-
- private List<PatchSetApproval> addedReviewers = new ArrayList<>();
- private Collection<Account.Id> addedCCs = new ArrayList<>();
- private Collection<Address> addedCCsByEmail = new ArrayList<>();
- private Change change;
- private PatchSet patchSet;
- private Result opResult;
-
- @Inject
- PostReviewersOp(
- ApprovalsUtil approvalsUtil,
- PatchSetUtil psUtil,
- ReviewerAdded reviewerAdded,
- AccountCache accountCache,
- ProjectCache projectCache,
- PostReviewersEmail postReviewersEmail,
- NotesMigration migration,
- Provider<IdentifiedUser> user,
- Provider<ReviewDb> dbProvider,
- @Assisted Set<Account.Id> reviewers,
- @Assisted Collection<Address> reviewersByEmail,
- @Assisted ReviewerState state,
- @Assisted @Nullable NotifyHandling notify,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
- checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
- this.approvalsUtil = approvalsUtil;
- this.psUtil = psUtil;
- this.reviewerAdded = reviewerAdded;
- this.accountCache = accountCache;
- this.projectCache = projectCache;
- this.postReviewersEmail = postReviewersEmail;
- this.migration = migration;
- this.user = user;
- this.dbProvider = dbProvider;
-
- this.reviewers = reviewers;
- this.reviewersByEmail = reviewersByEmail;
- this.state = state;
- this.notify = notify;
- this.accountsToNotify = accountsToNotify;
- }
-
- @Override
- public boolean updateChange(ChangeContext ctx)
- throws RestApiException, OrmException, IOException {
- change = ctx.getChange();
- if (!reviewers.isEmpty()) {
- if (migration.readChanges() && state == CC) {
- addedCCs =
- approvalsUtil.addCcs(
- ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), reviewers);
- if (addedCCs.isEmpty()) {
- return false;
- }
- } else {
- addedReviewers =
- approvalsUtil.addReviewers(
- ctx.getDb(),
- ctx.getNotes(),
- ctx.getUpdate(change.currentPatchSetId()),
- projectCache.checkedGet(change.getProject()).getLabelTypes(change.getDest()),
- change,
- reviewers);
- if (addedReviewers.isEmpty()) {
- return false;
- }
- }
- }
-
- for (Address a : reviewersByEmail) {
- ctx.getUpdate(change.currentPatchSetId())
- .putReviewerByEmail(a, ReviewerStateInternal.fromReviewerState(state));
- }
-
- patchSet = psUtil.current(dbProvider.get(), ctx.getNotes());
- return true;
- }
-
- @Override
- public void postUpdate(Context ctx) throws Exception {
- opResult =
- Result.builder()
- .setAddedReviewers(ImmutableList.copyOf(addedReviewers))
- .setAddedCCs(ImmutableList.copyOf(addedCCs))
- .build();
- postReviewersEmail.emailReviewers(
- user.get(),
- change,
- Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
- addedCCs == null ? ImmutableList.of() : addedCCs,
- reviewersByEmail,
- addedCCsByEmail,
- notify,
- accountsToNotify,
- !change.isWorkInProgress());
- if (!addedReviewers.isEmpty()) {
- List<AccountState> reviewers =
- addedReviewers
- .stream()
- .map(r -> accountCache.get(r.getAccountId()))
- .flatMap(Streams::stream)
- .collect(toList());
- reviewerAdded.fire(change, patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
- }
- }
-
- public Result getResult() {
- checkState(opResult != null, "Batch update wasn't executed yet");
- return opResult;
- }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index 9d626fc..7ca674f 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -28,13 +28,14 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ReviewerAdder;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
import com.google.gerrit.server.change.SetAssigneeOp;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.account.AccountsCollection;
-import com.google.gerrit.server.restapi.change.PostReviewers.Addition;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -51,27 +52,27 @@
public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo>
implements UiAction<ChangeResource> {
- private final AccountsCollection accounts;
+ private final AccountResolver accountResolver;
private final SetAssigneeOp.Factory assigneeFactory;
private final Provider<ReviewDb> db;
- private final PostReviewers postReviewers;
+ private final ReviewerAdder reviewerAdder;
private final AccountLoader.Factory accountLoaderFactory;
private final PermissionBackend permissionBackend;
@Inject
PutAssignee(
- AccountsCollection accounts,
+ AccountResolver accountResolver,
SetAssigneeOp.Factory assigneeFactory,
RetryHelper retryHelper,
Provider<ReviewDb> db,
- PostReviewers postReviewers,
+ ReviewerAdder reviewerAdder,
AccountLoader.Factory accountLoaderFactory,
PermissionBackend permissionBackend) {
super(retryHelper);
- this.accounts = accounts;
+ this.accountResolver = accountResolver;
this.assigneeFactory = assigneeFactory;
this.db = db;
- this.postReviewers = postReviewers;
+ this.reviewerAdder = reviewerAdder;
this.accountLoaderFactory = accountLoaderFactory;
this.permissionBackend = permissionBackend;
}
@@ -88,7 +89,7 @@
throw new BadRequestException("missing assignee field");
}
- IdentifiedUser assignee = accounts.parse(input.assignee);
+ IdentifiedUser assignee = accountResolver.parse(input.assignee);
if (!assignee.getAccount().isActive()) {
throw new UnprocessableEntityException(input.assignee + " is not active");
}
@@ -108,7 +109,7 @@
SetAssigneeOp op = assigneeFactory.create(assignee);
bu.addOp(rsrc.getId(), op);
- PostReviewers.Addition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
+ ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
bu.addOp(rsrc.getId(), reviewersAddition.op);
bu.execute();
@@ -116,14 +117,14 @@
}
}
- private Addition addAssigneeAsCC(ChangeResource rsrc, String assignee)
+ private ReviewerAddition addAssigneeAsCC(ChangeResource rsrc, String assignee)
throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
AddReviewerInput reviewerInput = new AddReviewerInput();
reviewerInput.reviewer = assignee;
reviewerInput.state = ReviewerState.CC;
reviewerInput.confirmed = true;
reviewerInput.notify = NotifyHandling.NONE;
- return postReviewers.prepareApplication(rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
+ return reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index b53c4e6..6610ebb 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -14,9 +14,7 @@
package com.google.gerrit.server.restapi.change;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -136,10 +134,7 @@
ChangeJson cjson = json.create(options);
cjson.setPluginDefinedAttributesFactory(this.imp);
- List<List<ChangeInfo>> res =
- cjson
- .lazyLoad(containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD))
- .formatQueryResults(results);
+ List<List<ChangeInfo>> res = cjson.formatQueryResults(results);
for (int n = 0; n < cnt; n++) {
List<ChangeInfo> info = res.get(n);
@@ -149,9 +144,4 @@
}
return res;
}
-
- private static boolean containsAnyOf(
- EnumSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
- return !Sets.intersection(toFind, set).isEmpty();
- }
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 3becf24..f7959c8 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -47,6 +47,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -383,7 +384,7 @@
int maxAllowedWithoutConfirmation = suggestReviewers.getMaxAllowedWithoutConfirmation();
logger.atFine().log("maxAllowedWithoutConfirmation: " + maxAllowedWithoutConfirmation);
- if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
+ if (!ReviewerAdder.isLegalReviewerGroup(group.getUUID())) {
logger.atFine().log("Ignore group %s that is not legal as reviewer", group.getUUID());
return result;
}
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 8ceddf0..773d12d 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -42,6 +42,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
@@ -55,7 +56,6 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.restapi.account.AccountsCollection;
import com.google.gerrit.server.submit.ChangeSet;
import com.google.gerrit.server.submit.MergeOp;
import com.google.gerrit.server.submit.MergeSuperSet;
@@ -114,7 +114,7 @@
private final ChangeNotes.Factory changeNotesFactory;
private final Provider<MergeOp> mergeOpProvider;
private final Provider<MergeSuperSet> mergeSuperSet;
- private final AccountsCollection accounts;
+ private final AccountResolver accountResolver;
private final String label;
private final String labelWithParents;
private final ParameterizedString titlePattern;
@@ -135,7 +135,7 @@
ChangeNotes.Factory changeNotesFactory,
Provider<MergeOp> mergeOpProvider,
Provider<MergeSuperSet> mergeSuperSet,
- AccountsCollection accounts,
+ AccountResolver accountResolver,
@GerritServerConfig Config cfg,
Provider<InternalChangeQuery> queryProvider,
PatchSetUtil psUtil,
@@ -147,7 +147,7 @@
this.changeNotesFactory = changeNotesFactory;
this.mergeOpProvider = mergeOpProvider;
this.mergeSuperSet = mergeSuperSet;
- this.accounts = accounts;
+ this.accountResolver = accountResolver;
this.label =
MoreObjects.firstNonNull(
Strings.emptyToNull(cfg.getString("change", null, "submitLabel")), "Submit");
@@ -472,7 +472,7 @@
perm.check(ChangePermission.SUBMIT_AS);
CurrentUser caller = rsrc.getUser();
- IdentifiedUser submitter = accounts.parseOnBehalfOf(caller, in.onBehalfOf);
+ IdentifiedUser submitter = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
try {
permissionBackend
.user(submitter)
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index 4ced4c2..4ca986b 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -67,14 +67,11 @@
private final Provider<MergeSuperSet> mergeSuperSet;
private final Provider<WalkSorter> sorter;
- private boolean lazyLoad = false;
-
@Option(name = "-o", usage = "Output options")
void addOption(String option) {
for (ListChangesOption o : ListChangesOption.values()) {
if (o.name().equalsIgnoreCase(option)) {
jsonOpt.add(o);
- lazyLoad |= ChangeJson.REQUIRE_LAZY_LOAD.contains(o);
return;
}
}
@@ -150,7 +147,7 @@
cds = sort(cds, hidden);
SubmittedTogetherInfo info = new SubmittedTogetherInfo();
- info.changes = json.create(jsonOpt).lazyLoad(lazyLoad).formatChangeDatas(cds);
+ info.changes = json.create(jsonOpt).formatChangeDatas(cds);
info.nonVisibleChanges = hidden;
return info;
} catch (OrmException | IOException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index b1d49f2..6e94218 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -19,6 +19,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.config.ConfigKey;
import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -98,12 +99,12 @@
this.suggestAccounts = (av != AccountVisibility.NONE);
}
- this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed", PostReviewers.DEFAULT_MAX_REVIEWERS);
+ this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed", ReviewerAdder.DEFAULT_MAX_REVIEWERS);
this.maxAllowedWithoutConfirmation =
cfg.getInt(
"addreviewer",
"maxWithoutConfirmation",
- PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+ ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
logger.atFine().log("AccountVisibility: %s", av.name());
}
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index a897e1a..bdf1c74 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -47,7 +47,6 @@
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.account.AccountsCollection;
import com.google.gerrit.server.restapi.group.AddMembers.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -90,7 +89,6 @@
private final AccountManager accountManager;
private final AuthType authType;
- private final AccountsCollection accounts;
private final AccountResolver accountResolver;
private final AccountCache accountCache;
private final AccountLoader.Factory infoFactory;
@@ -100,14 +98,12 @@
AddMembers(
AccountManager accountManager,
AuthConfig authConfig,
- AccountsCollection accounts,
AccountResolver accountResolver,
AccountCache accountCache,
AccountLoader.Factory infoFactory,
@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
this.accountManager = accountManager;
this.authType = authConfig.getAuthType();
- this.accounts = accounts;
this.accountResolver = accountResolver;
this.accountCache = accountCache;
this.infoFactory = infoFactory;
@@ -151,7 +147,7 @@
throws AuthException, UnprocessableEntityException, OrmException, IOException,
ConfigInvalidException {
try {
- return accounts.parse(nameOrEmailOrId).getAccount();
+ return accountResolver.parse(nameOrEmailOrId).getAccount();
} catch (UnprocessableEntityException e) {
// might be because the account does not exist or because the account is
// not visible
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index ca77ebf..9782ad3 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -32,6 +32,7 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.UserInitiated;
import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.GroupResource;
import com.google.gerrit.server.group.SubgroupResource;
import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -76,16 +77,16 @@
}
}
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private final Provider<GroupsUpdate> groupsUpdateProvider;
private final GroupJson json;
@Inject
public AddSubgroups(
- GroupsCollection groupsCollection,
+ GroupResolver groupResolver,
@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
GroupJson json) {
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
this.groupsUpdateProvider = groupsUpdateProvider;
this.json = json;
}
@@ -107,7 +108,7 @@
List<GroupInfo> result = new ArrayList<>();
Set<AccountGroup.UUID> subgroupUuids = new LinkedHashSet<>();
for (String subgroupIdentifier : input.groups) {
- GroupDescription.Basic subgroup = groupsCollection.parse(subgroupIdentifier);
+ GroupDescription.Basic subgroup = groupResolver.parse(subgroupIdentifier);
subgroupUuids.add(subgroup.getGroupUUID());
result.add(json.format(subgroup));
}
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index 6b01cda..0572114 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -42,6 +42,7 @@
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupUUID;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.GroupResource;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.InternalGroupDescription;
@@ -77,7 +78,7 @@
private final PersonIdent serverIdent;
private final Provider<GroupsUpdate> groupsUpdateProvider;
private final GroupCache groupCache;
- private final GroupsCollection groups;
+ private final GroupResolver groups;
private final GroupJson json;
private final PluginSetContext<GroupCreationValidationListener> groupCreationValidationListeners;
private final AddMembers addMembers;
@@ -91,7 +92,7 @@
@GerritPersonIdent PersonIdent serverIdent,
@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
GroupCache groupCache,
- GroupsCollection groups,
+ GroupResolver groups,
GroupJson json,
PluginSetContext<GroupCreationValidationListener> groupCreationValidationListeners,
AddMembers addMembers,
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index bcacb65..d197cb8 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -26,12 +26,12 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.group.GroupResource;
import com.google.gerrit.server.group.MemberResource;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gerrit.server.restapi.account.AccountsCollection;
import com.google.gerrit.server.restapi.group.AddMembers.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -44,13 +44,13 @@
@Singleton
public class DeleteMembers implements RestModifyView<GroupResource, Input> {
- private final AccountsCollection accounts;
+ private final AccountResolver accountResolver;
private final Provider<GroupsUpdate> groupsUpdateProvider;
@Inject
DeleteMembers(
- AccountsCollection accounts, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
- this.accounts = accounts;
+ AccountResolver accountResolver, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
+ this.accountResolver = accountResolver;
this.groupsUpdateProvider = groupsUpdateProvider;
}
@@ -69,7 +69,7 @@
Set<Account.Id> membersToRemove = new HashSet<>();
for (String nameOrEmail : input.members) {
- Account a = accounts.parse(nameOrEmail).getAccount();
+ Account a = accountResolver.parse(nameOrEmail).getAccount();
membersToRemove.add(a.getId());
}
AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index 934698b..c486af4 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -27,6 +27,7 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.UserInitiated;
import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.GroupResource;
import com.google.gerrit.server.group.SubgroupResource;
import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -43,14 +44,13 @@
@Singleton
public class DeleteSubgroups implements RestModifyView<GroupResource, Input> {
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private final Provider<GroupsUpdate> groupsUpdateProvider;
@Inject
DeleteSubgroups(
- GroupsCollection groupsCollection,
- @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
- this.groupsCollection = groupsCollection;
+ GroupResolver groupResolver, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
+ this.groupResolver = groupResolver;
this.groupsUpdateProvider = groupsUpdateProvider;
}
@@ -70,7 +70,7 @@
Set<AccountGroup.UUID> subgroupsToRemove = new HashSet<>();
for (String subgroupIdentifier : input.groups) {
- GroupDescription.Basic subgroup = groupsCollection.parse(subgroupIdentifier);
+ GroupDescription.Basic subgroup = groupResolver.parse(subgroupIdentifier);
subgroupsToRemove.add(subgroup.getGroupUUID());
}
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
index 40a11c7..52fe9d0 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -16,7 +16,6 @@
import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -26,20 +25,13 @@
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupBackends;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.GroupResource;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.group.InternalGroupDescription;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.Optional;
public class GroupsCollection
implements RestCollection<TopLevelResource, GroupResource>, NeedsParams {
@@ -47,8 +39,7 @@
private final Provider<ListGroups> list;
private final Provider<QueryGroups> queryGroups;
private final GroupControl.Factory groupControlFactory;
- private final GroupBackend groupBackend;
- private final GroupCache groupCache;
+ private final GroupResolver groupResolver;
private final Provider<CurrentUser> self;
private boolean hasQuery2;
@@ -59,15 +50,13 @@
Provider<ListGroups> list,
Provider<QueryGroups> queryGroups,
GroupControl.Factory groupControlFactory,
- GroupBackend groupBackend,
- GroupCache groupCache,
+ GroupResolver groupResolver,
Provider<CurrentUser> self) {
this.views = views;
this.list = list;
this.queryGroups = queryGroups;
this.groupControlFactory = groupControlFactory;
- this.groupBackend = groupBackend;
- this.groupCache = groupCache;
+ this.groupResolver = groupResolver;
this.self = self;
}
@@ -107,7 +96,7 @@
throw new ResourceNotFoundException(id);
}
- GroupDescription.Basic group = parseId(id.get());
+ GroupDescription.Basic group = groupResolver.parseId(id.get());
if (group == null) {
throw new ResourceNotFoundException(id.get());
}
@@ -118,80 +107,6 @@
return new GroupResource(ctl);
}
- /**
- * Parses a group ID from a request body and returns the group.
- *
- * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
- * @return the group
- * @throws UnprocessableEntityException thrown if the group ID cannot be resolved or if the group
- * is not visible to the calling user
- */
- public GroupDescription.Basic parse(String id) throws UnprocessableEntityException {
- GroupDescription.Basic group = parseId(id);
- if (group == null || !groupControlFactory.controlFor(group).isVisible()) {
- throw new UnprocessableEntityException(String.format("Group Not Found: %s", id));
- }
- return group;
- }
-
- /**
- * Parses a group ID from a request body and returns the group if it is a Gerrit internal group.
- *
- * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
- * @return the group
- * @throws UnprocessableEntityException thrown if the group ID cannot be resolved, if the group is
- * not visible to the calling user or if it's an external group
- */
- public GroupDescription.Internal parseInternal(String id) throws UnprocessableEntityException {
- GroupDescription.Basic group = parse(id);
- if (group instanceof GroupDescription.Internal) {
- return (GroupDescription.Internal) group;
- }
-
- throw new UnprocessableEntityException(String.format("External Group Not Allowed: %s", id));
- }
-
- /**
- * Parses a group ID and returns the group without making any permission check whether the current
- * user can see the group.
- *
- * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
- * @return the group, null if no group is found for the given group ID
- */
- public GroupDescription.Basic parseId(String id) {
- AccountGroup.UUID uuid = new AccountGroup.UUID(id);
- if (groupBackend.handles(uuid)) {
- GroupDescription.Basic d = groupBackend.get(uuid);
- if (d != null) {
- return d;
- }
- }
-
- // Might be a numeric AccountGroup.Id. -> Internal group.
- if (id.matches("^[1-9][0-9]*$")) {
- try {
- AccountGroup.Id groupId = AccountGroup.Id.parse(id);
- Optional<InternalGroup> group = groupCache.get(groupId);
- if (group.isPresent()) {
- return new InternalGroupDescription(group.get());
- }
- } catch (IllegalArgumentException e) {
- // Ignored
- }
- }
-
- // Might be a group name, be nice and accept unique names.
- GroupReference ref = GroupBackends.findExactSuggestion(groupBackend, id);
- if (ref != null) {
- GroupDescription.Basic d = groupBackend.get(ref.getUUID());
- if (d != null) {
- return d;
- }
- }
-
- return null;
- }
-
@Override
public DynamicMap<RestView<GroupResource>> views() {
return views;
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index bae5eff..968a7dd 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -39,6 +39,7 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.InternalGroupDescription;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -82,7 +83,7 @@
private final GroupJson json;
private final GroupBackend groupBackend;
private final Groups groups;
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private EnumSet<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class);
private boolean visibleToAll;
@@ -217,7 +218,7 @@
final Provider<IdentifiedUser> identifiedUser,
final IdentifiedUser.GenericFactory userFactory,
final GetGroups accountGetGroups,
- final GroupsCollection groupsCollection,
+ final GroupResolver groupResolver,
GroupJson json,
GroupBackend groupBackend,
Groups groups) {
@@ -230,7 +231,7 @@
this.json = json;
this.groupBackend = groupBackend;
this.groups = groups;
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
}
public void setOptions(EnumSet<ListGroupsOption> options) {
@@ -403,7 +404,7 @@
private List<GroupInfo> getGroupsOwnedBy(String id)
throws OrmException, RestApiException, IOException, ConfigInvalidException,
PermissionBackendException {
- String uuid = groupsCollection.parse(id).getGroupUUID().get();
+ String uuid = groupResolver.parse(id).getGroupUUID().get();
return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
}
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index 56f856d..6ebec05 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.group.GroupResource;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
@@ -39,16 +40,16 @@
@Singleton
public class PutOwner implements RestModifyView<GroupResource, OwnerInput> {
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private final Provider<GroupsUpdate> groupsUpdateProvider;
private final GroupJson json;
@Inject
PutOwner(
- GroupsCollection groupsCollection,
+ GroupResolver groupResolver,
@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
GroupJson json) {
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
this.groupsUpdateProvider = groupsUpdateProvider;
this.json = json;
}
@@ -68,7 +69,7 @@
throw new BadRequestException("owner is required");
}
- GroupDescription.Basic owner = groupsCollection.parse(input.owner);
+ GroupDescription.Basic owner = groupResolver.parse(input.owner);
if (!internalGroup.getOwnerGroupUUID().equals(owner.getGroupUUID())) {
AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
InternalGroupUpdate groupUpdate =
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 79ff3c9..09f8ab7 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -57,6 +57,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.RepositoryCaseMismatchException;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.plugincontext.PluginItemContext;
import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -67,7 +68,6 @@
import com.google.gerrit.server.project.ProjectNameLockManager;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
@@ -97,7 +97,7 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Provider<ProjectsCollection> projectsCollection;
- private final Provider<GroupsCollection> groupsCollection;
+ private final Provider<GroupResolver> groupResolver;
private final PluginSetContext<ProjectCreationValidationListener>
projectCreationValidationListeners;
private final ProjectJson json;
@@ -119,7 +119,7 @@
@Inject
CreateProject(
Provider<ProjectsCollection> projectsCollection,
- Provider<GroupsCollection> groupsCollection,
+ Provider<GroupResolver> groupResolver,
ProjectJson json,
PluginSetContext<ProjectCreationValidationListener> projectCreationValidationListeners,
GitRepositoryManager repoManager,
@@ -137,7 +137,7 @@
AllUsersName allUsers,
PluginItemContext<ProjectNameLockManager> lockManager) {
this.projectsCollection = projectsCollection;
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
this.projectCreationValidationListeners = projectCreationValidationListeners;
this.json = json;
this.repoManager = repoManager;
@@ -187,7 +187,7 @@
} else {
args.ownerIds = Lists.newArrayListWithCapacity(input.owners.size());
for (String owner : input.owners) {
- args.ownerIds.add(groupsCollection.get().parse(owner).getGroupUUID());
+ args.ownerIds.add(groupResolver.get().parse(owner).getGroupUUID());
}
}
args.contributorAgreements =
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 8c1b0b3..1fab368 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -42,6 +42,7 @@
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.ioutil.RegexListSearcher;
import com.google.gerrit.server.ioutil.StringUtil;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -50,7 +51,6 @@
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.gerrit.server.util.TreeFormatter;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
@@ -141,7 +141,7 @@
private final CurrentUser currentUser;
private final ProjectCache projectCache;
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private final GroupControl.Factory groupControlFactory;
private final GitRepositoryManager repoManager;
private final PermissionBackend permissionBackend;
@@ -262,7 +262,7 @@
protected ListProjects(
CurrentUser currentUser,
ProjectCache projectCache,
- GroupsCollection groupsCollection,
+ GroupResolver groupResolver,
GroupControl.Factory groupControlFactory,
GitRepositoryManager repoManager,
PermissionBackend permissionBackend,
@@ -270,7 +270,7 @@
WebLinks webLinks) {
this.currentUser = currentUser;
this.projectCache = projectCache;
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
this.groupControlFactory = groupControlFactory;
this.repoManager = repoManager;
this.permissionBackend = permissionBackend;
@@ -367,7 +367,7 @@
if (groupUuid != null
&& !e.getLocalGroups()
- .contains(GroupReference.forGroup(groupsCollection.parseId(groupUuid.get())))) {
+ .contains(GroupReference.forGroup(groupResolver.parseId(groupUuid.get())))) {
continue;
}
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index c5c8cf0..c8857a2 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -32,11 +32,11 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.group.GroupResolver;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.RefPattern;
import com.google.gerrit.server.restapi.config.ListCapabilities;
-import com.google.gerrit.server.restapi.group.GroupsCollection;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -48,18 +48,18 @@
@Singleton
public class SetAccessUtil {
- private final GroupsCollection groupsCollection;
+ private final GroupResolver groupResolver;
private final AllProjectsName allProjects;
private final Provider<SetParent> setParent;
private final ListCapabilities listCapabilities;
@Inject
private SetAccessUtil(
- GroupsCollection groupsCollection,
+ GroupResolver groupResolver,
AllProjectsName allProjects,
Provider<SetParent> setParent,
ListCapabilities listCapabilities) {
- this.groupsCollection = groupsCollection;
+ this.groupResolver = groupResolver;
this.allProjects = allProjects;
this.setParent = setParent;
this.listCapabilities = listCapabilities;
@@ -91,7 +91,7 @@
for (Map.Entry<String, PermissionRuleInfo> permissionRuleInfoEntry :
permissionEntry.getValue().rules.entrySet()) {
- GroupDescription.Basic group = groupsCollection.parseId(permissionRuleInfoEntry.getKey());
+ GroupDescription.Basic group = groupResolver.parseId(permissionRuleInfoEntry.getKey());
if (group == null) {
throw new UnprocessableEntityException(
permissionRuleInfoEntry.getKey() + " is not a valid group ID");
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index e0b10f0..07ac532 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
@@ -90,7 +91,7 @@
@Override
protected void updateRepoImpl(RepoContext ctx)
- throws IntegrationException, IOException, OrmException {
+ throws IntegrationException, IOException, OrmException, MethodNotAllowedException {
// If there is only one parent, a cherry-pick can be done by taking the
// delta relative to that one parent and redoing that on the current merge
// tip.
@@ -116,6 +117,7 @@
cherryPickCmtMsg,
args.rw,
0,
+ false,
false);
} catch (MergeConflictException mce) {
// Keep going in the case of a single merge failure; the goal is to
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index fa0fdde..cf3a44e 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -156,7 +156,8 @@
cherryPickCmtMsg,
args.rw,
0,
- true);
+ true,
+ false);
} catch (MergeConflictException mce) {
// Unlike in Cherry-pick case, this should never happen.
toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
diff --git a/java/com/google/gerrit/sshd/AbstractGitCommand.java b/java/com/google/gerrit/sshd/AbstractGitCommand.java
index 710b3dc..30b6408 100644
--- a/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -32,8 +32,6 @@
@Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
protected ProjectState projectState;
- @Inject private SshScope sshScope;
-
@Inject private GitRepositoryManager repoManager;
@Inject private SshSession session;
@@ -49,28 +47,24 @@
@Override
public void start(Environment env) {
Context ctx = context.subContext(newSession(), context.getCommandLine());
- final Context old = sshScope.set(ctx);
- try {
- startThread(
- new ProjectCommandRunnable() {
- @Override
- public void executeParseCommand() throws Exception {
- parseCommandLine();
- }
+ startThreadWithContext(
+ ctx,
+ new ProjectCommandRunnable() {
+ @Override
+ public void executeParseCommand() throws Exception {
+ parseCommandLine();
+ }
- @Override
- public void run() throws Exception {
- AbstractGitCommand.this.service();
- }
+ @Override
+ public void run() throws Exception {
+ AbstractGitCommand.this.service();
+ }
- @Override
- public Project.NameKey getProjectName() {
- return projectState.getNameKey();
- }
- });
- } finally {
- sshScope.set(old);
- }
+ @Override
+ public Project.NameKey getProjectName() {
+ return projectState.getNameKey();
+ }
+ });
}
private SshSession newSession() {
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 1d19a8a..3d5a440 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -50,6 +50,7 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
+import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicReference;
@@ -261,6 +262,38 @@
}
/**
+ * Spawn a function into its own thread with the provided context.
+ *
+ * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
+ *
+ * <pre>
+ * startThreadWithContext(SshScope.Context context, new CommandRunnable() {
+ * public void run() throws Exception {
+ * runImp();
+ * }
+ * });
+ * </pre>
+ *
+ * <p>If the function throws an exception, it is translated to a simple message for the client, a
+ * non-zero exit code, and the stack trace is logged.
+ *
+ * @param thunk the runnable to execute on the thread, performing the command's logic.
+ */
+ protected void startThreadWithContext(SshScope.Context context, CommandRunnable thunk) {
+ final TaskThunk tt = new TaskThunk(thunk, Optional.ofNullable(context));
+
+ if (isAdminHighPriorityCommand()) {
+ // Admin commands should not block the main work threads (there
+ // might be an interactive shell there), nor should they wait
+ // for the main work threads.
+ //
+ new Thread(tt, tt.toString()).start();
+ } else {
+ task.set(executor.submit(tt));
+ }
+ }
+
+ /**
* Spawn a function into its own thread.
*
* <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
@@ -278,18 +311,8 @@
*
* @param thunk the runnable to execute on the thread, performing the command's logic.
*/
- protected void startThread(CommandRunnable thunk) {
- final TaskThunk tt = new TaskThunk(thunk);
-
- if (isAdminHighPriorityCommand()) {
- // Admin commands should not block the main work threads (there
- // might be an interactive shell there), nor should they wait
- // for the main work threads.
- //
- new Thread(tt, tt.toString()).start();
- } else {
- task.set(executor.submit(tt));
- }
+ protected void startThread(final CommandRunnable thunk) {
+ startThreadWithContext(null, thunk);
}
private boolean isAdminHighPriorityCommand() {
@@ -416,18 +439,21 @@
private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
private final CommandRunnable thunk;
+ private final Context taskContext;
private final String taskName;
+
private Project.NameKey projectName;
- private TaskThunk(CommandRunnable thunk) {
+ private TaskThunk(CommandRunnable thunk, Optional<Context> oneOffContext) {
this.thunk = thunk;
this.taskName = getTaskName();
+ this.taskContext = oneOffContext.orElse(context);
}
@Override
public void cancel() {
synchronized (this) {
- final Context old = sshScope.set(context);
+ final Context old = sshScope.set(taskContext);
try {
onExit(STATUS_CANCEL);
} finally {
@@ -442,7 +468,7 @@
final Thread thisThread = Thread.currentThread();
final String thisName = thisThread.getName();
int rc = 0;
- final Context old = sshScope.set(context);
+ final Context old = sshScope.set(taskContext);
try {
context.started = TimeUtil.nowMs();
thisThread.setName("SSH " + taskName);
diff --git a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 1e177a1..8c9fc9f 100644
--- a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -123,7 +123,7 @@
name = "--project-state",
aliases = {"--ps"},
usage = "project's visibility state")
- private ProjectState state;
+ private com.google.gerrit.extensions.client.ProjectState state;
@Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
private String maxObjectSizeLimit;
@@ -138,7 +138,7 @@
configInput.useContentMerge = contentMerge;
configInput.useContributorAgreements = contributorAgreements;
configInput.useSignedOffBy = signedOffBy;
- configInput.state = state.getProject().getState();
+ configInput.state = state;
configInput.maxObjectSizeLimit = maxObjectSizeLimit;
// Description is different to other parameters, null won't result in
// keeping the existing description, it would delete it.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 63d3248..57f1be1 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -2190,6 +2190,58 @@
}
@Test
+ public void removeReviewerSelfFromMergedChangeNotPermitted() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ setApiUser(user);
+ recommend(changeId);
+
+ setApiUser(admin);
+ approve(changeId);
+ gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
+
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("remove reviewer not permitted");
+ gApi.changes().id(r.getChangeId()).reviewer("self").remove();
+ }
+
+ @Test
+ public void removeReviewerSelfFromAbandonedChangePermitted() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ setApiUser(user);
+ recommend(changeId);
+
+ setApiUser(admin);
+ gApi.changes().id(changeId).abandon();
+
+ setApiUser(user);
+ gApi.changes().id(r.getChangeId()).reviewer("self").remove();
+ eventRecorder.assertReviewerDeletedEvents(changeId, user.email);
+ }
+
+ @Test
+ public void removeOtherReviewerFromAbandonedChangeNotPermitted() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ setApiUser(user);
+ recommend(changeId);
+
+ setApiUser(admin);
+ approve(changeId);
+ gApi.changes().id(changeId).abandon();
+
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("remove reviewer not permitted");
+ gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove();
+ }
+
+ @Test
public void deleteVote() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index edbfe4f..505c165 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -59,6 +59,7 @@
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.FileInfo;
@@ -317,7 +318,9 @@
ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
assertThat(orig.get().messages).hasSize(1);
- ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
+ CherryPickChangeInfo changeInfo = orig.revision(r.getCommit().name()).cherryPickAsInfo(in);
+ assertThat(changeInfo.containsGitConflicts).isNull();
+ ChangeApi cherry = gApi.changes().id(changeInfo._number);
Collection<ChangeMessageInfo> messages =
gApi.changes().id(project.get() + "~master~" + r.getChangeId()).get().messages;
@@ -368,7 +371,7 @@
}
@Test
- public void cherryPickwithNoTopic() throws Exception {
+ public void cherryPickWithNoTopic() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master");
CherryPickInput in = new CherryPickInput();
in.destination = "foo";
@@ -495,6 +498,116 @@
}
@Test
+ public void cherryPickConflictWithAllowConflicts() throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+ // Create a branch and push a commit to it (by-passing review)
+ String destBranch = "foo";
+ gApi.projects().name(project.get()).branch(destBranch).create(new BranchInput());
+ String destContent = "some content";
+ PushOneCommit push =
+ pushFactory.create(
+ db,
+ admin.getIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ ImmutableMap.of(PushOneCommit.FILE_NAME, destContent, "foo.txt", "foo"));
+ push.to("refs/heads/" + destBranch);
+
+ // Create a change on master with a commit that conflicts with the commit on the other branch.
+ testRepo.reset(initial);
+ String changeContent = "another content";
+ push =
+ pushFactory.create(
+ db,
+ admin.getIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ ImmutableMap.of(PushOneCommit.FILE_NAME, changeContent, "bar.txt", "bar"));
+ PushOneCommit.Result r = push.to("refs/for/master%topic=someTopic");
+
+ // Verify before the cherry-pick that the change has exactly 1 message.
+ ChangeApi changeApi = gApi.changes().id(r.getChange().getId().get());
+ assertThat(changeApi.get().messages).hasSize(1);
+
+ // Cherry-pick the change to the other branch, that should fail with a conflict.
+ CherryPickInput in = new CherryPickInput();
+ in.destination = destBranch;
+ in.message = "Cherry-Pick";
+ try {
+ changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in);
+ fail("expected ResourceConflictException");
+ } catch (ResourceConflictException e) {
+ assertThat(e.getMessage()).isEqualTo("Cherry pick failed: merge conflict");
+ }
+
+ // Cherry-pick with auto merge should succeed.
+ in.allowConflicts = true;
+ CherryPickChangeInfo cherryPickChange =
+ changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in);
+ assertThat(cherryPickChange.containsGitConflicts).isTrue();
+
+ // Verify that subject and topic on the cherry-pick change have been correctly populated.
+ assertThat(cherryPickChange.subject).contains(in.message);
+ assertThat(cherryPickChange.topic).isEqualTo("someTopic-" + destBranch);
+
+ // Verify that the file content in the cherry-pick change is correct.
+ // We expect that it has conflict markers to indicate the conflict.
+ BinaryResult bin =
+ gApi.changes()
+ .id(cherryPickChange._number)
+ .current()
+ .file(PushOneCommit.FILE_NAME)
+ .content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ String fileContent = new String(os.toByteArray(), UTF_8);
+ String destSha1 = getRemoteHead(project, destBranch).abbreviate(6).name();
+ String changeSha1 = r.getCommit().abbreviate(6).name();
+ assertThat(fileContent)
+ .isEqualTo(
+ "<<<<<<< HEAD ("
+ + destSha1
+ + " test commit)\n"
+ + destContent
+ + "\n"
+ + "=======\n"
+ + changeContent
+ + "\n"
+ + ">>>>>>> CHANGE ("
+ + changeSha1
+ + " test commit)\n");
+
+ // Get details of cherry-pick change.
+ ChangeInfo cherryPickChangeWithDetails = gApi.changes().id(cherryPickChange._number).get();
+
+ // Verify that a message has been posted on the original change.
+ String cherryPickedRevision = cherryPickChangeWithDetails.currentRevision;
+ changeApi = gApi.changes().id(r.getChange().getId().get());
+ Collection<ChangeMessageInfo> messages = changeApi.get().messages;
+ assertThat(messages).hasSize(2);
+ Iterator<ChangeMessageInfo> origIt = messages.iterator();
+ origIt.next();
+ assertThat(origIt.next().message)
+ .isEqualTo(
+ String.format(
+ "Patch Set 1: Cherry Picked\n\n"
+ + "This patchset was cherry picked to branch %s as commit %s",
+ in.destination, cherryPickedRevision));
+
+ // Verify that a message has been posted on the cherry-pick change.
+ assertThat(cherryPickChangeWithDetails.messages).hasSize(1);
+ Iterator<ChangeMessageInfo> cherryIt = cherryPickChangeWithDetails.messages.iterator();
+ assertThat(cherryIt.next().message)
+ .isEqualTo(
+ "Patch Set 1: Cherry Picked from branch master.\n\n"
+ + "The following files contain Git conflicts:\n"
+ + "* "
+ + PushOneCommit.FILE_NAME
+ + "\n");
+ }
+
+ @Test
public void cherryPickToExistingChange() throws Exception {
PushOneCommit.Result r1 =
pushFactory
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 8891dee..488eb9f 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.git;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
@@ -64,6 +65,7 @@
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
@@ -945,6 +947,55 @@
assertThat(cr.all.get(0).value).isEqualTo(2);
}
+ @Test
+ public void pushForMasterWithForgedAuthorAndCommitter() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ // Create a commit with different forged author and committer.
+ RevCommit c =
+ commitBuilder()
+ .author(user.getIdent())
+ .committer(user2.getIdent())
+ .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+ .message(PushOneCommit.SUBJECT)
+ .create();
+ // Push commit as "Admnistrator".
+ pushHead(testRepo, "refs/for/master");
+
+ String changeId = GitUtil.getChangeId(testRepo, c).get();
+ assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email);
+ assertThat(getReviewerEmails(changeId, ReviewerState.REVIEWER))
+ .containsExactly(user.email, user2.email);
+
+ assertThat(sender.getMessages()).hasSize(1);
+ assertThat(sender.getMessages().get(0).rcpt())
+ .containsExactly(user.emailAddress, user2.emailAddress);
+ }
+
+ @Test
+ public void pushNewPatchSetForMasterWithForgedAuthorAndCommitter() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ // First patch set has author and committer matching change owner.
+ PushOneCommit.Result r = pushTo("refs/for/master");
+
+ assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email);
+ assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)).isEmpty();
+
+ amendBuilder()
+ .author(user.getIdent())
+ .committer(user2.getIdent())
+ .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2")
+ .create();
+ pushHead(testRepo, "refs/for/master");
+
+ assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email);
+ assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER))
+ .containsExactly(user.email, user2.email);
+
+ assertThat(sender.getMessages()).hasSize(1);
+ assertThat(sender.getMessages().get(0).rcpt())
+ .containsExactly(user.emailAddress, user2.emailAddress);
+ }
+
/**
* There was a bug that allowed a user with Forge Committer Identity access right to upload a
* commit and put *votes on behalf of another user* on it. This test checks that this is not
@@ -2336,4 +2387,17 @@
private PushOneCommit.Result amendChange(String changeId, String ref) throws Exception {
return amendChange(changeId, ref, admin, testRepo);
}
+
+ private String getOwnerEmail(String changeId) throws Exception {
+ return get(changeId, DETAILED_ACCOUNTS).owner.email;
+ }
+
+ private ImmutableList<String> getReviewerEmails(String changeId, ReviewerState state)
+ throws Exception {
+ Collection<AccountInfo> infos =
+ get(changeId, DETAILED_LABELS, DETAILED_ACCOUNTS).reviewers.get(state);
+ return infos != null
+ ? infos.stream().map(a -> a.email).collect(toImmutableList())
+ : ImmutableList.of();
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java b/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
new file mode 100644
index 0000000..a13c8c8
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/GitmodulesIT.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 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.git;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Test;
+
+public class GitmodulesIT extends AbstractDaemonTest {
+ @Test
+ public void invalidSubmoduleURLIsRejected() throws Exception {
+ pushGitmodules("name", "-invalid-url", "path", "Invalid submodule URL");
+ }
+
+ @Test
+ public void invalidSubmodulePathIsRejected() throws Exception {
+ pushGitmodules("name", "http://somewhere", "-invalid-path", "Invalid submodule path");
+ }
+
+ @Test
+ public void invalidSubmoduleNameIsRejected() throws Exception {
+ pushGitmodules("-invalid-name", "http://somewhere", "path", "Invalid submodule name");
+ }
+
+ private void pushGitmodules(String name, String url, String path, String expectedErrorMessage)
+ throws Exception {
+ Config config = new Config();
+ config.setString("submodule", name, "url", url);
+ config.setString("submodule", name, "path", path);
+ TestRepository<?> repo = cloneProject(project);
+ repo.branch("HEAD")
+ .commit()
+ .insertChangeId()
+ .message("subject: adding new subscription")
+ .add(".gitmodules", config.toText().toString())
+ .create();
+
+ exception.expectMessage(expectedErrorMessage);
+ exception.expect(TransportException.class);
+ repo.git().push().setRemote("origin").setRefSpecs(new RefSpec("HEAD:refs/for/master")).call();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 0661466..0f394aa7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -35,7 +35,7 @@
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.RevisionJson;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
@@ -54,7 +54,7 @@
return submitWholeTopicEnabledConfig();
}
- @Inject private ChangeJson.Factory changeJsonFactory;
+ @Inject private RevisionJson.Factory revisionJsonFactory;
@Inject private DynamicSet<ActionVisitor> actionVisitors;
@@ -394,7 +394,7 @@
// ...via ChangeJson directly.
ChangeData cd = changeDataFactory.create(db, project, changeId);
- changeJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(new PatchSet.Id(changeId, 1)));
+ revisionJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(new PatchSet.Id(changeId, 1)));
}
private void visitedCurrentRevisionActionsAssertions(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 06b67d8..257c88b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -27,6 +27,7 @@
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ReviewerState;
@@ -319,6 +320,42 @@
}
}
+ @Test
+ public void addExistingReviewerByEmailShortCircuits() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = "nonexisting@example.com";
+ input.state = ReviewerState.REVIEWER;
+
+ AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
+ assertThat(result.reviewers).hasSize(1);
+ ReviewerInfo info = result.reviewers.get(0);
+ assertThat(info._accountId).isNull();
+ assertThat(info.email).isEqualTo(input.reviewer);
+
+ assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).reviewers).isEmpty();
+ }
+
+ @Test
+ public void addExistingCcByEmailShortCircuits() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = "nonexisting@example.com";
+ input.state = ReviewerState.CC;
+ AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+ assertThat(result.ccs).hasSize(1);
+ AccountInfo info = result.ccs.get(0);
+ assertThat(info._accountId).isNull();
+ assertThat(info.email).isEqualTo(input.reviewer);
+
+ assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
+ }
+
private static String toRfcAddressString(AccountInfo info) {
return (new Address(info.name, info.email)).toString();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 2259999..d1f4d84 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -39,6 +39,7 @@
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -48,7 +49,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.restapi.change.PostReviewers;
+import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gson.stream.JsonReader;
import java.util.ArrayList;
@@ -68,8 +69,8 @@
String largeGroup = createGroup("largeGroup");
String mediumGroup = createGroup("mediumGroup");
- int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
- int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+ int largeGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS + 1;
+ int mediumGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
List<TestAccount> users = createAccounts(largeGroupSize, "addGroupAsReviewer");
List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
for (TestAccount u : users) {
@@ -470,8 +471,8 @@
@Test
public void reviewAndAddGroupReviewers() throws Exception {
- int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
- int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+ int largeGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS + 1;
+ int mediumGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
List<TestAccount> users = createAccounts(largeGroupSize, "reviewAndAddGroupReviewers");
List<String> usernames = new ArrayList<>(largeGroupSize);
for (TestAccount u : users) {
@@ -784,6 +785,40 @@
gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
}
+ @Test
+ public void addExistingReviewerShortCircuits() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = user.email;
+ input.state = ReviewerState.REVIEWER;
+
+ AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
+ assertThat(result.reviewers).hasSize(1);
+ ReviewerInfo info = result.reviewers.get(0);
+ assertThat(info._accountId).isEqualTo(user.id.get());
+
+ assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).reviewers).isEmpty();
+ }
+
+ @Test
+ public void addExistingCcShortCircuits() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = user.email;
+ input.state = ReviewerState.CC;
+
+ AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
+ assertThat(result.ccs).hasSize(1);
+ AccountInfo info = result.ccs.get(0);
+ assertThat(info._accountId).isEqualTo(user.id.get());
+
+ assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
+ }
+
private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
AccountInfo userInfo = new AccountInfo(user.fullName, user.emailAddress.getEmail());
userInfo._accountId = user.id.get();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 9645a94..2daccc0 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -566,15 +566,64 @@
addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(batch());
}
+ private void addNonUserReviewerByEmailInNoteDb(Adder adder) throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ StagedChange sc = stageReviewableChange();
+ addReviewer(adder, sc.changeId, sc.owner, "nonexistent@example.com");
+ assertThat(sender)
+ .sent("newchange", sc)
+ .to("nonexistent@example.com")
+ .cc(sc.reviewer)
+ .cc(sc.ccerByEmail, sc.reviewerByEmail)
+ .noOneElse();
+ }
+
+ @Test
+ public void addNonUserReviewerByEmailInNoteDbSingly() throws Exception {
+ addNonUserReviewerByEmailInNoteDb(singly(ReviewerState.REVIEWER));
+ }
+
+ @Test
+ public void addNonUserReviewerByEmailInNoteDbBatch() throws Exception {
+ addNonUserReviewerByEmailInNoteDb(batch(ReviewerState.REVIEWER));
+ }
+
+ private void addNonUserCcByEmailInNoteDb(Adder adder) throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ StagedChange sc = stageReviewableChange();
+ addReviewer(adder, sc.changeId, sc.owner, "nonexistent@example.com");
+ assertThat(sender)
+ .sent("newchange", sc)
+ .cc("nonexistent@example.com")
+ .cc(sc.reviewer)
+ .cc(sc.ccerByEmail, sc.reviewerByEmail)
+ .noOneElse();
+ }
+
+ @Test
+ public void addNonUserCcByEmailInNoteDbSingly() throws Exception {
+ addNonUserCcByEmailInNoteDb(singly(ReviewerState.CC));
+ }
+
+ @Test
+ public void addNonUserCcByEmailInNoteDbBatch() throws Exception {
+ addNonUserCcByEmailInNoteDb(batch(ReviewerState.CC));
+ }
+
private interface Adder {
void addReviewer(String changeId, String reviewer, @Nullable NotifyHandling notify)
throws Exception;
}
private Adder singly() {
+ return singly(ReviewerState.REVIEWER);
+ }
+
+ private Adder singly(ReviewerState reviewerState) {
return (String changeId, String reviewer, @Nullable NotifyHandling notify) -> {
AddReviewerInput in = new AddReviewerInput();
in.reviewer = reviewer;
+ in.state = reviewerState;
if (notify != null) {
in.notify = notify;
}
@@ -583,9 +632,13 @@
}
private Adder batch() {
+ return batch(ReviewerState.REVIEWER);
+ }
+
+ private Adder batch(ReviewerState reviewerState) {
return (String changeId, String reviewer, @Nullable NotifyHandling notify) -> {
ReviewInput in = ReviewInput.noScore();
- in.reviewer(reviewer);
+ in.reviewer(reviewer, reviewerState, false);
if (notify != null) {
in.notify = notify;
}
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 543ae1f..4cea41a 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 543ae1f0a24dda61d4f36b173b9bddfd52ead3b9
+Subproject commit 4cea41a51ff7ce98bc18e4c01577f99a93931562
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index e3596e7..b254182 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -91,22 +91,36 @@
},
_handleServerError(e) {
- Promise.all([
- e.detail.response.text(), this._getLoggedIn(),
- ]).then(values => {
- const text = values[0];
- const loggedIn = values[1];
- if (e.detail.response.status === 403 &&
- loggedIn &&
- text === AUTHENTICATION_REQUIRED) {
- // The app was logged at one point and is now getting auth errors.
- // This indicates the auth token is no longer valid.
- this._handleAuthError();
- } else if (!this._shouldSuppressError(text)) {
- this._showErrorDialog('Server error: ' + text);
- }
- console.error(text);
- });
+ const {request, response} = e.detail;
+ Promise.all([response.text(), this._getLoggedIn()])
+ .then(([errorText, loggedIn]) => {
+ const url = request && (request.anonymizedUrl || request.url);
+ const {status, statusText} = response;
+ if (response.status === 403 &&
+ loggedIn &&
+ errorText === AUTHENTICATION_REQUIRED) {
+ // The app was logged at one point and is now getting auth errors.
+ // This indicates the auth token is no longer valid.
+ this._handleAuthError();
+ } else if (!this._shouldSuppressError(errorText)) {
+ this._showErrorDialog(this._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ url,
+ }));
+ }
+ console.error(errorText);
+ });
+ },
+
+ _constructServerErrorMsg({errorText, status, statusText, url}) {
+ let err = `Error ${status}`;
+ if (statusText) { err += ` (${statusText})`; }
+ if (errorText || url) { err += ': '; }
+ if (errorText) { err += errorText; }
+ if (url) { err += `\nEndpoint: ${url}`; }
+ return err;
},
_handleShowAlert(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index e28b979..f92feae 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -86,7 +86,7 @@
'Log in is required to perform that action.', 'Log in.'));
});
- test('show normal server error', done => {
+ test('show normal Error', done => {
const showErrorStub = sandbox.stub(element, '_showErrorDialog');
const textSpy = sandbox.spy(() => { return Promise.resolve('ZOMG'); });
element.fire('server-error', {response: {status: 500, text: textSpy}});
@@ -98,11 +98,32 @@
]).then(() => {
assert.isTrue(showErrorStub.calledOnce);
assert.isTrue(showErrorStub.lastCall.calledWithExactly(
- 'Server error: ZOMG'));
+ 'Error 500: ZOMG'));
done();
});
});
+ test('_constructServerErrorMsg', () => {
+ const errorText = 'change conflicts';
+ const status = 409;
+ const statusText = 'Conflict';
+ const url = '/my/test/url';
+
+ assert.equal(element._constructServerErrorMsg({status}),
+ 'Error 409');
+ assert.equal(element._constructServerErrorMsg({status, url}),
+ 'Error 409: \nEndpoint: /my/test/url');
+ assert.equal(element._constructServerErrorMsg({status, statusText, url}),
+ 'Error 409 (Conflict): \nEndpoint: /my/test/url');
+ assert.equal(element._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ url,
+ }), 'Error 409 (Conflict): change conflicts' +
+ '\nEndpoint: /my/test/url');
+ });
+
test('suppress TOO_MANY_FILES error', done => {
const showAlertStub = sandbox.stub(element, '_showAlert');
const textSpy = sandbox.spy(() => {
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index 912deb9..29d554e 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -17,6 +17,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -26,10 +27,35 @@
<dom-module id="gr-account-info">
<template>
- <style include="shared-styles"></style>
+ <style include="shared-styles">
+ gr-avatar {
+ height: 10em;
+ width: 10em;
+ margin-right: .15em;
+ vertical-align: -.25em;
+ }
+ .hide {
+ display: none;
+ }
+ </style>
<style include="gr-form-styles"></style>
<div class="gr-form-styles">
<section>
+ <span class="title"></span>
+ <span class="value">
+ <gr-avatar account="[[_account]]"
+ image-size="32"></gr-avatar>
+ </span>
+ </section>
+ <section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
+ <span class="title"></span>
+ <span class="value">
+ <a href$="[[_avatarChangeUrl]]">
+ Change avatar
+ </a>
+ </span>
+ </section>
+ <section>
<span class="title">ID</span>
<span class="value">[[_account._account_id]]</span>
</section>
@@ -59,6 +85,7 @@
disabled="[[_saving]]"
on-keydown="_handleKeydown"
bind-value="{{_username}}">
+ </span>
</section>
<section id="nameSection">
<span class="title">Full name</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 3ad151a..fcc99aa 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -62,6 +62,10 @@
type: String,
observer: '_usernameChanged',
},
+ _avatarChangeUrl: {
+ type: String,
+ value: '',
+ },
},
observers: [
@@ -89,6 +93,10 @@
this._username = account.username;
}));
+ promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => {
+ this._avatarChangeUrl = url;
+ }));
+
return Promise.all(promises).then(() => {
this._loading = false;
});
@@ -167,5 +175,13 @@
this.save();
}
},
+
+ _hideAvatarChangeUrl(avatarChangeUrl) {
+ if (!avatarChangeUrl) {
+ return 'hide';
+ }
+
+ return '';
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index d5682e0..f91277a 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -330,5 +330,11 @@
assert.isTrue(element._hasUsernameChange);
});
+
+ test('_hideAvatarChangeUrl', () => {
+ assert.equal(element._hideAvatarChangeUrl(''), 'hide');
+
+ assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index f6c5954..b4a36b0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -298,7 +298,7 @@
req.errFn.call(null, response);
return;
}
- this.fire('server-error', {response});
+ this.fire('server-error', {request: req, response});
return;
}
return response && this.getResponseObject(response);
@@ -822,6 +822,18 @@
});
},
+ getAvatarChangeUrl() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/avatar.change.url',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache['/accounts/self/avatar.change.url'] = null;
+ }
+ },
+ });
+ },
+
getExternalIds() {
return this._fetchJSON({
url: '/accounts/self/external.ids',
@@ -1307,7 +1319,7 @@
if (opt_errFn) {
opt_errFn.call(null, response);
} else {
- this.fire('server-error', {response});
+ this.fire('server-error', {request: req, response});
}
return;
}
@@ -2086,7 +2098,7 @@
if (req.errFn) {
return req.errFn.call(undefined, response);
}
- this.fire('server-error', {response});
+ this.fire('server-error', {request: fetchReq, response});
}
return response;
}).catch(err => {