diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index f508a83152..49ebb49a84 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -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 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 428a19cd62..74f9f805ca 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 @@ 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(); } 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 94d564d918..86c2c1fec2 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.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 getReviewers(String changeId) throws Exception { ChangeInfo ci = gApi.changes().id(changeId).get(); Set 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 4acec9aef8..afcd5d0397 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 @@ public class ActionsIT extends AbstractDaemonTest { String changeId = createChangeWithTopic("foo1").getChangeId(); Map 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 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 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 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 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 0000000000..5f4a014e59 --- /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 202699ca76..b940cc9963 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 @@ 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(); 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 c919c6ba04..2844b5e898 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 @@ public interface GerritCss extends CssResource { 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 93881d2e76..37b90c4060 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 @@ 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") 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 b1ad58323b..ffe96279ad 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.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() { - @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() { + @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 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 f9c18cdafa..3af97f5c2d 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.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()) { { 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 0a29a0081f..1135491d4d 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 @@ 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 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 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; }-*/; 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 2bbb429332..00c4404a39 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 @@ public interface ChangeConstants extends Constants { 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 df1e2c5516..9bf3f39c80 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 @@ 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: 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 0d0f793ce6..8d961ca20d 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 @@ 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 { 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 3813af2401..00269f946d 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 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 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 aa32394d29..60b5f930d2 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.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 { 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(); - } } 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 2b6a866bc2..021f39c4a1 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 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 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 0000000000..58f806af8a --- /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 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 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() { + @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 0000000000..d7d5d6a42a --- /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 { + 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 6b4c1a19c2..c36faa2915 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.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); } 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 331dfe5d49..3f61b01682 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.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, +public class Rebase implements RestModifyView, UiAction { - public static class Input { - } + + private static final Logger log = + LoggerFactory.getLogger(Rebase.class); private final Provider rebaseChange; private final ChangeJson json; + private final Provider dbProvider; @Inject - public Rebase(Provider rebaseChange, ChangeJson json) { + public Rebase(Provider rebaseChange, ChangeJson json, + Provider 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, } 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, 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, .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 { - private final Provider dbProvider; + RestModifyView { private final Rebase rebase; @Inject - CurrentRevision(Provider 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 9bb67ea04b..f80a4740c4 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 @@ 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(),