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
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index f508a83..49ebb49 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -852,9 +852,17 @@
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 @@
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 @@
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
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 428a19c..74f9f80 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -215,6 +215,10 @@
queryProvider.get().byKeyPrefix(commit.getChangeId()));
}
+ public PatchSet getPatchSet() throws OrmException {
+ return getChange().currentPatchSet();
+ }
+
public PatchSet.Id getPatchSetId() throws OrmException {
return getChange().change().currentPatchSetId();
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 94d564d..86c2c1f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -23,6 +23,7 @@
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.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 @@
.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();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 4acec9a..afcd5d0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -43,7 +43,8 @@
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 @@
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 @@
} else {
noSubmitWholeTopicAssertions(actions);
}
- // no other actions
- assertThat(actions).hasSize(2);
}
@Test
@@ -73,10 +71,7 @@
// 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 @@
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 @@
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());
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RebaseInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
new file mode 100644
index 0000000..5f4a014
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
@@ -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;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 202699c..b940cc9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -34,6 +34,7 @@
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;
@@ -94,6 +95,11 @@
}
@Override
+ public ChangeApi rebase(RebaseInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public boolean canRebase() {
throw new NotImplementedException();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index c919c6b..2844b5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -158,6 +158,8 @@
String projectFilterLabel();
String projectFilterPanel();
String projectNameColumn();
+ String rebaseContentPanel();
+ String rebaseSuggestBox();
String registerScreenExplain();
String registerScreenNextLinks();
String registerScreenSection();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 93881d2..37b90c4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -176,7 +176,7 @@
@UiHandler("rebase")
void onRebase(@SuppressWarnings("unused") ClickEvent e) {
- RebaseAction.call(changeId, revision);
+ RebaseAction.call(rebase, project, changeInfo.branch(), changeId, revision);
}
@UiHandler("submit")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
index b1ad583..ffe9627 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
@@ -18,17 +18,42 @@
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();
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
index f9c18cd..3af97f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -19,7 +19,7 @@
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 @@
final String commitSubject) {
// TODO Replace ActionDialog with a nicer looking display.
b.setEnabled(false);
- new CommentedActionDialog(
+ new TextAreaActionDialog(
Util.C.revertChangeTitle(),
Util.C.headingRevertMessage()) {
{
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 0a29a00..1135491 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -216,10 +216,11 @@
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 @@
}
}
+ 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; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 2bbb429..00c4404 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -164,6 +164,11 @@
String cherryPickCommitMessage();
String cherryPickTitle();
+ String buttonRebaseChangeSend();
+ String rebaseConfirmMessage();
+ String rebasePlaceholderMessage();
+ String rebaseTitle();
+
String buttonAbandonChangeBegin();
String buttonAbandonChangeSend();
String headingAbandonMessage();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index df1e2c5..9bf3f39 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -150,6 +150,11 @@
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:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 0d0f793..8d961ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -1162,6 +1162,16 @@
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 {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
index 3813af2..00269f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -31,7 +31,7 @@
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;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
index aa32394..60b5f93 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -24,16 +24,15 @@
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 @@
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 @@
}
});
- 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 @@
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 void onSend();
-
- public String getMessageText() {
- return message.getText().trim();
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
index 2b6a866..021f39c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -31,7 +31,7 @@
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;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
new file mode 100644
index 0000000..58f806a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
@@ -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());
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java
new file mode 100644
index 0000000..d7d5d6a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java
@@ -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();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 6b4c1a1..c36faa2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -23,6 +23,7 @@
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 @@
@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);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 331dfe5..3f61b01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -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.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.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 @@
} 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 @@
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 @@
.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);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 9bb67ea..f80a474 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -99,6 +99,7 @@
* @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 @@
* @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 @@
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(),