Add cherry-pick action

When cherry pick was successful, open new created change. To implement
the URL computation, existing method _computeChangePath() was moved
from change view to REST client behaviour to reuse from change action
element.

TODOs:
- implement branch name suggestion oracle
- improve error handling when branch or message is missing (alert)
- improve error handling when conflict occurs (409)
- improve error handling when branch doesn't exist (400)

Test Plan:

1. Open a change
2. Click Cherry Pick button
3. Type destination branch and click confirm button
4. Confirm, that the change cherry-picked and new change is opened

Bug: Issue 3906
Change-Id: I587f541dbc3338dc138f306c400f5f8857fd16a3
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 1593fab..582a28a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -23,6 +23,7 @@
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 
+<link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
 <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
 
 <dom-module id="gr-change-actions">
@@ -78,6 +79,12 @@
           on-confirm="_handleRebaseConfirm"
           on-cancel="_handleConfirmDialogCancel"
           hidden></gr-confirm-rebase-dialog>
+      <gr-confirm-cherrypick-dialog id="confirmCherrypick"
+          class="confirmDialog"
+          message="[[commitMessage]]"
+          on-confirm="_handleCherrypickConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          hidden></gr-confirm-cherrypick-dialog>
     </gr-overlay>
   </template>
   <script src="gr-change-actions.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index a89c0aa..04b9bb5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -23,6 +23,7 @@
 
   // TODO(andybons): Add the rest of the revision actions.
   var RevisionActions = {
+    CHERRYPICK: 'cherrypick',
     DELETE: '/',
     PUBLISH: 'publish',
     REBASE: 'rebase',
@@ -44,6 +45,7 @@
       },
       changeNum: String,
       patchNum: String,
+      commitMessage: String,
       _loading: {
         type: Boolean,
         value: true,
@@ -67,12 +69,7 @@
     },
 
     _actionsChanged: function(actions, revisionActions) {
-      this.hidden =
-          revisionActions.rebase == null &&
-          revisionActions.submit == null &&
-          revisionActions.publish == null &&
-          actions.abandon == null &&
-          actions.restore == null;
+      this.hidden = actions.length == 0 && revisionActions.length == 0;
     },
 
     _computeRevisionActionsPath: function(changeNum, patchNum) {
@@ -100,6 +97,7 @@
 
     _computeLoadingLabel: function(action) {
       return {
+        'cherrypick': 'Cherry-Picking...',
         'rebase': 'Rebasing...',
         'submit': 'Submitting...',
       }[action];
@@ -124,7 +122,10 @@
       var type = el.getAttribute('data-action-type');
       if (type == 'revision') {
         if (key == RevisionActions.REBASE) {
-          this._showRebaseDialog();
+          this._showActionDialog(this.$.confirmRebase);
+          return;
+        } else if (key == RevisionActions.CHERRYPICK) {
+          this._showActionDialog(this.$.confirmCherrypick);
           return;
         }
         this._fireRevisionAction(this._prependSlash(key),
@@ -167,6 +168,28 @@
           payload);
     },
 
+    _handleCherrypickConfirm: function() {
+      var el = this.$.confirmCherrypick;
+      if (!el.branch) {
+        // TODO(davido): Fix error handling
+        alert('The destination branch can’t be empty.');
+        return;
+      }
+      if (!el.message) {
+        alert('The commit message can’t be empty.');
+        return;
+      }
+      this.$.overlay.close();
+      el.hidden = false;
+      this._fireRevisionAction('/cherrypick',
+          this._revisionActions.cherrypick,
+          {
+            destination: el.branch,
+            message: el.message,
+          }
+      );
+    },
+
     _fireChangeAction: function(endpoint, action) {
       this._send(action.method, {}, endpoint).then(
         function() {
@@ -193,8 +216,12 @@
       }
 
       this._send(action.method, opt_payload, endpoint, true).then(
-        function() {
-          this.fire('reload-change', null, {bubbles: false});
+        function(req) {
+          if (action.__key == RevisionActions.CHERRYPICK) {
+            page.show(this.changePath(req.response._number));
+          } else {
+            this.fire('reload-change', null, {bubbles: false});
+          }
           enableButton();
         }.bind(this)).catch(function(err) {
           // TODO(andybons): Handle merge conflict (409 status);
@@ -205,8 +232,8 @@
         });
     },
 
-    _showRebaseDialog: function() {
-      this.$.confirmRebase.hidden = false;
+    _showActionDialog: function(dialog) {
+      dialog.hidden = false;
       this.$.overlay.open();
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 1c5eb9c..ca2df18 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -96,10 +96,10 @@
       server.respond();
     });
 
-    test('submit and rebase buttons show', function(done) {
+    test('submit, rebase, and cherry-pick buttons show', function(done) {
       flush(function() {
         var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
-        assert.equal(buttonEls.length, 2);
+        assert.equal(buttonEls.length, 3);
         assert.isFalse(element.hidden);
         done();
       });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 1dbdbfb..ff47383 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -269,6 +269,7 @@
               actions="[[_change.actions]]"
               change-num="[[_changeNum]]"
               patch-num="[[_patchNum]]"
+              commit-message="[[_commitInfo.message]]"
               on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
         <div class="changeInfo-column commitAndRelated">
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index a42a379..b76b3c4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -116,10 +116,10 @@
       var currentPatchNum =
           this._change.revisions[this._change.current_revision]._number;
       if (patchNum == currentPatchNum) {
-        page.show(this._computeChangePath(this._changeNum));
+        page.show(this.changePath(this._changeNum));
         return;
       }
-      page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
+      page.show(this.changePath(this._changeNum) + '/' + patchNum);
     },
 
     _handleReplyTap: function(e) {
@@ -206,10 +206,6 @@
       this.fire('title-change', {title: title});
     },
 
-    _computeChangePath: function(changeNum) {
-      return '/c/' + changeNum;
-    },
-
     _computeChangePermalink: function(changeNum) {
       return '/' + changeNum;
     },
@@ -321,7 +317,7 @@
     },
 
     _handleReloadChange: function() {
-      page.show(this._computeChangePath(this._changeNum));
+      page.show(this.changePath(this._changeNum));
     },
 
     _reload: function() {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
new file mode 100644
index 0000000..b21575b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -0,0 +1,74 @@
+<!--
+Copyright (C) 2016 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.
+-->
+
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
+
+<dom-module id="gr-confirm-cherrypick-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+        width: 30em;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      label {
+        cursor: pointer;
+      }
+      iron-autogrow-textarea {
+        padding: 0;
+      }
+      .main label,
+      .main input[type="text"] {
+        display: block;
+        font: inherit;
+        width: 100%;
+      }
+      .main .message {
+        border: groove;
+        width: 100%;
+      }
+    </style>
+    <gr-confirm-dialog
+        confirm-label="Cherry Pick"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header">Cherry Pick Change to Another Branch</div>
+      <div class="main">
+        <label for="branchInput">
+          Cherry Pick to branch
+        </label>
+        <input is="iron-input"
+            type="text"
+            id="branchInput"
+            bind-value="{{branch}}"
+            placeholder="Destination branch">
+        <label for="messageInput">
+          Cherry Pick Commit Message
+        </label>
+        <iron-autogrow-textarea
+            id="messageInput"
+            class="message"
+            bind-value="{{message}}"></iron-autogrow-textarea>
+      </div>
+    </gr-confirm-dialog>
+  </template>
+  <script src="gr-confirm-cherrypick-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
new file mode 100644
index 0000000..f27e4e2
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -0,0 +1,47 @@
+// Copyright (C) 2016 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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-cherrypick-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      branch: String,
+      message: String,
+    },
+
+    _handleConfirmTap: function(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap: function(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();