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:
parent
a5f3a69707
commit
874aed7570
@ -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
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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")
|
||||
|
@ -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>() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
{
|
||||
|
@ -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; }-*/;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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:
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
if (heading != null) {
|
||||
panel.add(new SmallHeading(heading));
|
||||
panel.add(mwrap);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
String baseRev = newBaseRev;
|
||||
if (baseRev == null) {
|
||||
baseRev = findBaseRevision(patchSetId, db.get(),
|
||||
change.getDest(), git, null, null, null);
|
||||
final RevCommit baseCommit =
|
||||
rw.parseCommit(ObjectId.fromString(baseRev));
|
||||
}
|
||||
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(),
|
||||
|
Loading…
Reference in New Issue
Block a user