Merge branch 'stable-3.1' into stable-3.2

* stable-3.1:
  Set version to 3.1.9-SNAPSHOT
  Set version to 3.1.8
  Set version to 3.0.13-SNAPSHOT
  Set version to 3.0.12
  Include request latency in httpd_log.json
  Set version to 2.16.23-SNAPSHOT
  dev-release: Fix typo in verification step of release artifact
  dev-release: Install release artifact to local Maven repository
  Set version to 2.16.22
  Fork off a custom workspace_status.py with more heuristics
  Bump Jetty version to 9.4.30.v20200611

Change-Id: I28eca9d02c6628d1f04c60be2e673d1331f06f8f
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index eaf9905..9e93eac 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -109,13 +109,14 @@
 +
 ----
   bazel build release Documentation:searchfree
+  ./tools/maven/api.sh war_install
   ./tools/maven/api.sh install
 ----
 
 * Verify the WAR version:
 +
 ----
-  java -jar ~/dl/gerrit-$version.war --version
+  java -jar bazel-bin/release.war --version
 ----
 * Try upgrading a test site and launching the daemon
 
diff --git a/WORKSPACE b/WORKSPACE
index 3afd11e..5eb7af6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -892,48 +892,48 @@
     sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
 )
 
-JETTY_VERS = "9.4.27.v20200227"
+JETTY_VERS = "9.4.30.v20200611"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "c6354d1e53c41f839ae56f4d8622c866a1ad8487",
+    sha1 = "ca3dea2cd34ee88cec017001603af0c9e74781d6",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "aead56f2a1ac49d720a192cb7c1568e61e34ddae",
+    sha1 = "1a5261f6ad4081ad9e9bb01416d639931d391273",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "4ef690ce1277e3767d457f87621f2c436a001881",
+    sha1 = "e5ede3724d062717d0c04e4c77f74fe8115c2a6f",
 )
 
 maven_jar(
     name = "jetty-jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "df66265ec011d8b33a7fa541774257deb957ecb4",
+    sha1 = "653559eaec0f9a335a0d12e90bc764b28f341241",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "722ba6ef20eb58c55868f1ce85411e6af13be98e",
+    sha1 = "cd6223382e4f82b9ea807d8cdb04a23e5d629f1c",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "e85e7c4f298efb36b80cc53d635f2da776aa54c2",
+    sha1 = "9c360d08e903b2dbd5d1f8e889a32046948628ce",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "44087a126227af5196e3e327a5e11aad1b28852c",
+    sha1 = "39ec6aa4745952077f5407cb1394d8ba2db88b13",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/pgm/http/jetty/HttpLogJsonLayout.java b/java/com/google/gerrit/pgm/http/jetty/HttpLogJsonLayout.java
index 8ae0d4f..95a5b07 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HttpLogJsonLayout.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HttpLogJsonLayout.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.pgm.http.jetty.HttpLog.P_CONTENT_LENGTH;
 import static com.google.gerrit.pgm.http.jetty.HttpLog.P_HOST;
+import static com.google.gerrit.pgm.http.jetty.HttpLog.P_LATENCY;
 import static com.google.gerrit.pgm.http.jetty.HttpLog.P_METHOD;
 import static com.google.gerrit.pgm.http.jetty.HttpLog.P_PROTOCOL;
 import static com.google.gerrit.pgm.http.jetty.HttpLog.P_REFERER;
@@ -46,6 +47,7 @@
     public String protocol;
     public String status;
     public String contentLength;
+    public String latency;
     public String referer;
     public String userAgent;
 
@@ -59,6 +61,7 @@
       this.protocol = getMdcString(event, P_PROTOCOL);
       this.status = getMdcString(event, P_STATUS);
       this.contentLength = getMdcString(event, P_CONTENT_LENGTH);
+      this.latency = getMdcString(event, P_LATENCY);
       this.referer = getMdcString(event, P_REFERER);
       this.userAgent = getMdcString(event, P_USER_AGENT);
     }
diff --git a/tools/workspace_status_release.py b/tools/workspace_status_release.py
new file mode 100644
index 0000000..36535fb
--- /dev/null
+++ b/tools/workspace_status_release.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+
+# This is a variant of the `workspace_status.py` script that in addition to
+# plain `git describe` implements a few heuristics to arrive at more to the
+# point stamps for directories. But due to the implemented heuristics, it will
+# typically take longer to run (especially if you use lots of plugins that
+# come without tags) and might slow down your development cycle when used
+# as default.
+#
+# To use it, simply add
+#
+#   --workspace_status_command="python ./tools/workspace_status_release.py"
+#
+# to your bazel command. So for example instead of
+#
+#   bazel build release.war
+#
+# use
+#
+#   bazel build --workspace_status_command="python ./tools/workspace_status_release.py" release.war
+#
+# Alternatively, you can add
+#
+#   build --workspace_status_command="python ./tools/workspace_status_release.py"
+#
+# to `.bazelrc` in your home directory.
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+from __future__ import print_function
+import os
+import subprocess
+import sys
+import re
+
+ROOT = os.path.abspath(__file__)
+while not os.path.exists(os.path.join(ROOT, 'WORKSPACE')):
+    ROOT = os.path.dirname(ROOT)
+REVISION_CMD = ['git', 'describe', '--always', '--dirty']
+
+
+def run(command):
+    try:
+        return subprocess.check_output(command).strip().decode("utf-8")
+    except OSError as err:
+        print('could not invoke %s: %s' % (command[0], err), file=sys.stderr)
+        sys.exit(1)
+    except subprocess.CalledProcessError:
+        # ignore "not a git repository error" to report unknown version
+        return None
+
+
+def revision_with_match(pattern=None, prefix=False, all_refs=False,
+                        return_unmatched=False):
+    """Return a description of the current commit
+
+    Keyword arguments:
+    pattern    -- (Default: None) Use only refs that match this pattern.
+    prefix     -- (Default: False) If True, the pattern is considered a prefix
+                  and does not require an exact match.
+    all_refs   -- (Default: False) If True, consider all refs, not just tags.
+    return_unmatched -- (Default: False) If False and a pattern is given that
+                  cannot be matched, return the empty string. If True, return
+                  the unmatched description nonetheless.
+    """
+
+    command = REVISION_CMD[:]
+    if pattern:
+        command += ['--match', pattern + ('*' if prefix else '')]
+    if all_refs:
+        command += ['--all', '--long']
+
+    description = run(command)
+
+    if pattern and not return_unmatched and not description.startswith(pattern):
+        return ''
+    return description
+
+
+def branch_with_match(pattern):
+    for ref_kind in ['origin/', 'gerrit/', '']:
+        description = revision_with_match(ref_kind + pattern, all_refs=True,
+                                          return_unmatched=True)
+        for cutoff in ['heads/', 'remotes/', ref_kind]:
+            if description.startswith(cutoff):
+                description = description[len(cutoff):]
+        if description.startswith(pattern):
+            return description
+    return ''
+
+
+def revision(template=None):
+    if template:
+        # We use the version `v2.16.19-1-gec686a6352` as running example for the
+        # below comments. First, we split into ['v2', '16', '19']
+        parts = template.split('-')[0].split('.')
+
+        # Although we have releases with version tags containing 4 numbers, we
+        # treat only the first three numbers for simplicity. See discussion on
+        # Ib1681b2730cf2c443a3cb55fe6e282f6484e18de.
+
+        if len(parts) >= 3:
+            # Match for v2.16.19
+            version_part = '.'.join(parts[0:3])
+            description = revision_with_match(version_part)
+            if description:
+                return description
+
+        if len(parts) >= 2:
+            version_part = '.'.join(parts[0:2])
+
+            # Match for v2.16.*
+            description = revision_with_match(version_part + '.', prefix=True)
+            if description:
+                return description
+
+            # Match for v2.16
+            description = revision_with_match(version_part)
+            if description.startswith(version_part):
+                return description
+
+            if template.startswith('v'):
+                # Match for stable-2.16 branches
+                branch = 'stable-' + version_part[1:]
+                description = branch_with_match(branch)
+                if description:
+                    return description
+
+    # None of the template based methods worked out, so we're falling back to
+    # generic matches.
+
+    # Match for master branch
+    description = branch_with_match('master')
+    if description:
+        return description
+
+    # Match for anything that looks like a version tag
+    description = revision_with_match('v[0-9].', return_unmatched=True)
+    if description.startswith('v'):
+        return description
+
+    # Still no good tag, so we re-try without any matching
+    return revision_with_match()
+
+
+# prints the stamps for the current working directory
+def print_stamps_for_cwd(name, template):
+    workspace_status_script = os.path.join(
+        'tools', 'workspace_status_release.py')
+    if os.path.isfile(workspace_status_script):
+        # directory has own workspace_status_command, so we use stamps from that
+        for line in run(["python", workspace_status_script]).split('\n'):
+            if re.search("^STABLE_[a-zA-Z0-9().:@/_ -]*$", line):
+                print(line)
+    else:
+        # directory lacks own workspace_status_command, so we create a default
+        # stamp
+        v = revision(template)
+        print('STABLE_BUILD_%s_LABEL %s' % (name.upper(),
+                                            v if v else 'unknown'))
+
+
+# os.chdir is different from plain `cd` in shells in that it follows symlinks
+# and does not update the PWD environment. So when using os.chdir to change into
+# a symlinked directory from gerrit's `plugins` or `modules` directory, we
+# cannot recover gerrit's directory. This prevents the plugins'/modules'
+# `workspace_status_release.py` scripts to detect the name they were symlinked
+# as (E.g.: it-* plugins sometimes get linked in more than once under different
+# names) and to detect gerrit's root directory. To work around this problem, we
+# mimic the `cd` of ordinary shells. By using this function, symlink information
+# is preserved in the `PWD` environment variable (as it is for example also done
+# in bash) and plugin/module `workspace_status_release.py` scripts can pick up
+# the needed information from there.
+def cd(absolute_path):
+    os.environ['PWD'] = absolute_path
+    os.chdir(absolute_path)
+
+
+def print_stamps():
+    cd(ROOT)
+    gerrit_version = revision()
+    print("STABLE_BUILD_GERRIT_LABEL %s" % gerrit_version)
+    for kind in ['modules', 'plugins']:
+        kind_dir = os.path.join(ROOT, kind)
+        for d in os.listdir(kind_dir) if os.path.isdir(kind_dir) else []:
+            p = os.path.join(kind_dir, d)
+            if os.path.isdir(p):
+                cd(p)
+                name = os.path.basename(p)
+                print_stamps_for_cwd(name, gerrit_version)
+
+
+if __name__ == '__main__':
+    print_stamps()