| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.change; |
| |
| import com.google.gerrit.common.errors.EmailException; |
| 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.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.RestModifyView; |
| import com.google.gerrit.extensions.webui.UiAction; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.Change.Status; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.PatchSetAncestor; |
| import com.google.gerrit.reviewdb.client.RevId; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.changedetail.RebaseChange; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gerrit.server.project.InvalidChangeOperationException; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| |
| import java.util.ArrayList; |
| |
| @Singleton |
| public class Rebase implements RestModifyView<RevisionResource, RebaseInput>, |
| UiAction<RevisionResource> { |
| |
| private static final Logger log = |
| LoggerFactory.getLogger(Rebase.class); |
| |
| private final Provider<RebaseChange> rebaseChange; |
| private final ChangeJson json; |
| private final Provider<ReviewDb> dbProvider; |
| |
| @Inject |
| public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json, |
| Provider<ReviewDb> dbProvider) { |
| this.rebaseChange = rebaseChange; |
| this.json = json |
| .addOption(ListChangesOption.CURRENT_REVISION) |
| .addOption(ListChangesOption.CURRENT_COMMIT); |
| this.dbProvider = dbProvider; |
| } |
| |
| @Override |
| public ChangeInfo apply(RevisionResource rsrc, RebaseInput input) |
| throws AuthException, ResourceNotFoundException, |
| ResourceConflictException, EmailException, OrmException { |
| ChangeControl control = rsrc.getControl(); |
| Change change = rsrc.getChange(); |
| if (!control.canRebase()) { |
| throw new AuthException("rebase not permitted"); |
| } else if (!change.getStatus().isOpen()) { |
| throw new ResourceConflictException("change is " |
| + change.getStatus().name().toLowerCase()); |
| } else if (!hasOneParent(rsrc.getPatchSet().getId())) { |
| throw new ResourceConflictException( |
| "cannot rebase merge commits or commit with no ancestor"); |
| } |
| |
| String baseRev = null; |
| if (input != null && input.base != null) { |
| String base = input.base.trim(); |
| do { |
| if (base.equals("")) { |
| // remove existing dependency to other patch set |
| baseRev = change.getDest().get(); |
| break; |
| } |
| |
| ReviewDb db = dbProvider.get(); |
| PatchSet basePatchSet = parseBase(base); |
| if (basePatchSet == null) { |
| throw new ResourceConflictException("base revision is missing: " + base); |
| } else if (!rsrc.getControl().isPatchVisible(basePatchSet, db)) { |
| throw new AuthException("base revision not accessible: " + base); |
| } else if (change.getId().equals(basePatchSet.getId().getParentKey())) { |
| throw new ResourceConflictException("cannot depend on self"); |
| } |
| |
| Change baseChange = db.changes().get(basePatchSet.getId().getParentKey()); |
| if (baseChange != null) { |
| if (!baseChange.getProject().equals(change.getProject())) { |
| throw new ResourceConflictException("base change is in wrong project: " |
| + baseChange.getProject()); |
| } else if (!baseChange.getDest().equals(change.getDest())) { |
| throw new ResourceConflictException("base change is targetting wrong branch: " |
| + baseChange.getDest()); |
| } else if (baseChange.getStatus() == Status.ABANDONED) { |
| throw new ResourceConflictException("base change is abandoned: " |
| + baseChange.getKey()); |
| } else if (isDescendantOf(baseChange.getId(), rsrc.getPatchSet().getRevision())) { |
| throw new ResourceConflictException("base change " + baseChange.getKey() |
| + " is a descendant of the current " |
| + " change - recursion not allowed"); |
| } |
| baseRev = basePatchSet.getRevision().get(); |
| break; |
| } |
| } while (false); // just wanted to use the break statement |
| } |
| |
| try { |
| rebaseChange.get().rebase(change, rsrc.getPatchSet().getId(), |
| rsrc.getUser(), baseRev); |
| } catch (InvalidChangeOperationException e) { |
| throw new ResourceConflictException(e.getMessage()); |
| } catch (IOException e) { |
| throw new ResourceConflictException(e.getMessage()); |
| } catch (NoSuchChangeException e) { |
| throw new ResourceNotFoundException(change.getId().toString()); |
| } |
| |
| return json.format(change.getId()); |
| } |
| |
| private boolean isDescendantOf(Change.Id child, RevId ancestor) |
| throws OrmException { |
| ReviewDb db = dbProvider.get(); |
| |
| ArrayList<RevId> parents = new ArrayList<>(); |
| parents.add(ancestor); |
| while (!parents.isEmpty()) { |
| RevId parent = parents.remove(0); |
| // get direct descendants of change |
| for (PatchSetAncestor desc : db.patchSetAncestors().descendantsOf(parent)) { |
| PatchSet descPatchSet = db.patchSets().get(desc.getPatchSet()); |
| Change.Id descChangeId = descPatchSet.getId().getParentKey(); |
| if (child.equals(descChangeId)) { |
| PatchSet.Id descCurrentPatchSetId = |
| db.changes().get(descChangeId).currentPatchSetId(); |
| // it's only bad if the descendant patch set is current |
| return descPatchSet.getId().equals(descCurrentPatchSetId); |
| } else { |
| // process indirect descendants as well |
| parents.add(descPatchSet.getRevision()); |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| private PatchSet parseBase(final String base) throws OrmException { |
| ReviewDb db = dbProvider.get(); |
| |
| PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base); |
| if (basePatchSetId != null) { |
| // try parsing the base as a ref string |
| return db.patchSets().get(basePatchSetId); |
| } |
| |
| // try parsing base as a change number (assume current patch set) |
| PatchSet basePatchSet = null; |
| try { |
| Change.Id baseChangeId = Change.Id.parse(base); |
| if (baseChangeId != null) { |
| for (PatchSet ps : db.patchSets().byChange(baseChangeId)) { |
| if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()){ |
| basePatchSet = ps; |
| } |
| } |
| } |
| } catch (NumberFormatException e) { // probably a SHA1 |
| } |
| |
| // try parsing as SHA1 |
| if (basePatchSet == null) { |
| for (PatchSet ps : db.patchSets().byRevision(new RevId(base))) { |
| if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()) { |
| basePatchSet = ps; |
| } |
| } |
| } |
| |
| return basePatchSet; |
| } |
| |
| private boolean hasOneParent(final PatchSet.Id patchSetId) { |
| try { |
| // prevent rebase of exotic changes (merge commit, no ancestor). |
| return (dbProvider.get().patchSetAncestors() |
| .ancestorsOf(patchSetId).toList().size() == 1); |
| } catch (OrmException e) { |
| log.error("Failed to get ancestors of patch set " |
| + patchSetId.toRefName(), e); |
| return false; |
| } |
| } |
| |
| @Override |
| public UiAction.Description getDescription(RevisionResource resource) { |
| return new UiAction.Description() |
| .setLabel("Rebase") |
| .setTitle("Rebase onto tip of branch or parent change") |
| .setVisible(resource.getChange().getStatus().isOpen() |
| && resource.isCurrent() |
| && resource.getControl().canRebase() |
| && hasOneParent(resource.getPatchSet().getId())); |
| } |
| |
| public static class CurrentRevision implements |
| RestModifyView<ChangeResource, RebaseInput> { |
| private final Rebase rebase; |
| |
| @Inject |
| CurrentRevision(Rebase rebase) { |
| this.rebase = rebase; |
| } |
| |
| @Override |
| public ChangeInfo apply(ChangeResource rsrc, RebaseInput input) |
| throws AuthException, ResourceNotFoundException, |
| ResourceConflictException, EmailException, OrmException { |
| PatchSet ps = |
| rebase.dbProvider.get().patchSets() |
| .get(rsrc.getChange().currentPatchSetId()); |
| if (ps == null) { |
| throw new ResourceConflictException("current revision is missing"); |
| } else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) { |
| throw new AuthException("current revision not accessible"); |
| } |
| return rebase.apply(new RevisionResource(rsrc, ps), input); |
| } |
| } |
| } |