Files
gerrit/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
Stefan Beller 614f128571 Rebase, Cherrypick: only display on current revision
When introducing the `submitWholeTopic` series, the client decided to
trust the server for displaying which revision actions are allowed.
Unfortunately the server did no background checks on the revision
served.

This fixes the bug reported in I186ca5306a29e785981c809c92d51f7a0fb3e71a
which was found in I882b16a929b2ce0c66b1a6d9b64947220bb46d0b

Change-Id: Ie22731ede45b97b6b91121b8c75c6fd19c1fd107
2015-02-26 16:46:04 +09:00

217 lines
8.2 KiB
Java

// 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.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;
@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());
}
baseRev = basePatchSet.getRevision().get();
break;
}
} while (false); // just wanted to use the break statement
}
try {
rebaseChange.get().rebase(rsrc.getChange(), 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 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);
}
}
}