Add the skeleton of a new UI based on Polymer, PolyGerrit
This is the beginnings of an experimental new non-GWT web UI developed
using a modern JS web framework, http://www.polymer-project.org/. It
will coexist alongside the GWT UI until it is feature-complete.
The functionality of this change is light years from complete, with
a full laundry list of things that don't work. This change is simply
meant to get the starting work in and continue iteration afterward.
The contents of the polygerrit-ui directory started as the full tree of
https://github.com/andybons/polygerrit at 219f531, plus a few more
local changes since review started. In the future this directory will
be pruned, rearranged, and integrated with the Buck build.
Change-Id: Ifb6f5429e8031ee049225cdafa244ad1c21bf5b5
diff --git a/polygerrit-ui/.gitattributes b/polygerrit-ui/.gitattributes
new file mode 100644
index 0000000..2125666
--- /dev/null
+++ b/polygerrit-ui/.gitattributes
@@ -0,0 +1 @@
+* text=auto
\ No newline at end of file
diff --git a/polygerrit-ui/.gitignore b/polygerrit-ui/.gitignore
new file mode 100644
index 0000000..af8e683
--- /dev/null
+++ b/polygerrit-ui/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+dist
+bower_components
+.tmp
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
new file mode 100644
index 0000000..c894ebc
--- /dev/null
+++ b/polygerrit-ui/README.md
@@ -0,0 +1,11 @@
+# PolyGerrit
+
+For local testing against production data...
+
+```sh
+npm install
+bower install
+go run server.go
+```
+
+Then visit http://localhost:8081
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
new file mode 100644
index 0000000..bdac450
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -0,0 +1,116 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="./gr-change-list-view.html">
+<link rel="import" href="./gr-change-view.html">
+<link rel="import" href="./gr-diff-view.html">
+<link rel="import" href="./gr-search-bar.html">
+
+<dom-module id="gr-app">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ min-height: 100vh;
+ flex-direction: column;
+ }
+ :host[constrained] main {
+ margin: 0 auto;
+ width: 100%;
+ max-width: 980px;
+ }
+ header,
+ footer {
+ background-color: #eee;
+ padding: 20px;
+ }
+ header {
+ display: flex;
+ align-items: center;
+ }
+ main {
+ flex: 1;
+ padding: 20px 0;
+ }
+ .bigTitle {
+ color: #000;
+ font-size: 24px;
+ text-decoration: none;
+ }
+ .bigTitle:hover {
+ text-decoration: underline;
+ }
+ .searchContainer {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ gr-search-bar {
+ width: 500px;
+ }
+ </style>
+ <header role="banner">
+ <a href="/" class="bigTitle">PolyGerrit</a>
+ <div class="searchContainer">
+ <gr-search-bar value="[[params.query]]" role="search"></gr-search-bar>
+ </div>
+ </header>
+ <main>
+ <template is="dom-if" if="{{_showChangeList}}">
+ <gr-change-list-view params="[[params]]"></gr-change-list-view>
+ </template>
+ <template is="dom-if" if="{{_showChangeView}}" restamp="true">
+ <gr-change-view params="[[params]]"></gr-change-view>
+ </template>
+ <template is="dom-if" if="{{_showDiffView}}" restamp="true">
+ <gr-diff-view params="[[params]]"></gr-diff-view>
+ </template>
+ </main>
+ <footer role="contentinfo">Powered by PolyGerrit</footer>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-app',
+
+ properties: {
+ constrained: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ params: Object,
+ route: {
+ type: Object,
+ value: {},
+ observer: '_routeChanged',
+ }
+ },
+
+ _routeChanged: function(route) {
+ this.set('_showChangeList', route == 'gr-change-list');
+ this.set('_showChangeView', route == 'gr-change-view');
+ this.set('_showDiffView', route == 'gr-diff-view');
+ this.constrained = route == 'gr-change-view';
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html
new file mode 100644
index 0000000..5bbed82
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-list-item.html
@@ -0,0 +1,197 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="./gr-date-formatter.html">
+
+<dom-module id="gr-change-list-item">
+ <template>
+ <style>
+ :host {
+ display: table-row;
+ }
+ th, td {
+ border-bottom: 1px solid #eee;
+ padding: 2px 5px;
+ vertical-align: top;
+ }
+ th {
+ background: #eee;
+ text-align: left;
+ }
+ a {
+ color: #000;
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ .positionIndicator {
+ opacity: .1;
+ visibility: hidden;
+ }
+ .avatarImage {
+ border-radius: 50%;
+ height: 16px;
+ vertical-align: -3px;
+ width: 16px;
+ }
+ .u-monospace {
+ font-family: 'Source Code Pro';
+ }
+ .u-green {
+ color: #388E3C;
+ }
+ .u-red {
+ color: #D32F2F;
+ }
+ </style>
+ <template is="dom-if" if="[[header]]">
+ <th></th> <!-- keyboard position indicator -->
+ <th>Subject</th>
+ <th>Status</th>
+ <th>Owner</th>
+ <th>Project</th>
+ <th>Branch</th>
+ <th>Updated</th>
+ <th>Size</th>
+ <th title="Code-Review">CR</th>
+ <th title="Verified">V</th>
+ </template>
+ <template is="dom-if" if="[[!header]]">
+ <td>
+ <span class="positionIndicator">▶</span>
+ </td>
+ <td>
+ <a href$="[[changeURL()]]">[[change.subject]]</a>
+ </td>
+ <td>[[_computeChangeStatusString(change)]]</td>
+ <td>
+ <img class="avatarImage" src$="[[_computeAvatarURL(change.owner)]]">
+ <a href$="[[_computeOwnerLink(change.owner.email)]]"
+ title$="[[_computeOwnerTitle(change.owner)]]">[[change.owner.name]]</a>
+ </td>
+ <td>
+ <a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
+ </td>
+ <td>
+ <a href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
+ </td>
+ <td><gr-date-formatter date-str="[[change.updated]]"></gr-date-formatter></td>
+ <td class="u-monospace">
+ <span class="u-green"><span>+</span>[[change.insertions]]</span>,
+ <span class="u-red"><span>-</span>[[change.deletions]]</span>
+ </td>
+ <td title="Code-Review"
+ class$="[[_computeCodeReviewClass(change.labels.Code-Review)]]">[[_computeCodeReviewLabel(change.labels.Code-Review)]]</td>
+ <td title="Verified" class="u-green">[[_computeVerifiedLabel(change.labels.Verified)]]</td>
+ </template>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-list-item',
+
+ properties: {
+ header: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false,
+ },
+ selected: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ change: Object,
+ },
+
+ changeURL: function() {
+ if (!this.change) { return ''; }
+ return '/c/' + this.change._number + '/';
+ },
+
+ _computeChangeStatusString: function(change) {
+ if (!change.mergeable) {
+ return 'Merge Conflict';
+ }
+ return '';
+ },
+
+ _computeCodeReviewClass: function(codeReview) {
+ if (!codeReview) { return ''; }
+ if (codeReview.approved) {
+ return 'u-green';
+ }
+ if (codeReview.value == 1) {
+ return 'u-monospace u-green';
+ }
+ if (codeReview.value == -1) {
+ return 'u-monospace u-red';
+ }
+ return '';
+ },
+
+ _computeCodeReviewLabel: function(codeReview) {
+ if (!codeReview) { return ''; }
+ if (codeReview.approved) {
+ return '✓';
+ }
+ if (codeReview.value == 1) {
+ return '+1';
+ }
+ if (codeReview.value == -1) {
+ return '-1';
+ }
+ return '';
+ },
+
+ _computeVerifiedLabel: function(verified) {
+ if (verified && verified.approved) {
+ return '✓';
+ }
+ return ''
+ },
+
+ _computeAvatarURL: function(owner) {
+ if (!owner) { return ''; }
+ return '/accounts/' + owner.email + '/avatar?s=32'
+ },
+
+ _computeOwnerLink: function(email) {
+ if (!email) { return ''; }
+ return '/q/owner:' + encodeURIComponent(email) + '+status:open';
+ },
+
+ _computeOwnerTitle: function(owner) {
+ if (!owner) { return ''; }
+ // TODO: Is this safe from XSS attacks?
+ return owner.name + ' <' + owner.email + '>';
+ },
+
+ _computeProjectURL: function(project) {
+ return '/projects/' + project + ',dashboards/default';
+ },
+
+ _computeProjectBranchURL: function(project, branch) {
+ return '/q/status:open+project:' + project + '+branch:' + branch;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-view.html b/polygerrit-ui/app/elements/gr-change-list-view.html
new file mode 100644
index 0000000..09c9033
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-list-view.html
@@ -0,0 +1,127 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
+<link rel="import" href="./gr-change-list.html">
+
+<dom-module id="gr-change-list-view">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ gr-change-list {
+ margin: 0 20px;
+ width: calc(100% - 40px);
+ }
+ nav {
+ padding: 10px 0;
+ text-align: center;
+ }
+ nav a {
+ display: inline-block;
+ }
+ nav a:first-of-type {
+ margin-right: 10px;
+ }
+ [hidden] {
+ display: none !important;
+ }
+ </style>
+ <iron-ajax
+ auto
+ url="/changes/"
+ params="[[_computeQueryParams(query, offset)]]"
+ json-prefix=")]}'"
+ last-response="{{_changes}}"
+ debounce-duration="300"></iron-ajax>
+ <gr-change-list changes="{{_changes}}"></gr-change-list>
+ <nav>
+ <a href$="[[_computeNavLink(query, offset, -1)]]"
+ hidden$="[[_hidePrevArrow(offset)]]">← Prev</a>
+ <a href$="[[_computeNavLink(query, offset, 1)]]"
+ hidden$="[[_hideNextArrow(_changes.length)]]">Next →</a>
+ </nav>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ var DEFAULT_NUM_CHANGES = 25;
+
+ Polymer({
+ is: 'gr-change-list-view',
+
+ properties: {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Change objects loaded from the server.
+ */
+ _changes: Array,
+ },
+
+ _paramsChanged: function(value) {
+ this.query = value.query;
+ this.offset = value.offset || 0;
+ },
+
+ _computeQueryParams: function(query, offset) {
+ var options = Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.LABELS,
+ Changes.ListChangesOption.DETAILED_ACCOUNTS
+ );
+ var obj = {
+ n: DEFAULT_NUM_CHANGES, // Number of results to return.
+ O: options,
+ S: offset || 0,
+ };
+ if (query && query.length > 0) {
+ obj.q = query;
+ }
+ return obj;
+ },
+
+ _computeNavLink: function(query, offset, direction) {
+ // Offset could be a string when passed from the router.
+ offset = +(offset || 0);
+ var newOffset = Math.max(0, offset + (25 * direction));
+ var href = '/q/' + query;
+ if (newOffset > 0) {
+ href += ',' + newOffset;
+ }
+ return href;
+ },
+
+ _hidePrevArrow: function(offset) {
+ return offset == 0;
+ },
+
+ _hideNextArrow: function(changesLen) {
+ return changesLen < DEFAULT_NUM_CHANGES;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list.html b/polygerrit-ui/app/elements/gr-change-list.html
new file mode 100644
index 0000000..c4f4e0c
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-list.html
@@ -0,0 +1,126 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-a11y-keys/iron-a11y-keys.html">
+<link rel="import" href="./gr-change-list-item.html">
+
+<dom-module id="gr-change-list">
+ <template>
+ <style>
+ :host {
+ display: table;
+ border: 1px solid #eee;
+ border-collapse: collapse;
+ }
+ :host:focus gr-change-list-item[selected]::shadow {
+ background-color: #d8EdF9;
+ }
+ :host gr-change-list-item[selected]::shadow .positionIndicator {
+ visibility: visible;
+ }
+ :host:focus gr-change-list-item[selected]::shadow .positionIndicator {
+ opacity: 1;
+ }
+ </style>
+
+ <iron-a11y-keys
+ target="[[keyTarget]]"
+ keys="j k enter"
+ on-keys-pressed="_handleKey"></iron-a11y-keys>
+
+ <gr-change-list-item header></gr-change-list-item>
+ <template is="dom-repeat" items="{{changes}}" as="change">
+ <gr-change-list-item change="[[change]]"
+ selected="[[_isSelected(index)]]"></gr-change-list-item>
+ </template>
+ </template>
+
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-list',
+
+ hostAttributes: {
+ tabindex: 0,
+ },
+
+ properties: {
+ /**
+ * An array of ChangeInfo objects to render.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
+ */
+ changes: Array,
+ keyTarget: {
+ type: Object,
+ value: function() {
+ return document.body;
+ }
+ },
+ selectedIndex: {
+ type: Number,
+ value: 0,
+ observer: '_selectedIndexChanged',
+ },
+ },
+
+ _isSelected: function(index) {
+ return index == this.selectedIndex;
+ },
+
+ _selectedIndexChanged: function(value) {
+ // Don’t re-render the entire list.
+ var changeEls = this._getNonHeaderListItems();
+ for (var i = 0; i < changeEls.length; i++) {
+ changeEls[i].toggleAttribute('selected', i == value);
+ }
+ },
+
+ _handleKey: function(e) {
+ var len = (this.changes && this.changes.length) || 0;
+ switch(e.detail.combo) {
+ case 'j':
+ if (this.selectedIndex == len - 1) { return; }
+ this.selectedIndex += 1;
+ break;
+ case 'k':
+ if (this.selectedIndex == 0) { return; }
+ this.selectedIndex -= 1;
+ break;
+ case 'enter':
+ page(this._changeURLForIndex(this.selectedIndex));
+ break;
+ }
+ },
+
+ _changeURLForIndex: function(index) {
+ var changeEls = this._getNonHeaderListItems();
+ if (index < changeEls.length && changeEls[index]) {
+ return changeEls[index].changeURL();
+ }
+ return '';
+ },
+
+ _getNonHeaderListItems: function() {
+ return this.querySelectorAll('gr-change-list-item:not([header])');
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
new file mode 100644
index 0000000..40371c1
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -0,0 +1,195 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
+<link rel="import" href="./gr-date-formatter.html">
+<link rel="import" href="./gr-file-list.html">
+<link rel="import" href="./gr-messages-list.html">
+
+<dom-module id="gr-change-view">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .container {
+ margin: 0 20px;
+ }
+ .changeInfo,
+ .summary {
+ margin: 10px 0;
+ padding: 10px 0;
+ }
+ table {
+ border-collapse: collapse;
+ }
+ td {
+ padding: 2px 5px;
+ vertical-align: top;
+ }
+ .changeInfo-label {
+ font-weight: bold;
+ text-align: right;
+ }
+ .summary {
+ font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
+ margin: 10px 0;
+ overflow-x: auto;
+ padding: 10px 0;
+ white-space: pre-wrap;
+ }
+ .summary {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ }
+ </style>
+ <iron-ajax id="detailXHR"
+ url="[[_computeDetailPath(changeNum)]]"
+ params="[[_computeDetailQueryParams()]]"
+ json-prefix=")]}'"
+ last-response="{{change}}"
+ debounce-duration="300"></iron-ajax>
+ <iron-ajax id="commentsXHR"
+ url="[[_computeCommentsPath(changeNum)]]"
+ json-prefix=")]}'"
+ last-response="{{comments}}"
+ debounce-duration="300"></iron-ajax>
+
+ <div class="container">
+ <h2>[[change.subject]]</h2>
+ <div class="changeInfo">
+ <table>
+ <tr>
+ <td class="changeInfo-label">Owner</td>
+ <td>[[change.owner.name]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Reviewers</td>
+ <td>
+ <template is="dom-repeat"
+ items="[[_computeReviewers(change.labels.Code-Review.all, change.owner)]]"
+ as="reviewer">
+ <div>[[reviewer.name]]</div>
+ </template>
+ </td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Project</td>
+ <td>[[change.project]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Branch</td>
+ <td>[[change.branch]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Topic</td>
+ <td>[[change.topic]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Strategy</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Updated</td>
+ <td>
+ <gr-date-formatter
+ date-str="[[change.updated]]"></gr-date-formatter>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div class="summary">[[_computeCurrentRevisionMessage(change)]]</div>
+ <gr-file-list change-num="[[changeNum]]"
+ patch-num="[[_computePatchNum(change.current_revision)]]"
+ revision="[[change.current_revision]]"
+ comments="[[comments]]"></gr-file-list>
+ <gr-messages-list change-num="[[changeNum]]"
+ messages="[[change.messages]]"
+ comments="[[comments]]"></gr-messages-list>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-view',
+
+ properties: {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ changeNum: Number,
+ },
+
+ _paramsChanged: function(value) {
+ this.changeNum = value.changeNum;
+ if (!this.changeNum) {
+ this.change = null;
+ this.comments = null;
+ return;
+ }
+ this.$.detailXHR.generateRequest();
+ this.$.commentsXHR.generateRequest();
+ },
+
+ _computeDetailPath: function(changeNum) {
+ return '/changes/' + changeNum + '/detail';
+ },
+
+ _computeCommitInfoPath: function(changeNum, commitHash) {
+ return '/changes/' + changeNum + '/revisions/' + commitHash + '/commit';
+ },
+
+ _computeCommentsPath: function(changeNum) {
+ return '/changes/' + changeNum + '/comments';
+ },
+
+ _computePatchNum: function(revision) {
+ return this.change && this.change.revisions[revision]._number;
+ },
+
+ _computeDetailQueryParams: function() {
+ var options = Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.CURRENT_REVISION,
+ Changes.ListChangesOption.CURRENT_COMMIT,
+ Changes.ListChangesOption.CHANGE_ACTIONS
+ );
+ return { O: options };
+ },
+
+ _computeCurrentRevisionMessage: function(change) {
+ return change &&
+ change.revisions[change.current_revision].commit.message;
+ },
+
+ _computeReviewers: function(reviewers, owner) {
+ if (reviewers.length == 1) { return reviewers; }
+ return reviewers.filter(function(reviewer) {
+ return reviewer._account_id != owner._account_id;
+ });
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-date-formatter.html b/polygerrit-ui/app/elements/gr-date-formatter.html
new file mode 100644
index 0000000..55782f6
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-date-formatter.html
@@ -0,0 +1,90 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-date-formatter">
+ <template>
+ <style>
+ :host {
+ display: inline;
+ }
+ </style>
+ <span>[[_computeDateStr(dateStr)]]</span>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ var Duration = {
+ HOUR: 1000 * 60 * 60,
+ DAY: 1000 * 60 * 60 * 24,
+ };
+
+ var ShortMonthNames = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
+ 'Nov', 'Dec'
+ ];
+
+ Polymer({
+ is: 'gr-date-formatter',
+
+ properties: {
+ dateStr: {
+ type: String,
+ value: null,
+ notify: true
+ }
+ },
+
+ _computeDateStr: function(dateStr) {
+ return this._dateStr(this._parseDateStr(dateStr), new Date());
+ },
+
+ _parseDateStr: function(dateStr) {
+ if (!dateStr) { return null; }
+ return util.parseDate(dateStr);
+ },
+
+ _dateStr: function(t, now) {
+ if (!t) { return ''; }
+ var diff = now.getTime() - t.getTime();
+ if (diff < Duration.DAY && t.getDay() == now.getDay()) {
+ // Within 24 hours and on the same day:
+ // '2:14 AM'
+ var pm = t.getHours() >= 12;
+ var hours = t.getHours() === 0 ? 12 :
+ pm ? t.getHours() - 12 : t.getHours();
+ var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
+ t.getMinutes();
+ return hours + ':' + minutes + (pm ? ' PM' : ' AM');
+ } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
+ diff < 180 * Duration.DAY) {
+ // From one to six months:
+ // 'Aug 29'
+ return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
+ } else if (diff >= 180 * Duration.DAY) {
+ // More than six months:
+ // 'Aug 29, 1997'
+ return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
+ t.getFullYear();
+ }
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
new file mode 100644
index 0000000..f27440b
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff-view.html
@@ -0,0 +1,273 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-diff-view">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .diffContainer {
+ display: flex;
+ font-family: 'Source Code Pro', monospace;
+ white-space: pre;
+ }
+ .diffNumbers {
+ border-right: 1px solid #eee;
+ color: #aaa;
+ padding: 0 10px;
+ text-align: right;
+ }
+ .diffContent {
+ padding-left: 2px;
+ }
+ .lineNum {
+ cursor: pointer;
+ }
+ .lineNum:hover {
+ text-decoration: underline;
+ }
+ .lightRed {
+ background-color: #fee;
+ }
+ .darkRed,
+ .delete span {
+ background-color: #faa;
+ }
+ .lightGreen {
+ background-color: #efe;
+ }
+ .darkGreen,
+ .insert span {
+ background-color: #9f9;
+ }
+ </style>
+ <iron-ajax id="changeDetailXHR"
+ auto
+ url="[[_computeChangeDetailPath(_changeNum)]]"
+ params="[[_computeChangeDetailQueryParams()]]"
+ json-prefix=")]}'"
+ last-response="{{_change}}"
+ debounce-duration="300"></iron-ajax>
+ <iron-ajax
+ id="diffXHR"
+ url="[[_computeDiffPath(_changeNum, _patchNum, _path)]]"
+ json-prefix=")]}'"
+ on-response="_handleDiffResponse"
+ debounce-duration="300"></iron-ajax>
+ <div class="diffContainer" id="diffContainer">
+ <div class="diffNumbers" id="leftDiffNumbers"></div>
+ <div class="diffContent" id="leftDiffContent"></div>
+ <div class="diffNumbers" id="rightDiffNumbers"></div>
+ <div class="diffContent" id="rightDiffContent"></div>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-diff-view',
+
+ properties: {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ _change: Object,
+ _changeNum: String,
+ _basePatchNum: String,
+ _patchNum: String,
+ _path: String,
+ },
+
+ _paramsChanged: function(value) {
+ console.log(value)
+ this._changeNum = value.changeNum;
+ this._patchNum = value.patchNum;
+ this._basePatchNum = value.basePatchNum;
+ this._path = value.path;
+ console.log(this._basePatchNum);
+ if (!this._changeNum) {
+ this._change = null;
+ this._basePatchNum = null;
+ this._patchNum = null;
+ this._path = null;
+ return;
+ }
+ // Assign the params here since a computed binding relying on
+ // `_basePatchNum` won’t fire in the case where it’s not defined.
+ this.$.diffXHR.params = this._diffQueryParams();
+ this.$.diffXHR.generateRequest();
+ },
+
+ _computeChangeDetailPath: function(changeNum) {
+ return '/changes/' + changeNum + '/detail';
+ },
+
+ _computeChangeDetailQueryParams: function() {
+ var options = Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.ALL_REVISIONS
+ );
+ return { O: options };
+ },
+
+ _computeDiffPath: function(changeNum, patchNum, path) {
+ return '/changes/' + changeNum + '/revisions/' + patchNum + '/files/' +
+ encodeURIComponent(path) + '/diff';
+ },
+
+ _diffQueryParams: function(basePatchNum) {
+ var params = {
+ context: 'ALL',
+ intraline: null
+ };
+ if (!!basePatchNum) {
+ params.base = basePatchNum;
+ }
+ return params;
+ },
+
+ _handleDiffResponse: function(e, req) {
+ var diff = e.detail.response;
+ this._constructDOM(diff);
+ },
+
+ _constructDOM: function(diff) {
+ if (!diff.content) { return; }
+
+ var leftLineNum = 0 + (diff.content.skip || 0);
+ var rightLineNum = leftLineNum;
+ for (var i = 0; i < diff.content.length; i++) {
+ var diffChunk = diff.content[i];
+ if (diffChunk.ab) {
+ for (var j = 0; j < diffChunk.ab.length; j++) {
+ this._addRow(++leftLineNum, ++rightLineNum, diffChunk.ab[j],
+ diffChunk.ab[j]);
+ }
+ continue;
+ }
+ if (diffChunk.a || diffChunk.b) {
+ var aLen = (diffChunk.a && diffChunk.a.length) || 0;
+ var bLen = (diffChunk.b && diffChunk.b.length) || 0;
+ var maxLen = Math.max(aLen, bLen);
+ for (var j = 0; j < maxLen; j++) {
+ var leftContent;
+ if (diffChunk.a && j < diffChunk.a.length) {
+ leftContent = diffChunk.a[j];
+ leftLineNum++;
+ }
+ var rightContent;
+ if (diffChunk.b && j < diffChunk.b.length) {
+ rightContent = diffChunk.b[j];
+ rightLineNum++;
+ }
+ var leftHighlight;
+ if (diffChunk.edit_a && j < diffChunk.edit_a.length) {
+ leftHighlight = diffChunk.edit_a[j];
+ }
+ var rightHighlight;
+ if (diffChunk.edit_b && j < diffChunk.edit_b.length) {
+ rightHighlight = diffChunk.edit_b[j];
+ }
+ this._addRow(leftLineNum,
+ rightLineNum,
+ leftContent,
+ rightContent,
+ leftHighlight,
+ rightHighlight);
+ }
+ }
+ }
+ },
+
+ _addRow: function(leftLineNum,
+ rightLineNum,
+ leftContent,
+ rightContent,
+ leftHighlight,
+ rightHighlight) {
+ var leftLineNumEl = document.createElement('div');
+ var rightLineNumEl = document.createElement('div');
+ var leftColEl = document.createElement('div');
+ // These classes are added to account for Polymer’s polyfill behavior.
+ // In order to guarantee sufficient specificity within the CSS rules,
+ // these are added to every element. Since the Polymer DOM utility
+ // functions (which would do this automatically) are not being used for
+ // performance reasons, this is done manually.
+ leftColEl.className = 'style-scope gr-diff-view';
+ var rightColEl = document.createElement('div');
+ rightColEl.className = 'style-scope gr-diff-view';
+ leftLineNumEl.className = 'style-scope gr-diff-view lineNum';
+ rightLineNumEl.className = 'style-scope gr-diff-view lineNum';
+
+ // Ensure that all elements have content so they render at the correct
+ // height.
+ leftLineNumEl.textContent =
+ leftContent != undefined ? leftLineNum : '\n';
+ rightLineNumEl.textContent =
+ rightContent != undefined ? rightLineNum : '\n';
+ leftContent = leftContent || '\n';
+ rightContent = rightContent || '\n';
+
+ var leftHTML;
+ var rightHTML;
+ if (leftContent == rightContent) {
+ leftHTML = leftContent;
+ rightHTML = rightContent;
+ } else {
+ leftHTML = this._highlightedHTML(leftContent, leftHighlight);
+ rightHTML = this._highlightedHTML(rightContent, rightHighlight);
+ }
+
+ // If the html is just the text then it didn't get highlighted.
+ // Use textContent which is faster than innerHTML.
+ if (leftContent == leftHTML) {
+ leftColEl.textContent = leftContent;
+ } else {
+ leftColEl.innerHTML = leftHTML;
+ }
+ if (rightContent == rightHTML) {
+ rightColEl.textContent = rightContent;
+ } else {
+ rightColEl.innerHTML = rightHTML;
+ }
+
+ if (leftContent != rightContent) {
+ leftColEl.classList.add('delete');
+ rightColEl.classList.add('insert');
+ leftColEl.classList.add(leftHighlight ? 'lightRed' : 'darkRed');
+ rightColEl.classList.add(rightHighlight ? 'lightGreen' : 'darkGreen');
+ }
+ this.$.leftDiffNumbers.appendChild(leftLineNumEl);
+ this.$.leftDiffContent.appendChild(leftColEl);
+ this.$.rightDiffNumbers.appendChild(rightLineNumEl);
+ this.$.rightDiffContent.appendChild(rightColEl);
+ },
+
+ _highlightedHTML: function(content, range) {
+ return content;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-file-list.html b/polygerrit-ui/app/elements/gr-file-list.html
new file mode 100644
index 0000000..1a5dcf0
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-file-list.html
@@ -0,0 +1,129 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-file-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .tableContainer {
+ overflow-x: auto;
+ }
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ table a {
+ display: block;
+ }
+ td {
+ padding: 2px 0;
+ white-space: nowrap;
+ }
+ th {
+ text-align: left;
+ }
+ .status {
+ width: 20px;
+ }
+ </style>
+ <iron-ajax id="xhr"
+ url="[[_computeFilesURL(changeNum, revision)]]"
+ json-prefix=")]}'"
+ on-response="_handleResponse"
+ debounce-duration="300"></iron-ajax>
+ <div class="tableContainer">
+ <table>
+ <tr>
+ <th></th>
+ <th></th>
+ <th>Path</th>
+ <th>Stats</th>
+ </tr>
+ <template is="dom-repeat" items="{{files}}" as="file">
+ <tr>
+ <td></td>
+ <td class="status">[[file.status]]</td>
+ <td class="path">
+ <a class="file"
+ href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">[[file.__path]]</a>
+ </td>
+ <td>
+ +<span>[[file.lines_inserted]]</span> lines,
+ -<span>[[file.lines_deleted]]</span> lines
+ </td>
+ </tr>
+ </template>
+ </table>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-file-list',
+
+ properties: {
+ patchNum: Number,
+ changeNum: {
+ type: Number,
+ observer: '_changeNumOrRevisionChanged',
+ },
+ revision: {
+ type: String,
+ observer: '_changeNumOrRevisionChanged',
+ },
+ comments: {
+ type: Array,
+ value: [],
+ },
+ },
+
+ _changeNumOrRevisionChanged: function() {
+ if (!!this.changeNum && !!this.revision) {
+ this.$.xhr.generateRequest();
+ }
+ },
+
+ _computeFilesURL: function(changeNum, revision) {
+ return '/changes/' + changeNum + '/revisions/' + revision + '/files';
+ },
+
+ _handleResponse: function(e, req) {
+ var result = e.detail.response;
+ var paths = Object.keys(result).sort();
+ var files = [];
+ for (var i = 0; i < paths.length; i++) {
+ var info = result[paths[i]];
+ info.__path = paths[i];
+ info.lines_inserted = info.lines_inserted || 0;
+ info.lines_deleted = info.lines_deleted || 0;
+ files.push(info)
+ }
+ this.files = files;
+ },
+
+ _computeDiffURL: function(changeNum, patchNum, path) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + path;
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html
new file mode 100644
index 0000000..c0770a0
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-message.html
@@ -0,0 +1,217 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="./gr-date-formatter.html">
+
+<dom-module id="gr-message">
+ <template>
+ <style>
+ :host {
+ border-top: 1px solid #ddd;
+ display: block;
+ position: relative;
+ }
+ :host:not([expanded]) {
+ cursor: pointer;
+ }
+ .avatar {
+ border-radius: 50%;
+ }
+ .collapsed .contentContainer {
+ padding: 10px;
+ padding-right: 60px;
+ white-space: nowrap;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ }
+ .expanded .contentContainer {
+ padding: 7px 0 10px;
+ }
+ .expanded .contentContainer {
+ margin-left: 45px;
+ }
+ .collapsed .contentContainer {
+ color: #777;
+ margin-left: 35px;
+ }
+ .avatar {
+ position: absolute;
+ left: 0;
+ }
+ .collapsed .avatar {
+ top: 8px;
+ }
+ .expanded .avatar {
+ top: 10px;
+ }
+ .collapsed .avatar {
+ height: 25px;
+ width: 25px;
+ }
+ .expanded .avatar {
+ height: 35px;
+ width: 35px;
+ }
+ .name {
+ font-weight: bold;
+ }
+ .collapsed .name,
+ .collapsed .content,
+ .collapsed .message {
+ display: inline;
+ }
+ .collapsed .comments {
+ display: none;
+ }
+ .collapsed .name,
+ .collapsed gr-date-formatter {
+ color: #000;
+ }
+ .expanded .name {
+ cursor: pointer;
+ }
+ .expanded .message,
+ .expanded .commentMessage {
+ white-space: pre-wrap;
+ }
+ gr-date-formatter {
+ position: absolute;
+ right: 0;
+ top: 10px;
+ }
+ .file {
+ border-top: 1px solid #ddd;
+ font-weight: bold;
+ margin: 10px 0 3px;
+ padding: 10px 0 5px;
+ }
+ .commentContainer {
+ display: flex;
+ margin: 5px 0;
+ }
+ .lineNum {
+ margin-right: 10px;
+ min-width: 75px;
+ }
+ .commentMessage {
+ flex: 1;
+ }
+ </style>
+ <div class$="[[_computeClass(expanded)]]">
+ <img class="avatar" src$="[[_computeAvatarURL(message.author)]]">
+ <div class="contentContainer">
+ <div class="name" id="name">[[message.author.name]]</div>
+ <div class="content">
+ <div class="message">[[message.message]]</div>
+ <div class="comments">
+ <template is="dom-repeat" items="{{files}}" as="file">
+ <div class="file">
+ <a href$="[[_computeFileDiffURL(file, changeNum, message._revision_number)]]">[[file]]</a>:
+ </div>
+ <template is="dom-repeat"
+ items="[[_computeCommentsForFile(file)]]" as="comment">
+ <div class="commentContainer">
+ <a class="lineNum"
+ href$="[[_computeDiffLineURL(file, changeNum, message._revision_number, comment)]]">
+ <template is="dom-if" if="[[comment.line]]">
+ Line <span>[[comment.line]]</span>:
+ </template>
+ <template is="dom-if" if="[[!comment.line]]">
+ File comment:
+ </template>
+ </a>
+ <div class="commentMessage">[[comment.message]]</div>
+ </div>
+ </template>
+ </template>
+ </div>
+ </div>
+ <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
+ </div>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-message',
+
+ listeners: {
+ 'tap': '_tapHandler',
+ 'name.tap': '_collapseHandler',
+ },
+
+ properties: {
+ changeNum: Number,
+ message: Object,
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ expanded: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
+ },
+
+ _commentsChanged: function(value) {
+ this.files = Object.keys(value || {}).sort();
+ this.expanded = this.files.length > 0;
+ },
+
+ _computeFileDiffURL: function(file, changeNum, patchNum) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + file;
+ },
+
+ _computeDiffLineURL: function(file, changeNum, patchNum, comment) {
+ var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
+ if (comment.line) {
+ diffURL += '#' + comment.line;
+ }
+ return diffURL;
+ },
+
+ _computeCommentsForFile: function(file) {
+ return this.comments[file];
+ },
+
+ _tapHandler: function(e) {
+ if (this.expanded) { return; }
+ this.expanded = true;
+ },
+
+ _collapseHandler: function(e) {
+ if (!this.expanded) { return; }
+ e.stopPropagation();
+ this.expanded = false;
+ },
+
+ _computeClass: function(expanded) {
+ return expanded ? 'expanded' : 'collapsed';
+ },
+
+ _computeAvatarURL: function(author) {
+ if (!author) { return '' }
+ return '/accounts/' + author.email + '/avatar?s=100';
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-messages-list.html b/polygerrit-ui/app/elements/gr-messages-list.html
new file mode 100644
index 0000000..7771ef4
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-messages-list.html
@@ -0,0 +1,81 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="./gr-message.html">
+
+<dom-module id="gr-messages-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ h3 {
+ margin: 20px 0 5px;
+ }
+ </style>
+ <h3>Messages</h3>
+ <template is="dom-repeat" items="{{messages}}" as="message">
+ <gr-message change-num="[[changeNum]]"
+ message="[[message]]"
+ comments="[[_computeCommentsForMessage(comments, message, index)]]"></gr-message>
+ </template>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-messages-list',
+
+ properties: {
+ changeNum: Number,
+ messages: {
+ type: Array,
+ value: [],
+ },
+ comments: Object,
+ },
+
+ _computeCommentsForMessage: function(comments, message, index) {
+ var comments = comments || {};
+ var messages = this.messages || [];
+ var msgComments = {};
+ var mDate = util.parseDate(message.date);
+ var nextMDate;
+ if (index < messages.length - 1) {
+ nextMDate = util.parseDate(messages[index + 1].date);
+ }
+ for (var file in comments) {
+ var fileComments = comments[file];
+ for (var i = 0; i < fileComments.length; i++) {
+ var cDate = util.parseDate(fileComments[i].updated);
+ if (cDate >= mDate) {
+ if (nextMDate && cDate >= nextMDate) {
+ continue;
+ }
+ msgComments[file] = msgComments[file] || [];
+ msgComments[file].push(fileComments[i]);
+ }
+ }
+ }
+ return msgComments;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-search-bar.html b/polygerrit-ui/app/elements/gr-search-bar.html
new file mode 100644
index 0000000..fedd145
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-search-bar.html
@@ -0,0 +1,92 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-input/iron-input.html">
+
+<dom-module id="gr-search-bar">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ form {
+ display: flex;
+ }
+ input,
+ button {
+ border: 1px solid #aaa;
+ font-family: inherit;
+ font-size: 14px;
+ padding: 2px 5px;
+ }
+ input {
+ flex: 1;
+ border-radius: 2px 0 0 2px;
+ }
+ button {
+ background-color: #f1f2f3;
+ border-radius: 0 2px 2px 0;
+ border-left-width: 0;
+ }
+ </style>
+ <form>
+ <input is="iron-input" id="searchInput" bind-value="{{inputVal}}">
+ <button type="submit" id="searchButton">Search</button>
+ </form>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-search-bar',
+
+ listeners: {
+ 'value-changed': '_valueChangedHandler',
+ 'searchInput.keydown': '_inputKeyDownHandler',
+ 'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
+ },
+
+ properties: {
+ value: {
+ type: String,
+ value: '',
+ notify: true,
+ }
+ },
+
+ _valueChangedHandler: function(e) {
+ this.inputVal = e.detail.value;
+ },
+
+ _inputKeyDownHandler: function(e) {
+ if (e.keyCode == 13) {
+ // Enter was pressed.
+ this._preventDefaultAndNavigateToInputVal(e);
+ }
+ },
+
+ _preventDefaultAndNavigateToInputVal: function(e) {
+ e.preventDefault();
+ page.show('/q/' + this.inputVal);
+ },
+
+ });
+
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/favicon.ico b/polygerrit-ui/app/favicon.ico
new file mode 100644
index 0000000..155217b
--- /dev/null
+++ b/polygerrit-ui/app/favicon.ico
Binary files differ
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
new file mode 100644
index 0000000..f343288
--- /dev/null
+++ b/polygerrit-ui/app/index.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<html lang="en">
+<meta charset="utf-8">
+<meta name="description" content="Gerrit Code Review">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>PolyGerrit</title>
+
+<!-- build:css /styles/main.css -->
+<link rel="stylesheet" href="/styles/main.css">
+<!-- endbuild-->
+
+<!-- build:js /bower_components/webcomponentsjs/webcomponents-lite.min.js -->
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<!-- endbuild -->
+
+<!-- Use Shadow DOM where supported.
+ https://www.polymer-project.org/1.0/docs/devguide/settings.html -->
+<!--<script>
+window.Polymer = window.Polymer || {};
+window.Polymer.dom = 'shadow';
+</script>-->
+
+<!-- will be replaced with elements/gr-app.vulcanized.html -->
+<link rel="import" href="/elements/gr-app.html">
+<!-- endreplace-->
+
+<body unresolved>
+<template is="dom-bind" id="app">
+ <gr-app params="{{params}}" route="{{route}}"></gr-app>
+</template>
+
+<!-- build:js /scripts/app.js -->
+<script src="/bower_components/page/page.js"></script>
+<script src="/scripts/app.js"></script>
+<script src="/scripts/changes.js"></script>
+<script src="/scripts/util.js"></script>
+
+<!-- endbuild-->
diff --git a/polygerrit-ui/app/robots.txt b/polygerrit-ui/app/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/polygerrit-ui/app/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/polygerrit-ui/app/scripts/app.js b/polygerrit-ui/app/scripts/app.js
new file mode 100644
index 0000000..ac9ee7d
--- /dev/null
+++ b/polygerrit-ui/app/scripts/app.js
@@ -0,0 +1,66 @@
+// 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.
+
+(function(document) {
+ 'use strict';
+
+ // See https://github.com/Polymer/polymer/issues/1381
+ window.addEventListener('WebComponentsReady', function() {
+ // Middleware
+ function scrollToTop(ctx, next) {
+ document.body.scrollTop = 0;
+ next();
+ }
+
+ // Routes.
+ page('/', function() {
+ page.redirect('/q/status:open');
+ });
+
+ function queryHandler(data) {
+ app.route = 'gr-change-list';
+ app.params = data.params;
+ }
+
+ page('/q/:query,:offset', scrollToTop, queryHandler);
+ page('/q/:query', scrollToTop, queryHandler);
+
+ page(/^\/(\d+)\/?/, scrollToTop, function(ctx) {
+ page.redirect('/c/' + ctx.params[0]);
+ });
+
+ page('/c/:changeNum', scrollToTop, function(data) {
+ app.route = 'gr-change-view';
+ app.params = data.params;
+ });
+
+ page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, scrollToTop, function(ctx) {
+ app.route = 'gr-diff-view';
+ var params = {
+ changeNum: ctx.params[0],
+ basePatchNum: ctx.params[2],
+ patchNum: ctx.params[4],
+ path: ctx.params[5]
+ };
+ if (!params.patchNum) {
+ params.patchNum = params.basePatchNum;
+ delete(params.basePatchNum);
+ }
+ app.params = params;
+ });
+
+ page.start();
+ });
+
+})(document);
diff --git a/polygerrit-ui/app/scripts/changes.js b/polygerrit-ui/app/scripts/changes.js
new file mode 100644
index 0000000..7169450
--- /dev/null
+++ b/polygerrit-ui/app/scripts/changes.js
@@ -0,0 +1,73 @@
+// 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.
+
+'use strict';
+
+var Changes = Changes || {};
+
+// Must be kept in sync with the ListChangesOption enum and protobuf.
+Changes.ListChangesOption = {
+ LABELS: 0,
+ DETAILED_LABELS: 8,
+
+ // Return information on the current patch set of the change.
+ CURRENT_REVISION: 1,
+ ALL_REVISIONS: 2,
+
+ // If revisions are included, parse the commit object.
+ CURRENT_COMMIT: 3,
+ ALL_COMMITS: 4,
+
+ // If a patch set is included, include the files of the patch set.
+ CURRENT_FILES: 5,
+ ALL_FILES: 6,
+
+ // If accounts are included, include detailed account info.
+ DETAILED_ACCOUNTS: 7,
+
+ // Include messages associated with the change.
+ MESSAGES: 9,
+
+ // Include allowed actions client could perform.
+ CURRENT_ACTIONS: 10,
+
+ // Set the reviewed boolean for the caller.
+ REVIEWED: 11,
+
+ // Include download commands for the caller.
+ DOWNLOAD_COMMANDS: 13,
+
+ // Include patch set weblinks.
+ WEB_LINKS: 14,
+
+ // Include consistency check results.
+ CHECK: 15,
+
+ // Include allowed change actions client could perform.
+ CHANGE_ACTIONS: 16,
+
+ // Include a copy of commit messages including review footers.
+ COMMIT_FOOTERS: 17,
+
+ // Include push certificate information along with any patch sets.
+ PUSH_CERTIFICATES: 18
+};
+
+Changes.listChangesOptionsToHex = function() {
+ var v = 0;
+ for (var i = 0; i < arguments.length; i++) {
+ v |= 1 << arguments[i];
+ }
+ return v.toString(16);
+};
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
new file mode 100644
index 0000000..d798d9a
--- /dev/null
+++ b/polygerrit-ui/app/scripts/util.js
@@ -0,0 +1,25 @@
+// 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.
+
+'use strict';
+
+var util = util || {};
+
+util.parseDate = function(dateStr) {
+ // Timestamps are given in UTC and have the format
+ // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
+ // nanoseconds.
+ // Munge the date into an ISO 8061 format and parse that.
+ return new Date(dateStr.replace(' ', 'T') + 'Z');
+};
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
new file mode 100644
index 0000000..41ffec7
--- /dev/null
+++ b/polygerrit-ui/app/styles/main.css
@@ -0,0 +1,34 @@
+/*
+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.
+*/
+
+@import "//fonts.googleapis.com/css?family=Open+Sans:400,700";
+@import "//fonts.googleapis.com/css?family=Source+Code+Pro";
+
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+html,
+body {
+ height: 100%;
+ transition: none; /* Override the default Polymer fade-in. */
+}
+body {
+ font: 14px 'Open Sans', sans-serif;
+}
diff --git a/polygerrit-ui/app/test/gr-app-test.html b/polygerrit-ui/app/test/gr-app-test.html
new file mode 100644
index 0000000..8353186
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-app-test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-app</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<!-- Step 1: import the element to test -->
+<link rel="import" href="../elements/gr-app.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-app></gr-app>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-app tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-list-item-test.html b/polygerrit-ui/app/test/gr-change-list-item-test.html
new file mode 100644
index 0000000..d9cf2b6
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-list-item-test.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-list-item</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<link rel="import" href="../elements/gr-change-list-item.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list-item></gr-change-list-item>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list-item tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeChangeStatusString({mergeable: true}), '');
+ assert.equal(element._computeChangeStatusString({mergeable: false}),
+ 'Merge Conflict');
+
+ assert.equal(element._computeCodeReviewClass(), '');
+ assert.equal(element._computeCodeReviewClass({}), '');
+ assert.equal(element._computeCodeReviewClass({approved: true, value: 1}),
+ 'u-green');
+ assert.equal(element._computeCodeReviewClass({value: 1}),
+ 'u-monospace u-green');
+ assert.equal(element._computeCodeReviewClass({value: -1}),
+ 'u-monospace u-red');
+
+ assert.equal(element._computeCodeReviewLabel(), '');
+ assert.equal(element._computeCodeReviewLabel({}), '');
+ assert.equal(element._computeCodeReviewLabel({approved: true, value: 1}),
+ '✓');
+ assert.equal(element._computeCodeReviewLabel({value: 1}), '+1');
+ assert.equal(element._computeCodeReviewLabel({value: -1}), '-1');
+
+ assert.equal(element._computeVerifiedLabel(), '');
+ assert.equal(element._computeVerifiedLabel({}), '');
+ assert.equal(element._computeVerifiedLabel({approved: true}), '✓');
+
+
+ assert.equal(element._computeOwnerLink('andybons+gerrit@gmail.com'),
+ '/q/owner:andybons%2Bgerrit%40gmail.com+status:open');
+
+ assert.equal(element._computeOwnerTitle(
+ {
+ name: 'Andrew Bonventre',
+ email: 'andybons+gerrit@gmail.com'
+ }),
+ 'Andrew Bonventre <andybons+gerrit@gmail.com>');
+
+ // TODO(andybons): _computeProjectURL once it's not a constant.
+
+ assert.equal(element._computeProjectBranchURL(
+ 'combustible-stuff', 'lemons'),
+ '/q/status:open+project:combustible-stuff+branch:lemons');
+
+ element.change = { _number: 42 };
+ assert.equal(element.changeURL(), '/c/42/');
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-list-test.html b/polygerrit-ui/app/test/gr-change-list-test.html
new file mode 100644
index 0000000..79ce129
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-list-test.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-list</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+<link rel="import" href="../elements/gr-change-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list></gr-change-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list tests', function() {
+ var changeList;
+
+ setup(function() {
+ changeList = fixture('basic');
+ });
+
+ test('keyboard shortcuts', function() {
+ changeList.changes = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ flushAsynchronousOperations();
+ var changeListItems =
+ changeList.querySelectorAll('gr-change-list-item:not([header])');
+ assert.equal(changeListItems.length, 3);
+ assert.equal(changeList.selectedIndex, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(changeList, 74); // 'j'
+ flushAsynchronousOperations();
+ assert.equal(changeList.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(changeList, 74); // 'j'
+ flushAsynchronousOperations();
+ assert.equal(changeList.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
+ flushAsynchronousOperations();
+ assert.equal(changeList.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
+ flushAsynchronousOperations();
+ assert.equal(changeList.selectedIndex, 0);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-list-view-test.html b/polygerrit-ui/app/test/gr-change-list-view-test.html
new file mode 100644
index 0000000..d426082
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-list-view-test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-list-view</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<!-- Step 1: import the element to test -->
+<link rel="import" href="../elements/gr-change-list-view.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list-view></gr-change-list-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list-view tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-view-test.html b/polygerrit-ui/app/test/gr-change-view-test.html
new file mode 100644
index 0000000..3723019
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-view-test.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-view</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<script src="../scripts/changes.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<!-- Step 1: import the element to test -->
+<link rel="import" href="../elements/gr-change-view.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-view></gr-change-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-view tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-date-formatter-test.html b/polygerrit-ui/app/test/gr-date-formatter-test.html
new file mode 100644
index 0000000..d632755
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-date-formatter-test.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-date-formatter</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<script src="../scripts/util.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<link rel="import" href="../elements/gr-date-formatter.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-date-formatter tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('date is parsed correctly', function() {
+ assert.notOk((new Date('foo')).valueOf());
+ var d = element._parseDateStr(element.getAttribute('date-str'));
+ assert.isAbove(d.valueOf(), 0);
+ });
+
+ function normalizedDate(dateStr) {
+ var d = new Date(dateStr);
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
+ return d;
+ }
+
+ function testDates(nowStr, dateStr, expected) {
+ var now = normalizedDate(nowStr);
+ var t = normalizedDate(dateStr);
+ assert.equal(element._dateStr(t, now), expected);
+ }
+
+ test('dates strings are correct', function() {
+ // Within 24 hours on same day.
+ testDates('2015-07-29T20:34:00.000Z',
+ '2015-07-29T15:34:00.000Z',
+ '3:34 PM');
+
+ // Within 24 hours on different days.
+ testDates('2015-07-29T03:34:00.000Z',
+ '2015-07-28T20:25:00.000Z',
+ 'Jul 28');
+
+ // More than 24 hours. Less than six months.
+ testDates('2015-07-29T20:34:00.000Z',
+ '2015-06-15T03:25:00.000Z',
+ 'Jun 15');
+
+ // More than six months.
+ testDates('2015-09-15T20:34:00.000Z',
+ '2015-01-15T03:25:00.000Z',
+ 'Jan 15, 2015');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-view-test.html b/polygerrit-ui/app/test/gr-diff-view-test.html
new file mode 100644
index 0000000..257a76f
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-view-test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<link rel="import" href="../elements/gr-diff-view.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff></gr-diff>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-view tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-file-list-test.html b/polygerrit-ui/app/test/gr-file-list-test.html
new file mode 100644
index 0000000..6fc6b3a
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-file-list-test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-file-list</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<link rel="import" href="../elements/gr-file-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-file-list></gr-file-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-file-list tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-message-test.html b/polygerrit-ui/app/test/gr-message-test.html
new file mode 100644
index 0000000..772842d
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-message-test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-message</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<!-- Step 1: import the element to test -->
+<link rel="import" href="../elements/gr-message.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-message></gr-message>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-message tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-messages-list-test.html b/polygerrit-ui/app/test/gr-messages-list-test.html
new file mode 100644
index 0000000..52150ac
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-messages-list-test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-messages-list</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<!-- Step 1: import the element to test -->
+<link rel="import" href="../elements/gr-messages-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-messages-list></gr-messages-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-messages-list tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-search-bar-test.html b/polygerrit-ui/app/test/gr-search-bar-test.html
new file mode 100644
index 0000000..ee92928
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-search-bar-test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-search-bar</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
+<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
+
+<link rel="import" href="../elements/gr-search-bar.html">
+
+<test-fixture id="basic">
+ <template>
+ <search-bar></search-bar>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-search-bar tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('', function() {
+
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
new file mode 100644
index 0000000..cc1da34
--- /dev/null
+++ b/polygerrit-ui/app/test/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>Elements Test Runner</title>
+<meta charset="utf-8">
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script>
+ WCT.loadSuites([
+ 'gr-change-list-test.html',
+ 'gr-search-bar-test.html',
+ 'gr-date-formatter-test.html',
+ 'gr-diff-view-test.html',
+ 'gr-app-test.html',
+ 'gr-change-view-test.html',
+ 'gr-change-list-item-test.html',
+ 'gr-change-list-view-test.html',
+ 'gr-file-list-test.html',
+ 'gr-messages-list-test.html',
+ 'gr-message-test.html']);
+</script>
diff --git a/polygerrit-ui/bower.json b/polygerrit-ui/bower.json
new file mode 100644
index 0000000..3b930f4
--- /dev/null
+++ b/polygerrit-ui/bower.json
@@ -0,0 +1,21 @@
+{
+ "name": "polygerrit",
+ "version": "0.0.0",
+ "authors": [
+ "Andrew Bonventre <andybons@google.com>"
+ ],
+ "description": "Gerrit UI in Polymer",
+ "private": true,
+ "dependencies": {
+ "polymer": "Polymer/polymer#^1.1",
+ "page": "visionmedia/page.js#~1.6",
+ "iron-ajax": "PolymerElements/iron-ajax#~1.0",
+ "iron-a11y-keys": "PolymerElements/iron-a11y-keys#~1.0",
+ "iron-input": "PolymerElements/iron-input#~1.0",
+ "iron-test-helpers": "PolymerElements/iron-test-helpers#~1.0"
+ },
+ "devDependencies": {
+ "web-component-tester": "*",
+ "test-fixture": "PolymerElements/test-fixture#^1.0.0"
+ }
+}
diff --git a/polygerrit-ui/gulpfile.js b/polygerrit-ui/gulpfile.js
new file mode 100644
index 0000000..b4ea38c
--- /dev/null
+++ b/polygerrit-ui/gulpfile.js
@@ -0,0 +1,174 @@
+// 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.
+
+'use strict';
+
+// Include Gulp & tools we'll use
+var gulp = require('gulp');
+var $ = require('gulp-load-plugins')();
+var del = require('del');
+var runSequence = require('run-sequence');
+var merge = require('merge-stream');
+var path = require('path');
+var historyApiFallback = require('connect-history-api-fallback');
+
+var AUTOPREFIXER_BROWSERS = [
+ 'ie >= 10',
+ 'ie_mob >= 10',
+ 'ff >= 30',
+ 'chrome >= 34',
+ 'safari >= 7',
+ 'opera >= 23',
+ 'ios >= 7',
+ 'android >= 4.4',
+ 'bb >= 10'
+];
+
+var styleTask = function (stylesPath, srcs) {
+ return gulp.src(srcs.map(function(src) {
+ return path.join('app', stylesPath, src);
+ }))
+ .pipe($.changed(stylesPath, {extension: '.css'}))
+ .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS))
+ .pipe(gulp.dest('.tmp/' + stylesPath))
+ .pipe($.cssmin())
+ .pipe(gulp.dest('dist/' + stylesPath))
+ .pipe($.size({title: stylesPath}));
+};
+
+var imageOptimizeTask = function (src, dest) {
+ return gulp.src(src)
+ .pipe($.cache($.imagemin({
+ progressive: true,
+ interlaced: true
+ })))
+ .pipe(gulp.dest(dest))
+ .pipe($.size({title: 'images'}));
+};
+
+var optimizeHtmlTask = function (src, dest) {
+ var assets = $.useref.assets({searchPath: ['.tmp', 'app', 'dist']});
+
+ return gulp.src(src)
+ // Replace path for vulcanized assets
+ .pipe($.if('*.html', $.replace('elements/gr-app.html', 'elements/gr-app.vulcanized.html')))
+ .pipe(assets)
+ // Concatenate and minify JavaScript
+ .pipe($.if('*.js', $.uglify({preserveComments: 'some'})))
+ // Concatenate and minify styles
+ // In case you are still using useref build blocks
+ .pipe($.if('*.css', $.cssmin()))
+ .pipe(assets.restore())
+ .pipe($.useref())
+ // Minify any HTML
+ .pipe($.if('*.html', $.minifyHtml({
+ quotes: true,
+ empty: true,
+ spare: true
+ })))
+ // Output files
+ .pipe(gulp.dest(dest))
+ .pipe($.size({title: 'html'}));
+};
+
+// Compile and automatically prefix stylesheets
+gulp.task('styles', function () {
+ return styleTask('styles', ['**/*.css']);
+});
+
+gulp.task('elements', function () {
+ return styleTask('elements', ['**/*.css']);
+});
+
+// Optimize images
+gulp.task('images', function () {
+ return imageOptimizeTask('app/images/**/*', 'dist/images');
+});
+
+// Copy all files at the root level (app)
+gulp.task('copy', function () {
+ var app = gulp.src([
+ 'app/*',
+ '!app/test'
+ ], {
+ dot: true
+ }).pipe(gulp.dest('dist'));
+
+ var bower = gulp.src([
+ 'bower_components/**/*'
+ ]).pipe(gulp.dest('dist/bower_components'));
+
+ var elements = gulp.src(['app/elements/**/*.html',
+ 'app/elements/**/*.css',
+ 'app/elements/**/*.js'])
+ .pipe(gulp.dest('dist/elements'));
+
+ var vulcanized = gulp.src(['app/elements/gr-app.html'])
+ .pipe($.rename('gr-app.vulcanized.html'))
+ .pipe(gulp.dest('dist/elements'));
+
+ return merge(app, bower, elements, vulcanized)
+ .pipe($.size({title: 'copy'}));
+});
+
+// Copy web fonts to dist
+gulp.task('fonts', function () {
+ return gulp.src(['app/fonts/**'])
+ .pipe(gulp.dest('dist/fonts'))
+ .pipe($.size({title: 'fonts'}));
+});
+
+// Scan your HTML for assets & optimize them
+gulp.task('html', function () {
+ return optimizeHtmlTask(
+ ['app/**/*.html', '!app/{elements,test}/**/*.html'],
+ 'dist');
+});
+
+// Vulcanize granular configuration.
+gulp.task('vulcanize', function () {
+ var DEST_DIR = 'dist/elements';
+ return gulp.src('dist/elements/gr-app.vulcanized.html')
+ .pipe($.vulcanize({
+ stripComments: true,
+ inlineCss: true,
+ inlineScripts: true
+ }))
+ .pipe(gulp.dest(DEST_DIR))
+ .pipe($.size({title: 'vulcanize'}));
+});
+
+// Clean output directory
+gulp.task('clean', function (cb) {
+ del(['.tmp', 'dist'], cb);
+});
+
+
+// Build production files, the default task
+gulp.task('default', ['clean'], function (cb) {
+ // Uncomment 'cache-config' if you are going to use service workers.
+ runSequence(
+ ['copy', 'styles'],
+ 'elements',
+ ['images', 'fonts', 'html'],
+ 'vulcanize', // 'cache-config',
+ cb);
+});
+
+// Load tasks for web-component-tester
+// Adds tasks for `gulp test:local` and `gulp test:remote`
+require('web-component-tester').gulp.init(gulp);
+
+// Load custom tasks from the `tasks` directory
+try { require('require-dir')('tasks'); } catch (err) {}
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
new file mode 100644
index 0000000..ddbfe02
--- /dev/null
+++ b/polygerrit-ui/package.json
@@ -0,0 +1,39 @@
+{
+ "private": true,
+ "devDependencies": {
+ "browser-sync": "^2.7.7",
+ "connect-history-api-fallback": "^1.1.0",
+ "del": "^1.1.1",
+ "gulp": "^3.8.5",
+ "gulp-autoprefixer": "^2.1.0",
+ "gulp-cache": "^0.2.8",
+ "gulp-changed": "^1.0.0",
+ "gulp-cssmin": "^0.1.7",
+ "gulp-flatten": "0.0.4",
+ "gulp-if": "^1.2.1",
+ "gulp-imagemin": "^2.2.1",
+ "gulp-jshint": "^1.6.3",
+ "gulp-load-plugins": "^0.10.0",
+ "gulp-minify-html": "^1.0.2",
+ "gulp-rename": "^1.2.0",
+ "gulp-replace": "^0.5.3",
+ "gulp-size": "^1.0.0",
+ "gulp-uglify": "^1.2.0",
+ "gulp-useref": "^1.1.2",
+ "gulp-vulcanize": "^6.0.0",
+ "jshint-stylish": "^2.0.0",
+ "merge-stream": "^0.1.7",
+ "opn": "^1.0.0",
+ "require-dir": "^0.3.0",
+ "run-sequence": "^1.0.2",
+ "vulcanize": ">= 1.4.2",
+ "web-component-tester": "^3.1.3"
+ },
+ "scripts": {
+ "test": "gulp test:local",
+ "start": "gulp serve"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+}
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
new file mode 100644
index 0000000..008fe1d
--- /dev/null
+++ b/polygerrit-ui/server.go
@@ -0,0 +1,128 @@
+// 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 main
+
+import (
+ "bufio"
+ "compress/gzip"
+ "errors"
+ "flag"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+)
+
+var (
+ restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
+ port = flag.String("port", ":8081", "Port to serve HTTP requests on")
+ prod = flag.Bool("prod", false, "Serve production assets")
+)
+
+func main() {
+ flag.Parse()
+
+ if *prod {
+ http.Handle("/", http.FileServer(http.Dir("dist")))
+ } else {
+ http.Handle("/bower_components/",
+ http.StripPrefix("/bower_components/", http.FileServer(http.Dir("bower_components"))))
+ http.Handle("/", http.FileServer(http.Dir("app")))
+ }
+
+ http.HandleFunc("/changes/", handleRESTProxy)
+ http.HandleFunc("/accounts/", handleRESTProxy)
+ log.Println("Serving on port", *port)
+ log.Fatal(http.ListenAndServe(*port, &server{}))
+}
+
+func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ req := &http.Request{
+ Method: "GET",
+ URL: &url.URL{
+ Scheme: "https",
+ Host: *restHost,
+ Opaque: r.URL.EscapedPath(),
+ RawQuery: r.URL.RawQuery,
+ },
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer res.Body.Close()
+ if _, err := io.Copy(w, res.Body); err != nil {
+ log.Println("Error copying response to ResponseWriter:", err)
+ return
+ }
+}
+
+type gzipResponseWriter struct {
+ io.WriteCloser
+ http.ResponseWriter
+}
+
+func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter {
+ gz := gzip.NewWriter(w)
+ return &gzipResponseWriter{WriteCloser: gz, ResponseWriter: w}
+}
+
+func (w gzipResponseWriter) Write(b []byte) (int, error) {
+ return w.WriteCloser.Write(b)
+}
+
+func (w gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ h, ok := w.ResponseWriter.(http.Hijacker)
+ if !ok {
+ return nil, nil, errors.New("gzipResponseWriter: ResponseWriter does not satisfy http.Hijacker interface")
+ }
+ return h.Hijack()
+}
+
+type server struct{}
+
+// Any path prefixes that should resolve to index.html.
+var (
+ fePaths = []string{"/q/", "/c/"}
+ issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
+)
+
+func (_ *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ log.Printf("%s %s %s %s\n", r.Proto, r.Method, r.RemoteAddr, r.URL)
+ for _, prefix := range fePaths {
+ if strings.HasPrefix(r.URL.Path, prefix) {
+ r.URL.Path = "/"
+ log.Println("Redirecting to /")
+ break
+ } else if match := issueNumRE.Find([]byte(r.URL.Path)); match != nil {
+ r.URL.Path = "/"
+ log.Println("Redirecting to /")
+ break
+ }
+ }
+ if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+ http.DefaultServeMux.ServeHTTP(w, r)
+ return
+ }
+ w.Header().Set("Content-Encoding", "gzip")
+ gzw := newGzipResponseWriter(w)
+ defer gzw.Close()
+ http.DefaultServeMux.ServeHTTP(gzw, r)
+}
diff --git a/polygerrit-ui/wct.conf.js b/polygerrit-ui/wct.conf.js
new file mode 100644
index 0000000..b6a6251
--- /dev/null
+++ b/polygerrit-ui/wct.conf.js
@@ -0,0 +1,17 @@
+var path = require('path');
+
+var ret = {
+ suites: ['app/test'],
+ webserver: {
+ pathMappings: []
+ }
+};
+
+var mapping = {};
+var rootPath = (__dirname).split(path.sep).slice(-1)[0];
+
+mapping['/components/' + rootPath + '/app/bower_components'] = 'bower_components';
+
+ret.webserver.pathMappings.push(mapping);
+
+module.exports = ret;