PolyGerrit: Implement support for creating branches and tags

This adds a new dialog called "gr-create-pointer-dialog" which is used
to create branches or tags.

Change-Id: I5614a1642c45010b7af61135859a4d52f79fb87a
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
index 6137cea..1b6da09 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -74,24 +74,24 @@
       </table>
     </gr-list-view>
     <gr-overlay id="createOverlay" with-backdrop>
-        <gr-confirm-dialog
-            id="createDialog"
-            class="confirmDialog"
-            disabled="[[!_hasNewProjectName]]"
-            confirm-label="Create"
-            on-confirm="_handleCreateProject"
-            on-cancel="_handleCloseCreate">
-          <div class="header">
-            Create Project
-          </div>
-          <div class="main">
-            <gr-create-project-dialog
-                has-new-project-name="{{_hasNewProjectName}}"
-                params="[[params]]"
-                id="createNewModal"></gr-create-project-dialog>
-          </div>
-        </gr-confirm-dialog>
-      </gr-overlay>
+      <gr-confirm-dialog
+          id="createDialog"
+          class="confirmDialog"
+          disabled="[[!_hasNewProjectName]]"
+          confirm-label="Create"
+          on-confirm="_handleCreateProject"
+          on-cancel="_handleCloseCreate">
+        <div class="header">
+          Create Project
+        </div>
+        <div class="main">
+          <gr-create-project-dialog
+              has-new-project-name="{{_hasNewProjectName}}"
+              params="[[params]]"
+              id="createNewModal"></gr-create-project-dialog>
+        </div>
+      </gr-confirm-dialog>
+    </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-admin-project-list.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
new file mode 100644
index 0000000..465ecb7
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
@@ -0,0 +1,63 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+
+<dom-module id="gr-create-pointer-dialog">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      :host {
+        display: inline-block;
+      }
+      input {
+        width: 20em;
+      }
+    </style>
+
+    <div class="gr-form-styles">
+      <div id="form">
+        <section>
+          <span class="title">[[detailType]] name</span>
+          <input
+              is="iron-input"
+              id="itemNameInput"
+              placeholder="[[detailType]] Name"
+              bind-value="{{_itemName}}">
+        </section>
+        <section>
+          <span class="title">Initial Revision</span>
+          <input
+              is="iron-input"
+              id="itemRevisionInput"
+              placeholder="Revision (Branch or SHA-1)"
+              bind-value="{{_itemRevision}}">
+        </section>
+      </div>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-create-pointer-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
new file mode 100644
index 0000000..90cfd6b
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -0,0 +1,82 @@
+// 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.
+(function() {
+  'use strict';
+
+  const DETAIL_TYPES = {
+    branches: 'branches',
+    tags: 'tags',
+  };
+
+  Polymer({
+    is: 'gr-create-pointer-dialog',
+
+    properties: {
+      detailType: String,
+      projectName: String,
+      hasNewItemName: {
+        type: Boolean,
+        notify: true,
+        value: false,
+      },
+      itemDetail: String,
+      _itemName: String,
+      _itemRevision: String,
+    },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    observers: [
+      '_updateItemName(_itemName)',
+    ],
+
+    _updateItemName(name) {
+      this.hasNewItemName = !!name;
+    },
+
+    _computeItemUrl(project) {
+      if (this.itemDetail === DETAIL_TYPES.branches) {
+        return this.getBaseUrl() + '/admin/projects/' +
+            this.encodeURL(this.projectName, true) + ',branches';
+      } else if (this.itemDetail === DETAIL_TYPES.tags) {
+        return this.getBaseUrl() + '/admin/projects/' +
+            this.encodeURL(this.projectName, true) + ',tags';
+      }
+    },
+
+    handleCreateItem() {
+      const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
+      if (this.itemDetail === DETAIL_TYPES.branches) {
+        return this.$.restAPI.createProjectBranch(this.projectName,
+            this._itemName, {revision: USE_HEAD})
+            .then(itemRegistered => {
+              if (itemRegistered.status === 201) {
+                page.show(this._computeItemUrl(this.itemDetail));
+              }
+            });
+      } else if (this.itemDetail === DETAIL_TYPES.tag) {
+        return this.$.restAPI.createProjectTag(this.projectName,
+            this._itemName, {revision: USE_HEAD})
+            .then(itemRegistered => {
+              if (itemRegistered.status === 201) {
+                page.show(this._computeItemUrl(this.itemDetail));
+              }
+            });
+      }
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
new file mode 100644
index 0000000..1ce6d8a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-create-pointer-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-create-pointer-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-create-pointer-dialog></gr-create-pointer-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-create-pointer-dialog tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      stub('gr-rest-api-interface', {
+        getLoggedIn() { return Promise.resolve(true); },
+      });
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('branch created', () => {
+      sandbox.stub(element.$.restAPI, 'createProjectBranch', () => {
+        return Promise.resolve({});
+      });
+
+      assert.isFalse(element.hasNewItemName);
+
+      element._itemName = 'test-branch';
+      element.itemDetail = 'branches';
+
+      element.$.itemNameInput.bindValue = 'test-branch2';
+      element.$.itemRevisionInput.bindValue = 'HEAD';
+
+      assert.isTrue(element.hasNewItemName);
+
+      assert.equal(element._itemName, 'test-branch2');
+
+      assert.equal(element._itemRevision, 'HEAD');
+    });
+
+    test('tag created', () => {
+      sandbox.stub(element.$.restAPI, 'createProjectTag', () => {
+        return Promise.resolve({});
+      });
+
+      assert.isFalse(element.hasNewItemName);
+
+      element._itemName = 'test-tag';
+      element.itemDetail = 'tags';
+
+      element.$.itemNameInput.bindValue = 'test-tag2';
+      element.$.itemRevisionInput.bindValue = 'HEAD';
+
+      assert.isTrue(element.hasNewItemName);
+
+      assert.equal(element._itemName, 'test-tag2');
+
+      assert.equal(element._itemRevision, 'HEAD');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
index f480908..e0cada8 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
@@ -25,6 +25,7 @@
 <link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-create-pointer-dialog/gr-create-pointer-dialog.html">
 <link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
 
 <dom-module id="gr-project-detail-list">
@@ -59,11 +60,13 @@
     </style>
     <style include="gr-table-styles"></style>
     <gr-list-view
+        create-new="[[_isOwner]]"
         filter="[[_filter]]"
         items-per-page="[[_itemsPerPage]]"
         items="[[_items]]"
         loading="[[_loading]]"
         offset="[[_offset]]"
+        on-create-clicked="_handleCreateClicked"
         path="[[_getPath(_project, detailType)]]">
       <table id="list" class="genericList gr-form-styles">
         <tr class="headerRow">
@@ -141,6 +144,26 @@
             item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
       </gr-overlay>
     </gr-list-view>
+    <gr-overlay id="createOverlay" with-backdrop>
+      <gr-confirm-dialog
+          id="createDialog"
+          disabled="[[!_hasNewItemName]]"
+          confirm-label="Create"
+          on-confirm="_handleCreateItem"
+          on-cancel="_handleCloseCreate">
+        <div class="header">
+          Create [[_computeItemName(detailType)]]
+        </div>
+        <div class="main">
+          <gr-create-pointer-dialog
+              id="createNewModal"
+              detail-type="[[_computeItemName(detailType)]]"
+              has-new-item-name="{{_hasNewItemName}}"
+              item-detail="[[detailType]]"
+              project-name="[[_project]]"></gr-create-pointer-dialog>
+        </div>
+      </gr-confirm-dialog>
+    </gr-overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-project-detail-list.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js
index 8c2f743..2989ad0 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js
@@ -68,6 +68,7 @@
       },
       _filter: String,
       _refName: String,
+      _hasNewItemName: Boolean,
     },
 
     behaviors: [
@@ -230,5 +231,18 @@
       }
       return '';
     },
+
+    _handleCreateItem() {
+      this.$.createNewModal.handleCreateItem();
+      this._handleCloseCreate();
+    },
+
+    _handleCloseCreate() {
+      this.$.createOverlay.close();
+    },
+
+    _handleCreateClicked() {
+      this.$.createOverlay.open();
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html
index c36115d..f04e4c1 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html
@@ -393,5 +393,31 @@
         });
       });
     });
+
+    suite('create new', () => {
+      test('_handleCreateClicked called when create-click fired', () => {
+        sandbox.stub(element, '_handleCreateClicked');
+        element.$$('gr-list-view').fire('create-clicked');
+        assert.isTrue(element._handleCreateClicked.called);
+      });
+
+      test('_handleCreateClicked opens modal', () => {
+        const openStub = sandbox.stub(element.$.createOverlay, 'open');
+        element._handleCreateClicked();
+        assert.isTrue(openStub.called);
+      });
+
+      test('_handleCreateItem called when confirm fired', () => {
+        sandbox.stub(element, '_handleCreateItem');
+        element.$.createDialog.fire('confirm');
+        assert.isTrue(element._handleCreateItem.called);
+      });
+
+      test('_handleCloseCreate called when cancel fired', () => {
+        sandbox.stub(element, '_handleCloseCreate');
+        element.$.createDialog.fire('cancel');
+        assert.isTrue(element._handleCloseCreate.called);
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 9b27bb0..989b383 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -240,6 +240,23 @@
           opt_errFn, opt_ctx);
     },
 
+    createProjectBranch(name, branch, revision, opt_errFn, opt_ctx) {
+      if (!name || !branch || !revision) { return ''; }
+      const encodeName = encodeURIComponent(name);
+      const encodeBranch = encodeURIComponent(branch);
+      return this.send('PUT',
+          `/projects/${encodeName}/branches/${encodeBranch}`,
+          revision, opt_errFn, opt_ctx);
+    },
+
+    createProjectTag(name, tag, revision, opt_errFn, opt_ctx) {
+      if (!name || !tag || !revision) { return ''; }
+      const encodeName = encodeURIComponent(name);
+      const encodeTag = encodeURIComponent(tag);
+      return this.send('PUT', `/projects/${encodeName}/tags/${encodeTag}`,
+          revision, opt_errFn, opt_ctx);
+    },
+
     getVersion() {
       return this._fetchSharedCacheURL('/config/server/version');
     },
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 355a7b6..5c00d1d 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -35,6 +35,7 @@
     'admin/gr-admin-view/gr-admin-view_test.html',
     'admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html',
     'admin/gr-create-group-dialog/gr-create-group-dialog_test.html',
+    'admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html',
     'admin/gr-create-project-dialog/gr-create-project-dialog_test.html',
     'admin/gr-plugin-list/gr-plugin-list_test.html',
     'admin/gr-project/gr-project_test.html',