blob: 13f9904c2be79aafadffceb0361cf00e68e917ce [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance.api.change;
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.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.TestMetricMaker;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
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.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.RevisionCreatedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.FooterLine;
import org.junit.Test;
/**
* Tests for the {@link com.google.gerrit.server.restapi.change.RebaseChain} REST endpoint with the
* {@link RebaseInput#onBehalfOfUploader} option being set.
*
* <p>Rebasing a single change on behalf of the uploader is covered by {@link
* RebaseOnBehalfOfUploaderIT}.
*/
public class RebaseChainOnBehalfOfUploaderIT extends AbstractDaemonTest {
@Inject private AccountOperations accountOperations;
@Inject private ChangeOperations changeOperations;
@Inject private GroupOperations groupOperations;
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
@Inject private TestMetricMaker testMetricMaker;
@Test
public void cannotRebaseOnBehalfOfUploaderWithAllowConflicts() throws Exception {
Account.Id uploader = accountOperations.newAccount().create();
Change.Id changeId = changeOperations.newChange().owner(uploader).create();
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
rebaseInput.allowConflicts = true;
BadRequestException exception =
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(changeId.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo("allow_conflicts and on_behalf_of_uploader are mutually exclusive");
}
@Test
public void rebaseChangeOnBehalfOfUploader_withRebasePermission() throws Exception {
testRebaseChainOnBehalfOfUploader(Permission.REBASE);
}
@Test
public void rebaseChangeOnBehalfOfUploader_withSubmitPermission() throws Exception {
testRebaseChainOnBehalfOfUploader(Permission.SUBMIT);
}
private void testRebaseChainOnBehalfOfUploader(String permissionToAllow) throws Exception {
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
// Grant permission to rebaser that is required to rebase on behalf of the uploader.
AccountGroup.UUID allowedGroup =
groupOperations.newGroup().name("can-" + permissionToAllow).addMember(rebaser).create();
allowPermission(permissionToAllow, allowedGroup);
// Block push permission for rebaser, as in contrast to rebase, rebase on behalf of the uploader
// doesn't require the rebaser to have the push permission.
AccountGroup.UUID cannotUploadGroup =
groupOperations.newGroup().name("cannot-upload").addMember(rebaser).create();
blockPermission(Permission.PUSH, cannotUploadGroup);
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
// Create a chain of changes for being rebased, each change with a different uploader.
Account.Id uploader1 =
accountOperations.newAccount().preferredEmail("uploader1@example.com").create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader1).create();
Account.Id uploader2 =
accountOperations.newAccount().preferredEmail("uploader2@example.com").create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.childOf()
.change(changeToBeRebased1)
.owner(uploader2)
.create();
Account.Id uploader3 =
accountOperations.newAccount().preferredEmail("uploader3@example.com").create();
Change.Id changeToBeRebased3 =
changeOperations
.newChange()
.project(project)
.childOf()
.change(changeToBeRebased2)
.owner(uploader3)
.create();
Account.Id uploader4 =
accountOperations.newAccount().preferredEmail("uploader4@example.com").create();
Change.Id changeToBeRebased4 =
changeOperations
.newChange()
.project(project)
.childOf()
.change(changeToBeRebased3)
.owner(uploader4)
.create();
// Block rebase and submit permission for the uploaders. For rebase on behalf of the uploader
// only
// the rebaser needs to have these permission, but not the uploaders on whom's behalf the rebase
// is done.
AccountGroup.UUID cannotRebaseAndSubmitGroup =
groupOperations
.newGroup()
.name("cannot-rebase")
.addMember(uploader1)
.addMember(uploader2)
.addMember(uploader3)
.addMember(uploader4)
.create();
blockPermission(Permission.REBASE, cannotRebaseAndSubmitGroup);
blockPermission(Permission.SUBMIT, cannotRebaseAndSubmitGroup);
// Approve and submit the change that will be the new base for the chain 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 chain on behalf of the uploaders through changeToBeRebased4
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
TestRevisionCreatedListener testRevisionCreatedListener = new TestRevisionCreatedListener();
try (Registration registration =
extensionRegistry.newRegistration().add(testRevisionCreatedListener)) {
gApi.changes().id(changeToBeRebased4.get()).rebaseChain(rebaseInput);
testRevisionCreatedListener.assertUploaders(changeToBeRebased1, uploader1, rebaser);
testRevisionCreatedListener.assertUploaders(changeToBeRebased2, uploader2, rebaser);
testRevisionCreatedListener.assertUploaders(changeToBeRebased3, uploader3, rebaser);
testRevisionCreatedListener.assertUploaders(changeToBeRebased4, uploader4, rebaser);
}
assertRebase(changeToBeRebased1, 2, uploader1, rebaser);
assertRebase(changeToBeRebased2, 2, uploader2, rebaser);
assertRebase(changeToBeRebased3, 2, uploader3, rebaser);
assertRebase(changeToBeRebased4, 2, uploader4, rebaser);
}
@Test
public void rebaseChainOnBehalfOfUploaderMultipleTimesInARow() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
// Create a chain of changes for being rebased, each change with a different uploader.
Account.Id uploader1 =
accountOperations.newAccount().preferredEmail("uploader1@example.com").create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader1).create();
Account.Id uploader2 =
accountOperations.newAccount().preferredEmail("uploader2@example.com").create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.childOf()
.change(changeToBeRebased1)
.owner(uploader2)
.create();
// Approve and submit the change that will be the new base for the chain 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 chain on behalf of the uploaders.
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
assertRebase(changeToBeRebased1, 2, uploader1, rebaser);
assertRebase(changeToBeRebased2, 2, uploader2, rebaser);
// Create and submit another change so that we can rebase the chain once again.
requestScopeOperations.setApiUser(approver);
Change.Id changeToBeTheNewBase2 = changeOperations.newChange().project(project).create();
gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
// Rebase the chain once again on behalf of the uploaders.
requestScopeOperations.setApiUser(rebaser);
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
assertRebase(changeToBeRebased1, 3, uploader1, rebaser);
assertRebase(changeToBeRebased2, 3, uploader2, rebaser);
}
@Test
public void nonChangeOwnerWithoutSubmitAndRebasePermissionCannotRebaseChainOnBehalfOfUploader()
throws Exception {
Change.Id changeToBeRebased1 = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased2 =
changeOperations.newChange().project(project).childOf().change(changeToBeRebased1).create();
blockPermissionForAllUsers(Permission.REBASE);
blockPermissionForAllUsers(Permission.SUBMIT);
Account.Id rebaserId = accountOperations.newAccount().create();
requestScopeOperations.setApiUser(rebaserId);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
AuthException exception =
assertThrows(
AuthException.class,
() -> gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
"rebase on behalf of uploader not permitted (change owners and users with the 'Submit'"
+ " or 'Rebase' permission can rebase on behalf of the uploader)");
}
@Test
public void cannotRebaseChainOnBehalfOfUploaderIfTheUploaderHasNoReadPermission()
throws Exception {
String uploaderEmail = "uploader@example.com";
testCannotRebaseChainOnBehalfOfUploaderIfTheUploaderHasNoPermission(
uploaderEmail,
Permission.READ,
String.format("uploader %s cannot read change", uploaderEmail));
}
@Test
public void cannotRebaseChainOnBehalfOfUploaderIfTheUploaderHasNoPushPermission()
throws Exception {
String uploaderEmail = "uploader@example.com";
testCannotRebaseChainOnBehalfOfUploaderIfTheUploaderHasNoPermission(
uploaderEmail,
Permission.PUSH,
String.format("uploader %s cannot add patch set", uploaderEmail));
}
private void testCannotRebaseChainOnBehalfOfUploaderIfTheUploaderHasNoPermission(
String uploaderEmail, String permissionToBlock, String expectedErrorMessage)
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Block the required permission for uploader. Without this permission it should not be possible
// to rebase the change on behalf of the uploader.
AccountGroup.UUID blockedGroup =
groupOperations.newGroup().name("cannot-" + permissionToBlock).addMember(uploader).create();
blockPermission(permissionToBlock, blockedGroup);
// Try to rebase the chain on behalf of the uploader.
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(String.format("change %s: %s", changeToBeRebased1, expectedErrorMessage));
}
@Test
public void rebaseChainOnBehalfOfYourself() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id uploader =
accountOperations.newAccount().preferredEmail("uploader@example.com").create();
Account.Id approver = admin.id();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain 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 chain as uploader on behalf of the uploader
requestScopeOperations.setApiUser(uploader);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
assertRebase(changeToBeRebased1, 2, uploader, /* expectedRealUploader= */ null);
assertRebase(changeToBeRebased2, 2, uploader, /* expectedRealUploader= */ null);
}
@Test
public void cannotRebaseChangeOnBehalfOfYourselfWithoutPushPermission() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id uploader =
accountOperations.newAccount().preferredEmail("uploader@example.com").create();
Account.Id approver = admin.id();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Block push for the uploader aka the rebaser. This permission is required for creating the new
// patch set and if it is blocked we expect the rebase to fail.
AccountGroup.UUID cannotPushGroup =
groupOperations.newGroup().name("cannot-push").addMember(uploader).create();
blockPermission(Permission.PUSH, cannotPushGroup);
// Rebase the chain as uploader on behalf of the uploader
requestScopeOperations.setApiUser(uploader);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
AuthException exception =
assertThrows(
AuthException.class,
() -> gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
"rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ " permission can rebase if they have the 'Push' permission)");
}
@Test
public void rebaseChainOnBehalfOfUploaderWhenUploaderIsNotTheChangeOwner() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id changeOwner =
accountOperations.newAccount().preferredEmail("change-owner@example.com").create();
Account.Id uploader =
accountOperations.newAccount().preferredEmail("uploader@example.com").create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(changeOwner).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(changeOwner)
.childOf()
.change(changeToBeRebased1)
.create();
// Create a second patch set for the second change in the chain that will be rebased so that the
// uploader is different to the change owner.
// Set author and committer to the uploader so that rebasing on behalf of the uploader doesn't
// require the Forge Author and Forge Committer permission.
changeOperations
.change(changeToBeRebased2)
.newPatchset()
.uploader(uploader)
.author(uploader)
.committer(uploader)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Grant add patch set permission for uploader. Without the add patch set permission it is not
// possible to rebase the change on behalf of the uploader since the uploader cannot add a
// patch set to a change that is owned by another user.
AccountGroup.UUID canAddPatchSet =
groupOperations.newGroup().name("can-add-patch-set").addMember(uploader).create();
allowPermission(Permission.ADD_PATCH_SET, canAddPatchSet);
// Rebase the chain on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
assertRebase(changeToBeRebased1, 2, changeOwner, rebaser);
assertRebase(changeToBeRebased2, 3, uploader, rebaser);
}
@Test
public void
cannotRebaseChainOnBehalfOfUploaderWhenUploaderIsNotTheChangeOwnerAndDoesntHaveAddPatchSetPermission()
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id changeOwner = accountOperations.newAccount().create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(changeOwner).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(changeOwner)
.childOf()
.change(changeToBeRebased1)
.create();
// Create a second patch set for the second change in the chain that will be rebased so that the
// uploader is different to the change owner.
// Set author and committer to the uploader so that rebasing on behalf of the uploader doesn't
// require the Forge Author and Forge Committer permission.
changeOperations
.change(changeToBeRebased2)
.newPatchset()
.uploader(uploader)
.author(uploader)
.committer(uploader)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Block add patch set permission for uploader. Without the add patch set permission it should
// not possible to rebase the change on behalf of the uploader since the uploader cannot add a
// patch set to a change that is owned by another user.
AccountGroup.UUID cannotAddPatchSet =
groupOperations.newGroup().name("cannot-add-patch-set").addMember(uploader).create();
blockPermission(Permission.ADD_PATCH_SET, cannotAddPatchSet);
// Try to rebase the chain on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"change %s: uploader %s cannot add patch set", changeToBeRebased2, uploaderEmail));
}
@Test
public void rebaseChainWithForgedAuthorOnBehalfOfUploader() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String authorEmail = "author@example.com";
Account.Id author = accountOperations.newAccount().preferredEmail(authorEmail).create();
Account.Id uploader =
accountOperations.newAccount().preferredEmail("uploader@example.com").create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).author(author).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.author(author)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Grant forge author permission for uploader. Without the forge author permission it is not
// possible to rebase the change on behalf of the uploader.
AccountGroup.UUID canForgeAuthor =
groupOperations.newGroup().name("can-forge-author").addMember(uploader).create();
allowPermission(Permission.FORGE_AUTHOR, canForgeAuthor);
// Rebase the second change on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
RevisionInfo currentRevisionInfo =
gApi.changes().id(changeToBeRebased1.get()).get().getCurrentRevision();
// The change had 1 patch set before the rebase, now it should be 2
assertThat(currentRevisionInfo._number).isEqualTo(2);
assertThat(currentRevisionInfo.commit.author.email).isEqualTo(authorEmail);
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get());
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get());
currentRevisionInfo = gApi.changes().id(changeToBeRebased2.get()).get().getCurrentRevision();
// The change had 1 patch set before the rebase, now it should be 2
assertThat(currentRevisionInfo._number).isEqualTo(2);
assertThat(currentRevisionInfo.commit.author.email).isEqualTo(authorEmail);
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get());
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get());
}
@Test
public void
cannotRebaseChainWithForgedAuthorOnBehalfOfUploaderIfTheUploaderHasNoForgeAuthorPermission()
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id author = accountOperations.newAccount().create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).author(author).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.author(author)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Block forge author permission for uploader. Without the forge author permission it should not
// be possible to rebase the chain on behalf of the uploader.
AccountGroup.UUID cannotForgeAuthor =
groupOperations.newGroup().name("cannot-forge-author").addMember(uploader).create();
blockPermission(Permission.FORGE_AUTHOR, cannotForgeAuthor);
// Try to rebase the second change on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"change %s: author of patch set 1 is forged and the uploader %s cannot forge author",
changeToBeRebased1, uploaderEmail));
}
@Test
public void
rebaseChainWithForgedCommitterOnBehalfOfUploaderDoesntRequireForgeCommitterPermission()
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id committer =
accountOperations.newAccount().preferredEmail("committer@example.com").create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).committer(committer).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.committer(committer)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain 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;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
RevisionInfo currentRevisionInfo =
gApi.changes().id(changeToBeRebased1.get()).get().getCurrentRevision();
// The change had 1 patch set before the rebase, now it should be 2
assertThat(currentRevisionInfo._number).isEqualTo(2);
assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail);
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get());
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get());
currentRevisionInfo = gApi.changes().id(changeToBeRebased2.get()).get().getCurrentRevision();
// The change had 1 patch set before the rebase, now it should be 2
assertThat(currentRevisionInfo._number).isEqualTo(2);
assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail);
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get());
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get());
}
@Test
public void rebaseChainWithServerIdentOnBehalfOfUploader() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.authorIdent(serverIdent.get())
.create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.authorIdent(serverIdent.get())
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Grant forge author and forge server permission for uploader. Without these permissions it is
// not possible to rebase the change on behalf of the uploader.
AccountGroup.UUID canForgeAuthorAndForgeServer =
groupOperations
.newGroup()
.name("can-forge-author-and-forge-server")
.addMember(uploader)
.create();
allowPermission(Permission.FORGE_AUTHOR, canForgeAuthorAndForgeServer);
allowPermission(Permission.FORGE_SERVER, canForgeAuthorAndForgeServer);
// Rebase the chain on behalf of the uploader.
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
RevisionInfo currentRevisionInfo =
gApi.changes().id(changeToBeRebased1.get()).get().getCurrentRevision();
// The change had 1 patch set before the rebase, now it should be 2
assertThat(currentRevisionInfo._number).isEqualTo(2);
assertThat(currentRevisionInfo.commit.author.email)
.isEqualTo(serverIdent.get().getEmailAddress());
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get());
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get());
currentRevisionInfo = gApi.changes().id(changeToBeRebased2.get()).get().getCurrentRevision();
// The change had 1 patch set before the rebase, now it should be 2
assertThat(currentRevisionInfo._number).isEqualTo(2);
assertThat(currentRevisionInfo.commit.author.email)
.isEqualTo(serverIdent.get().getEmailAddress());
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get());
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get());
}
@Test
public void
cannotRebaseChainWithServerIdentOnBehalfOfUploaderIfTheUploaderHasNoForgeServerPermission()
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.authorIdent(serverIdent.get())
.create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.authorIdent(serverIdent.get())
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Grant forge author permission for uploader, but not the forge server permission. Without the
// forge server permission it is not possible to rebase the change on behalf of the uploader.
AccountGroup.UUID canForgeAuthor =
groupOperations.newGroup().name("can-forge-author").addMember(uploader).create();
allowPermission(Permission.FORGE_AUTHOR, canForgeAuthor);
// Try to rebase the chain on behalf of the uploader.
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"change %s: author of patch set 1 is the server identity and the uploader %s cannot forge"
+ " the server identity",
changeToBeRebased1, uploaderEmail));
}
@Test
public void rebaseChainActionEnabled_withRebasePermission() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
testRebaseChainActionEnabled();
}
@Test
public void rebaseChainActionEnabled_withSubmitPermission() throws Exception {
allowPermissionToAllUsers(Permission.SUBMIT);
testRebaseChainActionEnabled();
}
private void testRebaseChainActionEnabled() throws Exception {
Account.Id uploader = accountOperations.newAccount().create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
// Block push permission for rebaser, as in contrast to rebase, rebase on behalf of the uploader
// doesn't require the rebaser to have the push permission.
AccountGroup.UUID cannotUploadGroup =
groupOperations.newGroup().name("cannot-upload").addMember(rebaser).create();
blockPermission(Permission.PUSH, cannotUploadGroup);
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain so that the chain is
// rebasable.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
requestScopeOperations.setApiUser(rebaser);
ChangeInfo changeInfo = gApi.changes().id(changeToBeRebased2.get()).get();
assertThat(changeInfo.actions).containsKey("rebase:chain");
ActionInfo rebaseActionInfo = changeInfo.actions.get("rebase:chain");
assertThat(rebaseActionInfo.enabled).isTrue();
// rebase is disabled because rebaser doesn't have the 'Push' permission and hence cannot create
// new patch sets
assertThat(rebaseActionInfo.enabledOptions).containsExactly("rebase_on_behalf_of_uploader");
}
@Test
public void rebaseChainActionEnabled_forChangeOwner() throws Exception {
Account.Id changeOwner = accountOperations.newAccount().create();
Account.Id approver = admin.id();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(changeOwner).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(changeOwner)
.childOf()
.change(changeToBeRebased1)
.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();
requestScopeOperations.setApiUser(changeOwner);
ChangeInfo changeInfo = gApi.changes().id(changeToBeRebased2.get()).get();
assertThat(changeInfo.actions).containsKey("rebase:chain");
ActionInfo rebaseActionInfo = changeInfo.actions.get("rebase:chain");
assertThat(rebaseActionInfo.enabled).isTrue();
// rebase is enabled because change owner has the 'Push' permission and hence can create new
// patch sets
assertThat(rebaseActionInfo.enabledOptions)
.containsExactly("rebase", "rebase_on_behalf_of_uploader");
}
@UseLocalDisk
@Test
public void rebaseChainWithIdenticalUploadersOnBehalfOfUploaderRecordsUploaderInRefLog()
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
try (Repository repo = repoManager.openRepository(project)) {
String changeMetaRef1 = RefNames.changeMetaRef(changeToBeRebased1);
String patchSetRef1 = RefNames.patchSetRef(PatchSet.id(changeToBeRebased1, 2));
String changeMetaRef2 = RefNames.changeMetaRef(changeToBeRebased2);
String patchSetRef2 = RefNames.patchSetRef(PatchSet.id(changeToBeRebased2, 2));
createRefLogFileIfMissing(repo, changeMetaRef1);
createRefLogFileIfMissing(repo, patchSetRef1);
createRefLogFileIfMissing(repo, changeMetaRef2);
createRefLogFileIfMissing(repo, patchSetRef2);
// Rebase the chain on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
// The ref log for the patch set ref records the impersonated user aka the uploader.
ReflogEntry patchSetRefLogEntry1 = repo.getReflogReader(patchSetRef1).getLastEntry();
assertThat(patchSetRefLogEntry1.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
ReflogEntry patchSetRefLogEntry2 = repo.getReflogReader(patchSetRef2).getLastEntry();
assertThat(patchSetRefLogEntry2.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
// The ref log for the change meta ref records the impersonated user aka the uploader.
ReflogEntry changeMetaRefLogEntry1 = repo.getReflogReader(changeMetaRef1).getLastEntry();
assertThat(changeMetaRefLogEntry1.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
ReflogEntry changeMetaRefLogEntry2 = repo.getReflogReader(changeMetaRef2).getLastEntry();
assertThat(changeMetaRefLogEntry2.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
}
}
@UseLocalDisk
@Test
public void rebaseChainWithDifferentUploadersOnBehalfOfUploaderRecordsCombinedIdentityInRefLog()
throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Account.Id uploader1 = accountOperations.newAccount().create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader1).create();
Account.Id uploader2 = accountOperations.newAccount().create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader2)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
try (Repository repo = repoManager.openRepository(project)) {
String changeMetaRef1 = RefNames.changeMetaRef(changeToBeRebased1);
String patchSetRef1 = RefNames.patchSetRef(PatchSet.id(changeToBeRebased1, 2));
String changeMetaRef2 = RefNames.changeMetaRef(changeToBeRebased2);
String patchSetRef2 = RefNames.patchSetRef(PatchSet.id(changeToBeRebased2, 2));
createRefLogFileIfMissing(repo, changeMetaRef1);
createRefLogFileIfMissing(repo, patchSetRef1);
createRefLogFileIfMissing(repo, changeMetaRef2);
createRefLogFileIfMissing(repo, patchSetRef2);
// Rebase the chain on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
String combinedEmail = String.format("account-%s|account-%s@unknown", uploader1, uploader2);
// The ref log for the patch set ref records the impersonated user aka the uploader.
ReflogEntry patchSetRefLogEntry1 = repo.getReflogReader(patchSetRef1).getLastEntry();
assertThat(patchSetRefLogEntry1.getWho().getEmailAddress()).isEqualTo(combinedEmail);
ReflogEntry patchSetRefLogEntry2 = repo.getReflogReader(patchSetRef2).getLastEntry();
assertThat(patchSetRefLogEntry2.getWho().getEmailAddress()).isEqualTo(combinedEmail);
// The ref log for the change meta ref records the impersonated user aka the uploader.
ReflogEntry changeMetaRefLogEntry1 = repo.getReflogReader(changeMetaRef1).getLastEntry();
assertThat(changeMetaRefLogEntry1.getWho().getEmailAddress()).isEqualTo(combinedEmail);
ReflogEntry changeMetaRefLogEntry2 = repo.getReflogReader(changeMetaRef2).getLastEntry();
assertThat(changeMetaRefLogEntry2.getWho().getEmailAddress()).isEqualTo(combinedEmail);
}
}
@Test
public void rebaserCanApproveChainAfterRebasingOnBehalfOfUploader() throws Exception {
// Require a Code-Review approval from a non-uploader for submit.
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.upsertSubmitRequirement(
SubmitRequirement.builder()
.setName(TestLabels.codeReview().getName())
.setSubmittabilityExpression(
SubmitRequirementExpression.create(
String.format(
"label:%s=MAX,user=non_uploader", TestLabels.codeReview().getName())))
.setAllowOverrideInChildProjects(false)
.build());
u.save();
}
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase =
changeOperations.newChange().owner(uploader).project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Rebase it on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
// Approve the chain as the rebaser.
allowVotingOnCodeReviewToAllUsers();
gApi.changes().id(changeToBeRebased1.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeRebased2.get()).current().review(ReviewInput.approve());
// The chain is submittable because the approval is from a user (the rebaser) that is not the
// uploader.
assertThat(gApi.changes().id(changeToBeRebased1.get()).get().submittable).isTrue();
assertThat(gApi.changes().id(changeToBeRebased2.get()).get().submittable).isTrue();
// Create and submit another change so that we can rebase the chain once again.
requestScopeOperations.setApiUser(approver);
Change.Id changeToBeTheNewBase2 =
changeOperations.newChange().project(project).owner(uploader).create();
gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
// Doing a normal rebase (not on behalf of the uploader) makes the rebaser the uploader. This
// makse the chain non-submittable since the approval of the rebaser is ignored now (due to
// using 'user=non_uploader' in the submit requirement expression).
requestScopeOperations.setApiUser(rebaser);
rebaseInput.onBehalfOfUploader = false;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
gApi.changes().id(changeToBeRebased1.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeRebased2.get()).current().review(ReviewInput.approve());
assertThat(gApi.changes().id(changeToBeRebased1.get()).get().submittable).isFalse();
assertThat(gApi.changes().id(changeToBeRebased2.get()).get().submittable).isFalse();
}
@Test
public void testSubmittedWithRebaserApprovalMetric() throws Exception {
// Require a Code-Review approval from a non-uploader for submit.
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.upsertSubmitRequirement(
SubmitRequirement.builder()
.setName(TestLabels.codeReview().getName())
.setSubmittabilityExpression(
SubmitRequirementExpression.create(
String.format(
"label:%s=MAX,user=non_uploader", TestLabels.codeReview().getName())))
.setAllowOverrideInChildProjects(false)
.build());
u.save();
}
allowPermissionToAllUsers(Permission.REBASE);
String uploaderEmail = "uploader@example.com";
Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase =
changeOperations.newChange().owner(uploader).project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
testMetricMaker.reset();
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(0);
// Rebase it on behalf of the uploader
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
// Approve the chain as the rebaser.
allowVotingOnCodeReviewToAllUsers();
gApi.changes().id(changeToBeRebased1.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeRebased2.get()).current().review(ReviewInput.approve());
// The chain is submittable because the approval is from a user (the rebaser) that is not the
// uploader.
allowPermissionToAllUsers(Permission.SUBMIT);
testMetricMaker.reset();
gApi.changes().id(changeToBeRebased1.get()).current().submit();
gApi.changes().id(changeToBeRebased2.get()).current().submit();
assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(2);
}
@Test
public void testCountRebasesMetric() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
Account.Id uploader = accountOperations.newAccount().create();
Account.Id approver = admin.id();
Account.Id rebaser = accountOperations.newAccount().create();
Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
Change.Id changeToBeRebased1 =
changeOperations.newChange().project(project).owner(uploader).create();
Change.Id changeToBeRebased2 =
changeOperations
.newChange()
.project(project)
.owner(uploader)
.childOf()
.change(changeToBeRebased1)
.create();
// Approve and submit the change that will be the new base for the chain that will be rebased.
requestScopeOperations.setApiUser(approver);
gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
// Rebase it on behalf of the uploader
testMetricMaker.reset();
requestScopeOperations.setApiUser(rebaser);
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.onBehalfOfUploader = true;
gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
// field1 is on_behalf_of_uploader, field2 is rebase_chain
assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(1);
assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(0);
assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(0);
assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(0);
}
private void assertRebase(
Change.Id changeId,
int expectedPatchSetNum,
Account.Id expectedUploader,
@Nullable Account.Id expectedRealUploader)
throws RestApiException {
assertRebaseRevision(changeId, expectedPatchSetNum, expectedUploader, expectedRealUploader);
assetRebaseChangeMessage(changeId, expectedPatchSetNum, expectedUploader, expectedRealUploader);
assertRealUserForChangeUpdate(changeId, expectedRealUploader);
}
private void assertRebaseRevision(
Change.Id changeId,
int expectedPatchSetNum,
Account.Id expectedUploader,
@Nullable Account.Id expectedRealUploader)
throws RestApiException {
RevisionInfo currentRevisionInfo = gApi.changes().id(changeId.get()).get().getCurrentRevision();
assertThat(currentRevisionInfo._number).isEqualTo(expectedPatchSetNum);
assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(expectedUploader.get());
if (expectedRealUploader != null) {
assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(expectedRealUploader.get());
} else {
assertThat(currentRevisionInfo.realUploader).isNull();
}
String uploaderEmail = accountOperations.account(expectedUploader).get().preferredEmail().get();
assertThat(currentRevisionInfo.commit.author.email).isEqualTo(uploaderEmail);
assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail);
}
private void assetRebaseChangeMessage(
Change.Id changeId,
int expectedPatchSetNum,
Account.Id expectedUploader,
@Nullable Account.Id expectedRealUploader)
throws RestApiException {
Collection<ChangeMessageInfo> changeMessages = gApi.changes().id(changeId.get()).get().messages;
// Expect 1 change message per patch set.
assertThat(changeMessages).hasSize(expectedPatchSetNum);
ChangeMessageInfo changeMessage = Iterables.getLast(changeMessages);
assertThat(changeMessage.author._accountId).isEqualTo(expectedUploader.get());
if (expectedRealUploader != null) {
assertThat(changeMessage.message)
.isEqualTo(
String.format(
"Patch Set %d: Patch Set %d was rebased on behalf of %s",
expectedPatchSetNum,
expectedPatchSetNum - 1,
AccountTemplateUtil.getAccountTemplate(expectedUploader)));
assertThat(changeMessage.realAuthor._accountId).isEqualTo(expectedRealUploader.get());
} else {
assertThat(changeMessage.message)
.isEqualTo(
String.format(
"Patch Set %d: Patch Set %d was rebased",
expectedPatchSetNum, expectedPatchSetNum - 1));
assertThat(changeMessage.realAuthor).isNull();
}
}
private void assertRealUserForChangeUpdate(
Change.Id changeId, @Nullable Account.Id expectedRealUser) {
Optional<FooterLine> realUserFooter =
projectOperations.project(project).getHead(RefNames.changeMetaRef(changeId))
.getFooterLines().stream()
.filter(footerLine -> footerLine.matches(FOOTER_REAL_USER))
.findFirst();
if (expectedRealUser != null) {
assertThat(realUserFooter.map(FooterLine::getValue))
.hasValue(
String.format(
"%s <%s>",
ChangeNoteUtil.getAccountIdAsUsername(expectedRealUser),
changeNoteUtil.getAccountIdAsEmailAddress(expectedRealUser)));
} else {
assertThat(realUserFooter).isEmpty();
}
}
private void allowPermissionToAllUsers(String permission) {
allowPermission(permission, REGISTERED_USERS);
}
private void allowPermission(String permission, AccountGroup.UUID groupUuid) {
projectOperations
.project(project)
.forUpdate()
.add(allow(permission).ref("refs/*").group(groupUuid))
.update();
}
private void allowVotingOnCodeReviewToAllUsers() {
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(TestLabels.codeReview().getName())
.ref("refs/*")
.group(REGISTERED_USERS)
.range(-2, 2))
.update();
}
private void blockPermissionForAllUsers(String permission) {
blockPermission(permission, REGISTERED_USERS);
}
private void blockPermission(String permission, AccountGroup.UUID groupUuid) {
projectOperations
.project(project)
.forUpdate()
.add(block(permission).ref("refs/*").group(groupUuid))
.update();
}
private static class TestRevisionCreatedListener implements RevisionCreatedListener {
private Map<Change.Id, RevisionInfo> revisionInfos = new HashMap<>();
void assertUploaders(
Change.Id changeId, Account.Id expectedUploader, Account.Id expectedRealUploader) {
RevisionInfo revisionInfo = revisionInfos.get(changeId);
assertThat(revisionInfo.uploader._accountId).isEqualTo(expectedUploader.get());
assertThat(revisionInfo.realUploader._accountId).isEqualTo(expectedRealUploader.get());
}
@Override
public void onRevisionCreated(RevisionCreatedListener.Event event) {
revisionInfos.put(Change.id(event.getChange()._number), event.getRevision());
}
}
}