Merge changes from topic 'notedb-primary'

* changes:
  NoteDbChangeState: Add enum to indicate change's primary storage
  NoteDbChangeState: Refactor to move ref state into nested class
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 5ca6353..97124cb 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -7,7 +7,6 @@
 * Version stamping
 * Custom plugins
 * Eclipse project generation.
-* Publishing to maven.
 * Test suites for SSH, acceptance, etc.
 * tag tests as slow, flaky, etc.
 
@@ -81,7 +80,17 @@
   bazel-bin/gerrit-extension-api/extension-api_deploy.jar
 ----
 
-TODO - fix and document deployment to maven
+Install {extension,plugin,gwt}-api to the local maven repository:
+
+----
+  tools/maven/api.sh install bazel
+----
+
+Install gerrit.war to the local maven repository:
+
+----
+  tools/maven/api.sh war_install bazel
+----
 
 === Plugins
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 8741d80..023841c 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -154,7 +154,7 @@
 ----
   buck clean
   buck build --no-cache release docs
-  ./tools/maven/api.sh install
+  ./tools/maven/api.sh install <buck|bazel>
 ----
 
 * Sanity check WAR
@@ -186,13 +186,13 @@
 * Push the WAR to Maven Central:
 +
 ----
-  ./tools/maven/api.sh war_deploy
+  ./tools/maven/api.sh war_deploy <buck|bazel>
 ----
 
 * Push the plugin artifacts to Maven Central:
 +
 ----
-  ./tools/maven/api.sh deploy
+  ./tools/maven/api.sh deploy <buck|bazel>
 ----
 +
 If no artifacts are uploaded, clean the `buck-out` folder and retry:
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
index ed04efa..ec79be8 100644
--- a/gerrit-acceptance-framework/BUILD
+++ b/gerrit-acceptance-framework/BUILD
@@ -64,4 +64,5 @@
   title = 'Gerrit Acceptance Test Framework Documentation',
   libs = [':lib'],
   pkgs = ['com.google.gerrit.acceptance'],
+  visibility = ['//visibility:public'],
 )
diff --git a/gerrit-extension-api/BUILD b/gerrit-extension-api/BUILD
index cbe0e26..6f4df01 100644
--- a/gerrit-extension-api/BUILD
+++ b/gerrit-extension-api/BUILD
@@ -55,4 +55,5 @@
   libs = [':api'],
   pkgs = ['com.google.gerrit.extensions'],
   external_docs = [JGIT_DOC_URL, GUAVA_DOC_URL],
+  visibility = ['//visibility:public'],
 )
diff --git a/gerrit-gwtui/BUILD b/gerrit-gwtui/BUILD
index 833ffab..7e692e8 100644
--- a/gerrit-gwtui/BUILD
+++ b/gerrit-gwtui/BUILD
@@ -1,6 +1,6 @@
-load('//tools/bzl:gwt.bzl', 'gwt_module')
+load('//tools/bzl:gwt.bzl', 'gwt_genrule', 'gen_ui_module',
+     'gwt_user_agent_permutations')
 load('//tools/bzl:license.bzl', 'license_test')
-load(':gwt.bzl', 'gwt_binary', 'gwt_genrule', 'gen_ui_module')
 
 gwt_genrule()
 gwt_genrule('_r')
@@ -8,6 +8,8 @@
 gen_ui_module(name = 'ui_module')
 gen_ui_module(name = 'ui_module', suffix = '_r')
 
+gwt_user_agent_permutations()
+
 license_test(
   name = "ui_module_license_test",
   target = ":ui_module",
diff --git a/gerrit-gwtui/gwt.bzl b/gerrit-gwtui/gwt.bzl
deleted file mode 100644
index eb22063..0000000
--- a/gerrit-gwtui/gwt.bzl
+++ /dev/null
@@ -1,197 +0,0 @@
-# Copyright (C) 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Port of Buck native gwt_binary() rule. See discussion in context of
-# https://github.com/facebook/buck/issues/109
-load('//tools/bzl:genrule2.bzl', 'genrule2')
-load('//tools/bzl:gwt.bzl', 'gwt_module')
-
-jar_filetype = FileType(['.jar'])
-
-MODULE = 'com.google.gerrit.GerritGwtUI'
-
-GWT_COMPILER = "com.google.gwt.dev.Compiler"
-
-GWT_JVM_ARGS = ['-Xmx512m']
-
-GWT_COMPILER_ARGS = [
-  '-XdisableClassMetadata',
-]
-
-GWT_COMPILER_ARGS_RELEASE_MODE = GWT_COMPILER_ARGS + [
-  '-XdisableCastChecking',
-]
-
-GWT_TRANSITIVE_DEPS = [
-  '//lib/gwt:ant',
-  '//lib/gwt:colt',
-  '//lib/gwt:javax-validation',
-  '//lib/gwt:javax-validation_src',
-  '//lib/gwt:jsinterop-annotations',
-  '//lib/gwt:jsinterop-annotations_src',
-  '//lib/gwt:tapestry',
-  '//lib/gwt:w3c-css-sac',
-  '//lib/ow2:ow2-asm',
-  '//lib/ow2:ow2-asm-analysis',
-  '//lib/ow2:ow2-asm-commons',
-  '//lib/ow2:ow2-asm-tree',
-  '//lib/ow2:ow2-asm-util',
-]
-
-DEPS = GWT_TRANSITIVE_DEPS + [
-  '//gerrit-gwtexpui:CSS',
-  '//lib:gwtjsonrpc',
-  '//lib/gwt:dev',
-  '@jgit_src//file',
-]
-
-def _impl(ctx):
-  output_zip = ctx.outputs.output
-  output_dir = output_zip.path + '.gwt_output'
-  deploy_dir = output_zip.path + '.gwt_deploy'
-
-  deps = _get_transitive_closure(ctx)
-
-  paths = []
-  for dep in deps:
-    paths.append(dep.path)
-
-  cmd = "external/local_jdk/bin/java %s -Dgwt.normalizeTimestamps=true -cp %s %s -war %s -deploy %s " % (
-    " ".join(ctx.attr.jvm_args),
-    ":".join(paths),
-    GWT_COMPILER,
-    output_dir,
-    deploy_dir,
-  )
-  cmd += " ".join([
-    "-style %s" % ctx.attr.style,
-    "-optimize %s" % ctx.attr.optimize,
-    "-strict",
-    " ".join(ctx.attr.compiler_args),
-    " ".join(ctx.attr.modules) + "\n",
-    "rm -rf %s/gwt-unitCache\n" % output_dir,
-    "root=`pwd`\n",
-    "cd %s; $root/%s Cc ../%s $(find .)\n" % (
-      output_dir,
-      ctx.executable._zip.path,
-      output_zip.basename,
-    )
-  ])
-
-  ctx.action(
-    inputs = list(deps) + ctx.files._jdk + ctx.files._zip,
-    outputs = [output_zip],
-    mnemonic = "GwtBinary",
-    progress_message = "GWT compiling " + output_zip.short_path,
-    command = "set -e\n" + cmd,
-  )
-
-def _get_transitive_closure(ctx):
-  deps = set()
-  for dep in ctx.attr.module_deps:
-    deps += dep.java.transitive_runtime_deps
-    deps += dep.java.transitive_source_jars
-  for dep in ctx.attr.deps:
-    if hasattr(dep, 'java'):
-      deps += dep.java.transitive_runtime_deps
-    elif hasattr(dep, 'files'):
-      deps += dep.files
-
-  return deps
-
-gwt_binary = rule(
-  implementation = _impl,
-  attrs = {
-    "style": attr.string(default = "OBF"),
-    "optimize": attr.string(default = "9"),
-    "deps": attr.label_list(allow_files=jar_filetype),
-    "modules": attr.string_list(mandatory=True),
-    "module_deps": attr.label_list(allow_files=jar_filetype),
-    "compiler_args": attr.string_list(),
-    "jvm_args": attr.string_list(),
-    "_jdk": attr.label(
-      default=Label("//tools/defaults:jdk")),
-    "_zip": attr.label(
-      default=Label("@bazel_tools//tools/zip:zipper"),
-      cfg = "host",
-      executable=True,
-      single_file=True),
-  },
-  outputs = {
-    "output": "%{name}.zip",
-  },
-)
-
-def gwt_genrule(suffix = ""):
-  dbg = 'ui_dbg' + suffix
-  opt = 'ui_opt' + suffix
-  module_dep = ':ui_module' + suffix
-  args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
-
-  genrule2(
-    name = 'ui_optdbg' + suffix,
-    srcs = [
-      ':' + dbg,
-      ':' + opt,
-     ],
-    cmd = 'cd $$TMP;' +
-      'unzip -q $$ROOT/$(location :%s);' % dbg +
-      'mv' +
-      ' gerrit_ui/gerrit_ui.nocache.js' +
-      ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
-      'unzip -qo $$ROOT/$(location :%s);' % opt +
-      'mkdir -p $$(dirname $@);' +
-      'zip -qr $$ROOT/$@ .',
-    outs = ['ui_optdbg' + suffix + '.zip'],
-    visibility = ['//visibility:public'],
-   )
-
-  gwt_binary(
-    name = opt,
-    modules = [MODULE],
-    module_deps = [module_dep],
-    deps = DEPS,
-    compiler_args = args,
-    jvm_args = GWT_JVM_ARGS,
-  )
-
-  gwt_binary(
-    name = dbg,
-    modules = [MODULE],
-    style = 'PRETTY',
-    optimize = "0",
-    module_deps = [module_dep],
-    deps = DEPS,
-    compiler_args = GWT_COMPILER_ARGS,
-    jvm_args = GWT_JVM_ARGS,
-  )
-
-def gen_ui_module(name, suffix = ""):
-  gwt_module(
-    name = name + suffix,
-    srcs = native.glob(['src/main/java/**/*.java']),
-    gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
-    resources = native.glob(
-        ['src/main/java/**/*'],
-        exclude = ['src/main/java/**/*.java'] +
-        ['src/main/java/%s.gwt.xml' % MODULE.replace('.', '/')]),
-    deps = [
-      '//gerrit-gwtui-common:diffy_logo',
-      '//gerrit-gwtui-common:client',
-      '//gerrit-gwtexpui:CSS',
-      '//lib/codemirror:codemirror' + suffix,
-      '//lib/gwt:user',
-    ],
-    visibility = ['//visibility:public'],
-  )
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
index cd8fa74..85553f2 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -18,14 +18,14 @@
   'firefox',
   'gecko1_8',
   'safari',
-  'msie', 'ie8', 'ie9', 'ie10', 'ie11',
+  'msie', 'ie8', 'ie9', 'ie10',
   'edge',
 ]
 ALIASES = {
   'chrome': 'safari',
   'firefox': 'gecko1_8',
-  'msie': 'ie11',
-  'edge': 'edge',
+  'msie': 'ie10',
+  'edge': 'gecko1_8',
 }
 MODULE = 'com.google.gerrit.GerritGwtUI'
 CPU_COUNT = cpu_count()
@@ -124,7 +124,6 @@
     prebuilt_jar(
       name = '%s_gwtxml_lib' % gwt_name,
       binary_jar = ':%s_gwtxml_gen' % gwt_name,
-      gwt_jar = ':%s_gwtxml_gen' % gwt_name,
     )
     gwt_binary(
       name = gwt_name,
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index e2d8372..e231a02 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -103,4 +103,5 @@
     '//gerrit-gwtexpui:server',
     '//gerrit-reviewdb:server',
   ],
+  visibility = ['//visibility:public'],
 )
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 8ce5d5e..5ef548c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -337,6 +337,7 @@
       for (ChangeIndex i : getWriteIndexes()) {
         i.delete(id);
       }
+      log.info("Deleted change {} from index.", id.get());
       fireChangeDeletedFromIndexEvent(id.get());
       return null;
     }
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
index 3042287..6dc37b8 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -54,7 +54,7 @@
   {/if}
 
   {if $coverLetter}
-    {call .Pre}{param content: $coverLetter /}{/call}
+    <div style="white-space:pre-wrap">{$coverLetter}</div>
   {/if}
 
   <ul style="{$ulStyle}">
diff --git a/lib/js/BUILD b/lib/js/BUILD
index 6758ca1..71fa94f 100644
--- a/lib/js/BUILD
+++ b/lib/js/BUILD
@@ -32,18 +32,3 @@
   srcs = [ "//lib/highlightjs:highlight.min.js" ],
   data = ['//lib:LICENSE-highlightjs',],
 )
-
-bower_component(
-     name = 'iron-test-helpers',
-     seed = True,
-)
-
-bower_component(
-     name = 'test-fixture',
-     seed = True,
-)
-
-bower_component(
-     name = 'web-component-tester',
-     seed = True,
-)
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index d90a7cc..9c9a5a9 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -7,6 +7,21 @@
 load("//tools/bzl:js.bzl", "bower_archive")
 def load_bower_archives():
   bower_archive(
+    name = "accessibility-developer-tools",
+    package = "accessibility-developer-tools",
+    version = "2.11.0",
+    sha1 = "792cb24b649dafb316e7e536f8ae65d0d7b52bab")
+  bower_archive(
+    name = "async",
+    package = "async",
+    version = "1.5.2",
+    sha1 = "1ec975d3b3834646a7e3d4b7e68118b90ed72508")
+  bower_archive(
+    name = "chai",
+    package = "chai",
+    version = "3.5.0",
+    sha1 = "849ad3ee7c77506548b7b5db603a4e150b9431aa")
+  bower_archive(
     name = "iron-a11y-announcer",
     package = "iron-a11y-announcer",
     version = "1.0.5",
@@ -52,11 +67,36 @@
     version = "1.1.1",
     sha1 = "480423380be0536f948735d91bc472f6e7ced5b4")
   bower_archive(
+    name = "lodash",
+    package = "lodash",
+    version = "3.10.1",
+    sha1 = "2f207a8293c4c554bf6cf071241f7a00dc513d3a")
+  bower_archive(
+    name = "mocha",
+    package = "mocha",
+    version = "2.5.3",
+    sha1 = "22ef0d1f43ba5e2241369c501ac648f00c0440c0")
+  bower_archive(
     name = "neon-animation",
     package = "neon-animation",
     version = "1.2.4",
     sha1 = "e8ccbb930c4b7ff470b1450baa901618888a7fd3")
   bower_archive(
+    name = "sinon-chai",
+    package = "sinon-chai",
+    version = "2.8.0",
+    sha1 = "0464b5d944fdf8116bb23e0b02ecfbac945b3517")
+  bower_archive(
+    name = "sinonjs",
+    package = "sinonjs",
+    version = "1.17.1",
+    sha1 = "a26a6aab7358807de52ba738770f6ac709afd240")
+  bower_archive(
+    name = "stacky",
+    package = "stacky",
+    version = "1.3.2",
+    sha1 = "d6c07a0112ab2e9677fe085933744466a89232fb")
+  bower_archive(
     name = "web-animations-js",
     package = "web-animations-js",
     version = "2.2.2",
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index 480d6ce..74515e1 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -7,6 +7,18 @@
 load("//tools/bzl:js.bzl", "bower_component")
 def define_bower_components():
   bower_component(
+    name = "accessibility-developer-tools",
+    license = "//lib:LICENSE-Apache2.0",
+  )
+  bower_component(
+    name = "async",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
+    name = "chai",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
     name = "es6-promise",
     license = "//lib:LICENSE-polymer",
     seed = True,
@@ -112,6 +124,12 @@
     seed = True,
   )
   bower_component(
+    name = "iron-test-helpers",
+    license = "//lib:LICENSE-polymer",
+    deps = [ ":polymer" ],
+    seed = True,
+  )
+  bower_component(
     name = "iron-validatable-behavior",
     license = "//lib:LICENSE-polymer",
     deps = [
@@ -120,6 +138,14 @@
     ],
   )
   bower_component(
+    name = "lodash",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
+    name = "mocha",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
     name = "moment",
     license = "//lib:LICENSE-moment",
     seed = True,
@@ -153,10 +179,43 @@
     seed = True,
   )
   bower_component(
+    name = "sinon-chai",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
+    name = "sinonjs",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
+    name = "stacky",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
+    name = "test-fixture",
+    license = "//lib:LICENSE-polymer",
+    seed = True,
+  )
+  bower_component(
     name = "web-animations-js",
     license = "//lib:LICENSE-Apache2.0",
   )
   bower_component(
+    name = "web-component-tester",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":accessibility-developer-tools",
+      ":async",
+      ":chai",
+      ":lodash",
+      ":mocha",
+      ":sinon-chai",
+      ":sinonjs",
+      ":stacky",
+      ":test-fixture",
+    ],
+    seed = True,
+  )
+  bower_component(
     name = "webcomponentsjs",
     license = "//lib:LICENSE-polymer",
   )
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index ecd26ad..38c48c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
@@ -206,7 +207,7 @@
         }
       }
     </style>
-    <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
+    <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
     <div class="container" hidden$="{{_loading}}">
       <div class="header">
         <span class="header-title">
@@ -231,7 +232,7 @@
           <div id="change_plugins"></div>
         </div>
         <div class="changeInfo-column mainChangeInfo">
-          <div class="commitActions" hidden$="[[!_loggedIn]]"">
+          <div class="commitActions" hidden$="[[!_loggedIn]]">
             <gr-button
                 class="reply"
                 secondary
@@ -261,18 +262,24 @@
             <div class="relatedChanges">
               <gr-related-changes-list id="relatedChanges"
                   change="[[_change]]"
-                  patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"></gr-related-changes-list>
+                  patch-num="[[_computeLatestPatchNum(_allPatchSets)]]">
+              </gr-related-changes-list>
             </div>
           </div>
         </div>
       </section>
-      <section class$="patchInfo [[_computePatchInfoClass(_patchRange.patchNum, _allPatchSets)]]">
+      <section class$="patchInfo [[_computePatchInfoClass(_patchRange.patchNum,
+          _allPatchSets)]]">
         <div class="patchInfo-header">
           <div>
-            <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
-            <select id="patchSetSelect" on-change="_handlePatchChange">
-              <template is="dom-repeat" items="[[_allPatchSets]]" as="patchNumber">
-                <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
+            <label class="patchSelectLabel" for="patchSetSelect">
+              Patch set
+            </label>
+            <select id="patchSetSelect" bind-value="{{_selectedPatchSet}}"
+                is="gr-select" on-change="_handlePatchChange">
+              <template is="dom-repeat" items="[[_allPatchSets]]"
+                  as="patchNumber">
+                <option value$="[[patchNumber]]">
                   <span>[[patchNumber]]</span>
                   /
                   <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index f6ecc9e..bac25be 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -73,7 +73,10 @@
         type: String,
         value: '',
       },
-      _patchRange: Object,
+      _patchRange: {
+        type: Object,
+        observer: '_updateSelected',
+      },
       _allPatchSets: {
         type: Array,
         computed: '_computeAllPatchSets(_change)',
@@ -89,6 +92,7 @@
         value: 'Reply',
         computed: '_computeReplyButtonLabel(_diffDrafts.*)',
       },
+      _selectedPatchSet: String,
       _initialLoadComplete: {
         type: Boolean,
         value: false,
@@ -458,6 +462,8 @@
           this._patchRange.patchNum ||
               this._computeLatestPatchNum(this._allPatchSets));
 
+      this._updateSelected();
+
       var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
       this.fire('title-change', {title: title});
     },
@@ -528,10 +534,6 @@
       }
     },
 
-    _computePatchIndexIsSelected: function(index, patchNum) {
-      return this._allPatchSets[index] == patchNum;
-    },
-
     _computeLabelNames: function(labels) {
       return Object.keys(labels).sort();
     },
@@ -776,5 +778,9 @@
         this.$.fileList.reload(),
       ]);
     },
+
+    _updateSelected: function() {
+      this._selectedPatchSet = this._patchRange.patchNum;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 8358e40..e61d6b0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -277,12 +277,10 @@
       var optionEls = Polymer.dom(element.root).querySelectorAll(
           '.patchInfo-header option');
       assert.equal(optionEls.length, 4);
-      assert.isFalse(element.$$('.patchInfo-header option[value="1"]')
-          .hasAttribute('selected'));
-      assert.isTrue(element.$$('.patchInfo-header option[value="2"]')
-          .hasAttribute('selected'));
-      assert.isFalse(element.$$('.patchInfo-header option[value="3"]')
-          .hasAttribute('selected'));
+      var select = element.$$('.patchInfo-header #patchSetSelect').bindValue;
+      assert.notEqual(select, 1);
+      assert.equal(select, 2);
+      assert.notEqual(select, 3);
       assert.equal(optionEls[3].value, 13);
 
       var showStub = sandbox.stub(page, 'show');
@@ -329,12 +327,12 @@
       var optionEls = Polymer.dom(element.root).querySelectorAll(
           '.patchInfo-header option');
       assert.equal(optionEls.length, 4);
-      assert.isFalse(element.$$('.patchInfo-header option[value="1"]')
-          .hasAttribute('selected'));
-      assert.isTrue(element.$$('.patchInfo-header option[value="2"]')
-          .hasAttribute('selected'));
-      assert.isFalse(element.$$('.patchInfo-header option[value="3"]')
-          .hasAttribute('selected'));
+      assert.notEqual(
+        element.$$('.patchInfo-header #patchSetSelect').bindValue, 1);
+      assert.equal(
+        element.$$('.patchInfo-header #patchSetSelect').bindValue, 2);
+      assert.notEqual(
+        element.$$('.patchInfo-header #patchSetSelect').bindValue, 3);
       assert.equal(optionEls[3].value, 13);
 
       var showStub = sandbox.stub(page, 'show');
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 007c0fc..f11cca2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -22,6 +22,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
 
 <dom-module id="gr-file-list">
   <template>
@@ -182,13 +183,17 @@
         <span class="separator">/</span>
         <label>
           Diff against
-          <select id="patchChange" on-change="_handlePatchChange">
+          <select id="patchChange" bind-value="{{_diffAgainst}}" is="gr-select"
+              on-change="_handlePatchChange">
             <option value="PARENT">Base</option>
-            <template is="dom-repeat" items="[[_computePatchSets(revisions, patchRange.*)]]" as="patchNum">
-              <option
-                  value$="[[patchNum]]"
-                  selected$="[[_computePatchSetSelected(patchNum, patchRange.basePatchNum)]]"
-                  disabled$="[[_computePatchSetDisabled(patchNum, patchRange.patchNum)]]">[[patchNum]]</option>
+            <template 
+                is="dom-repeat" 
+                items="[[_computePatchSets(revisions, patchRange.*)]]"
+                as="patchNum">
+              <option value$="[[patchNum]]" disabled$=
+                  "[[_computePatchSetDisabled(patchNum, patchRange.patchNum)]]">
+                [[patchNum]]
+              </option>
             </template>
           </select>
         </label>
@@ -207,7 +212,8 @@
         <div class$="[[_computeClass('status', file.__path)]]">
           [[_computeFileStatus(file.status)]]
         </div>
-        <a class="path" href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]">
+        <a class="path"
+            href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]">
           <div title$="[[_computeFileDisplayName(file.__path)]]">
             [[_computeFileDisplayName(file.__path)]]
           </div>
@@ -217,7 +223,9 @@
           </div>
         </a>
         <div class="comments">
-          <span class="drafts">[[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]</span>
+          <span class="drafts">
+            [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]
+          </span>
           [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
         </div>
         <div class$="[[_computeClass('stats', file.__path)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 340cb63..bf6dcf3 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -25,7 +25,10 @@
     is: 'gr-file-list',
 
     properties: {
-      patchRange: Object,
+      patchRange: {
+        type: Object,
+        observer: '_updateSelected',
+      },
       patchNum: String,
       changeNum: String,
       comments: Object,
@@ -58,6 +61,7 @@
         type: Array,
         value: function() { return []; },
       },
+      _diffAgainst: String,
       _diffPrefs: Object,
       _userPrefs: Object,
       _localPrefs: Object,
@@ -181,10 +185,6 @@
       return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10);
     },
 
-    _computePatchSetSelected: function(patchNum, basePatchNum) {
-      return parseInt(patchNum, 10) === parseInt(basePatchNum, 10);
-    },
-
     _handleHiddenChange: function(e) {
       var model = e.model;
       model.set('file.__expanded', !model.file.__expanded);
@@ -527,6 +527,10 @@
       this._numFilesShown = this._files.length;
     },
 
+    _updateSelected: function(patchRange) {
+      this._diffAgainst = patchRange.basePatchNum;
+    },
+
     /**
      * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
      * the current state.
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 0caa6be..b530bae 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -436,7 +436,7 @@
       document.getElementById('blank').restore();
     });
 
-    test('show/hide diffs disabled for large amounds of files', function(done) {
+    test('show/hide diffs disabled for large amounts of files', function(done) {
       element._files = [];
       element.changeNum = '42';
       element.patchRange = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 2abd0a8..410a813 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -215,7 +215,9 @@
     _handleTextareaKeydown: function(e) {
       switch (e.keyCode) {
         case 27: // 'esc'
-          this._handleCancel(e);
+          if (this._messageText.length === 0) {
+            this._handleCancel(e);
+          }
           break;
         case 83: // 's'
           if (e.ctrlKey) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 11de0ba..0ad3e11 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -47,6 +47,7 @@
 
   suite('gr-diff-comment tests', function() {
     var element;
+    var sandbox;
     setup(function() {
       stub('gr-rest-api-interface', {
         getAccount: function() { return Promise.resolve(null); },
@@ -62,6 +63,11 @@
         message: 'is this a crossover episode!?',
         updated: '2015-12-08 19:48:33.843000000',
       };
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(function() {
+      sandbox.restore();
     });
 
     test('collapsible comments', function() {
@@ -149,6 +155,22 @@
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is is not visible');
     });
+
+    test('esc does not close comment unless text is empty', function(done) {
+      element.editing = true;
+      element._messageText = 'test';
+      var textarea = element.$.editTextarea;
+      var closeSpy = sandbox.spy(element, '_handleCancel');
+
+      flush(function() {
+        MockInteractions.pressAndReleaseKeyOn(textarea, 27); // esc
+        assert.isFalse(closeSpy.called);
+        element._messageText = '';
+        MockInteractions.pressAndReleaseKeyOn(textarea, 27); // esc
+        assert.isTrue(closeSpy.called);
+        done();
+      });
+    });
   });
 
   suite('gr-diff-comment draft tests', function() {
@@ -321,6 +343,7 @@
       MockInteractions.tap(element.$$('.cancel'));
       MockInteractions.tap(element.$$('.discard'));
       element.flushDebouncer('fire-update');
+      element._messageText = '';
       MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index c496703..54bccb3 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -13,7 +13,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-
+<link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <dom-module id="gr-patch-range-select">
@@ -28,12 +28,11 @@
     </style>
     Patch set:
     <span class="patchRange">
-      <select id="leftPatchSelect" on-change="_handlePatchChange">
-        <option value="PARENT"
-            selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option>
+      <select id="leftPatchSelect" bind-value="{{_leftSelected}}"
+          on-change="_handlePatchChange" is="gr-select">
+        <option value="PARENT">Base</option>
         <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
           <option value$="[[patchNum]]"
-              selected$="[[_computeLeftSelected(patchNum, patchRange)]]"
               disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
         </template>
       </select>
@@ -46,10 +45,10 @@
     </span>
     &rarr;
     <span class="patchRange">
-      <select id="rightPatchSelect" on-change="_handlePatchChange">
+      <select id="rightPatchSelect" bind-value="{{_rightSelected}}"
+          on-change="_handlePatchChange" is="gr-select">
         <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
           <option value$="[[patchNum]]"
-              selected$="[[_computeRightSelected(patchNum, patchRange)]]"
               disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
         </template>
       </select>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index b50043e..350429f 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -21,13 +21,23 @@
       availablePatches: Array,
       changeNum: String,
       filesWeblinks: Object,
-      patchRange: Object,
       path: String,
+      patchRange: {
+        type: Object,
+        observer: '_updateSelected'
+      },
+      _rightSelected: String,
+      _leftSelected: String,
+    },
+
+    _updateSelected: function() {
+      this._rightSelected = this.patchRange.patchNum;
+      this._leftSelected = this.patchRange.basePatchNum;
     },
 
     _handlePatchChange: function(e) {
-      var leftPatch = this.$.leftPatchSelect.value;
-      var rightPatch = this.$.rightPatchSelect.value;
+      var leftPatch = this._leftSelected;
+      var rightPatch = this._rightSelected;
       var rangeStr = rightPatch;
       if (leftPatch != 'PARENT') {
         rangeStr = leftPatch + '..' + rangeStr;
@@ -36,14 +46,6 @@
       e.target.blur();
     },
 
-    _computeLeftSelected: function(patchNum, patchRange) {
-      return patchNum == patchRange.basePatchNum;
-    },
-
-    _computeRightSelected: function(patchNum, patchRange) {
-      return patchNum == patchRange.patchNum;
-    },
-
     _computeLeftDisabled: function(patchNum, patchRange) {
       return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10);
     },
@@ -52,5 +54,18 @@
       if (patchRange.basePatchNum == 'PARENT') { return false; }
       return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10);
     },
+
+    // On page load, the dom-if for options getting added occurs after
+    // the value was set in the select. This ensures that after they
+    // are loaded, the correct value will get selected.  I attempted to
+    // debounce these, but because they are detecting two different
+    // events, sometimes the timing was off and one ended up missing.
+    _synchronizeSelectionRight: function() {
+      this.$.rightPatchSelect.value = this._rightSelected;
+    },
+
+    _synchronizeSelectionLeft: function() {
+      this.$.leftPatchSelect.value = this._leftSelected;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 95789bc..68eeaa9 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -67,6 +67,10 @@
       element.changeNum = '42';
       element.path = 'path/to/file.txt';
       element.availablePatches = ['1', '2', '3'];
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '3',
+      };
       flushAsynchronousOperations();
 
       var numEvents = 0;
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index 9e14f08..bef260e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -16,40 +16,33 @@
 
   Polymer({
     is: 'gr-select',
-
     extends: 'select',
-
     properties: {
       bindValue: {
         type: String,
         notify: true,
+        observer: '_updateValue',
       },
     },
 
-    observers: [
-      '_valueChanged(bindValue)',
-    ],
+    listeners: {
+      change: '_valueChanged',
+      'dom-change': '_updateValue',
+    },
 
-    attached: function() {
-      this.addEventListener('change', function() {
-        this.bindValue = this.value;
-      });
+    _updateValue: function() {
+      if (this.bindValue) {
+        this.value = this.bindValue;
+      }
+    },
+
+    _valueChanged: function() {
+      this.bindValue = this.value;
     },
 
     ready: function() {
       // If not set via the property, set bind-value to the element value.
       if (!this.bindValue) { this.bindValue = this.value; }
     },
-
-    _valueChanged: function(bindValue) {
-      var options = Polymer.dom(this.root).querySelectorAll('option');
-      for (var i = 0; i < options.length; i++) {
-        if (options[i].getAttribute('value') === bindValue + '') {
-          options[i].setAttribute('selected', true);
-          this.value = bindValue;
-          break;
-        }
-      }
-    },
   });
 })();
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 0e00f77..a5cddad 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -23,6 +23,7 @@
 <link rel="stylesheet" href="/styles/fonts.css">
 <link rel="stylesheet" href="/styles/main.css">
 <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="preload" href="/elements/gr-app.js">
 <link rel="import" href="/elements/gr-app.html">
 
 <body unresolved>
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index c27e28f..27e0740 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -12,10 +12,72 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# GWT Rules Skylark rules for building [GWT](http://www.gwtproject.org/)
-# modules using Bazel.
+# Port of Buck native gwt_binary() rule. See discussion in context of
+# https://github.com/facebook/buck/issues/109
+load('//tools/bzl:genrule2.bzl', 'genrule2')
 load('//tools/bzl:java.bzl', 'java_library2')
 
+jar_filetype = FileType(['.jar'])
+
+BROWSERS = [
+  'chrome',
+  'firefox',
+  'gecko1_8',
+  'safari',
+  'msie', 'ie8', 'ie9', 'ie10',
+  'edge',
+]
+ALIASES = {
+  'chrome': 'safari',
+  'firefox': 'gecko1_8',
+  'msie': 'ie10',
+  'edge': 'gecko1_8',
+}
+
+MODULE = 'com.google.gerrit.GerritGwtUI'
+
+GWT_COMPILER = "com.google.gwt.dev.Compiler"
+
+GWT_JVM_ARGS = ['-Xmx512m']
+
+GWT_COMPILER_ARGS = [
+  '-XdisableClassMetadata',
+]
+
+GWT_COMPILER_ARGS_RELEASE_MODE = GWT_COMPILER_ARGS + [
+  '-XdisableCastChecking',
+]
+
+GWT_TRANSITIVE_DEPS = [
+  '//lib/gwt:ant',
+  '//lib/gwt:colt',
+  '//lib/gwt:javax-validation',
+  '//lib/gwt:javax-validation_src',
+  '//lib/gwt:jsinterop-annotations',
+  '//lib/gwt:jsinterop-annotations_src',
+  '//lib/gwt:tapestry',
+  '//lib/gwt:w3c-css-sac',
+  '//lib/ow2:ow2-asm',
+  '//lib/ow2:ow2-asm-analysis',
+  '//lib/ow2:ow2-asm-commons',
+  '//lib/ow2:ow2-asm-tree',
+  '//lib/ow2:ow2-asm-util',
+]
+
+DEPS = GWT_TRANSITIVE_DEPS + [
+  '//gerrit-gwtexpui:CSS',
+  '//lib:gwtjsonrpc',
+  '//lib/gwt:dev',
+  '@jgit_src//file',
+]
+
+USER_AGENT_XML = """<module rename-to='gerrit_ui'>
+<inherits name='%s'/>
+<set-property name='user.agent' value='%s'/>
+<set-property name='locale' value='default'/>
+</module>
+"""
+
 def gwt_module(gwt_xml=None, resources=[], srcs=[], **kwargs):
   if gwt_xml:
     resources += [gwt_xml]
@@ -24,3 +86,201 @@
     srcs = srcs,
     resources = resources,
     **kwargs)
+
+def _gwt_user_agent_module(ctx):
+  """Generate user agent specific GWT module."""
+  if not ctx.attr.user_agent:
+    return None
+
+  ua = ctx.attr.user_agent
+  impl = ua
+  if ua in ALIASES:
+    impl = ALIASES[ua]
+
+  # intermediate artifact: user agent speific GWT xml file
+  gwt_user_agent_xml = ctx.new_file(ctx.label.name + "_gwt.xml")
+  ctx.file_action(output = gwt_user_agent_xml,
+                  content=USER_AGENT_XML % (MODULE, impl))
+
+  # intermediate artifact: user agent specific zip with GWT module
+  gwt_user_agent_zip = ctx.new_file(ctx.label.name + "_gwt.zip")
+  gwt = '%s_%s.gwt.xml' % (MODULE.replace('.', '/'), ua)
+  dir = gwt_user_agent_zip.path + ".dir"
+  cmd = " && ".join([
+    "p=$PWD",
+    "mkdir -p %s" % dir,
+    "cd %s" % dir,
+    "mkdir -p $(dirname %s)" % gwt,
+    "cp $p/%s %s" % (gwt_user_agent_xml.path, gwt),
+    "$p/%s cC $p/%s $(find . | sed 's|^./||')" % (ctx.executable._zip.path, gwt_user_agent_zip.path)
+  ])
+  ctx.action(
+    inputs = [gwt_user_agent_xml] + ctx.files._zip,
+    outputs = [gwt_user_agent_zip],
+    command = cmd,
+    mnemonic = "GenerateUserAgentGWTModule")
+
+  return struct(
+    zip=gwt_user_agent_zip,
+    module=MODULE + '_' + ua
+  )
+
+def _gwt_binary_impl(ctx):
+  module = MODULE
+  output_zip = ctx.outputs.output
+  output_dir = output_zip.path + '.gwt_output'
+  deploy_dir = output_zip.path + '.gwt_deploy'
+
+  deps = _get_transitive_closure(ctx)
+
+  paths = []
+  for dep in deps:
+    paths.append(dep.path)
+
+  gwt_user_agent_modules = []
+  ua = _gwt_user_agent_module(ctx)
+  if ua:
+    paths.append(ua.zip.path)
+    gwt_user_agent_modules.append(ua.zip)
+    module = ua.module
+
+  cmd = "external/local_jdk/bin/java %s -Dgwt.normalizeTimestamps=true -cp %s %s -war %s -deploy %s " % (
+    " ".join(ctx.attr.jvm_args),
+    ":".join(paths),
+    GWT_COMPILER,
+    output_dir,
+    deploy_dir,
+  )
+  # TODO(davido): clean up command concatenation
+  cmd += " ".join([
+    "-style %s" % ctx.attr.style,
+    "-optimize %s" % ctx.attr.optimize,
+    "-strict",
+    " ".join(ctx.attr.compiler_args),
+    module + "\n",
+    "rm -rf %s/gwt-unitCache\n" % output_dir,
+    "root=`pwd`\n",
+    "cd %s; $root/%s Cc ../%s $(find .)\n" % (
+      output_dir,
+      ctx.executable._zip.path,
+      output_zip.basename,
+    )
+  ])
+
+  ctx.action(
+    inputs = list(deps) + ctx.files._jdk + ctx.files._zip + gwt_user_agent_modules,
+    outputs = [output_zip],
+    mnemonic = "GwtBinary",
+    progress_message = "GWT compiling " + output_zip.short_path,
+    command = "set -e\n" + cmd,
+  )
+
+def _get_transitive_closure(ctx):
+  deps = set()
+  for dep in ctx.attr.module_deps:
+    deps += dep.java.transitive_runtime_deps
+    deps += dep.java.transitive_source_jars
+  for dep in ctx.attr.deps:
+    if hasattr(dep, 'java'):
+      deps += dep.java.transitive_runtime_deps
+    elif hasattr(dep, 'files'):
+      deps += dep.files
+
+  return deps
+
+gwt_binary = rule(
+  implementation = _gwt_binary_impl,
+  attrs = {
+    "user_agent": attr.string(),
+    "style": attr.string(default = "OBF"),
+    "optimize": attr.string(default = "9"),
+    "deps": attr.label_list(allow_files=jar_filetype),
+    "module_deps": attr.label_list(allow_files=jar_filetype),
+    "compiler_args": attr.string_list(),
+    "jvm_args": attr.string_list(),
+    "_jdk": attr.label(
+      default=Label("//tools/defaults:jdk")),
+    "_zip": attr.label(
+      default=Label("@bazel_tools//tools/zip:zipper"),
+      cfg = "host",
+      executable=True,
+      single_file=True),
+  },
+  outputs = {
+    "output": "%{name}.zip",
+  },
+)
+
+def gwt_genrule(suffix = ""):
+  dbg = 'ui_dbg' + suffix
+  opt = 'ui_opt' + suffix
+  module_dep = ':ui_module' + suffix
+  args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
+
+  genrule2(
+    name = 'ui_optdbg' + suffix,
+    srcs = [
+      ':' + dbg,
+      ':' + opt,
+     ],
+    cmd = 'cd $$TMP;' +
+      'unzip -q $$ROOT/$(location :%s);' % dbg +
+      'mv' +
+      ' gerrit_ui/gerrit_ui.nocache.js' +
+      ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
+      'unzip -qo $$ROOT/$(location :%s);' % opt +
+      'mkdir -p $$(dirname $@);' +
+      'zip -qr $$ROOT/$@ .',
+    out = 'ui_optdbg' + suffix + '.zip',
+    visibility = ['//visibility:public'],
+   )
+
+  gwt_binary(
+    name = opt,
+    module_deps = [module_dep],
+    deps = DEPS,
+    compiler_args = args,
+    jvm_args = GWT_JVM_ARGS,
+  )
+
+  gwt_binary(
+    name = dbg,
+    style = 'PRETTY',
+    optimize = "0",
+    module_deps = [module_dep],
+    deps = DEPS,
+    compiler_args = GWT_COMPILER_ARGS,
+    jvm_args = GWT_JVM_ARGS,
+  )
+
+def gen_ui_module(name, suffix = ""):
+  gwt_module(
+    name = name + suffix,
+    srcs = native.glob(['src/main/java/**/*.java']),
+    gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
+    resources = native.glob(
+        ['src/main/java/**/*'],
+        exclude = ['src/main/java/**/*.java'] +
+        ['src/main/java/%s.gwt.xml' % MODULE.replace('.', '/')]),
+    deps = [
+      '//gerrit-gwtui-common:diffy_logo',
+      '//gerrit-gwtui-common:client',
+      '//gerrit-gwtexpui:CSS',
+      '//lib/codemirror:codemirror' + suffix,
+      '//lib/gwt:user',
+    ],
+    visibility = ['//visibility:public'],
+  )
+
+def gwt_user_agent_permutations():
+  for ua in BROWSERS:
+    gwt_binary(
+      name = "ui_%s" % ua,
+      user_agent = ua,
+      style = 'PRETTY',
+      optimize = "0",
+      module_deps = [':ui_module'],
+      deps = DEPS,
+      compiler_args = GWT_COMPILER_ARGS,
+      jvm_args = GWT_JVM_ARGS,
+    )
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 33f7320..08a997f 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -39,6 +39,7 @@
   # TODO(hanwen): remove these, and add appropriate license files under //lib
   "BSD": "polymer",
   "MIT": "polymer",
+  "BSD-3-Clause": "polymer",
 }
 
 # list of licenses for packages that don't specify one in their bower.json file.
@@ -47,8 +48,11 @@
   "fetch": "fetch",
   "moment": "moment",
   "page": "page.js",
+  "lodash": "polymer", # MIT, actually.
   "promise-polyfill": "promise-polyfill",
   "webcomponentsjs": "polymer",   # self-identifies as BSD.
+  "sinon-chai": "polymer", # WTFPL & BSD.
+  "sinonjs": "polymer", # BSD.
 }
 
 
@@ -201,7 +205,7 @@
       license = license_map.get(license, license)
     else:
       if pkg_name not in package_licenses:
-        msg = "package %s does not specify license." % pkg_name
+        msg = "package %s does not specify license: %s" % (pkg_name, pkg)
         sys.stderr.write(msg)
         raise Exception(msg)
       license = package_licenses[pkg_name]
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index 322b5a2..0541fc0 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -1,6 +1,6 @@
-include_defs('//VERSION')
 include_defs('//tools/maven/package.defs')
 include_defs('//tools/maven/repository.defs')
+include_defs('//version.bzl')
 
 if GERRIT_VERSION.endswith('-SNAPSHOT'):
   URL = MAVEN_SNAPSHOT_URL
diff --git a/tools/maven/BUILD b/tools/maven/BUILD
new file mode 100644
index 0000000..14eb2be
--- /dev/null
+++ b/tools/maven/BUILD
@@ -0,0 +1,31 @@
+load('//:version.bzl', 'GERRIT_VERSION')
+load('//tools/maven:package.bzl', 'maven_package')
+
+MAVEN_REPOSITORY = 'sonatype-nexus-staging'
+# TODO(davido): support snapshot repositories
+MAVEN_RELEASE_URL = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
+
+maven_package(
+  repository = MAVEN_REPOSITORY,
+  url = MAVEN_RELEASE_URL,
+  version = GERRIT_VERSION,
+  jar = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework_deploy.jar',
+    'gerrit-extension-api': '//gerrit-extension-api:extension-api_deploy.jar',
+    'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api_deploy.jar',
+    'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api_deploy.jar',
+  },
+  src = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:liblib-src.jar',
+    'gerrit-extension-api': '//gerrit-extension-api:libapi-src.jar',
+    'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-sources_deploy.jar',
+    'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-source_deploy.jar',
+  },
+  doc = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework-javadoc',
+    'gerrit-extension-api': '//gerrit-extension-api:extension-api-javadoc',
+    'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-javadoc',
+    'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-javadoc',
+  },
+  war = {'gerrit-war': '//:release'},
+)
diff --git a/tools/maven/api.sh b/tools/maven/api.sh
index c7ce65e..93b5f2e 100755
--- a/tools/maven/api.sh
+++ b/tools/maven/api.sh
@@ -14,16 +14,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-if [[ "$#" == "0" ]] ; then
+if [[ "$#" != "2" ]] ; then
   cat <<EOF
-Usage: run "$0 COMMAND" from the top of your workspace, where
-COMMAND is one of
+Usage: run "$0 COMMAND BUILD-TOOL" from the top of your workspace,
+where COMMAND is one of
 
   install
   deploy
   war_install
   war_deploy
 
+and BUILD-TOOL is one of
+
+  buck
+  bazel
 Set VERBOSE in the environment to get more information.
 
 EOF
@@ -54,16 +58,33 @@
     ;;
 esac
 
+case "$2" in
+bazel)
+    buildProc=bazel
+    ;;
+buck)
+    buildProc=buck
+    ;;
+*)
+    echo "unknown build-tool $2. Should be buck or bazel."
+    exit 1
+    ;;
+esac
+
 if [[ "${VERBOSE:-x}" != "x" ]]; then
   set -o xtrace
 fi
 
-buck build //tools/maven:gen_${command} || \
-  { echo "buck failed to build gen_${command}. Use VERBOSE=1 for more info" ; exit 1 ; }
+$buildProc build //tools/maven:gen_${command} || \
+  { echo "$buildProc failed to build gen_${command}. Use VERBOSE=1 for more info" ; exit 1 ; }
 
-script="./buck-out/gen/tools/maven/gen_${command}/${command}.sh"
-
-# The PEX wrapper does some funky exit handling, so even if the script
-# does "exit(0)", the return status is '1'. So we can't tell if the
-# following invocation was successful.
-${script}
+if [[ "$buildProc" = "bazel" ]]; then
+  script="./bazel-genfiles/tools/maven/${command}.sh"
+  ${script}
+else
+  script="./buck-out/gen/tools/maven/gen_${command}/${command}.sh"
+  # The PEX wrapper does some funky exit handling, so even if the script
+  # does "exit(0)", the return status is '1'. So we can't tell if the
+  # following invocation was successful.
+  ${script}
+fi
diff --git a/tools/maven/package.bzl b/tools/maven/package.bzl
new file mode 100644
index 0000000..c996ac5
--- /dev/null
+++ b/tools/maven/package.bzl
@@ -0,0 +1,93 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+sh_bang_template = (' && '.join([
+  "echo '#!/bin/bash -eu' > $@",
+  'echo "# this script should run from the root of your workspace." >> $@',
+  'echo "" >> $@',
+  "echo 'if [[ -n \"$$$${VERBOSE:-}\" ]]; then set -x ; fi' >> $@",
+  'echo "" >> $@',
+  'echo %s >> $@',
+  'echo "" >> $@',
+  'echo %s >> $@']))
+
+def maven_package(
+    version,
+    repository = None,
+    url = None,
+    jar = {},
+    src = {},
+    doc = {},
+    war = {}):
+
+  build_cmd = ['bazel', 'build']
+  mvn_cmd = ['python', 'tools/maven/mvn.py', '-v', version]
+  api_cmd = mvn_cmd[:]
+  api_targets = []
+  for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]:
+    for a,t in sorted(d.items()):
+      api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
+      api_targets.append(t)
+
+  native.genrule(
+    name = 'gen_api_install',
+    cmd = sh_bang_template % (
+      ' '.join(build_cmd + api_targets),
+      ' '.join(api_cmd + ['-a', 'install'])),
+    srcs = api_targets,
+    outs = ['api_install.sh'],
+    executable = True,
+  )
+
+  if repository and url:
+    native.genrule(
+      name = 'gen_api_deploy',
+      cmd = sh_bang_template % (
+        ' '.join(build_cmd + api_targets),
+        ' '.join(api_cmd + ['-a', 'deploy',
+                            '--repository', repository,
+                            '--url', url])),
+      srcs = api_targets,
+      outs = ['api_deploy.sh'],
+      executable = True,
+    )
+
+  war_cmd = mvn_cmd[:]
+  war_targets = []
+  for a,t in sorted(war.items()):
+    war_cmd.append('-s %s:war:$(location %s)' % (a,t))
+    war_targets.append(t)
+
+  native.genrule(
+    name = 'gen_war_install',
+    cmd = sh_bang_template % (' '.join(build_cmd + war_targets),
+                              ' '.join(war_cmd + ['-a', 'install'])),
+    srcs = war_targets,
+    outs = ['war_install.sh'],
+    executable = True,
+  )
+
+  if repository and url:
+    native.genrule(
+      name = 'gen_war_deploy',
+      cmd = sh_bang_template % (
+          ' '.join(build_cmd + war_targets),
+          ' '.join(war_cmd + [
+        '-a', 'deploy',
+        '--repository', repository,
+        '--url', url])),
+      srcs = war_targets,
+      outs = ['war_deploy.sh'],
+      executable = True,
+    )
diff --git a/tools/version.py b/tools/version.py
index 9f03a59..eac2700 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -53,7 +53,7 @@
   replace_in_file(pom, src_pattern)
 
 src_pattern = re.compile(r"^(GERRIT_VERSION = ')([-.\w]+)(')$", re.MULTILINE)
-replace_in_file('VERSION', src_pattern)
+replace_in_file('version.bzl', src_pattern)
 
 src_pattern = re.compile(r'^(\s*-DarchetypeVersion=)([-.\w]+)(\s*\\)$',
                          re.MULTILINE)
diff --git a/VERSION b/version.bzl
similarity index 100%
rename from VERSION
rename to version.bzl