Merge "Fix n/p shortcuts for new diff"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ea95680..5026c5f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1472,6 +1472,9 @@
Optionally, the parent revision (of the oldest ancestor to be rebased) can be changed to another
change, revision or branch through the link:#rebase-input[RebaseInput] entity.
+Providing a `committer_email` through the link:#rebase-input[RebaseInput] entity is not supported
+when rebasing a chain.
+
If the chain is outdated, i.e., there's a change that depends on an old revision of its parent, the
result is the same as individually rebasing all outdated changes on top of their parent's latest
revision before running the rebase chain action.
@@ -8343,6 +8346,10 @@
In addition, rebasing on behalf of the uploader is only supported for the
current patch set of a change. +
If the caller is the uploader this flag is ignored and a normal rebase is done.
+|`committer_email`|optional|
+Rebase is committed using this email address. Only the registered emails
+of the calling user or uploader (when `on_behalf_of_uploader` is `true`) are
+considered valid. This option is not supported when rebasing a chain.
|`validation_options` |optional|
Map with key-value pairs that are forwarded as options to the commit validation
listeners (e.g. can be used to skip certain validations). Which validation
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 9ed4792..cc020ad 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -345,7 +345,7 @@
of the argument.
[[message]]
-message:'MESSAGE'::
+message:'MESSAGE'::, description:'MESSAGE'::, d:'MESSAGE'::
+
Changes that match 'MESSAGE' arbitrary string in the commit message body.
By default full text matching is used, but regular expressions can be
diff --git a/java/com/google/gerrit/extensions/api/changes/RebaseInput.java b/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
index 07e65d0..42dea8d 100644
--- a/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
@@ -51,4 +51,12 @@
public boolean onBehalfOfUploader;
public Map<String, String> validationOptions;
+
+ /**
+ * Rebase will be committed using this email address. Only the registered emails of the calling
+ * user or uploader (when onBehalfOfUploader is true) are considered valid.
+ *
+ * <p>This option is not supported when rebasing a chain.
+ */
+ public String committerEmail;
}
diff --git a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
index 6ee5654..1264478 100644
--- a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
+++ b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
@@ -62,7 +62,7 @@
}
private final Class<?> clazz;
- private final TypeLiteral clazzTl;
+ private final TypeLiteral<?> clazzTl;
private SerializedClassSubject(FailureMetadata metadata, Class<?> clazz) {
super(metadata, clazz);
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index f46196f..de3b7d5 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -400,6 +400,10 @@
return rebasedCommit;
}
+ public PatchSet getOriginalPatchSet() {
+ return originalPatchSet;
+ }
+
public PatchSet.Id getPatchSetId() {
checkState(rebasedPatchSetId != null, "getPatchSetId() only valid after updateRepo");
return rebasedPatchSetId;
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 48b052f..47a1e11 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
@@ -38,6 +39,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
@@ -45,6 +47,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
@@ -62,39 +65,38 @@
private final Provider<PersonIdent> serverIdent;
private final IdentifiedUser.GenericFactory userFactory;
private final PermissionBackend permissionBackend;
- private final ChangeResource.Factory changeResourceFactory;
private final GitRepositoryManager repoManager;
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeNotes.Factory notesFactory;
private final PatchSetUtil psUtil;
private final RebaseChangeOp.Factory rebaseFactory;
+ private final Provider<CurrentUser> self;
@Inject
RebaseUtil(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
IdentifiedUser.GenericFactory userFactory,
PermissionBackend permissionBackend,
- ChangeResource.Factory changeResourceFactory,
GitRepositoryManager repoManager,
Provider<InternalChangeQuery> queryProvider,
ChangeNotes.Factory notesFactory,
PatchSetUtil psUtil,
- RebaseChangeOp.Factory rebaseFactory) {
+ RebaseChangeOp.Factory rebaseFactory,
+ Provider<CurrentUser> self) {
this.serverIdent = serverIdent;
this.userFactory = userFactory;
this.permissionBackend = permissionBackend;
- this.changeResourceFactory = changeResourceFactory;
this.repoManager = repoManager;
this.queryProvider = queryProvider;
this.notesFactory = notesFactory;
this.psUtil = psUtil;
this.rebaseFactory = rebaseFactory;
+ this.self = self;
}
/**
- * Checks that the uploader has permissions to create a new patch set and creates a new {@link
- * RevisionResource} that contains the uploader (aka the impersonated user) as the current user
- * which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader.
+ * Checks that the uploader has permissions to create a new patch set as the current user which
+ * can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader.
*
* <p>The following permissions are required for the uploader:
*
@@ -137,10 +139,8 @@
*
* @param rsrc the revision resource that should be rebased
* @param rebaseInput the request input containing options for the rebase
- * @return revision resource that contains the uploader (aka the impersonated user) as the current
- * user which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader
*/
- public RevisionResource onBehalfOf(RevisionResource rsrc, RebaseInput rebaseInput)
+ public void checkCanRebaseOnBehalfOf(RevisionResource rsrc, RebaseInput rebaseInput)
throws IOException, PermissionBackendException, BadRequestException,
ResourceConflictException {
if (rebaseInput.allowConflicts) {
@@ -208,9 +208,6 @@
}
}
}
-
- return new RevisionResource(
- changeResourceFactory.create(rsrc.getNotes(), uploader), rsrc.getPatchSet());
}
private void checkPermissionForUploader(
@@ -538,23 +535,77 @@
return baseId;
}
- public RebaseChangeOp getRebaseOp(RevisionResource revRsrc, RebaseInput input, ObjectId baseRev) {
+ public RebaseChangeOp getRebaseOp(
+ RevWalk rw,
+ RevisionResource revRsrc,
+ RebaseInput input,
+ ObjectId baseRev,
+ IdentifiedUser rebaseAsUser)
+ throws ResourceConflictException, PermissionBackendException, IOException {
return applyRebaseInputToOp(
- rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseRev), input);
+ rw,
+ rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseRev),
+ input,
+ rebaseAsUser);
}
public RebaseChangeOp getRebaseOp(
- RevisionResource revRsrc, RebaseInput input, Change.Id baseChange) {
+ RevWalk rw,
+ RevisionResource revRsrc,
+ RebaseInput input,
+ Change.Id baseChange,
+ IdentifiedUser rebaseAsUser)
+ throws ResourceConflictException, PermissionBackendException, IOException {
return applyRebaseInputToOp(
- rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseChange), input);
+ rw,
+ rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseChange),
+ input,
+ rebaseAsUser);
}
- private RebaseChangeOp applyRebaseInputToOp(RebaseChangeOp op, RebaseInput input) {
- return op.setForceContentMerge(true)
- .setAllowConflicts(input.allowConflicts)
- .setMergeStrategy(input.strategy)
- .setValidationOptions(
- ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions))
- .setFireRevisionCreated(true);
+ private RebaseChangeOp applyRebaseInputToOp(
+ RevWalk rw, RebaseChangeOp op, RebaseInput input, IdentifiedUser rebaseAsUser)
+ throws ResourceConflictException, PermissionBackendException, IOException {
+ RebaseChangeOp rebaseChangeOp =
+ op.setForceContentMerge(true)
+ .setAllowConflicts(input.allowConflicts)
+ .setMergeStrategy(input.strategy)
+ .setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions))
+ .setFireRevisionCreated(true);
+
+ String originalPatchSetCommitterEmail =
+ rw.parseCommit(rebaseChangeOp.getOriginalPatchSet().commitId())
+ .getCommitterIdent()
+ .getEmailAddress();
+
+ if (input.committerEmail != null) {
+ if (!self.get().hasSameAccountId(rebaseAsUser)
+ && !input.committerEmail.equals(rebaseAsUser.getAccount().preferredEmail())
+ && !input.committerEmail.equals(originalPatchSetCommitterEmail)
+ && !permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
+ throw new ResourceConflictException(
+ String.format(
+ "Cannot rebase using committer email '%s'. It can only be done using the "
+ + "preferred email or the committer email of the uploader",
+ input.committerEmail));
+ }
+
+ ImmutableSet<String> emails = rebaseAsUser.getEmailAddresses();
+ if (!emails.contains(input.committerEmail)) {
+ throw new ResourceConflictException(
+ String.format(
+ "Cannot rebase using committer email '%s' as it is not a registered "
+ + "email of the user on whose behalf the rebase operation is performed",
+ input.committerEmail));
+ }
+ rebaseChangeOp.setCommitterIdent(
+ new PersonIdent(
+ rebaseAsUser.getName(),
+ input.committerEmail,
+ TimeUtil.now(),
+ serverIdent.get().getZoneId()));
+ }
+ return rebaseChangeOp;
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index f8a4a99..51f43ce 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -1165,6 +1165,16 @@
}
@Operator
+ public Predicate<ChangeData> d(String text) throws QueryParseException {
+ return message(text);
+ }
+
+ @Operator
+ public Predicate<ChangeData> description(String text) throws QueryParseException {
+ return message(text);
+ }
+
+ @Operator
public Predicate<ChangeData> message(String text) throws QueryParseException {
if (text.startsWith("^")) {
checkFieldAvailable(
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 167f784..98a3f83 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -29,6 +29,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
@@ -68,6 +69,7 @@
private final ProjectCache projectCache;
private final PatchSetUtil patchSetUtil;
private final RebaseMetrics rebaseMetrics;
+ private final IdentifiedUser.GenericFactory userFactory;
@Inject
public Rebase(
@@ -78,7 +80,8 @@
PermissionBackend permissionBackend,
ProjectCache projectCache,
PatchSetUtil patchSetUtil,
- RebaseMetrics rebaseMetrics) {
+ RebaseMetrics rebaseMetrics,
+ IdentifiedUser.GenericFactory userFactory) {
this.updateFactory = updateFactory;
this.repoManager = repoManager;
this.rebaseUtil = rebaseUtil;
@@ -87,16 +90,20 @@
this.projectCache = projectCache;
this.patchSetUtil = patchSetUtil;
this.rebaseMetrics = rebaseMetrics;
+ this.userFactory = userFactory;
}
@Override
public Response<ChangeInfo> apply(RevisionResource rsrc, RebaseInput input)
throws UpdateException, RestApiException, IOException, PermissionBackendException {
-
+ IdentifiedUser rebaseAsUser;
if (input.onBehalfOfUploader && !rsrc.getPatchSet().uploader().equals(rsrc.getAccountId())) {
+ rebaseAsUser =
+ userFactory.runAs(/*remotePeer= */ null, rsrc.getPatchSet().uploader(), rsrc.getUser());
rsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
- rsrc = rebaseUtil.onBehalfOf(rsrc, input);
+ rebaseUtil.checkCanRebaseOnBehalfOf(rsrc, input);
} else {
+ rebaseAsUser = rsrc.getUser().asIdentifiedUser();
input.onBehalfOfUploader = false;
rsrc.permissions().check(ChangePermission.REBASE);
}
@@ -113,14 +120,16 @@
ObjectReader reader = oi.newReader();
RevWalk rw = CodeReviewCommit.newRevWalk(reader);
BatchUpdate bu =
- updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
+ updateFactory.create(change.getProject(), rebaseAsUser, TimeUtil.now())) {
rebaseUtil.verifyRebasePreconditions(rw, rsrc.getNotes(), rsrc.getPatchSet());
RebaseChangeOp rebaseOp =
rebaseUtil.getRebaseOp(
+ rw,
rsrc,
input,
- rebaseUtil.parseOrFindBaseRevision(repo, rw, permissionBackend, rsrc, input, true));
+ rebaseUtil.parseOrFindBaseRevision(repo, rw, permissionBackend, rsrc, input, true),
+ rebaseAsUser);
// TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
bu.setNotify(NotifyResolver.Result.none());
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index 343fb72..76c5253 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -36,6 +36,7 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
@@ -89,6 +90,7 @@
private final PatchSetUtil patchSetUtil;
private final ChangeJson.Factory json;
private final RebaseMetrics rebaseMetrics;
+ private final IdentifiedUser.GenericFactory userFactory;
@Inject
RebaseChain(
@@ -103,7 +105,8 @@
ProjectCache projectCache,
PatchSetUtil patchSetUtil,
ChangeJson.Factory json,
- RebaseMetrics rebaseMetrics) {
+ RebaseMetrics rebaseMetrics,
+ IdentifiedUser.GenericFactory userFactory) {
this.repoManager = repoManager;
this.getRelatedChangesUtil = getRelatedChangesUtil;
this.changeDataFactory = changeDataFactory;
@@ -116,11 +119,18 @@
this.patchSetUtil = patchSetUtil;
this.json = json;
this.rebaseMetrics = rebaseMetrics;
+ this.userFactory = userFactory;
}
@Override
public Response<RebaseChainInfo> apply(ChangeResource tipRsrc, RebaseInput input)
throws IOException, PermissionBackendException, RestApiException, UpdateException {
+ IdentifiedUser rebaseAsUser;
+ if (input.committerEmail != null) {
+ // TODO: committer_email can be supported if all changes in the chain
+ // belong to the same uploader. It can be attempted in future as needed.
+ throw new BadRequestException("committer_email is not supported when rebasing a chain");
+ }
if (input.onBehalfOfUploader) {
tipRsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
if (input.allowConflicts) {
@@ -160,10 +170,14 @@
new RevisionResource(changeResourceFactory.create(changeData, user), ps);
if (input.onBehalfOfUploader
&& !revRsrc.getPatchSet().uploader().equals(revRsrc.getAccountId())) {
- revRsrc = rebaseUtil.onBehalfOf(revRsrc, input);
+ rebaseAsUser =
+ userFactory.runAs(
+ /*remotePeer= */ null, revRsrc.getPatchSet().uploader(), revRsrc.getUser());
+ rebaseUtil.checkCanRebaseOnBehalfOf(revRsrc, input);
revRsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
anyRebaseOnBehalfOfUploader = true;
} else {
+ rebaseAsUser = revRsrc.getUser().asIdentifiedUser();
revRsrc.permissions().check(ChangePermission.REBASE);
}
rebaseUtil.verifyRebasePreconditions(rw, changeData.notes(), ps);
@@ -177,7 +191,7 @@
if (currentBase(rw, ps).equals(desiredBase)) {
isUpToDate = true;
} else {
- rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, desiredBase);
+ rebaseOp = rebaseUtil.getRebaseOp(rw, revRsrc, input, desiredBase, rebaseAsUser);
}
} else {
if (ancestorsAreUpToDate) {
@@ -187,7 +201,8 @@
isUpToDate = currentBase(rw, ps).equals(latestCommittedBase);
}
if (!isUpToDate) {
- rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, chain.get(i - 1).id());
+ rebaseOp =
+ rebaseUtil.getRebaseOp(rw, revRsrc, input, chain.get(i - 1).id(), rebaseAsUser);
}
}
@@ -196,7 +211,7 @@
continue;
}
ancestorsAreUpToDate = false;
- bu.addOp(revRsrc.getChange().getId(), revRsrc.getUser(), rebaseOp);
+ bu.addOp(revRsrc.getChange().getId(), rebaseAsUser, rebaseOp);
rebaseOps.put(revRsrc.getChange().getId(), rebaseOp);
}
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 09951b2..e0c699a 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -143,37 +143,41 @@
if (!changes.isEmpty()) {
return true;
}
+ if (commit.getParents() != null && commit.getParents().length > 0) {
+ // Maybe the commit was a merge commit of a change. Try to find promising candidates for
+ // branches to check, by seeing if its parents were associated to changes.
+ // Only request changes from the index if the commit has parents. If size(parents) == 0, then
+ // the query does not make sense (it would request all changes from the project).
+ ImmutableList<Predicate<ChangeData>> parentPredicates =
+ Arrays.stream(commit.getParents())
+ .map(parent -> ChangePredicates.commitPrefix(parent.getId().getName()))
+ .collect(toImmutableList());
+ Predicate<ChangeData> pred =
+ Predicate.and(ChangePredicates.project(project), Predicate.or(parentPredicates));
+ changes =
+ retryHelper
+ .changeIndexQuery(
+ "queryChangesByProjectCommit", q -> q.enforceVisibility(true).query(pred))
+ .call();
+ Set<Ref> branchesForCommitParents = new HashSet<>(changes.size());
+ for (ChangeData cd : changes) {
+ Ref ref = repo.exactRef(cd.change().getDest().branch());
+ if (ref != null) {
+ branchesForCommitParents.add(ref);
+ }
+ }
- // Maybe the commit was a merge commit of a change. Try to find promising candidates for
- // branches to check, by seeing if its parents were associated to changes.
- Predicate<ChangeData> pred =
- Predicate.and(
- ChangePredicates.project(project),
- Predicate.or(
- Arrays.stream(commit.getParents())
- .map(parent -> ChangePredicates.commitPrefix(parent.getId().getName()))
- .collect(toImmutableList())));
- changes =
- retryHelper
- .changeIndexQuery(
- "queryChangesByProjectCommit", q -> q.enforceVisibility(true).query(pred))
- .call();
-
- Set<Ref> branchesForCommitParents = new HashSet<>(changes.size());
- for (ChangeData cd : changes) {
- Ref ref = repo.exactRef(cd.change().getDest().branch());
- if (ref != null) {
- branchesForCommitParents.add(ref);
+ if (reachable.fromRefs(
+ project, repo, commit, branchesForCommitParents.stream().collect(Collectors.toList()))) {
+ return true;
}
}
+ // This check covers 2 situations:
+ // 1) The commit does not have any parents. Check if it is visible from any ref in the project.
+ // Exclude change refs, since it is confirmed the commit is not a patchset of any change.
- if (reachable.fromRefs(
- project, repo, commit, branchesForCommitParents.stream().collect(Collectors.toList()))) {
- return true;
- }
-
- // If we have already checked change refs using the change index, spare any further checks for
- // changes.
+ // 2) If we have already checked change refs using the change index, spare any further checks
+ // for changes.
List<Ref> refs =
repo.getRefDatabase()
.getRefsByPrefixWithExclusions(RefDatabase.ALL, ImmutableSet.of(RefNames.REFS_CHANGES));
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
index 7d1ddfc..297579c 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
@@ -100,6 +100,22 @@
}
@Test
+ public void cannotRebaseOnBehalfOfUploaderWithCommitterEmail() throws Exception {
+ Account.Id uploader = accountOperations.newAccount().create();
+ Change.Id changeId = changeOperations.newChange().owner(uploader).create();
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ rebaseInput.committerEmail = "admin@example.com";
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.changes().id(changeId.get()).rebaseChain(rebaseInput));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo("committer_email is not supported when rebasing a chain");
+ }
+
+ @Test
public void rebaseChangeOnBehalfOfUploader_withRebasePermission() throws Exception {
testRebaseChainOnBehalfOfUploader(Permission.REBASE);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index d9b079a..c637916 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -63,6 +63,7 @@
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
@@ -144,6 +145,82 @@
}
@Test
+ public void rebaseWithCommitterEmail() throws Exception {
+ // Create three changes with the same parent
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r3 = createChange();
+
+ // Create new user with a secondary email and with permission to rebase
+ Account.Id userWithSecondaryEmail =
+ accountOperations
+ .newAccount()
+ .preferredEmail("preferred@domain.org")
+ .addSecondaryEmail("secondary@domain.org")
+ .create();
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Approve and submit the r1
+ RevisionApi revision = gApi.changes().id(r1.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase r2 as the new user with its primary email
+ RebaseInput ri = new RebaseInput();
+ ri.committerEmail = "preferred@domain.org";
+ requestScopeOperations.setApiUser(userWithSecondaryEmail);
+ rebaseCallWithInput.call(r2.getChangeId(), ri);
+ assertThat(r2.getChange().getCommitter().getEmailAddress()).isEqualTo(ri.committerEmail);
+
+ // Approve and submit the r3
+ requestScopeOperations.setApiUser(admin.id());
+ revision = gApi.changes().id(r3.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase r2 as the new user with its secondary email
+ ri = new RebaseInput();
+ ri.committerEmail = "secondary@domain.org";
+ requestScopeOperations.setApiUser(userWithSecondaryEmail);
+ rebaseCallWithInput.call(r2.getChangeId(), ri);
+ assertThat(r2.getChange().getCommitter().getEmailAddress()).isEqualTo(ri.committerEmail);
+ }
+
+ @Test
+ public void cannotRebaseWithInvalidCommitterEmail() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result c1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result c2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(c1.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the second change with invalid committer email
+ RebaseInput ri = new RebaseInput();
+ ri.committerEmail = "invalid@example.com";
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(c2.getChangeId(), ri));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Cannot rebase using committer email '%s' as it is not a registered email of "
+ + "the user on whose behalf the rebase operation is performed",
+ ri.committerEmail));
+ }
+
+ @Test
public void rebaseAbandonedChange() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
@@ -1096,6 +1173,72 @@
assertThat(thrown).hasMessageThat().contains("The whole chain is already up to date.");
}
+ @Override
+ @Test
+ public void rebaseWithCommitterEmail() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r1.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Create new user with a secondary email and with permission to rebase
+ Account.Id userWithSecondaryEmail =
+ accountOperations
+ .newAccount()
+ .preferredEmail("preferred@domain.org")
+ .addSecondaryEmail("secondary@domain.org")
+ .create();
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the chain through r2 with the new user and with its secondary email.
+ RebaseInput ri = new RebaseInput();
+ ri.committerEmail = "secondary@domain.org";
+ requestScopeOperations.setApiUser(userWithSecondaryEmail);
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class, () -> gApi.changes().id(r2.getChangeId()).rebaseChain(ri));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo("committer_email is not supported when rebasing a chain");
+ }
+
+ @Override
+ @Test
+ public void cannotRebaseWithInvalidCommitterEmail() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result c1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result c2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(c1.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the second change with invalid committer email
+ RebaseInput ri = new RebaseInput();
+ ri.committerEmail = "invalid@example.com";
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class, () -> gApi.changes().id(c2.getChangeId()).rebaseChain(ri));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo("committer_email is not supported when rebasing a chain");
+ }
+
@Test
public void rebaseChain() throws Exception {
// Create changes with the following hierarchy:
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index 968c1f7..319c0cd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -36,6 +37,7 @@
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
@@ -100,6 +102,103 @@
}
@Test
+ public void rebaseOnBehalfOfUploaderWithCommitterEmail() throws Exception {
+ allowPermissionToAllUsers(Permission.REBASE);
+
+ String uploaderPreferredEmail = "uploader.preferred@example.com";
+ String uploaderSecondaryEmail = "uploader.secondary@example.com";
+ Account.Id uploader =
+ accountOperations
+ .newAccount()
+ .preferredEmail(uploaderPreferredEmail)
+ .addSecondaryEmail(uploaderSecondaryEmail)
+ .create();
+ Account.Id approver = admin.id();
+ Account.Id rebaser = accountOperations.newAccount().create();
+
+ projectOperations
+ .allProjectsForUpdate()
+ .add(allowCapability(GlobalCapability.VIEW_SECONDARY_EMAILS).group(REGISTERED_USERS))
+ .update();
+
+ // Create two changes both with the same parent.
+ requestScopeOperations.setApiUser(uploader);
+ Change.Id changeToBeTheNewBase =
+ changeOperations.newChange().project(project).owner(uploader).create();
+ Change.Id changeToBeRebased =
+ changeOperations.newChange().project(project).owner(uploader).create();
+
+ // Approve and submit the change that will be the new base for the change that will be rebased.
+ requestScopeOperations.setApiUser(approver);
+ gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
+
+ // Rebase the second change on behalf of the uploader
+ requestScopeOperations.setApiUser(rebaser);
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ rebaseInput.committerEmail = uploaderSecondaryEmail;
+ gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
+
+ assertThat(
+ gApi.changes()
+ .id(changeToBeRebased.get())
+ .get()
+ .getCurrentRevision()
+ .commit
+ .committer
+ .email)
+ .isEqualTo(uploaderSecondaryEmail);
+ }
+
+ @Test
+ public void cannotRebaseOnBehalfOfUploaderWithCommitterEmailWithoutViewSecondaryEmails()
+ throws Exception {
+ allowPermissionToAllUsers(Permission.REBASE);
+
+ String uploaderPreferredEmail = "uploader.preferred@example.com";
+ String uploaderSecondaryEmail = "uploader.secondary@example.com";
+ Account.Id uploader =
+ accountOperations
+ .newAccount()
+ .preferredEmail(uploaderPreferredEmail)
+ .addSecondaryEmail(uploaderSecondaryEmail)
+ .create();
+ Account.Id approver = admin.id();
+ Account.Id rebaser = accountOperations.newAccount().create();
+
+ // Create two changes both with the same parent.
+ requestScopeOperations.setApiUser(uploader);
+ Change.Id changeToBeTheNewBase =
+ changeOperations.newChange().project(project).owner(uploader).create();
+ Change.Id changeToBeRebased =
+ changeOperations.newChange().project(project).owner(uploader).create();
+
+ // Approve and submit the change that will be the new base for the change that will be rebased.
+ requestScopeOperations.setApiUser(approver);
+ gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
+
+ // Rebase the second change on behalf of the uploader
+ requestScopeOperations.setApiUser(rebaser);
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ rebaseInput.committerEmail = uploaderSecondaryEmail;
+
+ ResourceConflictException exception =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Cannot rebase using committer email '%s'. It can only be done using "
+ + "the preferred email or the committer email of the uploader",
+ uploaderSecondaryEmail));
+ }
+
+ @Test
public void cannotRebaseNonCurrentPatchSetOnBehalfOfUploader() throws Exception {
Account.Id uploader = accountOperations.newAccount().create();
Change.Id changeId = changeOperations.newChange().owner(uploader).create();
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 2c012fa..c90f5d4 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1107,40 +1107,70 @@
}
@Test
- public void byMessageExact() throws Exception {
- repo = createAndOpenProject("repo");
- RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
- RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
- RevCommit commit3 = repo.parseBody(repo.commit().message("A great \"fix\" to my bug").create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
-
- assertQuery("message:foo");
- assertQuery("message:one", change1);
- assertQuery("message:two", change2);
- assertQuery("message:\"great \\\"fix\\\" to\"", change3);
+ public void byMessageExact_byAlias_d() throws Exception {
+ byMessageExact("d:", "d_repo");
}
@Test
- public void byMessageRegEx() throws Exception {
+ public void byMessageExact_byAlias_description() throws Exception {
+ byMessageExact("description:", "description_repo");
+ }
+
+ @Test
+ public void byMessageExact_byMainOperator() throws Exception {
+ byMessageExact("message:", "message_repo");
+ }
+
+ private void byMessageExact(String searchOperator, String projectName) throws Exception {
+ repo = createAndOpenProject(projectName);
+ RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
+ Change change1 = insert(projectName, newChangeForCommit(repo, commit1));
+ RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
+ Change change2 = insert(projectName, newChangeForCommit(repo, commit2));
+ RevCommit commit3 = repo.parseBody(repo.commit().message("A great \"fix\" to my bug").create());
+ Change change3 = insert(projectName, newChangeForCommit(repo, commit3));
+
+ assertQuery(searchOperator + "foo");
+ assertQuery(searchOperator + "one", change1);
+ assertQuery(searchOperator + "two", change2);
+ assertQuery(searchOperator + "\"great \\\"fix\\\" to\"", change3);
+ }
+
+ @Test
+ public void byMessageRegEx_byAlias_d() throws Exception {
+ byMessageRegEx("d:", "d_repo");
+ }
+
+ @Test
+ public void byMessageRegEx_byAlias_description() throws Exception {
+ byMessageRegEx("description:", "description_repo");
+ }
+
+ @Test
+ public void byMessageRegEx_byMainOperator() throws Exception {
+ byMessageRegEx("message:", "message_repo");
+ }
+
+ private void byMessageRegEx(String searchOperator, String projectName) throws Exception {
assume().that(getSchema().hasField(ChangeField.COMMIT_MESSAGE_EXACT)).isTrue();
- repo = createAndOpenProject("repo");
+ repo = createAndOpenProject(projectName);
RevCommit commit1 = repo.parseBody(repo.commit().message("aaaabcc").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(projectName, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("aaaacc").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(projectName, newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("Title\n\nHELLO WORLD").create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(projectName, newChangeForCommit(repo, commit3));
RevCommit commit4 =
repo.parseBody(repo.commit().message("Title\n\nfoobar hello WORLD").create());
- Change change4 = insert("repo", newChangeForCommit(repo, commit4));
+ Change change4 = insert(projectName, newChangeForCommit(repo, commit4));
- assertQuery("message:\"^aaaa(b|c)*\"", change2, change1);
- assertQuery("message:\"^aaaa(c)*c.*\"", change2);
- assertQuery("message:\"^.*HELLO WORLD.*\"", change3);
+ assertQuery(searchOperator + "\"^aaaa(b|c)*\"", change2, change1);
+ assertQuery(searchOperator + "\"^aaaa(c)*c.*\"", change2);
+ assertQuery(searchOperator + "\"^.*HELLO WORLD.*\"", change3);
assertQuery(
- "message:\"^.*(H|h)(E|e)(L|l)(L|l)(O|o) (W|w)(O|o)(R|r)(L|l)(D|d).*\"", change4, change3);
+ searchOperator + "\"^.*(H|h)(E|e)(L|l)(L|l)(O|o) (W|w)(O|o)(R|r)(L|l)(D|d).*\"",
+ change4,
+ change3);
}
@Test
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 8f835de..31c2b660 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1563,6 +1563,7 @@
base: e.detail.base,
allow_conflicts: e.detail.allowConflicts,
on_behalf_of_uploader: e.detail.onBehalfOfUploader,
+ committer_email: e.detail.committerEmail,
};
const rebaseChain = !!e.detail.rebaseChain;
this.fireAction(
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 15500de..b953eec 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -629,6 +629,7 @@
allowConflicts: false,
rebaseChain: false,
onBehalfOfUploader: true,
+ committerEmail: 'test@default.org',
},
})
);
@@ -636,7 +637,12 @@
'/rebase',
assertUIActionInfo(rebaseAction),
true,
- {base: '1234', allow_conflicts: false, on_behalf_of_uploader: true},
+ {
+ base: '1234',
+ allow_conflicts: false,
+ on_behalf_of_uploader: true,
+ committer_email: 'test@default.org',
+ },
{allow_conflicts: false, on_behalf_of_uploader: true},
]);
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 8946a83..7a4caa7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -8,18 +8,20 @@
import {customElement, property, query, state} from 'lit/decorators.js';
import {when} from 'lit/directives/when.js';
import {
- NumericChangeId,
- BranchName,
- ChangeActionDialog,
AccountDetailInfo,
AccountInfo,
+ BranchName,
+ ChangeActionDialog,
+ EmailInfo,
+ NumericChangeId,
+ GitPersonInfo,
} from '../../../types/common';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-autocomplete/gr-autocomplete';
import {
- GrAutocomplete,
AutocompleteQuery,
AutocompleteSuggestion,
+ GrAutocomplete,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getAppContext} from '../../../services/app-context';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -43,6 +45,7 @@
allowConflicts: boolean;
rebaseChain: boolean;
onBehalfOfUploader: boolean;
+ committerEmail: string | null;
}
@customElement('gr-confirm-rebase-dialog')
@@ -92,6 +95,18 @@
@state()
allowConflicts = false;
+ @state()
+ selectedEmailForRebase: string | null | undefined;
+
+ @state()
+ currentUserEmails: EmailInfo[] = [];
+
+ @state()
+ uploaderEmails: EmailInfo[] = [];
+
+ @state()
+ committerEmailDropdownItems: EmailInfo[] = [];
+
@query('#rebaseOnParentInput')
private rebaseOnParentInput?: HTMLInputElement;
@@ -116,6 +131,9 @@
@state()
uploader?: AccountInfo;
+ @state()
+ latestCommitter?: GitPersonInfo;
+
private readonly restApiService = getAppContext().restApiService;
private readonly getChangeModel = resolve(this, changeModelToken);
@@ -150,6 +168,16 @@
() => this.getRelatedChangesModel().hasParent$,
x => (this.hasParent = x)
);
+ subscribe(
+ this,
+ () => this.getChangeModel().latestCommitter$,
+ x => (this.latestCommitter = x)
+ );
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.loadCommitterEmailDropdownItems();
}
override willUpdate(changedProperties: PropertyValues): void {
@@ -194,6 +222,9 @@
.rebaseOnBehalfMsg {
margin-top: var(--spacing-m);
}
+ .rebaseWithCommitterEmail {
+ margin-top: var(--spacing-m);
+ }
`,
];
}
@@ -288,6 +319,7 @@
type="checkbox"
@change=${() => {
this.allowConflicts = !!this.rebaseAllowConflicts?.checked;
+ this.loadCommitterEmailDropdownItems();
}}
/>
<label for="rebaseAllowConflicts"
@@ -311,6 +343,9 @@
type="checkbox"
@change=${() => {
this.shouldRebaseChain = !!this.rebaseChain?.checked;
+ if (this.shouldRebaseChain) {
+ this.selectedEmailForRebase = undefined;
+ }
}}
/>
<label for="rebaseChain">Rebase all ancestors</label>
@@ -325,6 +360,18 @@
></gr-account-chip
><span></div>`
)}
+ ${when(
+ this.canShowCommitterEmailDropdown(),
+ () => html`<div class="rebaseWithCommitterEmail"
+ >Rebase with committer email
+ <gr-dropdown-list
+ .items=${this.getCommitterEmailDropdownItems()}
+ .value=${this.selectedEmailForRebase}
+ @value-change=${this.handleCommitterEmailDropdownItems}
+ >
+ </gr-dropdown-list>
+ <span></div>`
+ )}
</div>
</gr-dialog>
`;
@@ -377,6 +424,69 @@
);
}
+ private setPreferredAsSelectedEmailForRebase(emails: EmailInfo[]) {
+ emails.forEach(e => {
+ if (e.preferred) {
+ this.selectedEmailForRebase = e.email;
+ }
+ });
+ }
+
+ private canShowCommitterEmailDropdown() {
+ return (
+ this.committerEmailDropdownItems &&
+ this.committerEmailDropdownItems.length > 1 &&
+ !this.shouldRebaseChain
+ );
+ }
+
+ private getCommitterEmailDropdownItems() {
+ return this.committerEmailDropdownItems?.map(e => {
+ return {
+ text: e.email,
+ value: e.email,
+ };
+ });
+ }
+
+ private isLatestCommitterEmailInDropdownItems(): boolean {
+ return this.committerEmailDropdownItems?.some(
+ e => e.email === this.latestCommitter?.email.toString()
+ );
+ }
+
+ public setSelectedEmailForRebase() {
+ if (this.isLatestCommitterEmailInDropdownItems()) {
+ this.selectedEmailForRebase = this.latestCommitter?.email;
+ } else {
+ this.setPreferredAsSelectedEmailForRebase(
+ this.committerEmailDropdownItems
+ );
+ }
+ }
+
+ async loadCommitterEmailDropdownItems() {
+ if (this.isCurrentUserEqualToLatestUploader() || this.allowConflicts) {
+ const currentUserEmails = await this.restApiService.getAccountEmails();
+ this.committerEmailDropdownItems = currentUserEmails || [];
+ } else if (this.uploader && this.uploader.email) {
+ const currentUploaderEmails =
+ await this.restApiService.getAccountEmailsFor(
+ this.uploader.email.toString()
+ );
+ this.committerEmailDropdownItems = currentUploaderEmails || [];
+ } else {
+ this.committerEmailDropdownItems = [];
+ }
+ if (this.committerEmailDropdownItems) {
+ this.setSelectedEmailForRebase();
+ }
+ }
+
+ private handleCommitterEmailDropdownItems(e: CustomEvent<{value: string}>) {
+ this.selectedEmailForRebase = e.detail.value;
+ }
+
filterChanges(
input: string,
changes: RebaseChange[]
@@ -436,6 +546,7 @@
allowConflicts: !!this.rebaseAllowConflicts?.checked,
rebaseChain: !!this.rebaseChain?.checked,
onBehalfOfUploader: this.rebaseOnBehalfOfUploader(),
+ committerEmail: this.selectedEmailForRebase || null,
};
fireNoBubbleNoCompose(this, 'confirm-rebase', detail);
this.text = '';
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index f43e521..e6326b3 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -8,11 +8,18 @@
import {GrConfirmRebaseDialog, RebaseChange} from './gr-confirm-rebase-dialog';
import {
pressKey,
+ query,
queryAndAssert,
stubRestApi,
waitUntil,
} from '../../../test/test-utils';
-import {NumericChangeId, BranchName, Timestamp} from '../../../types/common';
+import {
+ NumericChangeId,
+ BranchName,
+ Timestamp,
+ AccountId,
+ EmailAddress,
+} from '../../../types/common';
import {
createAccountWithEmail,
createChangeViewChange,
@@ -25,6 +32,7 @@
import {changeModelToken} from '../../../models/change/change-model';
import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
import {LoadingStatus} from '../../../types/types';
+import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
suite('gr-confirm-rebase-dialog tests', () => {
let element: GrConfirmRebaseDialog;
@@ -157,6 +165,131 @@
});
});
+ suite('rebase with committer email', () => {
+ setup(async () => {
+ element.branch = 'test' as BranchName;
+ await element.updateComplete;
+ });
+
+ test('hide rebaseWithCommitterEmail dialog when committer has single email', async () => {
+ element.committerEmailDropdownItems = [
+ {
+ email: 'test1@example.com',
+ preferred: true,
+ pending_confirmation: true,
+ },
+ ];
+ await element.updateComplete;
+ assert.isNotOk(query(element, '.rebaseWithCommitterEmail'));
+ });
+
+ test('show rebaseWithCommitterEmail dialog when committer has more than one email', async () => {
+ element.committerEmailDropdownItems = [
+ {
+ email: 'test1@example.com',
+ preferred: true,
+ pending_confirmation: true,
+ },
+ {
+ email: 'test2@example.com',
+ pending_confirmation: true,
+ },
+ ];
+ await element.updateComplete;
+ const committerEmail = queryAndAssert(
+ element,
+ '.rebaseWithCommitterEmail'
+ );
+ assert.dom.equal(
+ committerEmail,
+ /* HTML */ `<div class="rebaseWithCommitterEmail"
+ >Rebase with committer email
+ <gr-dropdown-list>
+ </gr-dropdown-list>
+ <span></div>`
+ );
+ const dropdownList: GrDropdownList = queryAndAssert(
+ committerEmail,
+ 'gr-dropdown-list'
+ );
+ assert.strictEqual(dropdownList.items!.length, 2);
+ });
+
+ test('hide rebaseWithCommitterEmail dialog when RebaseChain is set', async () => {
+ element.shouldRebaseChain = true;
+ await element.updateComplete;
+ assert.isNotOk(query(element, '.rebaseWithCommitterEmail'));
+ });
+
+ test('show current user emails in the dropdown list when rebase with conflicts is allowed', async () => {
+ element.allowConflicts = true;
+ element.latestCommitter = {
+ email: 'commit@example.com' as EmailAddress,
+ name: 'committer',
+ date: '2023-06-12 18:32:08.000000000' as Timestamp,
+ };
+ element.committerEmailDropdownItems = [
+ {
+ email: 'currentuser1@example.com',
+ preferred: true,
+ pending_confirmation: true,
+ },
+ {
+ email: 'currentuser2@example.com',
+ pending_confirmation: true,
+ },
+ ];
+ await element.updateComplete;
+ const committerEmail = queryAndAssert(
+ element,
+ '.rebaseWithCommitterEmail'
+ );
+ const dropdownList: GrDropdownList = queryAndAssert(
+ committerEmail,
+ 'gr-dropdown-list'
+ );
+ assert.deepStrictEqual(
+ dropdownList.items!.map(e => e.value),
+ element.committerEmailDropdownItems.map(e => e.email)
+ );
+ });
+
+ test('show uploader emails in the dropdown list when rebase with conflicts is not allowed', async () => {
+ element.allowConflicts = false;
+ element.uploader = {_account_id: 2 as AccountId, name: '2'};
+ element.latestCommitter = {
+ email: 'commit@example.com' as EmailAddress,
+ name: 'committer',
+ date: '2023-06-12 18:32:08.000000000' as Timestamp,
+ };
+ element.committerEmailDropdownItems = [
+ {
+ email: 'uploader1@example.com',
+ preferred: true,
+ pending_confirmation: true,
+ },
+ {
+ email: 'uploader2@example.com',
+ preferred: false,
+ pending_confirmation: true,
+ },
+ ];
+ await element.updateComplete;
+ const committerEmail = queryAndAssert(
+ element,
+ '.rebaseWithCommitterEmail'
+ );
+ const dropdownList: GrDropdownList = queryAndAssert(
+ committerEmail,
+ 'gr-dropdown-list'
+ );
+ assert.deepStrictEqual(
+ dropdownList.items!.map(e => e.value),
+ element.committerEmailDropdownItems.map(e => e.email)
+ );
+ });
+ });
+
test('disableActions property disables dialog confirm', async () => {
element.disableActions = false;
await element.updateComplete;
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 68d13d9..952ba3c 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -803,6 +803,26 @@
} else return;
}
+ getAccountEmailsFor(email: string) {
+ return this.getLoggedIn()
+ .then(isLoggedIn => {
+ if (isLoggedIn) {
+ return this.getAccountCapabilities();
+ } else {
+ return undefined;
+ }
+ })
+ .then((capabilities: AccountCapabilityInfo | undefined) => {
+ if (capabilities && capabilities.viewSecondaryEmails) {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/' + email + '/emails',
+ reportUrlAsIs: true,
+ }) as Promise<EmailInfo[] | undefined>;
+ }
+ return undefined;
+ });
+ }
+
addAccountEmail(email: string): Promise<Response> {
return this._restApiHelper.send({
method: HttpMethod.PUT,
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index b081ff8..c70f780 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -229,6 +229,7 @@
saveEditPreferences(prefs: EditPreferencesInfo): Promise<Response>;
getAccountEmails(): Promise<EmailInfo[] | undefined>;
+ getAccountEmailsFor(email: string): Promise<EmailInfo[] | undefined>;
deleteAccountEmail(email: string): Promise<Response>;
setPreferredAccountEmail(email: string): Promise<void>;
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index b6fef81..e0a1682 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -191,6 +191,9 @@
getAccountEmails(): Promise<EmailInfo[] | undefined> {
return Promise.resolve([]);
},
+ getAccountEmailsFor(): Promise<EmailInfo[] | undefined> {
+ return Promise.resolve([]);
+ },
getAccountGPGKeys(): Promise<Record<string, GpgKeyInfo>> {
return Promise.resolve({});
},
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 7f197d6..8396abc 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1292,6 +1292,7 @@
viewConnections?: boolean;
viewPlugins?: boolean;
viewQueue?: boolean;
+ viewSecondaryEmails?: boolean;
}
/**