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">&#x25b6;</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)]]">&larr; Prev</a>
+      <a href$="[[_computeNavLink(query, offset, 1)]]"
+         hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</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;