| // 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, field3 is allow_conflicts |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(1); |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(0); |
| |
| // Create and submit another change so that we can rebase the change 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(); |
| |
| // Rebase the change once again, this time as the uploader. |
| // If the uploader sets on_behalf_of_uploader = true, the flag is ignored and a normal rebase is |
| // done, hence the metric should count this as a a rebase with on_behalf_of_uploader = false. |
| requestScopeOperations.setApiUser(uploader); |
| testMetricMaker.reset(); |
| gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput); |
| // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(1); |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).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()); |
| } |
| } |
| } |