Added support for changing parent revision during rebase

It is a common use-case of configuration managers (CMs) to restructure
a sequence of patches towards a target branch.

This patch allows restructuring of patches from the UI and REST API
using the rebase action.

It is now possible to:
  - Introduce a new dependency towards another change.
  - Remove dependency towards another change.

There is a non-obvious limitation regarding the parent revisions: It has
to be a valid patch set towards the same target branch.

Change-Id: I882b16a929b2ce0c66b1a6d9b64947220bb46d0b
This commit is contained in:
Zalan Blenessy 2015-01-12 13:26:18 +01:00 committed by David Pursehouse
parent a5f3a69707
commit 874aed7570
22 changed files with 492 additions and 69 deletions

View File

@ -852,9 +852,17 @@ the error message is contained in the response body.
Rebases a change.
Optionally, the parent revision can be changed to another patch set through the
link:#rebase-input[RebaseInput] entity.
.Request
----
POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/rebase HTTP/1.0
Content-Type: application/json;charset=UTF-8
{
"base" : "1234",
}
----
As response a link:#change-info[ChangeInfo] entity is returned that
@ -2200,9 +2208,17 @@ change edit fails with `409 Conflict`.
Rebases a revision.
Optionally, the parent revision can be changed to another patch set through the
link:#rebase-input[RebaseInput] entity.
.Request
----
POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/rebase HTTP/1.0
Content-Type: application/json;charset=UTF-8
{
"base" : "1234",
}
----
As response a link:#change-info[ChangeInfo] entity is returned that
@ -3877,6 +3893,21 @@ If `status` is set, an additional plaintext message describing the
outcome of the fix.
|===========================
[[rebase-input]]
=== RebaseInput
The `RebaseInput` entity contains information for changing parent when rebasing.
[options="header",width="50%",cols="1,^1,5"]
|===========================
|Field Name ||Description
|`base` |optional|
The new parent revision. This can be a ref or a SHA1 to a concrete patchset. +
Alternatively, a change number can be specified, in which case the current
patch set is inferred. +
Empty string is used for rebasing directly on top of the target branch,
which effectively breaks dependency towards a parent change.
|===========================
[[related-change-and-commit-info]]
=== RelatedChangeAndCommitInfo

View File

@ -215,6 +215,10 @@ public class PushOneCommit {
queryProvider.get().byKeyPrefix(commit.getChangeId()));
}
public PatchSet getPatchSet() throws OrmException {
return getChange().currentPatchSet();
}
public PatchSet.Id getPatchSetId() throws OrmException {
return getChange().change().currentPatchSetId();
}

View File

@ -23,6 +23,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
@ -32,6 +33,7 @@ import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
import org.eclipse.jgit.lib.Constants;
import org.junit.Test;
@ -110,6 +112,49 @@ public class ChangeIT extends AbstractDaemonTest {
.rebase();
}
@Test
public void rebaseChangeBase() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
PushOneCommit.Result r3 = createChange();
RebaseInput ri = new RebaseInput();
// rebase r3 directly onto master (break dep. towards r2)
ri.base = "";
gApi.changes()
.id(r3.getChangeId())
.revision(r3.getCommit().name())
.rebase(ri);
PatchSet ps3 = r3.getPatchSet();
assertThat(ps3.getId().get()).is(2);
// rebase r2 onto r3 (referenced by ref)
ri.base = ps3.getId().toRefName();
gApi.changes()
.id(r2.getChangeId())
.revision(r2.getCommit().name())
.rebase(ri);
PatchSet ps2 = r2.getPatchSet();
assertThat(ps2.getId().get()).is(2);
// rebase r1 onto r2 (referenced by commit)
ri.base = ps2.getRevision().get();
gApi.changes()
.id(r1.getChangeId())
.revision(r1.getCommit().name())
.rebase(ri);
PatchSet ps1 = r1.getPatchSet();
assertThat(ps1.getId().get()).is(2);
// rebase r1 onto r3 (referenced by change number)
ri.base = String.valueOf(r3.getChange().getId().get());
gApi.changes()
.id(r1.getChangeId())
.revision(ps1.getRevision().get())
.rebase(ri);
assertThat(r1.getPatchSetId().get()).is(3);
}
private Set<Account.Id> getReviewers(String changeId) throws Exception {
ChangeInfo ci = gApi.changes().id(changeId).get();
Set<Account.Id> result = Sets.newHashSet();

View File

@ -43,7 +43,8 @@ public class ActionsIT extends AbstractDaemonTest {
String changeId = createChangeWithTopic("foo1").getChangeId();
Map<String, ActionInfo> actions = getActions(changeId);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).hasSize(1);
assertThat(actions).containsKey("rebase");
assertThat(actions).hasSize(2);
}
@Test
@ -51,8 +52,7 @@ public class ActionsIT extends AbstractDaemonTest {
String changeId = createChangeWithTopic("foo1").getChangeId();
approve(changeId);
Map<String, ActionInfo> actions = getActions(changeId);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).containsKey("submit");
commonActionsAssertions(actions);
if (isSubmitWholeTopicEnabled()) {
ActionInfo info = actions.get("submit");
assertThat(info.enabled).isTrue();
@ -62,8 +62,6 @@ public class ActionsIT extends AbstractDaemonTest {
} else {
noSubmitWholeTopicAssertions(actions);
}
// no other actions
assertThat(actions).hasSize(2);
}
@Test
@ -73,10 +71,7 @@ public class ActionsIT extends AbstractDaemonTest {
// create another change with the same topic
createChangeWithTopic("foo2").getChangeId();
Map<String, ActionInfo> actions = getActions(changeId);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).containsKey("submit");
// no other actions:
assertThat(actions).hasSize(2);
commonActionsAssertions(actions);
if (isSubmitWholeTopicEnabled()) {
ActionInfo info = actions.get("submit");
assertThat(info.enabled).isNull();
@ -96,10 +91,7 @@ public class ActionsIT extends AbstractDaemonTest {
String changeId2 = createChangeWithTopic("foo2").getChangeId();
approve(changeId2);
Map<String, ActionInfo> actions = getActions(changeId);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).containsKey("submit");
// no other actions:
assertThat(actions).hasSize(2);
commonActionsAssertions(actions);
if (isSubmitWholeTopicEnabled()) {
ActionInfo info = actions.get("submit");
assertThat(info.enabled).isTrue();
@ -128,6 +120,13 @@ public class ActionsIT extends AbstractDaemonTest {
assertThat(info.title).isEqualTo("Submit patch set 1 into master");
}
private void commonActionsAssertions(Map<String, ActionInfo> actions) {
assertThat(actions).hasSize(3);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).containsKey("submit");
assertThat(actions).containsKey("rebase");
}
private PushOneCommit.Result createChangeWithTopic(String topic) throws GitAPIException,
IOException {
PushOneCommit push = pushFactory.create(db, admin.getIdent());

View File

@ -0,0 +1,19 @@
// Copyright (C) 2015 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.extensions.api.changes;
public class RebaseInput {
public String base;
}

View File

@ -34,6 +34,7 @@ public interface RevisionApi {
void publish() throws RestApiException;
ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
ChangeApi rebase() throws RestApiException;
ChangeApi rebase(RebaseInput in) throws RestApiException;
boolean canRebase();
void setReviewed(String path, boolean reviewed) throws RestApiException;
@ -93,6 +94,11 @@ public interface RevisionApi {
throw new NotImplementedException();
}
@Override
public ChangeApi rebase(RebaseInput in) throws RestApiException {
throw new NotImplementedException();
}
@Override
public boolean canRebase() {
throw new NotImplementedException();

View File

@ -158,6 +158,8 @@ public interface GerritCss extends CssResource {
String projectFilterLabel();
String projectFilterPanel();
String projectNameColumn();
String rebaseContentPanel();
String rebaseSuggestBox();
String registerScreenExplain();
String registerScreenNextLinks();
String registerScreenSection();

View File

@ -176,7 +176,7 @@ class Actions extends Composite {
@UiHandler("rebase")
void onRebase(@SuppressWarnings("unused") ClickEvent e) {
RebaseAction.call(changeId, revision);
RebaseAction.call(rebase, project, changeInfo.branch(), changeId, revision);
}
@UiHandler("submit")

View File

@ -18,17 +18,42 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.RebaseDialog;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.PopupPanel;
class RebaseAction {
static void call(final Change.Id id, String revision) {
ChangeApi.rebase(id.get(), revision,
new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
Gerrit.display(PageLinks.toChange(id));
}
});
static void call(final Button b, final String project, final String branch,
final Change.Id id, final String revision) {
b.setEnabled(false);
new RebaseDialog(project, branch, id) {
@Override
public void onSend() {
ChangeApi.rebase(id.get(), revision, getBase(), new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
sent = true;
hide();
Gerrit.display(PageLinks.toChange(id));
}
@Override
public void onFailure(Throwable caught) {
enableButtons(true);
super.onFailure(caught);
}
});
}
@Override
public void onClose(CloseEvent<PopupPanel> event) {
super.onClose(event);
b.setEnabled(true);
}
}.center();
}
}

View File

@ -19,7 +19,7 @@ import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.CommentedActionDialog;
import com.google.gerrit.client.ui.TextAreaActionDialog;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Button;
@ -29,7 +29,7 @@ class RevertAction {
final String commitSubject) {
// TODO Replace ActionDialog with a nicer looking display.
b.setEnabled(false);
new CommentedActionDialog(
new TextAreaActionDialog(
Util.C.revertChangeTitle(),
Util.C.headingRevertMessage()) {
{

View File

@ -216,10 +216,11 @@ public class ChangeApi {
change(id).view("edit:rebase").post(in, cb);
}
/** Rebase a revision onto the branch tip. */
public static void rebase(int id, String commit, AsyncCallback<ChangeInfo> cb) {
JavaScriptObject in = JavaScriptObject.createObject();
call(id, commit, "rebase").post(in, cb);
/** Rebase a revision onto the branch tip or another change. */
public static void rebase(int id, String commit, String base, AsyncCallback<ChangeInfo> cb) {
RebaseInput rebaseInput = RebaseInput.create();
rebaseInput.setBase(base);
call(id, commit, "rebase").post(rebaseInput, cb);
}
private static class Input extends JavaScriptObject {
@ -260,6 +261,17 @@ public class ChangeApi {
}
}
private static class RebaseInput extends JavaScriptObject {
final native void setBase(String b) /*-{ this.base = b; }-*/;
static RebaseInput create() {
return (RebaseInput) createObject();
}
protected RebaseInput() {
}
}
private static class SubmitInput extends JavaScriptObject {
final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;

View File

@ -164,6 +164,11 @@ public interface ChangeConstants extends Constants {
String cherryPickCommitMessage();
String cherryPickTitle();
String buttonRebaseChangeSend();
String rebaseConfirmMessage();
String rebasePlaceholderMessage();
String rebaseTitle();
String buttonAbandonChangeBegin();
String buttonAbandonChangeSend();
String headingAbandonMessage();

View File

@ -150,6 +150,11 @@ headingCherryPickBranch = Cherry Pick to Branch:
cherryPickCommitMessage = Cherry Pick Commit Message:
cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
buttonRebaseChangeSend = Rebase
rebaseConfirmMessage = Change parent revision
rebasePlaceholderMessage = (subject, change number, or leave empty)
rebaseTitle = Code Review - Rebase Change
buttonRestoreChangeBegin = Restore Change
restoreChangeTitle = Code Review - Restore Change
headingRestoreMessage = Restore Message:

View File

@ -1162,6 +1162,16 @@ a:hover.downloadLink {
white-space: nowrap;
font-size: small;
}
.commentedActionDialog .rebaseContentPanel {
margin-left: 10px;
background: trimColor;
padding: 5px 5px 5px 5px;
width: 300px;
}
.commentedActionDialog .rebaseContentPanel .rebaseSuggestBox {
font-size: small;
width: 100%;
}
/** PatchBrowserPopup **/
.patchBrowserPopup {

View File

@ -31,7 +31,7 @@ import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
import java.util.LinkedList;
import java.util.List;
public abstract class CherryPickDialog extends CommentedActionDialog {
public abstract class CherryPickDialog extends TextAreaActionDialog {
private SuggestBox newBranch;
private List<BranchInfo> branches;

View File

@ -24,16 +24,15 @@ import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.user.client.AutoCenterDialogBox;
public abstract class CommentedActionDialog extends AutoCenterDialogBox
implements CloseHandler<PopupPanel> {
protected final FlowPanel panel;
protected final NpTextArea message;
protected final Button sendButton;
protected final Button cancelButton;
protected final FlowPanel buttonPanel;
protected final FlowPanel contentPanel;
protected FocusWidget focusOn;
protected boolean sent = false;
@ -45,11 +44,6 @@ public abstract class CommentedActionDialog extends AutoCenterDialogBox
addStyleName(Gerrit.RESOURCES.css().commentedActionDialog());
message = new NpTextArea();
message.setCharacterWidth(60);
message.setVisibleLines(10);
message.getElement().setPropertyBoolean("spellcheck", true);
setFocusOn(message);
sendButton = new Button(Util.C.commentedActionButtonSend());
sendButton.addClickHandler(new ClickHandler() {
@Override
@ -68,9 +62,8 @@ public abstract class CommentedActionDialog extends AutoCenterDialogBox
}
});
final FlowPanel mwrap = new FlowPanel();
mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
mwrap.add(message);
contentPanel = new FlowPanel();
contentPanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
buttonPanel = new FlowPanel();
buttonPanel.add(sendButton);
@ -78,8 +71,10 @@ public abstract class CommentedActionDialog extends AutoCenterDialogBox
buttonPanel.getElement().getStyle().setProperty("marginTop", "4px");
panel = new FlowPanel();
panel.add(new SmallHeading(heading));
panel.add(mwrap);
if (heading != null) {
panel.add(new SmallHeading(heading));
}
panel.add(contentPanel);
panel.add(buttonPanel);
add(panel);
@ -110,8 +105,4 @@ public abstract class CommentedActionDialog extends AutoCenterDialogBox
}
public abstract void onSend();
public String getMessageText() {
return message.getText().trim();
}
}

View File

@ -31,7 +31,7 @@ import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
import java.util.ArrayList;
import java.util.List;
public abstract class CreateChangeDialog extends CommentedActionDialog {
public abstract class CreateChangeDialog extends TextAreaActionDialog {
private SuggestBox newChange;
private List<BranchInfo> branches;

View File

@ -0,0 +1,119 @@
// Copyright (C) 2015 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.client.ui;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeList;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
import java.util.LinkedList;
import java.util.List;
public abstract class RebaseDialog extends CommentedActionDialog {
private final SuggestBox base;
private final CheckBox cb;
private List<ChangeInfo> changes;
public RebaseDialog(final String project, final String branch,
final Change.Id changeId) {
super(Util.C.rebaseTitle(), null);
sendButton.setText(Util.C.buttonRebaseChangeSend());
// create the suggestion box
base = new SuggestBox(new HighlightSuggestOracle() {
@Override
protected void onRequestSuggestions(Request request, Callback done) {
String query = request.getQuery().toLowerCase();
LinkedList<ChangeSuggestion> suggestions = new LinkedList<>();
for (final ChangeInfo ci : changes) {
if (changeId.equals(ci.legacy_id())) {
continue; // do not suggest current change
}
String id = String.valueOf(ci.legacy_id().get());
if (id.contains(query) || ci.subject().toLowerCase().contains(query)) {
suggestions.add(new ChangeSuggestion(ci));
if (suggestions.size() >= 50) { // limit to 50 suggestions
break;
}
}
}
done.onSuggestionsReady(request, new Response(suggestions));
}
});
base.setEnabled(false);
base.getElement().setAttribute("placeholder",
Util.C.rebasePlaceholderMessage());
base.setStyleName(Gerrit.RESOURCES.css().rebaseSuggestBox());
// the checkbox which must be clicked before the change list is populated
cb = new CheckBox(Util.C.rebaseConfirmMessage());
cb.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
boolean checked = ((CheckBox) event.getSource()).getValue();
if (checked) {
ChangeList.next("project:" + project + " AND branch:" + branch
+ " AND is:open NOT age:90d", 0, 1000,
new GerritCallback<ChangeList>() {
@Override
public void onSuccess(ChangeList result) {
changes = Natives.asList(result);
base.setEnabled(true);
}
});
} else {
base.setEnabled(false);
}
}
});
// add the checkbox and suggestbox widgets to the content panel
contentPanel.add(cb);
contentPanel.add(base);
contentPanel.setStyleName(Gerrit.RESOURCES.css().rebaseContentPanel());
}
public String getBase() {
return cb.getValue() ? base.getText() : null;
}
private static class ChangeSuggestion implements Suggestion {
private ChangeInfo change;
public ChangeSuggestion(ChangeInfo change) {
this.change = change;
}
@Override
public String getDisplayString() {
return String.valueOf(change.legacy_id().get()) + ": " + change.subject();
}
@Override
public String getReplacementString() {
return String.valueOf(change.legacy_id().get());
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (C) 2015 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.client.ui;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwtexpui.globalkey.client.NpTextArea;
public abstract class TextAreaActionDialog extends CommentedActionDialog
implements CloseHandler<PopupPanel> {
protected final NpTextArea message;
public TextAreaActionDialog(String title, String heading) {
super(title, heading);
message = new NpTextArea();
message.setCharacterWidth(60);
message.setVisibleLines(10);
message.getElement().setPropertyBoolean("spellcheck", true);
setFocusOn(message);
contentPanel.add(message);
}
public String getMessageText() {
return message.getText().trim();
}
}

View File

@ -23,6 +23,7 @@ import com.google.gerrit.extensions.api.changes.CommentApi;
import com.google.gerrit.extensions.api.changes.DraftApi;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.FileApi;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
@ -179,8 +180,14 @@ class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi
@Override
public ChangeApi rebase() throws RestApiException {
RebaseInput in = new RebaseInput();
return rebase(in);
}
@Override
public ChangeApi rebase(RebaseInput in) throws RestApiException {
try {
return changes.id(rebase.apply(revision, null)._number);
return changes.id(rebase.apply(revision, in)._number);
} catch (OrmException | EmailException e) {
throw new RestApiException("Cannot rebase ps", e);
}

View File

@ -15,6 +15,7 @@
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;
@ -23,9 +24,10 @@ 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.change.Rebase.Input;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
@ -35,27 +37,34 @@ 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, Input>,
public class Rebase implements RestModifyView<RevisionResource, RebaseInput>,
UiAction<RevisionResource> {
public static class Input {
}
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) {
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, Input input)
public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
throws AuthException, ResourceNotFoundException,
ResourceConflictException, EmailException, OrmException {
ChangeControl control = rsrc.getControl();
@ -65,11 +74,52 @@ public class Rebase implements RestModifyView<RevisionResource, Input>,
} 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());
rsrc.getUser(), baseRev);
} catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
} catch (IOException e) {
@ -81,6 +131,53 @@ public class Rebase implements RestModifyView<RevisionResource, Input>,
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()
@ -88,30 +185,28 @@ public class Rebase implements RestModifyView<RevisionResource, Input>,
.setTitle("Rebase onto tip of branch or parent change")
.setVisible(resource.getChange().getStatus().isOpen()
&& resource.getControl().canRebase()
&& rebaseChange.get().canRebase(resource));
&& hasOneParent(resource.getPatchSet().getId()));
}
public static class CurrentRevision implements
RestModifyView<ChangeResource, Input> {
private final Provider<ReviewDb> dbProvider;
RestModifyView<ChangeResource, RebaseInput> {
private final Rebase rebase;
@Inject
CurrentRevision(Provider<ReviewDb> dbProvider, Rebase rebase) {
this.dbProvider = dbProvider;
CurrentRevision(Rebase rebase) {
this.rebase = rebase;
}
@Override
public ChangeInfo apply(ChangeResource rsrc, Input input)
public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
throws AuthException, ResourceNotFoundException,
ResourceConflictException, EmailException, OrmException {
PatchSet ps =
dbProvider.get().patchSets()
rebase.dbProvider.get().patchSets()
.get(rsrc.getChange().currentPatchSetId());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
} else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
} else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) {
throw new AuthException("current revision not accessible");
}
return rebase.apply(new RevisionResource(rsrc, ps), input);

View File

@ -99,6 +99,7 @@ public class RebaseChange {
* @param change the change to perform the rebase for
* @param patchSetId the id of the patch set
* @param uploader the user that creates the rebased patch set
* @param newBaseRev the commit that should be the new base
* @throws NoSuchChangeException thrown if the change to which the patch set
* belongs does not exist or is not visible to the user
* @throws EmailException thrown if sending the e-mail to notify about the new
@ -107,9 +108,9 @@ public class RebaseChange {
* @throws IOException thrown if rebase is not possible or not needed
* @throws InvalidChangeOperationException thrown if rebase is not allowed
*/
public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader)
throws NoSuchChangeException, EmailException, OrmException, IOException,
InvalidChangeOperationException {
public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader,
final String newBaseRev) throws NoSuchChangeException, EmailException, OrmException,
IOException, InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl changeControl =
changeControlFactory.validateFor(change, uploader);
@ -126,10 +127,17 @@ public class RebaseChange {
rw = new RevWalk(git);
inserter = git.newObjectInserter();
final String baseRev = findBaseRevision(patchSetId, db.get(),
change.getDest(), git, null, null, null);
final RevCommit baseCommit =
rw.parseCommit(ObjectId.fromString(baseRev));
String baseRev = newBaseRev;
if (baseRev == null) {
baseRev = findBaseRevision(patchSetId, db.get(),
change.getDest(), git, null, null, null);
}
ObjectId baseObjectId = git.resolve(baseRev);
if (baseObjectId == null) {
throw new InvalidChangeOperationException(
"Cannot rebase: Failed to resolve baseRev: " + baseRev);
}
final RevCommit baseCommit = rw.parseCommit(baseObjectId);
PersonIdent committerIdent =
uploader.newCommitterIdent(TimeUtil.nowTs(),