Merge "Documentation/replace_macros: support --site-search option"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 1e54cbf..a3166b7 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -34,10 +34,10 @@
 == Getting started
 
 To get started with the development of a plugin clone the sample
-plugin:
+plugins:
 
 ----
-$ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin
+$ git clone https://gerrit.googlesource.com/plugins/examples
 ----
 
 This is a project that demonstrates the various features of the
diff --git a/WORKSPACE b/WORKSPACE
index b4b1a64..0aa31f6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -14,9 +14,9 @@
 
 http_archive(
     name = "io_bazel_rules_closure",
-    sha256 = "0e6de40666f2ebb2b30dc0339745a274d9999334a249b05a3b1f46462e489adf",
-    strip_prefix = "rules_closure-87d24b1df8b62405de8dd059cb604fd9d4b1e395",
-    urls = ["https://github.com/bazelbuild/rules_closure/archive/87d24b1df8b62405de8dd059cb604fd9d4b1e395.tar.gz"],
+    sha256 = "ddce3b3a3909f99b28b25071c40b7fec7e2e1d1d1a4b2e933f3082aa99517105",
+    strip_prefix = "rules_closure-316e6133888bfc39fb860a4f1a31cfcbae485aef",
+    urls = ["https://github.com/bazelbuild/rules_closure/archive/316e6133888bfc39fb860a4f1a31cfcbae485aef.tar.gz"],
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -69,12 +69,6 @@
 
 # Dependencies for PolyGerrit local dev server.
 go_repository(
-    name = "com_github_robfig_soy",
-    commit = "82face14ebc0883b4ca9c901b5aaf3738b9f6a24",
-    importpath = "github.com/robfig/soy",
-)
-
-go_repository(
     name = "com_github_howeyc_fsnotify",
     commit = "441bbc86b167f3c1f4786afae9931403b99fdacf",
     importpath = "github.com/howeyc/fsnotify",
diff --git a/antlr3/BUILD b/antlr3/BUILD
index fc96715..2d3050e 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -20,7 +20,8 @@
     name = "query_parser",
     srcs = [":query"],
     visibility = [
-        "//java/com/google/gerrit/index:__pkg__",
+        "//java/com/google/gerrit/index:__subpackages__",
+        "//javatests/com/google/gerrit:__subpackages__",
         "//javatests/com/google/gerrit/index:__pkg__",
         "//plugins:__pkg__",
     ],
diff --git a/antlr3/com/google/gerrit/index/query/Query.g b/antlr3/com/google/gerrit/index/query/Query.g
index 953a473..1bf20aa 100644
--- a/antlr3/com/google/gerrit/index/query/Query.g
+++ b/antlr3/com/google/gerrit/index/query/Query.g
@@ -120,12 +120,24 @@
   ;
 conditionBase
   : '('! conditionOr ')'!
-  | (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue
+  | (FIELD_NAME COLON) => FIELD_NAME^ COLON! fieldValue
   | fieldValue -> ^(DEFAULT_FIELD fieldValue)
   ;
 
 fieldValue
-  : n=FIELD_NAME   -> SINGLE_WORD[n]
+  // Rewrite by invoking SINGLE_WORD fragment lexer rule, passing the field name as an argument.
+  : n=FIELD_NAME -> SINGLE_WORD[n]
+
+  // Allow field values to contain a colon. We can't do this at the lexer level, because we need to
+  // emit a separate token for the field name. If we were to allow ':' in SINGLE_WORD, then
+  // everything would just lex as DEFAULT_FIELD.
+  //
+  // Field values with a colon may be lexed either as <field>:<rest> or <word>:<rest>, depending on
+  // whether the part before the colon looks like a field name.
+  // TODO(dborowitz): Field values ending in colon still don't work.
+  | (FIELD_NAME COLON) => n=FIELD_NAME COLON fieldValue -> SINGLE_WORD[n] COLON fieldValue
+  | (SINGLE_WORD COLON) => SINGLE_WORD COLON fieldValue
+
   | SINGLE_WORD
   | EXACT_PHRASE
   ;
@@ -134,6 +146,8 @@
 OR:  'OR'  ;
 NOT: 'NOT' ;
 
+COLON: ':' ;
+
 WS
   :  ( ' ' | '\r' | '\t' | '\n' ) { $channel=HIDDEN; }
   ;
@@ -172,7 +186,7 @@
      // '-'  permit
      // '.'  permit
      // '/'  permit
-     | ':'
+     | COLON
      | ';'
      // '<' permit
      // '=' permit
diff --git a/contrib/mitm-ui/README.md b/contrib/mitm-ui/README.md
index ad23140..1ec8dd4 100644
--- a/contrib/mitm-ui/README.md
+++ b/contrib/mitm-ui/README.md
@@ -8,7 +8,10 @@
    cd ~/gerrit
    ~/mitm-gerrit/mitm-serve-app-dev.sh
    ```
-3. Install MITM certificates
+3. Make sure that the browser uses the proxy provided by the command line,
+   e.g. if you are a Googler check that the BeyondCorp extension uses the
+   "System/Alternative" proxy.
+4. Install MITM certificates
    - Open http://mitm.it in the proxied browser window
    - Follow the instructions to install MITM certs
 
diff --git a/contrib/mitm-ui/mitm-docker.sh b/contrib/mitm-ui/mitm-docker.sh
index 77f209e..a1206f7 100755
--- a/contrib/mitm-ui/mitm-docker.sh
+++ b/contrib/mitm-ui/mitm-docker.sh
@@ -36,6 +36,7 @@
        -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \
        -v ${mitm_dir}:${mitm_dir} \
        -v ${gerrit_dir}:${gerrit_dir} \
+       -v ${gerrit_dir}/bazel-out:${gerrit_dir}/bazel-out \
        -v ${extra_volume} \
        -p 8888:8888 \
        mitmproxy/mitmproxy:2.0.2 \
diff --git a/contrib/mitm-ui/mitm-serve-app-dev.sh b/contrib/mitm-ui/mitm-serve-app-dev.sh
index 4fa8958..d4c72cc 100755
--- a/contrib/mitm-ui/mitm-serve-app-dev.sh
+++ b/contrib/mitm-ui/mitm-serve-app-dev.sh
@@ -8,6 +8,8 @@
 
 mitm_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
 
+bazel build //polygerrit-ui/app:test_components &
+
 ${mitm_dir}/dev-chrome.sh &
 
-${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/"
+${mitm_dir}/mitm-docker.sh "serve-app-dev.py --app $(pwd)/polygerrit-ui/app/ --components $(pwd)/bazel-bin/polygerrit-ui/app/"
diff --git a/contrib/mitm-ui/serve-app-dev.py b/contrib/mitm-ui/serve-app-dev.py
index 18e9de1..cdf7bfc 100644
--- a/contrib/mitm-ui/serve-app-dev.py
+++ b/contrib/mitm-ui/serve-app-dev.py
@@ -28,16 +28,19 @@
 
 from mitmproxy import http
 from mitmproxy.script import concurrent
-import re
 import argparse
-import os.path
 import json
 import mimetypes
+import os.path
+import re
+import zipfile
 
 class Server:
-    def __init__(self, devpath, plugins, pluginroot, assets, strip_assets, theme):
+    def __init__(self, devpath, components, plugins, pluginroot, assets, strip_assets, theme):
         if devpath:
             print("Serving app from " + devpath)
+        if components:
+            print("Serving components from " + components)
         if pluginroot:
             print("Serving plugins from " + pluginroot)
         if assets:
@@ -52,6 +55,7 @@
         else:
             self.plugins = {}
         self.devpath = devpath
+        self.components = components
         self.pluginroot = pluginroot
         self.strip_assets = strip_assets
         self.theme = theme
@@ -92,6 +96,7 @@
     m = re.match(".+polygerrit_ui/\d+\.\d+/(.+)", flow.request.path)
     pluginmatch = re.match("^/plugins/(.+)", flow.request.path)
     localfile = ""
+    content = ""
     if flow.request.path == "/config/server/info":
         config = json.loads(flow.response.content[5:].decode('utf8'))
         if server.theme:
@@ -105,6 +110,9 @@
         flow.response.content = str.encode(")]}'\n" + json.dumps(config))
     if m is not None:
         filepath = m.groups()[0]
+        if (filepath.startswith("bower_components/")):
+            with zipfile.ZipFile(server.components + "test_components.zip") as bower_zip:
+                content = bower_zip.read(filepath)
         localfile = server.devpath + filepath
     elif pluginmatch is not None:
         pluginfile = flow.request.path_components[-1]
@@ -131,7 +139,10 @@
     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)
+        content = server.readfile(localfile)
+
+    if content:
+        flow.response.content = content
         flow.response.status_code = 200
         localtype = mimetypes.guess_type(localfile)
         if localtype and localtype[0]:
@@ -142,13 +153,15 @@
 
 parser = argparse.ArgumentParser()
 parser.add_argument("--app", type=str, default="", help="Path to /polygerrit-ui/app/")
+parser.add_argument("--components", type=str, default="", help="Path to test_components.zip")
 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.")
-parser.add_argument("--theme", type=str, help="Path to the default site theme to be used.")
+parser.add_argument("--theme", default="", type=str, help="Path to the default site theme to be used.")
 args = parser.parse_args()
 server = Server(expandpath(args.app) + '/',
+                expandpath(args.components) + '/',
                 args.plugins,
                 expandpath(args.plugin_root) + '/',
                 args.assets and expandpath(args.assets),
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index 1988d66..265d590 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -20,14 +20,17 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 public class GroupReferenceSubject extends Subject<GroupReferenceSubject, GroupReference> {
 
   public static GroupReferenceSubject assertThat(GroupReference group) {
-    return assertAbout(GroupReferenceSubject::new).that(group);
+    return assertAbout(groupReferences()).that(group);
+  }
+
+  public static Subject.Factory<GroupReferenceSubject, GroupReference> groupReferences() {
+    return GroupReferenceSubject::new;
   }
 
   private GroupReferenceSubject(FailureMetadata metadata, GroupReference group) {
@@ -37,12 +40,12 @@
   public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
     isNotNull();
     GroupReference group = actual();
-    return Truth.assertThat(group.getUUID()).named("groupUuid");
+    return check("groupUuid()").that(group.getUUID());
   }
 
   public StringSubject name() {
     isNotNull();
     GroupReference group = actual();
-    return Truth.assertThat(group.getName()).named("name");
+    return check("name()").that(group.getName());
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 6679104..7092d21 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -6,6 +6,7 @@
     deps = [
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/truth",
+        "//lib:guava",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
diff --git a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
index 6dd5ce4..f0f5516 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
@@ -15,18 +15,23 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.GitPersonSubject.gitPersons;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.truth.ListSubject;
 
 public class CommitInfoSubject extends Subject<CommitInfoSubject, CommitInfo> {
 
   public static CommitInfoSubject assertThat(CommitInfo commitInfo) {
-    return assertAbout(CommitInfoSubject::new).that(commitInfo);
+    return assertAbout(commits()).that(commitInfo);
+  }
+
+  public static Subject.Factory<CommitInfoSubject, CommitInfo> commits() {
+    return CommitInfoSubject::new;
   }
 
   private CommitInfoSubject(FailureMetadata failureMetadata, CommitInfo commitInfo) {
@@ -36,31 +41,30 @@
   public StringSubject commit() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return Truth.assertThat(commitInfo.commit).named("commit");
+    return check("commit()").that(commitInfo.commit);
   }
 
   public ListSubject<CommitInfoSubject, CommitInfo> parents() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return ListSubject.assertThat(commitInfo.parents, CommitInfoSubject::assertThat)
-        .named("parents");
+    return check("parents()").about(elements()).thatCustom(commitInfo.parents, commits());
   }
 
   public GitPersonSubject committer() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return GitPersonSubject.assertThat(commitInfo.committer).named("committer");
+    return check("committer()").about(gitPersons()).that(commitInfo.committer);
   }
 
   public GitPersonSubject author() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return GitPersonSubject.assertThat(commitInfo.author).named("author");
+    return check("author()").about(gitPersons()).that(commitInfo.author);
   }
 
   public StringSubject message() {
     isNotNull();
     CommitInfo commitInfo = actual();
-    return Truth.assertThat(commitInfo.message).named("message");
+    return check("message").that(commitInfo.message);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
index 5fc8ba6..25750c1 100644
--- a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
@@ -14,21 +14,27 @@
 
 package com.google.gerrit.extensions.common.testing;
 
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
 import com.google.gerrit.truth.ListSubject;
 
 public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEntry> {
 
   public static ContentEntrySubject assertThat(ContentEntry contentEntry) {
-    return assertAbout(ContentEntrySubject::new).that(contentEntry);
+    return assertAbout(contentEntries()).that(contentEntry);
+  }
+
+  public static Subject.Factory<ContentEntrySubject, ContentEntry> contentEntries() {
+    return ContentEntrySubject::new;
   }
 
   private ContentEntrySubject(FailureMetadata failureMetadata, ContentEntry contentEntry) {
@@ -38,54 +44,54 @@
   public void isDueToRebase() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    Truth.assertWithMessage("Entry should be marked 'dueToRebase'")
-        .that(contentEntry.dueToRebase)
-        .named("dueToRebase")
-        .isTrue();
+    if (contentEntry.dueToRebase == null || !contentEntry.dueToRebase) {
+      failWithActual(simpleFact("expected entry to be marked 'dueToRebase'"));
+    }
   }
 
   public void isNotDueToRebase() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    Truth.assertWithMessage("Entry should not be marked 'dueToRebase'")
-        .that(contentEntry.dueToRebase)
-        .named("dueToRebase")
-        .isNull();
+    if (contentEntry.dueToRebase != null && contentEntry.dueToRebase) {
+      failWithActual(simpleFact("expected entry not to be marked 'dueToRebase'"));
+    }
   }
 
   public ListSubject<StringSubject, String> commonLines() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.ab, Truth::assertThat).named("common lines");
+    return check("commonLines()")
+        .about(elements())
+        .that(contentEntry.ab, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfA() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.a, Truth::assertThat).named("lines of 'a'");
+    return check("linesOfA()").about(elements()).that(contentEntry.a, StandardSubjectBuilder::that);
   }
 
   public ListSubject<StringSubject, String> linesOfB() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return ListSubject.assertThat(contentEntry.b, Truth::assertThat).named("lines of 'b'");
+    return check("linesOfB()").about(elements()).that(contentEntry.b, StandardSubjectBuilder::that);
   }
 
   public IterableSubject intralineEditsOfA() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.editA).named("intraline edits of 'a'");
+    return check("intralineEditsOfA()").that(contentEntry.editA);
   }
 
   public IterableSubject intralineEditsOfB() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.editB).named("intraline edits of 'b'");
+    return check("intralineEditsOfB()").that(contentEntry.editB);
   }
 
   public IntegerSubject numberOfSkippedLines() {
     isNotNull();
     ContentEntry contentEntry = actual();
-    return Truth.assertThat(contentEntry.skip).named("number of skipped lines");
+    return check("numberOfSkippedLines()").that(contentEntry.skip);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index 057a1a2..ee37bde 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FileMetaSubject.fileMetas;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.ComparableSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
@@ -38,25 +39,26 @@
   public ListSubject<ContentEntrySubject, ContentEntry> content() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return ListSubject.assertThat(diffInfo.content, ContentEntrySubject::assertThat)
-        .named("content");
+    return check("content()")
+        .about(elements())
+        .thatCustom(diffInfo.content, ContentEntrySubject.contentEntries());
   }
 
   public ComparableSubject<?, ChangeType> changeType() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return Truth.assertThat(diffInfo.changeType).named("changeType");
+    return check("changeType()").that(diffInfo.changeType);
   }
 
   public FileMetaSubject metaA() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return FileMetaSubject.assertThat(diffInfo.metaA).named("metaA");
+    return check("metaA()").about(fileMetas()).that(diffInfo.metaA);
   }
 
   public FileMetaSubject metaB() {
     isNotNull();
     DiffInfo diffInfo = actual();
-    return FileMetaSubject.assertThat(diffInfo.metaB).named("metaB");
+    return check("metaB()").about(fileMetas()).that(diffInfo.metaB);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
index 84ad61c..1c99141 100644
--- a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.truth.OptionalSubject;
 import java.util.Optional;
@@ -27,12 +27,16 @@
 public class EditInfoSubject extends Subject<EditInfoSubject, EditInfo> {
 
   public static EditInfoSubject assertThat(EditInfo editInfo) {
-    return assertAbout(EditInfoSubject::new).that(editInfo);
+    return assertAbout(edits()).that(editInfo);
+  }
+
+  private static Subject.Factory<EditInfoSubject, EditInfo> edits() {
+    return EditInfoSubject::new;
   }
 
   public static OptionalSubject<EditInfoSubject, EditInfo> assertThat(
       Optional<EditInfo> editInfoOptional) {
-    return OptionalSubject.assertThat(editInfoOptional, EditInfoSubject::assertThat);
+    return OptionalSubject.assertThat(editInfoOptional, edits());
   }
 
   private EditInfoSubject(FailureMetadata failureMetadata, EditInfo editInfo) {
@@ -42,12 +46,12 @@
   public CommitInfoSubject commit() {
     isNotNull();
     EditInfo editInfo = actual();
-    return CommitInfoSubject.assertThat(editInfo.commit).named("commit");
+    return check("commit()").about(commits()).that(editInfo.commit);
   }
 
   public StringSubject baseRevision() {
     isNotNull();
     EditInfo editInfo = actual();
-    return Truth.assertThat(editInfo.baseRevision).named("baseRevision");
+    return check("baseRevision()").that(editInfo.baseRevision);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
index b088016..3ebf838 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.FileInfo;
 
 public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> {
@@ -36,18 +35,18 @@
   public IntegerSubject linesInserted() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.linesInserted).named("linesInserted");
+    return check("linesInserted()").that(fileInfo.linesInserted);
   }
 
   public IntegerSubject linesDeleted() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.linesDeleted).named("linesDeleted");
+    return check("linesDeleted()").that(fileInfo.linesDeleted);
   }
 
   public ComparableSubject<?, Character> status() {
     isNotNull();
     FileInfo fileInfo = actual();
-    return Truth.assertThat(fileInfo.status).named("status");
+    return check("status()").that(fileInfo.status);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
index e77eef1..d1b2031 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -19,13 +19,16 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
 
 public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
 
   public static FileMetaSubject assertThat(FileMeta fileMeta) {
-    return assertAbout(FileMetaSubject::new).that(fileMeta);
+    return assertAbout(fileMetas()).that(fileMeta);
+  }
+
+  public static Subject.Factory<FileMetaSubject, FileMeta> fileMetas() {
+    return FileMetaSubject::new;
   }
 
   private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
@@ -35,6 +38,6 @@
   public IntegerSubject totalLineCount() {
     isNotNull();
     FileMeta fileMeta = actual();
-    return Truth.assertThat(fileMeta.lines).named("total line count");
+    return check("totalLineCount()").that(fileMeta.lines);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
index b56d399..6ba5f8b 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixReplacementInfoSubject.java
@@ -15,18 +15,22 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.RangeSubject.ranges;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 
 public class FixReplacementInfoSubject
     extends Subject<FixReplacementInfoSubject, FixReplacementInfo> {
 
   public static FixReplacementInfoSubject assertThat(FixReplacementInfo fixReplacementInfo) {
-    return assertAbout(FixReplacementInfoSubject::new).that(fixReplacementInfo);
+    return assertAbout(fixReplacements()).that(fixReplacementInfo);
+  }
+
+  public static Subject.Factory<FixReplacementInfoSubject, FixReplacementInfo> fixReplacements() {
+    return FixReplacementInfoSubject::new;
   }
 
   private FixReplacementInfoSubject(
@@ -35,14 +39,17 @@
   }
 
   public StringSubject path() {
-    return Truth.assertThat(actual().path).named("path");
+    isNotNull();
+    return check("path()").that(actual().path);
   }
 
   public RangeSubject range() {
-    return RangeSubject.assertThat(actual().range).named("range");
+    isNotNull();
+    return check("range()").about(ranges()).that(actual().range);
   }
 
   public StringSubject replacement() {
-    return Truth.assertThat(actual().replacement).named("replacement");
+    isNotNull();
+    return check("replacement()").that(actual().replacement);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
index 7a6da9c..98dac38 100644
--- a/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FixSuggestionInfoSubject.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FixReplacementInfoSubject.fixReplacements;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
@@ -27,7 +29,11 @@
 public class FixSuggestionInfoSubject extends Subject<FixSuggestionInfoSubject, FixSuggestionInfo> {
 
   public static FixSuggestionInfoSubject assertThat(FixSuggestionInfo fixSuggestionInfo) {
-    return assertAbout(FixSuggestionInfoSubject::new).that(fixSuggestionInfo);
+    return assertAbout(fixSuggestions()).that(fixSuggestionInfo);
+  }
+
+  public static Subject.Factory<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
+    return FixSuggestionInfoSubject::new;
   }
 
   private FixSuggestionInfoSubject(
@@ -40,8 +46,10 @@
   }
 
   public ListSubject<FixReplacementInfoSubject, FixReplacementInfo> replacements() {
-    return ListSubject.assertThat(actual().replacements, FixReplacementInfoSubject::assertThat)
-        .named("replacements");
+    isNotNull();
+    return check("replacements()")
+        .about(elements())
+        .thatCustom(actual().replacements, fixReplacements());
   }
 
   public FixReplacementInfoSubject onlyReplacement() {
@@ -49,6 +57,7 @@
   }
 
   public StringSubject description() {
-    return Truth.assertThat(actual().description).named("description");
+    isNotNull();
+    return check("description()").that(actual().description);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index cdbef34..dee0636 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common.testing;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.ComparableSubject;
@@ -21,7 +22,6 @@
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.common.GitPerson;
 import java.sql.Timestamp;
 import java.util.Date;
@@ -30,7 +30,11 @@
 public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> {
 
   public static GitPersonSubject assertThat(GitPerson gitPerson) {
-    return assertAbout(GitPersonSubject::new).that(gitPerson);
+    return assertAbout(gitPersons()).that(gitPerson);
+  }
+
+  public static Factory<GitPersonSubject, GitPerson> gitPersons() {
+    return GitPersonSubject::new;
   }
 
   private GitPersonSubject(FailureMetadata failureMetadata, GitPerson gitPerson) {
@@ -40,30 +44,30 @@
   public StringSubject name() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.name).named("name");
+    return check("name()").that(gitPerson.name);
   }
 
   public StringSubject email() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.email).named("email");
+    return check("email()").that(gitPerson.email);
   }
 
   public ComparableSubject<?, Timestamp> date() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.date).named("date");
+    return check("date()").that(gitPerson.date);
   }
 
   public IntegerSubject tz() {
     isNotNull();
     GitPerson gitPerson = actual();
-    return Truth.assertThat(gitPerson.tz).named("tz");
+    return check("tz()").that(gitPerson.tz);
   }
 
   public void hasSameDateAs(GitPerson other) {
+    checkNotNull(other, "'other' GitPerson must not be null");
     isNotNull();
-    assertThat(other).named("other").isNotNull();
     date().isEqualTo(other.date);
     tz().isEqualTo(other.tz);
   }
@@ -72,9 +76,7 @@
     isNotNull();
     name().isEqualTo(ident.getName());
     email().isEqualTo(ident.getEmailAddress());
-    Truth.assertThat(new Date(actual().date.getTime()))
-        .named("rounded date")
-        .isEqualTo(ident.getWhen());
+    check("roundedDate()").that(new Date(actual().date.getTime())).isEqualTo(ident.getWhen());
     tz().isEqualTo(ident.getTimeZoneOffset());
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
index db7f0d1..12acb8d 100644
--- a/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RangeSubject.java
@@ -14,19 +14,22 @@
 
 package com.google.gerrit.extensions.common.testing;
 
-import static com.google.common.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.client.Comment;
 
 public class RangeSubject extends Subject<RangeSubject, Comment.Range> {
 
   public static RangeSubject assertThat(Comment.Range range) {
-    return assertAbout(RangeSubject::new).that(range);
+    return assertAbout(ranges()).that(range);
+  }
+
+  public static Subject.Factory<RangeSubject, Comment.Range> ranges() {
+    return RangeSubject::new;
   }
 
   private RangeSubject(FailureMetadata failureMetadata, Comment.Range range) {
@@ -34,32 +37,32 @@
   }
 
   public IntegerSubject startLine() {
-    return Truth.assertThat(actual().startLine).named("startLine");
+    return check("startLine()").that(actual().startLine);
   }
 
   public IntegerSubject startCharacter() {
-    return Truth.assertThat(actual().startCharacter).named("startCharacter");
+    return check("startCharacter()").that(actual().startCharacter);
   }
 
   public IntegerSubject endLine() {
-    return Truth.assertThat(actual().endLine).named("endLine");
+    return check("endLine()").that(actual().endLine);
   }
 
   public IntegerSubject endCharacter() {
-    return Truth.assertThat(actual().endCharacter).named("endCharacter");
+    return check("endCharacter()").that(actual().endCharacter);
   }
 
   public void isValid() {
     isNotNull();
     if (!actual().isValid()) {
-      failWithoutActual(fact("expected", "valid"));
+      failWithActual(simpleFact("expected to be valid"));
     }
   }
 
   public void isInvalid() {
     isNotNull();
     if (actual().isValid()) {
-      failWithoutActual(fact("expected", "not valid"));
+      failWithActual(simpleFact("expected to be invalid"));
     }
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
index c2bed86..033f54b 100644
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.common.testing;
 
 import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
@@ -27,12 +28,15 @@
 
   public static ListSubject<RobotCommentInfoSubject, RobotCommentInfo> assertThatList(
       List<RobotCommentInfo> robotCommentInfos) {
-    return ListSubject.assertThat(robotCommentInfos, RobotCommentInfoSubject::assertThat)
-        .named("robotCommentInfos");
+    return ListSubject.assertThat(robotCommentInfos, robotComments()).named("robotCommentInfos");
   }
 
   public static RobotCommentInfoSubject assertThat(RobotCommentInfo robotCommentInfo) {
-    return assertAbout(RobotCommentInfoSubject::new).that(robotCommentInfo);
+    return assertAbout(robotComments()).that(robotCommentInfo);
+  }
+
+  private static Factory<RobotCommentInfoSubject, RobotCommentInfo> robotComments() {
+    return RobotCommentInfoSubject::new;
   }
 
   private RobotCommentInfoSubject(
@@ -41,8 +45,9 @@
   }
 
   public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
-    return ListSubject.assertThat(actual().fixSuggestions, FixSuggestionInfoSubject::assertThat)
-        .named("fixSuggestions");
+    return check("fixSuggestions()")
+        .about(elements())
+        .thatCustom(actual().fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
   }
 
   public FixSuggestionInfoSubject onlyFixSuggestion() {
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
index 1867308..5109205 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
+++ b/java/com/google/gerrit/extensions/restapi/testing/BinaryResultSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.PrimitiveByteArraySubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.truth.OptionalSubject;
 import java.io.ByteArrayOutputStream;
@@ -30,12 +29,16 @@
 public class BinaryResultSubject extends Subject<BinaryResultSubject, BinaryResult> {
 
   public static BinaryResultSubject assertThat(BinaryResult binaryResult) {
-    return assertAbout(BinaryResultSubject::new).that(binaryResult);
+    return assertAbout(binaryResults()).that(binaryResult);
+  }
+
+  private static Subject.Factory<BinaryResultSubject, BinaryResult> binaryResults() {
+    return BinaryResultSubject::new;
   }
 
   public static OptionalSubject<BinaryResultSubject, BinaryResult> assertThat(
       Optional<BinaryResult> binaryResultOptional) {
-    return OptionalSubject.assertThat(binaryResultOptional, BinaryResultSubject::assertThat);
+    return OptionalSubject.assertThat(binaryResultOptional, binaryResults());
   }
 
   private BinaryResultSubject(FailureMetadata failureMetadata, BinaryResult binaryResult) {
@@ -48,7 +51,7 @@
     // be used afterwards. Besides, closing it doesn't have an effect for most
     // implementations of a BinaryResult.
     BinaryResult binaryResult = actual();
-    return Truth.assertThat(binaryResult.asString());
+    return check("asString()").that(binaryResult.asString());
   }
 
   public PrimitiveByteArraySubject bytes() throws IOException {
@@ -60,6 +63,6 @@
     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     binaryResult.writeTo(byteArrayOutputStream);
     byte[] bytes = byteArrayOutputStream.toByteArray();
-    return Truth.assertThat(bytes);
+    return check("bytes()").that(bytes);
   }
 }
diff --git a/java/com/google/gerrit/git/testing/CommitSubject.java b/java/com/google/gerrit/git/testing/CommitSubject.java
index 198ddff..0873107 100644
--- a/java/com/google/gerrit/git/testing/CommitSubject.java
+++ b/java/com/google/gerrit/git/testing/CommitSubject.java
@@ -19,7 +19,6 @@
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import java.sql.Timestamp;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -69,9 +68,7 @@
   public void hasCommitMessage(String expectedCommitMessage) {
     isNotNull();
     RevCommit commit = actual();
-    Truth.assertThat(commit.getFullMessage())
-        .named("commit message")
-        .isEqualTo(expectedCommitMessage);
+    check("commitMessage()").that(commit.getFullMessage()).isEqualTo(expectedCommitMessage);
   }
 
   /**
@@ -84,7 +81,7 @@
     RevCommit commit = actual();
     long timestampDiffMs =
         Math.abs(commit.getCommitTime() * 1000L - expectedCommitTimestamp.getTime());
-    Truth.assertThat(timestampDiffMs).named("commit timestamp diff").isAtMost(SECONDS.toMillis(1));
+    check("commitTimestampDiff()").that(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
   }
 
   /**
@@ -95,6 +92,6 @@
   public void hasSha1(ObjectId expectedSha1) {
     isNotNull();
     RevCommit commit = actual();
-    Truth.assertThat(commit).named("SHA1").isEqualTo(expectedSha1);
+    check("sha1()").that(commit).isEqualTo(expectedSha1);
   }
 }
diff --git a/java/com/google/gerrit/git/testing/ObjectIdSubject.java b/java/com/google/gerrit/git/testing/ObjectIdSubject.java
index 0fd3b73..5fe91f9 100644
--- a/java/com/google/gerrit/git/testing/ObjectIdSubject.java
+++ b/java/com/google/gerrit/git/testing/ObjectIdSubject.java
@@ -18,12 +18,15 @@
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import org.eclipse.jgit.lib.ObjectId;
 
 public class ObjectIdSubject extends Subject<ObjectIdSubject, ObjectId> {
   public static ObjectIdSubject assertThat(ObjectId objectId) {
-    return assertAbout(ObjectIdSubject::new).that(objectId);
+    return assertAbout(objectIds()).that(objectId);
+  }
+
+  public static Factory<ObjectIdSubject, ObjectId> objectIds() {
+    return ObjectIdSubject::new;
   }
 
   private ObjectIdSubject(FailureMetadata metadata, ObjectId actual) {
@@ -33,6 +36,6 @@
   public void hasName(String expectedName) {
     isNotNull();
     ObjectId objectId = actual();
-    Truth.assertThat(objectId.getName()).named("name").isEqualTo(expectedName);
+    check("name()").that(objectId.getName()).isEqualTo(expectedName);
   }
 }
diff --git a/java/com/google/gerrit/git/testing/PushResultSubject.java b/java/com/google/gerrit/git/testing/PushResultSubject.java
index 929e182..724ca17 100644
--- a/java/com/google/gerrit/git/testing/PushResultSubject.java
+++ b/java/com/google/gerrit/git/testing/PushResultSubject.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertAbout;
-import static java.util.stream.Collectors.joining;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
@@ -25,11 +24,10 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StreamSubject;
 import com.google.common.truth.Subject;
 import com.google.common.truth.Truth;
-import com.google.common.truth.Truth8;
 import com.google.gerrit.common.Nullable;
-import java.util.Arrays;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 
@@ -43,7 +41,8 @@
   }
 
   public void hasNoMessages() {
-    Truth.assertWithMessage("expected no messages")
+    check()
+        .withMessage("expected no messages")
         .that(Strings.nullToEmpty(trimMessages()))
         .isEqualTo("");
   }
@@ -51,14 +50,14 @@
   public void hasMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
-    Truth.assertThat(trimMessages()).isEqualTo(Arrays.stream(expectedLines).collect(joining("\n")));
+    check("messages()").that(trimMessages()).isEqualTo(String.join("\n", expectedLines));
   }
 
   public void containsMessages(String... expectedLines) {
     checkArgument(expectedLines.length > 0, "use hasNoMessages()");
     isNotNull();
     Iterable<String> got = Splitter.on("\n").split(trimMessages());
-    Truth.assertThat(got).containsAllIn(expectedLines).inOrder();
+    check("messages()").that(got).containsAllIn(expectedLines).inOrder();
   }
 
   private String trimMessages() {
@@ -90,10 +89,7 @@
               messages, Throwables.getStackTraceAsString(e));
       return;
     }
-    Truth.assertThat(actual)
-        .named("processed commands")
-        .containsExactlyEntriesIn(expected)
-        .inOrder();
+    check("processedCommands()").that(actual).containsExactlyEntriesIn(expected).inOrder();
   }
 
   @VisibleForTesting
@@ -129,7 +125,9 @@
   }
 
   public RemoteRefUpdateSubject onlyRef(String refName) {
-    Truth8.assertThat(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
+    check("setOfRefs()")
+        .about(StreamSubject.streams())
+        .that(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
         .named("set of refs")
         .containsExactly(refName);
     return ref(refName);
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index cb1e965..d09b4dd 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -215,7 +215,15 @@
       return expiresAt;
     }
 
-    Account.Id getAccountId() {
+    /**
+     * Parse an Account.Id.
+     *
+     * <p>This is public so that plugins that implement a web session, can also implement a way to
+     * clear per user sessions.
+     *
+     * @return account ID.
+     */
+    public Account.Id getAccountId() {
       return accountId;
     }
 
diff --git a/java/com/google/gerrit/index/query/QueryBuilder.java b/java/com/google/gerrit/index/query/QueryBuilder.java
index 12d1dd6..04a77ca 100644
--- a/java/com/google/gerrit/index/query/QueryBuilder.java
+++ b/java/com/google/gerrit/index/query/QueryBuilder.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.index.query.Predicate.not;
 import static com.google.gerrit.index.query.Predicate.or;
 import static com.google.gerrit.index.query.QueryParser.AND;
+import static com.google.gerrit.index.query.QueryParser.COLON;
 import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
 import static com.google.gerrit.index.query.QueryParser.EXACT_PHRASE;
 import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
@@ -217,41 +218,41 @@
         return defaultField(onlyChildOf(r));
 
       case FIELD_NAME:
-        return operator(r.getText(), onlyChildOf(r));
+        return operator(r.getText(), concatenateChildText(r));
 
       default:
         throw error("Unsupported operator: " + r);
     }
   }
 
-  private Predicate<T> operator(String name, Tree val) throws QueryParseException {
-    switch (val.getType()) {
-        // Expand multiple values, "foo:(a b c)", as though they were written
-        // out with the longer form, "foo:a foo:b foo:c".
-        //
-      case AND:
-      case OR:
-        {
-          List<Predicate<T>> p = new ArrayList<>(val.getChildCount());
-          for (int i = 0; i < val.getChildCount(); i++) {
-            final Tree c = val.getChild(i);
-            if (c.getType() != DEFAULT_FIELD) {
-              throw error("Nested operator not expected: " + c);
-            }
-            p.add(operator(name, onlyChildOf(c)));
-          }
-          return val.getType() == AND ? and(p) : or(p);
-        }
+  private static String concatenateChildText(Tree r) throws QueryParseException {
+    if (r.getChildCount() == 0) {
+      throw error("Expected children under: " + r);
+    }
+    if (r.getChildCount() == 1) {
+      return getFieldValue(r.getChild(0));
+    }
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < r.getChildCount(); i++) {
+      sb.append(getFieldValue(r.getChild(i)));
+    }
+    return sb.toString();
+  }
 
+  private static String getFieldValue(Tree r) throws QueryParseException {
+    if (r.getChildCount() != 0) {
+      throw error("Expected no children under: " + r);
+    }
+    switch (r.getType()) {
       case SINGLE_WORD:
+      case COLON:
       case EXACT_PHRASE:
-        if (val.getChildCount() != 0) {
-          throw error("Expected no children under: " + val);
-        }
-        return operator(name, val.getText());
-
+        return r.getText();
       default:
-        throw error("Unsupported node in operator " + name + ": " + val);
+        throw error(
+            String.format(
+                "Unsupported %s node in operator %s: %s",
+                QueryParser.tokenNames[r.getType()], r.getParent(), r));
     }
   }
 
diff --git a/java/com/google/gerrit/index/query/testing/BUILD b/java/com/google/gerrit/index/query/testing/BUILD
new file mode 100644
index 0000000..030b327
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/BUILD
@@ -0,0 +1,16 @@
+package(
+    default_testonly = True,
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "testing",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//antlr3:query_parser",
+        "//java/com/google/gerrit/index",
+        "//lib:guava",
+        "//lib/antlr:java-runtime",
+        "//lib/truth",
+    ],
+)
diff --git a/java/com/google/gerrit/index/query/testing/TreeSubject.java b/java/com/google/gerrit/index/query/testing/TreeSubject.java
new file mode 100644
index 0000000..c60b363
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/TreeSubject.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2019 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 com.google.gerrit.index.query.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.index.query.QueryParser;
+import org.antlr.runtime.tree.Tree;
+
+public class TreeSubject extends Subject<TreeSubject, Tree> {
+  public static TreeSubject assertThat(Tree actual) {
+    return assertAbout(TreeSubject::new).that(actual);
+  }
+
+  private TreeSubject(FailureMetadata failureMetadata, Tree tree) {
+    super(failureMetadata, tree);
+  }
+
+  public void hasType(int expectedType) {
+    isNotNull();
+    check("getType()").that(typeName(actual().getType())).isEqualTo(typeName(expectedType));
+  }
+
+  public void hasText(String expectedText) {
+    requireNonNull(expectedText);
+    isNotNull();
+    check("getText()").that(actual().getText()).isEqualTo(expectedText);
+  }
+
+  public void hasNoChildren() {
+    isNotNull();
+    check("getChildCount()").that(actual().getChildCount()).isEqualTo(0);
+  }
+
+  public void hasChildCount(int expectedChildCount) {
+    checkArgument(
+        expectedChildCount > 0, "expected child count must be positive: %s", expectedChildCount);
+    isNotNull();
+    check("getChildCount()").that(actual().getChildCount()).isEqualTo(expectedChildCount);
+  }
+
+  public TreeSubject child(int childIndex) {
+    isNotNull();
+    return check("getChild(%s)", childIndex)
+        .about(TreeSubject::new)
+        .that(actual().getChild(childIndex));
+  }
+
+  private static String typeName(int type) {
+    checkArgument(
+        type >= 0 && type < QueryParser.tokenNames.length,
+        "invalid token type %s, max is %s",
+        type,
+        QueryParser.tokenNames.length - 1);
+    return QueryParser.tokenNames[type];
+  }
+}
diff --git a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
index a5cf3ca..546ff89 100644
--- a/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
+++ b/java/com/google/gerrit/proto/testing/SerializedClassSubject.java
@@ -15,9 +15,8 @@
 package com.google.gerrit.proto.testing;
 
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Fact.simpleFact;
 import static com.google.common.truth.Truth.assertAbout;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
@@ -67,21 +66,22 @@
 
   public void isAbstract() {
     isNotNull();
-    assertWithMessage("expected class %s to be abstract", actual().getName())
-        .that(Modifier.isAbstract(actual().getModifiers()))
-        .isTrue();
+    if (!Modifier.isAbstract(actual().getModifiers())) {
+      failWithActual(simpleFact("expected class to be abstract"));
+    }
   }
 
   public void isConcrete() {
     isNotNull();
-    assertWithMessage("expected class %s to be concrete", actual().getName())
-        .that(!Modifier.isAbstract(actual().getModifiers()))
-        .isTrue();
+    if (Modifier.isAbstract(actual().getModifiers())) {
+      failWithActual(simpleFact("expected class to be concrete"));
+    }
   }
 
   public void hasFields(Map<String, Type> expectedFields) {
     isConcrete();
-    assertThat(
+    check("fields()")
+        .that(
             FieldUtils.getAllFieldsList(actual()).stream()
                 .filter(f -> !Modifier.isStatic(f.getModifiers()))
                 .collect(toImmutableMap(Field::getName, Field::getGenericType)))
@@ -91,20 +91,20 @@
   public void hasAutoValueMethods(Map<String, Type> expectedMethods) {
     // Would be nice if we could check clazz is an @AutoValue, but the retention is not RUNTIME.
     isAbstract();
-    assertThat(
+    check("noArgumentAbstractMethodsOn(%s)", actual().getName())
+        .that(
             Arrays.stream(actual().getDeclaredMethods())
                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
                 .filter(m -> Modifier.isAbstract(m.getModifiers()))
                 .filter(m -> m.getParameters().length == 0)
                 .collect(toImmutableMap(Method::getName, Method::getGenericReturnType)))
-        .named("no-argument abstract methods on %s", actual().getName())
         .isEqualTo(expectedMethods);
   }
 
   public void extendsClass(Type superclassType) {
     isNotNull();
-    assertThat(actual().getGenericSuperclass())
-        .named("superclass of %s", actual().getName())
+    check("superclass(%s)", actual().getName())
+        .that(actual().getGenericSuperclass())
         .isEqualTo(superclassType);
   }
 }
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 39df152..1229df1 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -50,13 +50,9 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** Utility functions to manipulate Comments. */
 @Singleton
@@ -293,26 +289,6 @@
     update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
   }
 
-  public void deleteAllDraftsFromAllUsers(Change.Id changeId) throws IOException {
-    try (Repository repo = repoManager.openRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
-      for (Ref ref : getDraftRefs(repo, changeId)) {
-        bru.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
-      }
-      bru.setRefLogMessage("Delete drafts from NoteDb", false);
-      bru.execute(rw, NullProgressMonitor.INSTANCE);
-      for (ReceiveCommand cmd : bru.getCommands()) {
-        if (cmd.getResult() != ReceiveCommand.Result.OK) {
-          throw new IOException(
-              String.format(
-                  "Failed to delete draft comment ref %s at %s: %s (%s)",
-                  cmd.getRefName(), cmd.getOldId(), cmd.getResult(), cmd.getMessage()));
-        }
-      }
-    }
-  }
-
   private static List<Comment> commentsOnFile(Collection<Comment> allComments, String file) {
     List<Comment> result = new ArrayList<>(allComments.size());
     for (Comment c : allComments) {
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index e0eb3f2..66e66ca 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -115,17 +115,25 @@
 
   private class Worker implements ProjectRunnable {
     final MultiProgressMonitor progress;
+    final String name;
 
     private final Collection<ReceiveCommand> commands;
 
-    private Worker(Collection<ReceiveCommand> commands) {
+    private Worker(Collection<ReceiveCommand> commands, String name) {
       this.commands = commands;
+      this.name = name;
       progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
     }
 
     @Override
     public void run() {
-      receiveCommits.processCommands(commands, progress);
+      String oldName = Thread.currentThread().getName();
+      Thread.currentThread().setName(oldName + "-for-" + name);
+      try {
+        receiveCommits.processCommands(commands, progress);
+      } finally {
+        Thread.currentThread().setName(oldName);
+      }
     }
 
     @Override
@@ -334,7 +342,7 @@
     }
 
     long startNanos = System.nanoTime();
-    Worker w = new Worker(commands);
+    Worker w = new Worker(commands, Thread.currentThread().getName());
     try {
       w.progress.waitFor(
           executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index f0ab638..2f91394 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -23,7 +23,6 @@
 import com.google.common.truth.IterableSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
@@ -32,7 +31,11 @@
 public class InternalGroupSubject extends Subject<InternalGroupSubject, InternalGroup> {
 
   public static InternalGroupSubject assertThat(InternalGroup group) {
-    return assertAbout(InternalGroupSubject::new).that(group);
+    return assertAbout(internalGroups()).that(group);
+  }
+
+  public static Subject.Factory<InternalGroupSubject, InternalGroup> internalGroups() {
+    return InternalGroupSubject::new;
   }
 
   private InternalGroupSubject(FailureMetadata metadata, InternalGroup actual) {
@@ -42,66 +45,66 @@
   public ComparableSubject<?, AccountGroup.UUID> groupUuid() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getGroupUUID()).named("groupUuid");
+    return check("groupUuid()").that(group.getGroupUUID());
   }
 
   public ComparableSubject<?, AccountGroup.NameKey> nameKey() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getNameKey()).named("nameKey");
+    return check("nameKey()").that(group.getNameKey());
   }
 
   public StringSubject name() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getName()).named("name");
+    return check("name()").that(group.getName());
   }
 
-  public DefaultSubject id() {
+  public Subject<DefaultSubject, Object> id() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getId()).named("id");
+    return check("id()").that(group.getId());
   }
 
   public StringSubject description() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getDescription()).named("description");
+    return check("description()").that(group.getDescription());
   }
 
   public ComparableSubject<?, AccountGroup.UUID> ownerGroupUuid() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getOwnerGroupUUID()).named("ownerGroupUuid");
+    return check("ownerGroupUuid()").that(group.getOwnerGroupUUID());
   }
 
   public BooleanSubject visibleToAll() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.isVisibleToAll()).named("visibleToAll");
+    return check("visibleToAll()").that(group.isVisibleToAll());
   }
 
   public ComparableSubject<?, Timestamp> createdOn() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getCreatedOn()).named("createdOn");
+    return check("createdOn()").that(group.getCreatedOn());
   }
 
   public IterableSubject members() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getMembers()).named("members");
+    return check("members()").that(group.getMembers());
   }
 
   public IterableSubject subgroups() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getSubgroups()).named("subgroups");
+    return check("subgroups()").that(group.getSubgroups());
   }
 
   public ComparableSubject<?, ObjectId> refState() {
     isNotNull();
     InternalGroup group = actual();
-    return Truth.assertThat(group.getRefState()).named("refState");
+    return check("refState()").that(group.getRefState());
   }
 }
diff --git a/java/com/google/gerrit/truth/ListSubject.java b/java/com/google/gerrit/truth/ListSubject.java
index bd9df30..9a839dd 100644
--- a/java/com/google/gerrit/truth/ListSubject.java
+++ b/java/com/google/gerrit/truth/ListSubject.java
@@ -18,28 +18,34 @@
 import static com.google.common.truth.Fact.fact;
 import static com.google.common.truth.Truth.assertAbout;
 
+import com.google.common.collect.Iterables;
+import com.google.common.truth.CustomSubjectBuilder;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.Subject;
 import java.util.List;
-import java.util.function.Function;
+import java.util.function.BiFunction;
 
 public class ListSubject<S extends Subject<S, E>, E> extends IterableSubject {
 
-  private final Function<E, S> elementAssertThatFunction;
+  private final BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator;
 
-  @SuppressWarnings("unchecked")
   public static <S extends Subject<S, E>, E> ListSubject<S, E> assertThat(
-      List<E> list, Function<E, S> elementAssertThatFunction) {
-    // The ListSubjectFactory always returns ListSubjects. -> Casting is appropriate.
-    return (ListSubject<S, E>)
-        assertAbout(new ListSubjectFactory<>(elementAssertThatFunction)).that(list);
+      List<E> list, Subject.Factory<S, E> subjectFactory) {
+    return assertAbout(elements()).thatCustom(list, subjectFactory);
+  }
+
+  public static CustomSubjectBuilder.Factory<ListSubjectBuilder> elements() {
+    return ListSubjectBuilder::new;
   }
 
   private ListSubject(
-      FailureMetadata failureMetadata, List<E> list, Function<E, S> elementAssertThatFunction) {
+      FailureMetadata failureMetadata,
+      List<E> list,
+      BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
     super(failureMetadata, list);
-    this.elementAssertThatFunction = elementAssertThatFunction;
+    this.elementSubjectCreator = elementSubjectCreator;
   }
 
   public S element(int index) {
@@ -49,20 +55,21 @@
     if (index >= list.size()) {
       failWithoutActual(fact("expected to have element at index", index));
     }
-    return elementAssertThatFunction.apply(list.get(index));
+    return elementSubjectCreator.apply(check("element(%s)", index), list.get(index));
   }
 
   public S onlyElement() {
     isNotNull();
     hasSize(1);
-    return element(0);
+    List<E> list = getActualList();
+    return elementSubjectCreator.apply(check("onlyElement()"), Iterables.getOnlyElement(list));
   }
 
   public S lastElement() {
     isNotNull();
     isNotEmpty();
     List<E> list = getActualList();
-    return element(list.size() - 1);
+    return elementSubjectCreator.apply(check("lastElement()"), Iterables.getLast(list));
   }
 
   @SuppressWarnings("unchecked")
@@ -78,20 +85,20 @@
     return (ListSubject<S, E>) super.named(s, objects);
   }
 
-  private static class ListSubjectFactory<S extends Subject<S, T>, T>
-      implements Subject.Factory<IterableSubject, Iterable<?>> {
+  public static class ListSubjectBuilder extends CustomSubjectBuilder {
 
-    private Function<T, S> elementAssertThatFunction;
-
-    ListSubjectFactory(Function<T, S> elementAssertThatFunction) {
-      this.elementAssertThatFunction = elementAssertThatFunction;
+    ListSubjectBuilder(FailureMetadata failureMetadata) {
+      super(failureMetadata);
     }
 
-    @SuppressWarnings("unchecked")
-    @Override
-    public ListSubject<S, T> createSubject(FailureMetadata failureMetadata, Iterable<?> objects) {
-      // The constructor of ListSubject only accepts lists. -> Casting is appropriate.
-      return new ListSubject<>(failureMetadata, (List<T>) objects, elementAssertThatFunction);
+    public <S extends Subject<S, E>, E> ListSubject<S, E> thatCustom(
+        List<E> list, Subject.Factory<S, E> subjectFactory) {
+      return that(list, (builder, element) -> builder.about(subjectFactory).that(element));
+    }
+
+    public <S extends Subject<S, E>, E> ListSubject<S, E> that(
+        List<E> list, BiFunction<StandardSubjectBuilder, E, S> elementSubjectCreator) {
+      return new ListSubject<>(metadata(), list, elementSubjectCreator);
     }
   }
 }
diff --git a/java/com/google/gerrit/truth/OptionalSubject.java b/java/com/google/gerrit/truth/OptionalSubject.java
index d91f07b..b5fc5d0 100644
--- a/java/com/google/gerrit/truth/OptionalSubject.java
+++ b/java/com/google/gerrit/truth/OptionalSubject.java
@@ -17,41 +17,52 @@
 import static com.google.common.truth.Fact.fact;
 import static com.google.common.truth.Truth.assertAbout;
 
+import com.google.common.truth.CustomSubjectBuilder;
 import com.google.common.truth.DefaultSubject;
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StandardSubjectBuilder;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import java.util.Optional;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 public class OptionalSubject<S extends Subject<S, ? super T>, T>
     extends Subject<OptionalSubject<S, T>, Optional<T>> {
 
-  private final Function<? super T, ? extends S> valueAssertThatFunction;
+  private final BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator;
 
-  public static <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> assertThat(
+  // TODO(aliceks): Remove when all relevant usages are adapted to new check()/factory approach.
+  public static <S extends Subject<S, T>, T> OptionalSubject<S, T> assertThat(
       Optional<T> optional, Function<? super T, ? extends S> elementAssertThatFunction) {
-    OptionalSubjectFactory<S, T> optionalSubjectFactory =
-        new OptionalSubjectFactory<>(elementAssertThatFunction);
-    return assertAbout(optionalSubjectFactory).that(optional);
+    Subject.Factory<S, T> valueSubjectFactory =
+        (metadata, value) -> elementAssertThatFunction.apply(value);
+    return assertThat(optional, valueSubjectFactory);
+  }
+
+  public static <S extends Subject<S, T>, T> OptionalSubject<S, T> assertThat(
+      Optional<T> optional, Subject.Factory<S, T> valueSubjectFactory) {
+    return assertAbout(optionals()).thatCustom(optional, valueSubjectFactory);
   }
 
   public static OptionalSubject<DefaultSubject, ?> assertThat(Optional<?> optional) {
-    // Unfortunately, we need to cast to DefaultSubject as Truth.assertThat()
+    // Unfortunately, we need to cast to DefaultSubject as StandardSubjectBuilder#that
     // only returns Subject<DefaultSubject, Object>. There shouldn't be a way
     // for that method not to return a DefaultSubject because the generic type
     // definitions of a Subject are quite strict.
-    Function<Object, DefaultSubject> valueAssertThatFunction =
-        value -> (DefaultSubject) Truth.assertThat(value);
-    return assertThat(optional, valueAssertThatFunction);
+    return assertAbout(optionals())
+        .that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+  }
+
+  public static CustomSubjectBuilder.Factory<OptionalSubjectBuilder> optionals() {
+    return OptionalSubjectBuilder::new;
   }
 
   private OptionalSubject(
       FailureMetadata failureMetadata,
       Optional<T> optional,
-      Function<? super T, ? extends S> valueAssertThatFunction) {
+      BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
     super(failureMetadata, optional);
-    this.valueAssertThatFunction = valueAssertThatFunction;
+    this.valueSubjectCreator = valueSubjectCreator;
   }
 
   public void isPresent() {
@@ -78,22 +89,28 @@
     isNotNull();
     isPresent();
     Optional<T> optional = actual();
-    return valueAssertThatFunction.apply(optional.get());
+    return valueSubjectCreator.apply(check("value()"), optional.get());
   }
 
-  private static class OptionalSubjectFactory<S extends Subject<S, ? super T>, T>
-      implements Subject.Factory<OptionalSubject<S, T>, Optional<T>> {
+  public static class OptionalSubjectBuilder extends CustomSubjectBuilder {
 
-    private Function<? super T, ? extends S> valueAssertThatFunction;
-
-    OptionalSubjectFactory(Function<? super T, ? extends S> valueAssertThatFunction) {
-      this.valueAssertThatFunction = valueAssertThatFunction;
+    OptionalSubjectBuilder(FailureMetadata failureMetadata) {
+      super(failureMetadata);
     }
 
-    @Override
-    public OptionalSubject<S, T> createSubject(
-        FailureMetadata failureMetadata, Optional<T> optional) {
-      return new OptionalSubject<>(failureMetadata, optional, valueAssertThatFunction);
+    public <S extends Subject<S, T>, T> OptionalSubject<S, T> thatCustom(
+        Optional<T> optional, Subject.Factory<S, T> valueSubjectFactory) {
+      return that(optional, (builder, value) -> builder.about(valueSubjectFactory).that(value));
+    }
+
+    public OptionalSubject<DefaultSubject, ?> that(Optional<?> optional) {
+      return that(optional, (builder, value) -> (DefaultSubject) builder.that(value));
+    }
+
+    public <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> that(
+        Optional<T> optional,
+        BiFunction<StandardSubjectBuilder, ? super T, ? extends S> valueSubjectCreator) {
+      return new OptionalSubject<>(metadata(), optional, valueSubjectCreator);
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 5aed312..e638e9c 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -77,6 +77,7 @@
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
+import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
@@ -1214,6 +1215,36 @@
   }
 
   @Test
+  public void deleteChangeRemovesDraftComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    requestScopeOperations.setApiUser(user.getId());
+
+    DraftInput dri = new DraftInput();
+    dri.message = "hello";
+    dri.path = "a.txt";
+    dri.line = 1;
+
+    gApi.changes().id(r.getChangeId()).current().createDraft(dri);
+    Change.Id num = r.getChange().getId();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(
+              repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.getId())))
+          .isNotEmpty();
+    }
+
+    requestScopeOperations.setApiUser(admin.getId());
+
+    gApi.changes().id(r.getChangeId()).delete();
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(
+              repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.getId())))
+          .isEmpty();
+    }
+  }
+
+  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
     exception.expect(ResourceConflictException.class);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
index a664869..3f09db9 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
 import static com.google.gerrit.truth.ListSubject.assertThat;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
@@ -163,11 +164,11 @@
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
       Optional<InternalGroup> updatedGroup) {
-    return assertThat(updatedGroup, InternalGroupSubject::assertThat);
+    return assertThat(updatedGroup, internalGroups());
   }
 
   private static ListSubject<InternalGroupSubject, InternalGroup> assertThatGroups(
       List<InternalGroup> parentGroups) {
-    return assertThat(parentGroups, InternalGroupSubject::assertThat);
+    return assertThat(parentGroups, internalGroups());
   }
 }
diff --git a/javatests/com/google/gerrit/index/BUILD b/javatests/com/google/gerrit/index/BUILD
index e3436bc..a1f60de 100644
--- a/javatests/com/google/gerrit/index/BUILD
+++ b/javatests/com/google/gerrit/index/BUILD
@@ -9,6 +9,7 @@
         "//antlr3:query_parser",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
+        "//java/com/google/gerrit/index/query/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:junit",
diff --git a/javatests/com/google/gerrit/index/query/QueryBuilderTest.java b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
new file mode 100644
index 0000000..d275fa8
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2019 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 com.google.gerrit.index.query;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.ThrowableSubject;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.util.Collection;
+import java.util.Objects;
+import org.junit.Test;
+
+public class QueryBuilderTest extends GerritBaseTests {
+  private static class TestPredicate extends Predicate<Object> {
+    private final String field;
+    private final String value;
+
+    TestPredicate(String field, String value) {
+      this.field = field;
+      this.value = value;
+    }
+
+    @Override
+    public Predicate<Object> copy(Collection<? extends Predicate<Object>> children) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(field, value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof TestPredicate)) {
+        return false;
+      }
+      TestPredicate p = (TestPredicate) o;
+      return Objects.equals(field, p.field) && Objects.equals(value, p.value);
+    }
+  }
+
+  private static class TestQueryBuilder extends QueryBuilder<Object> {
+    TestQueryBuilder() {
+      super(new QueryBuilder.Definition<>(TestQueryBuilder.class));
+    }
+
+    @Operator
+    public Predicate<Object> a(String value) {
+      return new TestPredicate("a", value);
+    }
+  }
+
+  @Test
+  public void fieldNameAndValue() throws Exception {
+    assertThat(parse("a:foo")).isEqualTo(new TestPredicate("a", "foo"));
+  }
+
+  @Test
+  public void fieldWithParenthesizedValues() throws Exception {
+    assertThatParseException("a:(foo bar)")
+        .hasMessageThat()
+        .isEqualTo("line 1:2 no viable alternative at input '('");
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
+    assertThat(parse("a:foo:bar")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
+    assertThat(parse("a:*:bar")).isEqualTo(new TestPredicate("a", "*:bar"));
+  }
+
+  @Test
+  public void fieldNameAndValueWithMultipleColons() throws Exception {
+    assertThat(parse("a:*:*:*")).isEqualTo(new TestPredicate("a", "*:*:*"));
+  }
+
+  @Test
+  public void exactPhraseWithQuotes() throws Exception {
+    assertThat(parse("a:\"foo bar\"")).isEqualTo(new TestPredicate("a", "foo bar"));
+  }
+
+  @Test
+  public void exactPhraseWithQuotesAndColon() throws Exception {
+    assertThat(parse("a:\"foo:bar\"")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  @Test
+  public void exactPhraseWithBraces() throws Exception {
+    assertThat(parse("a:{foo bar}")).isEqualTo(new TestPredicate("a", "foo bar"));
+  }
+
+  @Test
+  public void exactPhraseWithBracesAndColon() throws Exception {
+    assertThat(parse("a:{foo:bar}")).isEqualTo(new TestPredicate("a", "foo:bar"));
+  }
+
+  private static Predicate<Object> parse(String query) throws Exception {
+    return new TestQueryBuilder().parse(query);
+  }
+
+  private static ThrowableSubject assertThatParseException(String query) {
+    try {
+      new TestQueryBuilder().parse(query);
+      throw new AssertionError("expected QueryParseException for " + query);
+    } catch (QueryParseException e) {
+      return assertThat(e);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/index/query/QueryParserTest.java b/javatests/com/google/gerrit/index/query/QueryParserTest.java
index 2175f7d..250c40c 100644
--- a/javatests/com/google/gerrit/index/query/QueryParserTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryParserTest.java
@@ -14,7 +14,13 @@
 
 package com.google.gerrit.index.query;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.index.query.QueryParser.AND;
+import static com.google.gerrit.index.query.QueryParser.COLON;
+import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
+import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
+import static com.google.gerrit.index.query.QueryParser.parse;
+import static com.google.gerrit.index.query.testing.TreeSubject.assertThat;
 
 import com.google.gerrit.testing.GerritBaseTests;
 import org.antlr.runtime.tree.Tree;
@@ -22,27 +28,172 @@
 
 public class QueryParserTest extends GerritBaseTests {
   @Test
-  public void projectBare() throws QueryParseException {
-    Tree r;
-
-    r = parse("project:tools/gerrit");
-    assertSingleWord("project", "tools/gerrit", r);
-
-    r = parse("project:tools/*");
-    assertSingleWord("project", "tools/*", r);
+  public void fieldNameAndValue() throws Exception {
+    Tree r = parse("project:tools/gerrit");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(1);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("tools/gerrit");
+    assertThat(r).child(0).hasNoChildren();
   }
 
-  private static void assertSingleWord(String name, String value, Tree r) {
-    assertEquals(QueryParser.FIELD_NAME, r.getType());
-    assertEquals(name, r.getText());
-    assertEquals(1, r.getChildCount());
-    final Tree c = r.getChild(0);
-    assertEquals(QueryParser.SINGLE_WORD, c.getType());
-    assertEquals(value, c.getText());
-    assertEquals(0, c.getChildCount());
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColon() throws Exception {
+    // This should work, but doesn't due to a known issue.
+    assertParseFails("project:foo:");
   }
 
-  private static Tree parse(String str) throws QueryParseException {
-    return QueryParser.parse(str);
+  @Test
+  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
+    Tree r = parse("project:foo:bar");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("foo");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("bar");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
+    Tree r = parse("project:x*y:a*b");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(3);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("x*y");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("a*b");
+    assertThat(r).child(2).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueThatLooksLikeWordColon() throws Exception {
+    // This should work, but doesn't due to a known issue.
+    assertParseFails("project:x*y:");
+  }
+
+  @Test
+  public void fieldNameAndValueWithMultipleColons() throws Exception {
+    Tree r = parse("project:*:*:*");
+    assertThat(r).hasType(FIELD_NAME);
+    assertThat(r).hasText("project");
+    assertThat(r).hasChildCount(5);
+    assertThat(r).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).hasText("*");
+    assertThat(r).child(0).hasNoChildren();
+    assertThat(r).child(1).hasType(COLON);
+    assertThat(r).child(1).hasText(":");
+    assertThat(r).child(1).hasNoChildren();
+    assertThat(r).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(2).hasText("*");
+    assertThat(r).child(2).hasNoChildren();
+    assertThat(r).child(3).hasType(COLON);
+    assertThat(r).child(3).hasText(":");
+    assertThat(r).child(3).hasNoChildren();
+    assertThat(r).child(4).hasType(SINGLE_WORD);
+    assertThat(r).child(4).hasText("*");
+    assertThat(r).child(4).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByAnotherField() throws Exception {
+    Tree r = parse("project:foo:bar file:baz");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByOpenParen() throws Exception {
+    Tree r = parse("project:foo:bar (file:baz)");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  @Test
+  public void fieldNameAndValueWithColonFollowedByCloseParen() throws Exception {
+    Tree r = parse("(project:foo:bar) file:baz");
+    assertThat(r).hasType(AND);
+    assertThat(r).hasChildCount(2);
+
+    assertThat(r).child(0).hasType(FIELD_NAME);
+    assertThat(r).child(0).hasText("project");
+    assertThat(r).child(0).hasChildCount(3);
+    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(0).hasText("foo");
+    assertThat(r).child(0).child(0).hasNoChildren();
+    assertThat(r).child(0).child(1).hasType(COLON);
+    assertThat(r).child(0).child(1).hasText(":");
+    assertThat(r).child(0).child(1).hasNoChildren();
+    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
+    assertThat(r).child(0).child(2).hasText("bar");
+    assertThat(r).child(0).child(2).hasNoChildren();
+
+    assertThat(r).child(1).hasType(FIELD_NAME);
+    assertThat(r).child(1).hasText("file");
+    assertThat(r).child(1).hasChildCount(1);
+    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
+    assertThat(r).child(1).child(0).hasText("baz");
+    assertThat(r).child(1).child(0).hasNoChildren();
+  }
+
+  private static void assertParseFails(String query) {
+    try {
+      parse(query);
+      assert_().fail("expected parse to fail: %s", query);
+    } catch (QueryParseException e) {
+      // Expected.
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
index 574c795..265b24e 100644
--- a/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
@@ -20,7 +20,6 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.common.truth.Truth;
 import com.google.gerrit.extensions.restapi.RawInput;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -41,16 +40,16 @@
 
   public StringSubject filePath() {
     isNotNull();
-    return Truth.assertThat(actual().getFilePath()).named("filePath");
+    return check("filePath()").that(actual().getFilePath());
   }
 
   public StringSubject newContent() throws IOException {
     isNotNull();
     RawInput newContent = actual().getNewContent();
-    Truth.assertThat(newContent).named("newContent").isNotNull();
+    check("newContent()").that(newContent).isNotNull();
     String contentString =
         CharStreams.toString(
             new InputStreamReader(newContent.getInputStream(), StandardCharsets.UTF_8));
-    return Truth.assertThat(contentString).named("newContent");
+    return check("newContent()").that(contentString);
   }
 }
diff --git a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
index 59ee2b7..bd9d4df 100644
--- a/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
+++ b/javatests/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
@@ -24,12 +24,17 @@
 public class TreeModificationSubject extends Subject<TreeModificationSubject, TreeModification> {
 
   public static TreeModificationSubject assertThat(TreeModification treeModification) {
-    return assertAbout(TreeModificationSubject::new).that(treeModification);
+    return assertAbout(treeModifications()).that(treeModification);
+  }
+
+  private static Factory<TreeModificationSubject, TreeModification> treeModifications() {
+    return TreeModificationSubject::new;
   }
 
   public static ListSubject<TreeModificationSubject, TreeModification> assertThatList(
       List<TreeModification> treeModifications) {
-    return ListSubject.assertThat(treeModifications, TreeModificationSubject::assertThat)
+    return assertAbout(ListSubject.elements())
+        .thatCustom(treeModifications, treeModifications())
         .named("treeModifications");
   }
 
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index 6f43380..c9ba72e 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static org.hamcrest.CoreMatchers.instanceOf;
 
@@ -1673,6 +1674,6 @@
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
       Optional<InternalGroup> loadedGroup) {
-    return assertThat(loadedGroup, InternalGroupSubject::assertThat);
+    return assertThat(loadedGroup, internalGroups());
   }
 }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index cff8189..55eb0a8 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -16,7 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.common.data.testing.GroupReferenceSubject.groupReferences;
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -584,11 +586,11 @@
 
   private static OptionalSubject<GroupReferenceSubject, GroupReference> assertThatGroup(
       Optional<GroupReference> group) {
-    return assertThat(group, GroupReferenceSubject::assertThat);
+    return assertThat(group, groupReferences());
   }
 
   private static ListSubject<CommitInfoSubject, CommitInfo> assertThatCommits(
       List<CommitInfo> commits) {
-    return ListSubject.assertThat(commits, CommitInfoSubject::assertThat);
+    return ListSubject.assertThat(commits, commits());
   }
 }
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 3988f95..0e9b4bb 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -57,11 +57,8 @@
     data = [
         ":fonts.zip",
         "//polygerrit-ui/app:test_components.zip",
-        "//resources/com/google/gerrit/httpd/raw",
     ],
     deps = [
-        "@com_github_robfig_soy//:go_default_library",
-        "@com_github_robfig_soy//soyhtml:go_default_library",
         "@org_golang_x_tools//godoc/vfs/httpfs:go_default_library",
         "@org_golang_x_tools//godoc/vfs/zipfs:go_default_library",
     ],
diff --git a/polygerrit-ui/app/embed/gr-diff.html b/polygerrit-ui/app/embed/gr-diff.html
new file mode 100644
index 0000000..6aa9370
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff.html
@@ -0,0 +1,25 @@
+<!--
+@license
+Copyright (C) 2019 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.
+-->
+<script>
+  // Needed for JSCompiler to understand it's global.
+  // eslint-disable-next-line no-unused-vars, prefer-const
+  let Gerrit = window.Gerrit || {};
+  window.Gerrit = Gerrit;
+</script>
+<link rel="import" href="../styles/themes/app-theme.html">
+<link rel="import" href="../elements/diff/gr-diff/gr-diff.html">
+<link rel="import" href="../elements/diff/gr-diff-cursor/gr-diff-cursor.html">
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index ba685184..2f5df90 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -17,6 +17,7 @@
 import (
 	"archive/zip"
 	"bufio"
+	"bytes"
 	"compress/gzip"
 	"encoding/json"
 	"errors"
@@ -32,20 +33,16 @@
 	"regexp"
 	"strings"
 
-	"github.com/robfig/soy"
-	"github.com/robfig/soy/soyhtml"
 	"golang.org/x/tools/godoc/vfs/httpfs"
 	"golang.org/x/tools/godoc/vfs/zipfs"
 )
 
 var (
-	plugins  = flag.String("plugins", "", "comma seperated plugin paths to serve")
-	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
-	prod     = flag.Bool("prod", false, "Serve production assets")
-	restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
-	scheme   = flag.String("scheme", "https", "URL scheme")
-
-	tofu *soyhtml.Tofu
+	plugins    = flag.String("plugins", "", "comma seperated plugin paths to serve")
+	port       = flag.String("port", ":8081", "Port to serve HTTP requests on")
+	host       = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
+	scheme     = flag.String("scheme", "https", "URL scheme")
+	cdnPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*")
 )
 
 func main() {
@@ -61,55 +58,35 @@
 		log.Fatal(err)
 	}
 
-	tofu, err = resolveIndexTemplate()
-	if err != nil {
-		log.Fatal(err)
-	}
-
 	workspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
 	if err := os.Chdir(filepath.Join(workspace, "polygerrit-ui")); err != nil {
 		log.Fatal(err)
 	}
 
-	http.HandleFunc("/index.html", handleIndex)
-
-	if *prod {
-		http.Handle("/", http.FileServer(http.Dir("dist")))
-	} else {
-		http.Handle("/", http.FileServer(http.Dir("app")))
-	}
-
+	http.Handle("/", http.FileServer(http.Dir("app")))
 	http.Handle("/bower_components/",
 		http.FileServer(httpfs.New(zipfs.New(componentsArchive, "bower_components"))))
 	http.Handle("/fonts/",
 		http.FileServer(httpfs.New(zipfs.New(fontsArchive, "fonts"))))
 
-	http.HandleFunc("/changes/", handleRESTProxy)
-	http.HandleFunc("/accounts/", handleRESTProxy)
-	http.HandleFunc("/config/", handleRESTProxy)
-	http.HandleFunc("/projects/", handleRESTProxy)
+	http.HandleFunc("/index.html", handleIndex)
+	http.HandleFunc("/changes/", handleProxy)
+	http.HandleFunc("/accounts/", handleProxy)
+	http.HandleFunc("/config/", handleProxy)
+	http.HandleFunc("/projects/", handleProxy)
 	http.HandleFunc("/accounts/self/detail", handleAccountDetail)
+
 	if len(*plugins) > 0 {
 		http.Handle("/plugins/", http.StripPrefix("/plugins/",
 			http.FileServer(http.Dir("../plugins"))))
 		log.Println("Local plugins from", "../plugins")
 	} else {
-		http.HandleFunc("/plugins/", handleRESTProxy)
+		http.HandleFunc("/plugins/", handleProxy)
 	}
 	log.Println("Serving on port", *port)
 	log.Fatal(http.ListenAndServe(*port, &server{}))
 }
 
-func resolveIndexTemplate() (*soyhtml.Tofu, error) {
-	basePath, err := resourceBasePath()
-	if err != nil {
-		return nil, err
-	}
-	return soy.NewBundle().
-		AddTemplateFile(basePath + ".runfiles/gerrit/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy").
-		CompileToTofu()
-}
-
 func openDataArchive(path string) (*zip.ReadCloser, error) {
 	absBinPath, err := resourceBasePath()
 	if err != nil {
@@ -122,40 +99,40 @@
 	return filepath.Abs(os.Args[0])
 }
 
-func handleIndex(w http.ResponseWriter, r *http.Request) {
-	var obj = map[string]interface{}{
-		"canonicalPath":      "",
-		"staticResourcePath": "",
+func handleIndex(writer http.ResponseWriter, originalRequest *http.Request) {
+	fakeRequest := &http.Request{
+		URL: &url.URL{
+			Path: "/",
+		},
 	}
-	w.Header().Set("Content-Type", "text/html")
-	tofu.Render(w, "com.google.gerrit.httpd.raw.Index", obj)
+	handleProxy(writer, fakeRequest)
 }
 
-func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
-	req := &http.Request{
+func handleProxy(writer http.ResponseWriter, originalRequest *http.Request) {
+	patchedRequest := &http.Request{
 		Method: "GET",
 		URL: &url.URL{
 			Scheme:   *scheme,
-			Host:     *restHost,
-			Opaque:   r.URL.EscapedPath(),
-			RawQuery: r.URL.RawQuery,
+			Host:     *host,
+			Opaque:   originalRequest.URL.EscapedPath(),
+			RawQuery: originalRequest.URL.RawQuery,
 		},
 	}
-	res, err := http.DefaultClient.Do(req)
+	response, err := http.DefaultClient.Do(patchedRequest)
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	defer res.Body.Close()
-	for name, values := range res.Header {
+	defer response.Body.Close()
+	for name, values := range response.Header {
 		for _, value := range values {
 			if name != "Content-Length" {
-				w.Header().Add(name, value)
+				writer.Header().Add(name, value)
 			}
 		}
 	}
-	w.WriteHeader(res.StatusCode)
-	if _, err := io.Copy(w, patchResponse(r, res)); err != nil {
+	writer.WriteHeader(response.StatusCode)
+	if _, err := io.Copy(writer, patchResponse(originalRequest, response)); err != nil {
 		log.Println("Error copying response to ResponseWriter:", err)
 		return
 	}
@@ -188,8 +165,10 @@
 	}
 }
 
-func patchResponse(r *http.Request, res *http.Response) io.Reader {
-	switch r.URL.EscapedPath() {
+func patchResponse(req *http.Request, res *http.Response) io.Reader {
+	switch req.URL.EscapedPath() {
+	case "/":
+		return replaceCdn(res.Body)
 	case "/config/server/info":
 		return injectLocalPlugins(res.Body)
 	default:
@@ -197,13 +176,23 @@
 	}
 }
 
-func injectLocalPlugins(r io.Reader) io.Reader {
+func replaceCdn(reader io.Reader) io.Reader {
+	buf := new(bytes.Buffer)
+	buf.ReadFrom(reader)
+	original := buf.String()
+
+	replaced := cdnPattern.ReplaceAllString(original, "")
+
+	return strings.NewReader(replaced)
+}
+
+func injectLocalPlugins(reader io.Reader) io.Reader {
 	if len(*plugins) == 0 {
-		return r
+		return reader
 	}
 	// Skip escape prefix
-	io.CopyN(ioutil.Discard, r, 5)
-	dec := json.NewDecoder(r)
+	io.CopyN(ioutil.Discard, reader, 5)
+	dec := json.NewDecoder(reader)
 
 	var response map[string]interface{}
 	err := dec.Decode(&response)