| // Copyright (C) 2022 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.restapi.change; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; | |
| import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Sets; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.extensions.api.changes.RebaseInput; | |
| import com.google.gerrit.extensions.client.ListChangesOption; | |
| import com.google.gerrit.extensions.common.ChangeInfo; | |
| import com.google.gerrit.extensions.common.RebaseChainInfo; | |
| import com.google.gerrit.extensions.restapi.BadRequestException; | |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; | |
| import com.google.gerrit.extensions.restapi.Response; | |
| import com.google.gerrit.extensions.restapi.RestApiException; | |
| import com.google.gerrit.extensions.restapi.RestModifyView; | |
| import com.google.gerrit.extensions.webui.UiAction; | |
| import com.google.gerrit.server.CurrentUser; | |
| import com.google.gerrit.server.PatchSetUtil; | |
| import com.google.gerrit.server.change.ChangeJson; | |
| import com.google.gerrit.server.change.ChangeResource; | |
| import com.google.gerrit.server.change.GetRelatedChangesUtil; | |
| import com.google.gerrit.server.change.NotifyResolver; | |
| import com.google.gerrit.server.change.RebaseChangeOp; | |
| import com.google.gerrit.server.change.RebaseUtil; | |
| import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData; | |
| import com.google.gerrit.server.change.RevisionResource; | |
| import com.google.gerrit.server.git.CodeReviewCommit; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.notedb.ChangeNotes; | |
| import com.google.gerrit.server.permissions.ChangePermission; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.query.change.ChangeData; | |
| import com.google.gerrit.server.update.BatchUpdate; | |
| import com.google.gerrit.server.update.UpdateException; | |
| import com.google.gerrit.server.update.context.RefUpdateContext; | |
| import com.google.gerrit.server.util.time.TimeUtil; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ObjectInserter; | |
| import org.eclipse.jgit.lib.ObjectReader; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| /** Rest API for rebasing an ancestry chain of changes. */ | |
| @Singleton | |
| public class RebaseChain | |
| implements RestModifyView<ChangeResource, RebaseInput>, UiAction<ChangeResource> { | |
| private static final ImmutableSet<ListChangesOption> OPTIONS = | |
| Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT); | |
| private final GitRepositoryManager repoManager; | |
| private final RebaseUtil rebaseUtil; | |
| private final GetRelatedChangesUtil getRelatedChangesUtil; | |
| private final ChangeResource.Factory changeResourceFactory; | |
| private final ChangeData.Factory changeDataFactory; | |
| private final PermissionBackend permissionBackend; | |
| private final BatchUpdate.Factory updateFactory; | |
| private final ChangeNotes.Factory notesFactory; | |
| private final ProjectCache projectCache; | |
| private final PatchSetUtil patchSetUtil; | |
| private final ChangeJson.Factory json; | |
| private final RebaseMetrics rebaseMetrics; | |
| @Inject | |
| RebaseChain( | |
| GitRepositoryManager repoManager, | |
| RebaseUtil rebaseUtil, | |
| GetRelatedChangesUtil getRelatedChangesUtil, | |
| ChangeResource.Factory changeResourceFactory, | |
| ChangeData.Factory changeDataFactory, | |
| PermissionBackend permissionBackend, | |
| BatchUpdate.Factory updateFactory, | |
| ChangeNotes.Factory notesFactory, | |
| ProjectCache projectCache, | |
| PatchSetUtil patchSetUtil, | |
| ChangeJson.Factory json, | |
| RebaseMetrics rebaseMetrics) { | |
| this.repoManager = repoManager; | |
| this.getRelatedChangesUtil = getRelatedChangesUtil; | |
| this.changeDataFactory = changeDataFactory; | |
| this.rebaseUtil = rebaseUtil; | |
| this.changeResourceFactory = changeResourceFactory; | |
| this.permissionBackend = permissionBackend; | |
| this.updateFactory = updateFactory; | |
| this.notesFactory = notesFactory; | |
| this.projectCache = projectCache; | |
| this.patchSetUtil = patchSetUtil; | |
| this.json = json; | |
| this.rebaseMetrics = rebaseMetrics; | |
| } | |
| @Override | |
| public Response<RebaseChainInfo> apply(ChangeResource tipRsrc, RebaseInput input) | |
| throws IOException, PermissionBackendException, RestApiException, UpdateException { | |
| if (input.onBehalfOfUploader) { | |
| tipRsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER); | |
| if (input.allowConflicts) { | |
| throw new BadRequestException( | |
| "allow_conflicts and on_behalf_of_uploader are mutually exclusive"); | |
| } | |
| } else { | |
| tipRsrc.permissions().check(ChangePermission.REBASE); | |
| } | |
| Project.NameKey project = tipRsrc.getProject(); | |
| projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite(); | |
| CurrentUser user = tipRsrc.getUser(); | |
| boolean anyRebaseOnBehalfOfUploader = false; | |
| List<Change.Id> upToDateAncestors = new ArrayList<>(); | |
| Map<Change.Id, RebaseChangeOp> rebaseOps = new LinkedHashMap<>(); | |
| try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) { | |
| try (Repository repo = repoManager.openRepository(project); | |
| ObjectInserter oi = repo.newObjectInserter(); | |
| ObjectReader reader = oi.newReader(); | |
| RevWalk rw = CodeReviewCommit.newRevWalk(reader); | |
| BatchUpdate bu = updateFactory.create(project, user, TimeUtil.now())) { | |
| List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc); | |
| boolean ancestorsAreUpToDate = true; | |
| for (int i = 0; i < chain.size(); i++) { | |
| ChangeData changeData = chain.get(i).data(); | |
| PatchSet ps = patchSetUtil.current(changeData.notes()); | |
| if (ps == null) { | |
| throw new IllegalStateException( | |
| "current revision is missing for change " + changeData.getId()); | |
| } | |
| RevisionResource revRsrc = | |
| new RevisionResource(changeResourceFactory.create(changeData, user), ps); | |
| if (input.onBehalfOfUploader | |
| && !revRsrc.getPatchSet().uploader().equals(revRsrc.getAccountId())) { | |
| revRsrc = rebaseUtil.onBehalfOf(revRsrc, input); | |
| revRsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER); | |
| anyRebaseOnBehalfOfUploader = true; | |
| } else { | |
| revRsrc.permissions().check(ChangePermission.REBASE); | |
| } | |
| rebaseUtil.verifyRebasePreconditions(rw, changeData.notes(), ps); | |
| boolean isUpToDate = false; | |
| RebaseChangeOp rebaseOp = null; | |
| if (i == 0) { | |
| ObjectId desiredBase = | |
| rebaseUtil.parseOrFindBaseRevision( | |
| repo, rw, permissionBackend, revRsrc, input, false); | |
| if (currentBase(rw, ps).equals(desiredBase)) { | |
| isUpToDate = true; | |
| } else { | |
| rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, desiredBase); | |
| } | |
| } else { | |
| if (ancestorsAreUpToDate) { | |
| ObjectId latestCommittedBase = | |
| PatchSetUtil.getCurrentCommittedRevCommit( | |
| project, rw, notesFactory, chain.get(i - 1).id()); | |
| isUpToDate = currentBase(rw, ps).equals(latestCommittedBase); | |
| } | |
| if (!isUpToDate) { | |
| rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, chain.get(i - 1).id()); | |
| } | |
| } | |
| if (isUpToDate) { | |
| upToDateAncestors.add(changeData.getId()); | |
| continue; | |
| } | |
| ancestorsAreUpToDate = false; | |
| bu.addOp(revRsrc.getChange().getId(), revRsrc.getUser(), rebaseOp); | |
| rebaseOps.put(revRsrc.getChange().getId(), rebaseOp); | |
| } | |
| if (ancestorsAreUpToDate) { | |
| throw new ResourceConflictException("The whole chain is already up to date."); | |
| } | |
| bu.setNotify(NotifyResolver.Result.none()); | |
| bu.setRepository(repo, rw, oi); | |
| bu.execute(); | |
| } | |
| } | |
| rebaseMetrics.countRebaseChain(anyRebaseOnBehalfOfUploader, input.allowConflicts); | |
| RebaseChainInfo res = new RebaseChainInfo(); | |
| res.rebasedChanges = new ArrayList<>(); | |
| ChangeJson changeJson = json.create(OPTIONS); | |
| for (Change.Id c : upToDateAncestors) { | |
| res.rebasedChanges.add(changeJson.format(project, c)); | |
| } | |
| for (Map.Entry<Change.Id, RebaseChangeOp> e : rebaseOps.entrySet()) { | |
| Change.Id id = e.getKey(); | |
| RebaseChangeOp op = e.getValue(); | |
| ChangeInfo changeInfo = changeJson.format(project, id); | |
| changeInfo.containsGitConflicts = | |
| !op.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null; | |
| res.rebasedChanges.add(changeInfo); | |
| } | |
| if (res.rebasedChanges.stream() | |
| .anyMatch(i -> i.containsGitConflicts != null && i.containsGitConflicts)) { | |
| res.containsGitConflicts = true; | |
| } | |
| return Response.ok(res); | |
| } | |
| @Override | |
| public Description getDescription(ChangeResource tipRsrc) throws Exception { | |
| UiAction.Description description = | |
| new UiAction.Description() | |
| .setLabel("Rebase Chain") | |
| .setTitle( | |
| "Rebase the ancestry chain onto the tip of the target branch. Makes you the " | |
| + "uploader of the changes which can affect validity of approvals.") | |
| .setVisible(false); | |
| Change tip = tipRsrc.getChange(); | |
| if (!tip.isNew()) { | |
| return description; | |
| } | |
| if (!projectCache | |
| .get(tipRsrc.getProject()) | |
| .orElseThrow(illegalState(tipRsrc.getProject())) | |
| .statePermitsWrite()) { | |
| return description; | |
| } | |
| if (patchSetUtil.isPatchSetLocked(tipRsrc.getNotes())) { | |
| return description; | |
| } | |
| boolean visible = true; | |
| boolean enabled; | |
| try (Repository repo = repoManager.openRepository(tipRsrc.getProject()); | |
| RevWalk rw = new RevWalk(repo)) { | |
| List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc); | |
| if (chain.size() <= 1) { | |
| return description; | |
| } | |
| PatchSetData oldestAncestor = chain.get(0); | |
| enabled = | |
| rebaseUtil.canRebase( | |
| oldestAncestor.patchSet(), oldestAncestor.data().change().getDest(), repo, rw); | |
| ImmutableList<RevisionResource> chainAsRevisionResources = | |
| chain.stream() | |
| .map( | |
| ps -> | |
| new RevisionResource( | |
| changeResourceFactory.create(ps.data(), tipRsrc.getUser()), | |
| ps.patchSet())) | |
| .collect(toImmutableList()); | |
| boolean canRebase = | |
| chainAsRevisionResources.stream() | |
| .allMatch(psRsrc -> psRsrc.permissions().testOrFalse(ChangePermission.REBASE)); | |
| boolean canRebaseOnBehalfOfUploader = | |
| chainAsRevisionResources.stream() | |
| .allMatch( | |
| psRsrc -> | |
| psRsrc | |
| .permissions() | |
| .testOrFalse(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER)); | |
| if (!canRebase && !canRebaseOnBehalfOfUploader) { | |
| visible = false; | |
| } else { | |
| for (RevisionResource psRsrc : chainAsRevisionResources) { | |
| if (patchSetUtil.isPatchSetLocked(psRsrc.getNotes()) | |
| || !RebaseUtil.hasOneParent(rw, psRsrc.getPatchSet())) { | |
| enabled = false; | |
| break; | |
| } | |
| } | |
| } | |
| return description | |
| .setVisible(visible) | |
| .setOption("rebase", canRebase) | |
| .setOption("rebase_on_behalf_of_uploader", canRebaseOnBehalfOfUploader) | |
| .setEnabled(enabled); | |
| } | |
| } | |
| private ObjectId currentBase(RevWalk rw, PatchSet ps) throws IOException { | |
| return rw.parseCommit(ps.commitId()).getParent(0); | |
| } | |
| private List<PatchSetData> getChainForCurrentPatchSet(ChangeResource rsrc) | |
| throws PermissionBackendException, IOException { | |
| List<PatchSetData> ancestors = | |
| Lists.reverse( | |
| getRelatedChangesUtil.getAncestors( | |
| changeDataFactory.create(rsrc.getNotes()), | |
| patchSetUtil.current(rsrc.getNotes()), | |
| true)); | |
| int eldestOpenAncestor = 0; | |
| for (PatchSetData ps : ancestors) { | |
| if (ps.data().change().isMerged()) { | |
| eldestOpenAncestor++; | |
| } | |
| } | |
| return ancestors.subList(eldestOpenAncestor, ancestors.size()); | |
| } | |
| } |