Merge "Use <> operator where possible"
diff --git a/contrib/mitm-ui/README.md b/contrib/mitm-ui/README.md
new file mode 100644
index 0000000..c8df490
--- /dev/null
+++ b/contrib/mitm-ui/README.md
@@ -0,0 +1,47 @@
+# Scripts for PolyGerrit local development against prod using MitmProxy.
+
+## Installation (OSX)
+
+1. Install Docker from http://docker.com
+2. Start the proxy and create a new proxied browser instance
+   ```
+   cd ~/gerrit
+   ~/mitm-gerrit/mitm-serve-app-dev.sh
+   ```
+3. Install MITM certificates
+   - Open http://mitm.it in the proxied browser window
+   - Follow the instructions to install MITM certs
+
+## Usage
+
+### Add or replace a single plugin containing static content
+
+To develop unminified plugin that loads multiple files, use this.
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   ~/mitm-gerrit/mitm-single-plugin.sh ./path/to/static/plugin.html
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. plugin.html and ./path/to/static/* will be served
+
+### Add or replace a minified plugin for *.googlesource.com
+
+This flow assumes no additional .html/.js are needed, i.e. the plugin is a single file.
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   ~/mitm-gerrit/mitm-plugins.sh ./path/to/plugin.html,./maybe/one/more.js
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. plugin.html and more.js are served
+
+### Serve uncompiled PolyGerrit
+
+1. Create a new proxied browser window and start mitmproxy via Docker:
+   ```
+   cd ~/gerrit
+   ~/mitm-gerrit/mitm-serve-app-dev.sh
+   ```
+2. Open any *.googlesource.com domain in proxied window
+3. Instead of prod UI (gr-app.html, gr-app.js), local source files will be served
diff --git a/contrib/mitm-ui/add-header.py b/contrib/mitm-ui/add-header.py
new file mode 100644
index 0000000..f9b2b12
--- /dev/null
+++ b/contrib/mitm-ui/add-header.py
@@ -0,0 +1,5 @@
+# mitmdump -s add-header.py
+def response(flow):
+    if flow.request.host == 'gerrit-review.googlesource.com' and flow.request.path == "/c/92000?1":
+        #flow.response.headers['any'] = '<meta.rdf>; rel=meta'
+        flow.response.headers['Link'] = '</changes/98000/detail?O=11640c>;rel="preload";crossorigin;'
diff --git a/contrib/mitm-ui/dev-chrome.sh b/contrib/mitm-ui/dev-chrome.sh
new file mode 100755
index 0000000..adcb296
--- /dev/null
+++ b/contrib/mitm-ui/dev-chrome.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [[ "$OSTYPE" != "darwin"* ]]; then
+    echo Only works on OSX.
+    exit 1
+fi
+
+/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=${HOME}/devchrome --proxy-server="127.0.0.1:8888"
diff --git a/contrib/mitm-ui/force-version.py b/contrib/mitm-ui/force-version.py
new file mode 100644
index 0000000..a69c885
--- /dev/null
+++ b/contrib/mitm-ui/force-version.py
@@ -0,0 +1,22 @@
+# mitmdump -q -p 8888 -s "force-version.py --version $1"
+# Request URL is not changed, only the response context
+from mitmproxy import http
+import argparse
+import re
+
+class Server:
+    def __init__(self, version):
+        self.version = version
+
+    def request(self, flow: http.HTTPFlow) -> None:
+        if "gr-app." in flow.request.pretty_url:
+            flow.request.url = re.sub(
+                r"polygerrit_ui/([\d.]+)/elements",
+                "polygerrit_ui/" + self.version + "/elements",
+                flow.request.url)
+
+def start():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--version", type=str, help="Rapid release version, e.g. 432.0")
+    args = parser.parse_args()
+    return Server(args.version)
diff --git a/contrib/mitm-ui/mitm-docker.sh b/contrib/mitm-ui/mitm-docker.sh
new file mode 100755
index 0000000..77f209e
--- /dev/null
+++ b/contrib/mitm-ui/mitm-docker.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+extra_volume='/tmp:/tmp'
+
+POSITIONAL=()
+while [[ $# -gt 0 ]]
+do
+key="$1"
+
+case $key in
+    -v|--volume)
+    extra_volume="$2"
+    shift # past argument
+    shift # past value
+    ;;
+    *)    # unknown option
+    POSITIONAL+=("$1") # save it in an array for later
+    shift # past argument
+    ;;
+esac
+done
+set -- "${POSITIONAL[@]}" # restore positional parameters
+
+if [[ -z "$1" ]]; then
+    echo This is a runner for higher-level scripts, e.g. mitm-serve-app-dev.sh
+    echo Alternatively, pass mitmproxy script from the same dir as a parameter, e.g. serve-app-dev.py
+    exit 1
+fi
+
+gerrit_dir=$(pwd)
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+CMD="${mitm_dir}/$1"
+
+docker run --rm -it \
+       -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \
+       -v ${mitm_dir}:${mitm_dir} \
+       -v ${gerrit_dir}:${gerrit_dir} \
+       -v ${extra_volume} \
+       -p 8888:8888 \
+       mitmproxy/mitmproxy:2.0.2 \
+       mitmdump -q -p 8888 -s "${CMD}"
diff --git a/contrib/mitm-ui/mitm-plugins.sh b/contrib/mitm-ui/mitm-plugins.sh
new file mode 100755
index 0000000..992ef07
--- /dev/null
+++ b/contrib/mitm-ui/mitm-plugins.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+if [[ -z "$1" ]]; then
+    echo This script injects plugins for *.googlesource.com.
+    echo Provide plugin paths, comma-separated, as a parameter.
+    echo This script assumes files do not have dependencies, i.e. minified.
+    exit 1
+fi
+
+realpath() {
+    [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
+}
+
+join () {
+  local IFS="$1"
+  shift
+  echo "$*"
+}
+
+plugins=$1
+plugin_paths=()
+for plugin in $(echo ${plugins} | sed "s/,/ /g")
+do
+    plugin_paths+=($(realpath ${plugin}))
+done
+
+absolute_plugin_paths=$(join , "${plugin_paths[@]}")
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+${mitm_dir}/dev-chrome.sh &
+
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --plugins ${absolute_plugin_paths} --strip_assets"
diff --git a/contrib/mitm-ui/mitm-serve-app-dev.sh b/contrib/mitm-ui/mitm-serve-app-dev.sh
new file mode 100755
index 0000000..4fa8958
--- /dev/null
+++ b/contrib/mitm-ui/mitm-serve-app-dev.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+workspace="./WORKSPACE"
+if [[ ! -f ${workspace} ]] || [[ ! $(head -n 1 ${workspace}) == *"gerrit"* ]]; then
+    echo Please change to cloned Gerrit repo from https://gerrit.googlesource.com/gerrit/
+    exit 1
+fi
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+${mitm_dir}/dev-chrome.sh &
+
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/mitm-single-plugin.sh b/contrib/mitm-ui/mitm-single-plugin.sh
new file mode 100755
index 0000000..4acae7f
--- /dev/null
+++ b/contrib/mitm-ui/mitm-single-plugin.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+if [[ -z "$1" ]]; then
+    echo This script serves one plugin with the rest of static content.
+    echo Provide path to index plugin file, e.g. buildbucket.html for buildbucket plugin
+    exit 1
+fi
+
+realpath() {
+  OURPWD=$PWD
+  cd "$(dirname "$1")"
+  LINK=$(basename "$1")
+  while [ -L "$LINK" ]; do
+      LINK=$(readlink "$LINK")
+      cd "$(dirname "$LINK")"
+      LINK="$(basename "$1")"
+  done
+  REAL_DIR=`pwd -P`
+  RESULT=$REAL_DIR/$LINK
+  cd "$OURPWD"
+  echo "$RESULT"
+}
+
+plugin=$(realpath $1)
+plugin_root=$(dirname ${plugin})
+
+mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+
+${mitm_dir}/dev-chrome.sh &
+
+${mitm_dir}/mitm-docker.sh -v ${plugin_root}:${plugin_root} "serve-app-dev.py --plugins ${plugin} --strip_assets --plugin_root ${plugin_root}"
diff --git a/contrib/mitm-ui/serve-app-dev.py b/contrib/mitm-ui/serve-app-dev.py
new file mode 100644
index 0000000..bd054e5
--- /dev/null
+++ b/contrib/mitm-ui/serve-app-dev.py
@@ -0,0 +1,139 @@
+# 1. install and setup mitmproxy v2.0.2: https://mitmproxy.readthedocs.io/en/v2.0.2/install.html
+#   (In case of python versions trouble, use https://www.anaconda.com/)
+# 2. mitmdump -q -s -p 8888 \
+#   "serve-app-dev.py --app /path/to/polygerrit-ui/app/"
+# 3. start Chrome with --proxy-server="127.0.0.1:8888" --user-data-dir=/tmp/devchrome
+# 4. open, say, gerrit-review.googlesource.com. Or chromium-review.googlesource.com. Any.
+# 5. uncompiled source files are served and you can log in, too.
+# 6. enjoy!
+#
+# P.S. For replacing plugins, use --plugins or --plugin_root
+#
+# --plugin takes comma-separated list of plugins to add or replace.
+#
+# Example: Adding a new plugin to the server response:
+# --plugins ~/gerrit-testsite/plugins/myplugin.html
+#
+# Example: Replace all matching plugins with local versions:
+# --plugins ~/gerrit-testsite/plugins/
+# Following files will be served if they exist for /plugins/tricium/static/tricium.html:
+#  ~/gerrit-testsite/plugins/tricium.html
+#  ~/gerrit-testsite/plugins/tricium/static/tricium.html
+#
+# --assets takes assets bundle.html, expecting rest of the assets files to be in the same folder
+#
+# Example:
+#  --assets ~/gerrit-testsite/assets/a3be19f.html
+#
+
+from mitmproxy import http
+from mitmproxy.script import concurrent
+import re
+import argparse
+import os.path
+import json
+
+class Server:
+    def __init__(self, devpath, plugins, pluginroot, assets, strip_assets):
+        if devpath:
+            print("Serving app from " + devpath)
+        if pluginroot:
+            print("Serving plugins from " + pluginroot)
+        if assets:
+            self.assets_root, self.assets_file = os.path.split(assets)
+            print("Assets: using " + self.assets_file + " from " + self.assets_root)
+        else:
+            self.assets_root = None
+        if plugins:
+            self.plugins = {path.split("/")[-1:][0]: path for path in map(expandpath, plugins.split(","))}
+            for filename, path in self.plugins.items():
+                print("Serving " + filename + " from " + path)
+        else:
+            self.plugins = {}
+        self.devpath = devpath
+        self.pluginroot = pluginroot
+        self.strip_assets = strip_assets
+
+    def readfile(self, path):
+        with open(path, 'rb') as contentfile:
+            return contentfile.read()
+
+@concurrent
+def response(flow: http.HTTPFlow) -> None:
+    if server.strip_assets:
+        assets_bundle = 'googlesource.com/polygerrit_assets'
+        assets_pos = flow.response.text.find(assets_bundle)
+        if assets_pos != -1:
+            t = flow.response.text
+            flow.response.text = t[:t.rfind('<', 0, assets_pos)] + t[t.find('>', assets_pos) + 1:]
+            return
+
+    if server.assets_root:
+        marker = 'webcomponents-lite.js"></script>'
+        pos = flow.response.text.find(marker)
+        if pos != -1:
+            pos += len(marker)
+            flow.response.text = ''.join([
+                flow.response.text[:pos],
+                '<link rel="import" href="/gerrit_assets/123.0/' + server.assets_file + '">',
+                flow.response.text[pos:]
+            ])
+
+        assets_prefix = "/gerrit_assets/123.0/"
+        if flow.request.path.startswith(assets_prefix):
+            assets_file = flow.request.path[len(assets_prefix):]
+            flow.response.content = server.readfile(server.assets_root + '/' + assets_file)
+            flow.response.status_code = 200
+            if assets_file.endswith('.js'):
+                flow.response.headers['Content-type'] = 'text/javascript'
+            return
+    m = re.match(".+polygerrit_ui/\d+\.\d+/(.+)", flow.request.path)
+    pluginmatch = re.match("^/plugins/(.+)", flow.request.path)
+    localfile = ""
+    if flow.request.path == "/config/server/info":
+        config = json.loads(flow.response.content[5:].decode('utf8'))
+        for filename, path in server.plugins.items():
+            pluginname = filename.split(".")[0]
+            payload = config["plugin"]["js_resource_paths" if filename.endswith(".js") else "html_resource_paths"]
+            if list(filter(lambda url: filename in url, payload)):
+                continue
+            payload.append("plugins/" + pluginname + "/static/" + filename)
+        flow.response.content = str.encode(")]}'\n" + json.dumps(config))
+    if m is not None:
+        filepath = m.groups()[0]
+        localfile = server.devpath + filepath
+    elif pluginmatch is not None:
+        pluginfile = flow.request.path_components[-1]
+        if server.plugins and pluginfile in server.plugins:
+            if os.path.isfile(server.plugins[pluginfile]):
+                localfile = server.plugins[pluginfile]
+            else:
+                print("Can't find file " + server.plugins[pluginfile] + " for " + flow.request.path)
+        elif server.pluginroot:
+            pluginurl = pluginmatch.groups()[0]
+            if os.path.isfile(server.pluginroot + pluginfile):
+                localfile = server.pluginroot + pluginfile
+            elif os.path.isfile(server.pluginroot + pluginurl):
+                localfile = server.pluginroot + pluginurl
+    if localfile and os.path.isfile(localfile):
+        if pluginmatch is not None:
+            print("Serving " + flow.request.path + " from " + localfile)
+        flow.response.content = server.readfile(localfile)
+        flow.response.status_code = 200
+        if localfile.endswith('.js'):
+            flow.response.headers['Content-type'] = 'text/javascript'
+
+def expandpath(path):
+    return os.path.realpath(os.path.expanduser(path))
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--app", type=str, default="", help="Path to /polygerrit-ui/app/")
+parser.add_argument("--plugins", type=str, default="", help="Comma-separated list of plugin files to add/replace")
+parser.add_argument("--plugin_root", type=str, default="", help="Path containing individual plugin files to replace")
+parser.add_argument("--assets", type=str, default="", help="Path containing assets file to import.")
+parser.add_argument("--strip_assets", action="store_true", help="Strip plugin bundles from the response.")
+args = parser.parse_args()
+server = Server(expandpath(args.app) + '/',
+                args.plugins, expandpath(args.plugin_root) + '/',
+                args.assets and expandpath(args.assets),
+                args.strip_assets)
diff --git a/contrib/mitm-ui/serve-app-locally.py b/contrib/mitm-ui/serve-app-locally.py
new file mode 100644
index 0000000..636c684
--- /dev/null
+++ b/contrib/mitm-ui/serve-app-locally.py
@@ -0,0 +1,46 @@
+# bazel build polygerrit-ui/app:gr-app
+# mitmdump -s "serve-app-locally.py ~/gerrit/bazel-bin/polygerrit-ui/app"
+from mitmproxy import http
+import argparse
+import os
+import zipfile
+
+class Server:
+    def __init__(self, bundle):
+        self.bundle = bundle
+        self.bundlemtime = 0
+        self.files = {
+            'polygerrit_ui/elements/gr-app.js': '',
+            'polygerrit_ui/elements/gr-app.html': '',
+            'polygerrit_ui/styles/main.css': '',
+        }
+        self.read_files()
+
+    def read_files(self):
+        if not os.path.isfile(self.bundle):
+            print("bundle not found!")
+            return
+        mtime = os.stat(self.bundle).st_mtime
+        if mtime <= self.bundlemtime:
+            return
+        self.bundlemtime = mtime
+        with zipfile.ZipFile(self.bundle) as z:
+            for fname in self.files:
+                print('Reading new content for ' + fname)
+                with z.open(fname, 'r') as content_file:
+                    self.files[fname] = content_file.read()
+
+    def response(self, flow: http.HTTPFlow) -> None:
+        self.read_files()
+        for name in self.files:
+            if name.rsplit('/', 1)[1] in flow.request.pretty_url:
+                flow.response.content = self.files[name]
+
+def expandpath(path):
+    return os.path.expanduser(path)
+
+def start():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("bundle", type=str)
+    args = parser.parse_args()
+    return Server(expandpath(args.bundle))
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 1853c53..e74e1a2 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -526,6 +526,8 @@
             (tagName, traceId) -> addMessage(tagName + ": " + traceId))) {
       traceContext.addTag(RequestId.Type.RECEIVE_ID, new RequestId(project.getNameKey().get()));
 
+      logger.atFinest().log("Calling user: %s", user.getLoggableName());
+
       // Log the push options here, rather than in parsePushOptions(), so that they are included
       // into the trace if tracing is enabled.
       logger.atFine().log("push options: %s", receivePack.getPushOptions());
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index e0c7c37..54c283d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -441,28 +441,32 @@
 
     test('installing preloaded plugin', () => {
       let plugin;
-      window.ASSETS_PATH = 'http://blips.com/chitz/';
+      window.ASSETS_PATH = 'http://blips.com/chitz';
       Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
       assert.strictEqual(plugin.getPluginName(), 'foo');
       assert.strictEqual(plugin.url('/some/thing.html'),
-          'http://blips.com/plugins/foo/some/thing.html');
+          'http://blips.com/chitz/plugins/foo/some/thing.html');
       delete window.ASSETS_PATH;
     });
 
     suite('test plugin with base url', () => {
+      let baseUrlPlugin;
+
       setup(() => {
         sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
 
         Gerrit._setPluginsCount(1);
-        Gerrit.install(p => { plugin = p; }, '0.1',
-            'http://test.com/r/plugins/testplugin/static/test.js');
+        Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
+            'http://test.com/r/plugins/baseurlplugin/static/test.js');
       });
 
       test('url', () => {
-        assert.notEqual(plugin.url(), 'http://test.com/plugins/testplugin/');
-        assert.equal(plugin.url(), 'http://test.com/r/plugins/testplugin/');
-        assert.equal(plugin.url('/static/test.js'),
-            'http://test.com/r/plugins/testplugin/static/test.js');
+        assert.notEqual(baseUrlPlugin.url(),
+            'http://test.com/plugins/baseurlplugin/');
+        assert.equal(baseUrlPlugin.url(),
+            'http://test.com/r/plugins/baseurlplugin/');
+        assert.equal(baseUrlPlugin.url('/static/test.js'),
+            'http://test.com/r/plugins/baseurlplugin/static/test.js');
       });
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 36a428d..8bf4a2d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -217,9 +217,14 @@
   };
 
   Plugin.prototype.url = function(opt_path) {
-    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
-    return this._url.origin + base + '/plugins/' +
-        this._name + (opt_path || '/');
+    const relPath = '/plugins/' + this._name + (opt_path || '/');
+    if (window.location.origin === this._url.origin) {
+      // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
+      return this._url.origin + Gerrit.BaseUrlBehavior.getBaseUrl() + relPath;
+    } else {
+      // Plugin loaded from assets bundle, expect assets placed along with it.
+      return this._url.href.split('/plugins/' + this._name)[0] + relPath;
+    }
   };
 
   Plugin.prototype.screenUrl = function(opt_screenName) {