GWT UI: Support moving changes to another branch

Bug: Issue 117
Bug: Issue 6844
Change-Id: I9ad38b5be0a8be4ad31a051ef17a2de46b15b7eb
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 b0b1e35..e4f5e576 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
@@ -43,6 +43,7 @@
     "description",
     "followup",
     "hashtags",
+    "move",
     "publish",
     "rebase",
     "restore",
@@ -58,6 +59,7 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   @UiField Button cherrypick;
+  @UiField Button move;
   @UiField Button rebase;
   @UiField Button revert;
   @UiField Button submit;
@@ -124,6 +126,7 @@
     if (hasUser) {
       a2b(actions, "abandon", abandon);
       a2b(actions, "/", deleteChange);
+      a2b(actions, "move", move);
       a2b(actions, "restore", restore);
       a2b(actions, "revert", revert);
       a2b(actions, "followup", followUp);
@@ -236,6 +239,11 @@
     CherryPickAction.call(cherrypick, changeInfo, revision, project, message);
   }
 
+  @UiHandler("move")
+  void onMove(@SuppressWarnings("unused") ClickEvent e) {
+    MoveAction.call(move, changeInfo, project);
+  }
+
   @UiHandler("revert")
   void onRevert(@SuppressWarnings("unused") ClickEvent e) {
     RevertAction.call(revert, changeId, project, revision, subject);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index 60efc8c..8aeba90 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -63,6 +63,9 @@
     <g:Button ui:field='cherrypick' styleName='' visible='false'>
       <div><ui:msg>Cherry Pick</ui:msg></div>
     </g:Button>
+    <g:Button ui:field='move' styleName='' visible='false'>
+      <div><ui:msg>Move Change</ui:msg></div>
+    </g:Button>
     <g:Button ui:field='rebase' styleName='' visible='false'>
       <div><ui:msg>Rebase</ui:msg></div>
     </g:Button>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java
new file mode 100644
index 0000000..e3e9525
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2017 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.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.MoveDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+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 MoveAction {
+  static void call(Button b, ChangeInfo info, Project.NameKey project) {
+    b.setEnabled(false);
+    new MoveDialog(project) {
+      {
+        sendButton.setText(Util.C.moveChangeSend());
+      }
+
+      @Override
+      public void onSend() {
+        ChangeApi.move(
+            info.project(),
+            info.legacyId().get(),
+            getDestinationBranch(),
+            getMessageText(),
+            new GerritCallback<ChangeInfo>() {
+              @Override
+              public void onSuccess(ChangeInfo result) {
+                sent = true;
+                hide();
+                Gerrit.display(PageLinks.toChange(project, result.legacyId()));
+              }
+
+              @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/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index b1f7561..c353e6f 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
@@ -228,6 +228,15 @@
     call(project, id, commit, "cherrypick").post(cherryPickInput, cb);
   }
 
+  /** Move change to another branch. */
+  public static void move(
+      String project, int id, String destination, String message, AsyncCallback<ChangeInfo> cb) {
+    MoveInput moveInput = MoveInput.create();
+    moveInput.setMessage(message);
+    moveInput.setDestinationBranch(destination);
+    change(project, id).view("move").post(moveInput, cb);
+  }
+
   /** Edit commit message for specific revision of a change. */
   public static void message(
       @Nullable String project,
@@ -356,6 +365,18 @@
     protected CherryPickInput() {}
   }
 
+  private static class MoveInput extends JavaScriptObject {
+    static MoveInput create() {
+      return (MoveInput) createObject();
+    }
+
+    final native void setDestinationBranch(String d) /*-{ this.destination_branch = d; }-*/;
+
+    final native void setMessage(String m) /*-{ this.message = m; }-*/;
+
+    protected MoveInput() {}
+  }
+
   private static class PrivateInput extends JavaScriptObject {
     static PrivateInput create() {
       return (PrivateInput) createObject();
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 80049df..402179c 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
@@ -145,6 +145,14 @@
 
   String cherryPickTitle();
 
+  String moveChangeSend();
+
+  String headingMoveBranch();
+
+  String moveChangeMessage();
+
+  String moveTitle();
+
   String buttonRebaseChangeSend();
 
   String rebaseConfirmMessage();
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 8a9f323..dd11a60 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
@@ -78,6 +78,11 @@
 cherryPickCommitMessage = Cherry Pick Commit Message:
 cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
 
+headingMoveBranch = Move Change to Branch:
+moveChangeSend = Move Change
+moveChangeMessage = Move Change Message:
+moveTitle = Code Review - Move Change to Another Branch
+
 buttonRebaseChangeSend = Rebase
 rebaseConfirmMessage = Change parent revision
 rebaseNotPossibleMessage = Change is already up to date
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java
new file mode 100644
index 0000000..3821e93
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2017 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.Util;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class MoveDialog extends TextAreaActionDialog {
+  private SuggestBox newBranch;
+  private List<BranchInfo> branches;
+
+  public MoveDialog(Project.NameKey project) {
+    super(Util.C.moveTitle(), Util.C.moveChangeMessage());
+    ProjectApi.getBranches(
+        project,
+        new GerritCallback<JsArray<BranchInfo>>() {
+          @Override
+          public void onSuccess(JsArray<BranchInfo> result) {
+            branches = Natives.asList(result);
+          }
+        });
+
+    newBranch =
+        new SuggestBox(
+            new HighlightSuggestOracle() {
+              @Override
+              protected void onRequestSuggestions(Request request, Callback done) {
+                List<BranchSuggestion> suggestions = new ArrayList<>();
+                for (BranchInfo b : branches) {
+                  if (b.ref().contains(request.getQuery())) {
+                    suggestions.add(new BranchSuggestion(b));
+                  }
+                }
+                done.onSuggestionsReady(request, new Response(suggestions));
+              }
+            });
+
+    newBranch.setWidth("100%");
+    newBranch.getElement().getStyle().setProperty("boxSizing", "border-box");
+    message.setCharacterWidth(70);
+
+    FlowPanel mwrap = new FlowPanel();
+    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    mwrap.add(newBranch);
+
+    panel.insert(mwrap, 0);
+    panel.insert(new SmallHeading(Util.C.headingMoveBranch()), 0);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    newBranch.setFocus(true);
+  }
+
+  public String getDestinationBranch() {
+    return newBranch.getText();
+  }
+
+  static class BranchSuggestion implements Suggestion {
+    private BranchInfo branch;
+
+    BranchSuggestion(BranchInfo branch) {
+      this.branch = branch;
+    }
+
+    @Override
+    public String getDisplayString() {
+      String refsHeads = "refs/heads/";
+      if (branch.ref().startsWith(refsHeads)) {
+        return branch.ref().substring(refsHeads.length());
+      }
+      return branch.ref();
+    }
+
+    @Override
+    public String getReplacementString() {
+      return branch.getShortName();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index ffc0dc4..f895bf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -60,7 +62,8 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo> {
+public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo>
+    implements UiAction<ChangeResource> {
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
@@ -209,4 +212,16 @@
       return true;
     }
   }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Move Change")
+        .setTitle("Move change to a different branch")
+        .setVisible(
+            permissionBackend
+                .user(rsrc.getUser())
+                .project(rsrc.getProject())
+                .testOrFalse(ProjectPermission.CREATE_CHANGE));
+  }
 }