Merge changes I235f2510,I9d97b664,I47a02929

* changes:
  Update org.apache.mina:mina-core to 2.0.27
  Update JGit to f22643b39 and apache sshd to 2.15.0
  Update bouncycastle to 1.80
diff --git a/contrib/migrate-to-h2-v2.sh b/contrib/migrate-to-h2-v2.sh
new file mode 100755
index 0000000..221b68c
--- /dev/null
+++ b/contrib/migrate-to-h2-v2.sh
@@ -0,0 +1,76 @@
+#!/bin/bash -e
+
+# Copyright (C) 2025 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.
+
+OLD_H2_VERSION=1.3.176
+NEW_H2_VERSION=2.3.232
+
+usage() {
+    me=`basename "$0"`
+    echo >&2 "Usage: $me [--help] [--site SITE] [--output DST]"
+    exit 1
+}
+
+while test $# -gt 0 ; do
+  case "$1" in
+  --help)
+    usage
+    ;;
+
+  --site)
+    shift
+    SITE=$1
+    shift
+    ;;
+
+  --output)
+    shift
+    DST=$1
+    shift
+    ;;
+  *)
+    break
+  esac
+done
+
+test -z $SITE && usage
+SRC=$SITE/cache
+
+test -z $DST && DST=$SRC
+
+mkdir -p $DST
+rm -rf $DST/*-v2.mv.db
+
+test -f h2-$NEW_H2_VERSION.jar || \
+    wget https://repo1.maven.org/maven2/com/h2database/h2/$NEW_H2_VERSION/h2-$NEW_H2_VERSION.jar
+test -f h2-$OLD_H2_VERSION.jar || \
+    wget https://repo1.maven.org/maven2/com/h2database/h2/$OLD_H2_VERSION/h2-$OLD_H2_VERSION.jar
+
+for filepath in $SRC/*.h2.db; do
+    DB_NAME=$(basename "$filepath" .h2.db)
+
+    echo "Exporting database $DB_NAME ..."
+    cp $filepath $DST/${DB_NAME}_tmp.h2.db
+    java -cp h2-$OLD_H2_VERSION.jar org.h2.tools.Shell -url jdbc:h2:$DST/${DB_NAME}_tmp -sql 'ALTER TABLE public.data DROP COLUMN IF EXISTS space;'
+    java -cp h2-$OLD_H2_VERSION.jar org.h2.tools.Script -url jdbc:h2:$DST/${DB_NAME}_tmp -script backup-$DB_NAME.zip -options compression zip
+
+    echo "Importing data of $DB_NAME..."
+    java -cp h2-$NEW_H2_VERSION.jar org.h2.tools.RunScript -url jdbc:h2:$DST/$DB_NAME-v2 -script ./backup-$DB_NAME.zip -options compression zip FROM_1X
+    java -cp h2-$NEW_H2_VERSION.jar org.h2.tools.Shell -url jdbc:h2:$DST/$DB_NAME-v2 -sql 'ALTER TABLE public.data ADD COLUMN IF NOT EXISTS space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v);'
+
+    rm -f backup-$DB_NAME.zip
+    rm -rf $DST/${DB_NAME}_tmp.h2.db
+    echo "$DB_NAME migrated succesfully"
+done
diff --git a/java/com/google/gerrit/extensions/api/projects/TagInfo.java b/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index 61ea518..9bb15a9 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -26,7 +26,7 @@
   public String message;
   public GitPerson tagger;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp created;
 
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index 187c84f..4bfa566 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -40,7 +40,7 @@
   public Range range;
   public String inReplyTo;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp updated;
 
@@ -54,14 +54,14 @@
 
   public List<FixSuggestionInfo> fixSuggestions;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public Instant getUpdated() {
     return updated.toInstant();
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setUpdated(Instant when) {
diff --git a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
index a76a7f9..a891d8c 100644
--- a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
@@ -28,7 +28,7 @@
  */
 public class AccountDetailInfo extends AccountInfo {
   /** The timestamp of when the account was registered. */
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp registeredOn;
 
@@ -36,7 +36,7 @@
     super(id);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setRegisteredOn(Instant registeredOn) {
diff --git a/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 4519add..9147b51 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -44,7 +44,7 @@
   public Integer value;
 
   /** The time and date describing when the approval was made. */
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp date;
 
@@ -91,7 +91,7 @@
     }
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setDate(Instant date) {
diff --git a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
index 81dbc88..b220371 100644
--- a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
+++ b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
@@ -31,7 +31,7 @@
   public AccountInfo account;
 
   /** The timestamp of the last update. */
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp lastUpdate;
 
@@ -56,7 +56,7 @@
     this.reasonAccount = reasonAccount;
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public AttentionSetInfo(
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 63e9c61..28d99de 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -59,7 +59,7 @@
   public String subject;
   public ChangeStatus status;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp created;
   public Timestamp updated;
@@ -140,42 +140,42 @@
     this.revisions = ImmutableMap.copyOf(revisions);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public Instant getCreated() {
     return created.toInstant();
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setCreated(Instant when) {
     created = Timestamp.from(when);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public Instant getUpdated() {
     return updated.toInstant();
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setUpdated(Instant when) {
     updated = Timestamp.from(when);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public Instant getSubmitted() {
     return submitted.toInstant();
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setSubmitted(Instant when, AccountInfo who) {
diff --git a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index 51fe57c..c128f36 100644
--- a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -27,7 +27,7 @@
   public AccountInfo author;
   public AccountInfo realAuthor;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp date;
 
@@ -41,7 +41,7 @@
     this.message = message;
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setDate(Instant when) {
diff --git a/java/com/google/gerrit/extensions/common/GitPerson.java b/java/com/google/gerrit/extensions/common/GitPerson.java
index df3e488..98481c5 100644
--- a/java/com/google/gerrit/extensions/common/GitPerson.java
+++ b/java/com/google/gerrit/extensions/common/GitPerson.java
@@ -22,13 +22,13 @@
   public String name;
   public String email;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp date;
 
   public int tz;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setDate(Instant when) {
diff --git a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
index 9a13713..de0f4e0 100644
--- a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
@@ -30,11 +30,11 @@
   public Type type;
   public AccountInfo user;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp date;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public static UserMemberAuditEventInfo createAddUserEvent(
@@ -47,7 +47,7 @@
     return new UserMemberAuditEventInfo(Type.ADD_USER, user, date, member);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public static UserMemberAuditEventInfo createRemoveUserEvent(
@@ -61,7 +61,7 @@
     return new UserMemberAuditEventInfo(Type.REMOVE_USER, user, date, member);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public static GroupMemberAuditEventInfo createAddGroupEvent(
@@ -74,7 +74,7 @@
     return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date, member);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public static GroupMemberAuditEventInfo createRemoveGroupEvent(
@@ -94,7 +94,7 @@
     this.date = date.orElse(null);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   protected GroupAuditEventInfo(Type type, AccountInfo user, @Nullable Instant date) {
diff --git a/java/com/google/gerrit/extensions/common/GroupInfo.java b/java/com/google/gerrit/extensions/common/GroupInfo.java
index edbaa01..cde2aa4 100644
--- a/java/com/google/gerrit/extensions/common/GroupInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupInfo.java
@@ -28,7 +28,7 @@
   public String owner;
   public String ownerId;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp createdOn;
 
@@ -38,14 +38,14 @@
   public List<AccountInfo> members;
   public List<GroupInfo> includes;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public Instant getCreatedOn() {
     return createdOn.toInstant();
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setCreatedOn(Instant when) {
diff --git a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
index 36682f6..8f2d38c 100644
--- a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
+++ b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
@@ -20,7 +20,7 @@
 import java.util.Objects;
 
 public class ReviewerUpdateInfo {
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp updated;
 
@@ -30,7 +30,7 @@
 
   public ReviewerUpdateInfo() {}
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public ReviewerUpdateInfo(
diff --git a/java/com/google/gerrit/extensions/common/RevisionInfo.java b/java/com/google/gerrit/extensions/common/RevisionInfo.java
index dc134fb..8a88dd2 100644
--- a/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -28,7 +28,7 @@
   public ChangeKind kind;
   public int _number;
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   public Timestamp created;
 
@@ -69,7 +69,7 @@
     this.uploader = uploader;
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void setCreated(Instant date) {
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index f75ec66..de7e5f1 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -70,7 +70,7 @@
     tz().isEqualTo(other.tz);
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   public void matches(PersonIdent ident) {
diff --git a/java/com/google/gerrit/httpd/EnableTracingFilter.java b/java/com/google/gerrit/httpd/EnableTracingFilter.java
new file mode 100644
index 0000000..c91be4b
--- /dev/null
+++ b/java/com/google/gerrit/httpd/EnableTracingFilter.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2024 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.httpd;
+
+import static com.google.gerrit.httpd.GerritHeaders.X_GERRIT_TRACE;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.restapi.ParameterParser;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This filter associates a trace ID to each http request. If requested, forced tracing is also
+ * enabled.
+ *
+ * <p>There are 2 ways to force tracing for http requests: 1. by using the 'trace' or
+ * 'trace=<trace-id>' request parameter 2. by setting the 'X-Gerrit-Trace:' or
+ * 'X-Gerrit-Trace:<trace-id>' header
+ */
+@Singleton
+public class EnableTracingFilter implements Filter {
+
+  public static final String REQUEST_TRACE_CONTEXT = "REQUEST_TRACE_CONTEXT";
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {}
+
+  private int count;
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    System.out.printf("%d EnableTracingFilter.doFilter\n", ++count);
+    HttpServletRequest req = (HttpServletRequest) request;
+    HttpServletResponse res = (HttpServletResponse) response;
+    try (TraceContext traceContext = enableTracing(req, res)) {
+      request.setAttribute(REQUEST_TRACE_CONTEXT, traceContext);
+      chain.doFilter(request, response);
+    }
+  }
+
+  private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
+    String traceValueFromHeader = req.getHeader(X_GERRIT_TRACE);
+    String traceValueFromRequestParam = req.getParameter(ParameterParser.TRACE_PARAMETER);
+    boolean forceLogging = traceValueFromHeader != null || traceValueFromRequestParam != null;
+
+    // Check whether no trace ID, one trace ID or 2 different trace IDs have been specified.
+    String traceId1;
+    String traceId2;
+    if (!Strings.isNullOrEmpty(traceValueFromHeader)) {
+      traceId1 = traceValueFromHeader;
+      if (!Strings.isNullOrEmpty(traceValueFromRequestParam)
+          && !traceValueFromHeader.equals(traceValueFromRequestParam)) {
+        traceId2 = traceValueFromRequestParam;
+      } else {
+        traceId2 = null;
+      }
+    } else {
+      traceId1 = Strings.emptyToNull(traceValueFromRequestParam);
+      traceId2 = null;
+    }
+
+    // Use the first trace ID to start tracing. If this trace ID is null, a trace ID will be
+    // generated.
+    TraceContext traceContext =
+        TraceContext.newTrace(
+            forceLogging, traceId1, (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId));
+    // If a second trace ID was specified, add a tag for it as well.
+    if (traceId2 != null) {
+      traceContext.addTag(RequestId.Type.TRACE_ID, traceId2);
+      res.addHeader(X_GERRIT_TRACE, traceId2);
+    }
+    return traceContext;
+  }
+
+  @Override
+  public void destroy() {}
+}
diff --git a/java/com/google/gerrit/httpd/GerritHeaders.java b/java/com/google/gerrit/httpd/GerritHeaders.java
new file mode 100644
index 0000000..e5be906
--- /dev/null
+++ b/java/com/google/gerrit/httpd/GerritHeaders.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2024 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.httpd;
+
+public class GerritHeaders {
+  public static final String X_GERRIT_TRACE = "X-Gerrit-Trace";
+}
diff --git a/java/com/google/gerrit/httpd/HttpRequestTraceModule.java b/java/com/google/gerrit/httpd/HttpRequestTraceModule.java
new file mode 100644
index 0000000..ea36fbc
--- /dev/null
+++ b/java/com/google/gerrit/httpd/HttpRequestTraceModule.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2024 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.httpd;
+
+import static com.google.gerrit.httpd.EnableTracingFilter.REQUEST_TRACE_CONTEXT;
+
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.inject.Provides;
+import com.google.inject.name.Named;
+import com.google.inject.servlet.RequestScoped;
+import com.google.inject.servlet.ServletModule;
+import javax.servlet.http.HttpServletRequest;
+
+public class HttpRequestTraceModule extends ServletModule {
+
+  @Provides
+  @RequestScoped
+  @Named(REQUEST_TRACE_CONTEXT)
+  public TraceContext provideTraceContext(HttpServletRequest req) {
+    return (TraceContext) req.getAttribute(REQUEST_TRACE_CONTEXT);
+  }
+
+  @Override
+  protected void configureServlets() {
+    filter("/*").through(EnableTracingFilter.class);
+  }
+}
diff --git a/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java b/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java
index b0a8013..9b8c827 100644
--- a/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java
+++ b/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java
@@ -20,6 +20,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import org.eclipse.jgit.lib.Config;
 
@@ -34,7 +35,7 @@
   ProxyPropertiesProvider(@GerritServerConfig Config config) throws MalformedURLException {
     String proxyUrlStr = config.getString("http", null, "proxy");
     if (!Strings.isNullOrEmpty(proxyUrlStr)) {
-      proxyUrl = new URL(proxyUrlStr);
+      proxyUrl = URI.create(proxyUrlStr).toURL();
       proxyUser = config.getString("http", null, "proxyUsername");
       proxyPassword = config.getString("http", null, "proxyPassword");
       String userInfo = proxyUrl.getUserInfo();
diff --git a/java/com/google/gerrit/httpd/WebModule.java b/java/com/google/gerrit/httpd/WebModule.java
index 5a6a84c..d0c8250 100644
--- a/java/com/google/gerrit/httpd/WebModule.java
+++ b/java/com/google/gerrit/httpd/WebModule.java
@@ -49,6 +49,8 @@
 
   @Override
   protected void configure() {
+    install(new HttpRequestTraceModule());
+
     bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
     bind(HttpRequestContext.class);
 
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 8fe7d79..66544c2 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -19,6 +19,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+import static com.google.gerrit.httpd.EnableTracingFilter.REQUEST_TRACE_CONTEXT;
 import static java.math.RoundingMode.CEILING;
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -142,6 +143,7 @@
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
 import com.google.inject.util.Providers;
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -245,6 +247,7 @@
     final DeadlineChecker.Factory deadlineCheckerFactory;
     final CancellationMetrics cancellationMetrics;
     final AclInfoController aclInfoController;
+    final Provider<TraceContext> requestTraceContext;
 
     @Inject
     Globals(
@@ -265,7 +268,8 @@
         DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
         DeadlineChecker.Factory deadlineCheckerFactory,
         CancellationMetrics cancellationMetrics,
-        AclInfoController aclInfoController) {
+        AclInfoController aclInfoController,
+        @Named(REQUEST_TRACE_CONTEXT) Provider<TraceContext> requestTraceContext) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
@@ -285,6 +289,7 @@
       this.deadlineCheckerFactory = deadlineCheckerFactory;
       this.cancellationMetrics = cancellationMetrics;
       this.aclInfoController = aclInfoController;
+      this.requestTraceContext = requestTraceContext;
     }
   }
 
@@ -331,456 +336,444 @@
     String sessionId = globals.webSession.get().getSessionId();
     CurrentUser currentUser = globals.currentUser.get();
 
-    try (TraceContext traceContext = enableTracing(req, res)) {
-      String requestUri = requestUri(req);
+    String requestUri = requestUri(req);
 
-      try (PerThreadCache ignored = PerThreadCache.create()) {
-        List<IdString> path = splitPath(req);
-        RequestInfo requestInfo = createRequestInfo(traceContext, req, requestUri, path);
-        globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
+    try (PerThreadCache ignored = PerThreadCache.create()) {
+      List<IdString> path = splitPath(req);
+      TraceContext traceContext = globals.requestTraceContext.get();
+      RequestInfo requestInfo = createRequestInfo(traceContext, req, requestUri, path);
+      globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
 
-        globals.aclInfoController.enableAclLoggingIfUserCanViewAccess(traceContext);
+      globals.aclInfoController.enableAclLoggingIfUserCanViewAccess(traceContext);
 
-        // It's important that the PerformanceLogContext is closed before the response is sent to
-        // the client. Only this way it is ensured that the invocation of the PerformanceLogger
-        // plugins happens before the client sees the response. This is needed for being able to
-        // test performance logging from an acceptance test (see
-        // TraceIT#performanceLoggingForRestCall()).
-        try (RequestStateContext requestStateContext =
-                RequestStateContext.open()
-                    .addRequestStateProvider(
-                        globals.deadlineCheckerFactory.create(
-                            requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
-            PerformanceLogContext performanceLogContext =
-                new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
-          traceRequestData(req);
+      // It's important that the PerformanceLogContext is closed before the response is sent to
+      // the client. Only this way it is ensured that the invocation of the PerformanceLogger
+      // plugins happens before the client sees the response. This is needed for being able to
+      // test performance logging from an acceptance test (see
+      // TraceIT#performanceLoggingForRestCall()).
+      try (RequestStateContext requestStateContext =
+              RequestStateContext.open()
+                  .addRequestStateProvider(
+                      globals.deadlineCheckerFactory.create(
+                          requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
+          PerformanceLogContext performanceLogContext =
+              new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
+        traceRequestData(req);
 
-          if (corsResponder.filterCorsPreflight(req, res)) {
-            return;
+        if (corsResponder.filterCorsPreflight(req, res)) {
+          return;
+        }
+
+        qp = ParameterParser.getQueryParams(req);
+        corsResponder.checkCors(req, res, qp.hasXdOverride());
+        if (qp.hasXdOverride()) {
+          req = applyXdOverrides(req, qp);
+        }
+        checkUserSession(req);
+
+        RestCollection<RestResource, RestResource> rc = members.get();
+        globals
+            .permissionBackend
+            .currentUser()
+            .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
+
+        viewData = new ViewData(null, null);
+
+        if (path.isEmpty()) {
+          globals.quotaChecker.enforce(req);
+          if (rc instanceof NeedsParams) {
+            ((NeedsParams) rc).setParams(qp.params());
           }
 
-          qp = ParameterParser.getQueryParams(req);
-          corsResponder.checkCors(req, res, qp.hasXdOverride());
-          if (qp.hasXdOverride()) {
-            req = applyXdOverrides(req, qp);
+          if (isRead(req)) {
+            viewData = new ViewData(null, rc.list());
+          } else if (isPost(req)) {
+            RestView<RestResource> restCollectionView =
+                rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
+            if (restCollectionView != null) {
+              viewData = new ViewData(null, restCollectionView);
+            } else {
+              throw methodNotAllowed(req);
+            }
+          } else {
+            // DELETE on root collections is not supported
+            throw methodNotAllowed(req);
           }
-          checkUserSession(req);
+        } else {
+          IdString id = path.remove(0);
+          try {
+            rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, rc, rsrc, id);
+            globals.quotaChecker.enforce(rsrc, req);
+            if (path.isEmpty()) {
+              checkPreconditions(req);
+            }
+          } catch (ResourceNotFoundException e) {
+            if (!path.isEmpty()) {
+              throw e;
+            }
+            globals.quotaChecker.enforce(req);
 
-          RestCollection<RestResource, RestResource> rc = members.get();
-          globals
-              .permissionBackend
-              .currentUser()
-              .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
+            if (isPost(req) || isPut(req)) {
+              RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
+              if (createView != null) {
+                viewData = new ViewData(null, createView);
+                path.add(id);
+              } else {
+                throw e;
+              }
+            } else if (isDelete(req)) {
+              RestView<RestResource> deleteView =
+                  rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
+              if (deleteView != null) {
+                viewData = new ViewData(null, deleteView);
+                path.add(id);
+              } else {
+                throw e;
+              }
+            } else {
+              throw e;
+            }
+          }
+          if (viewData.view == null) {
+            viewData = view(rc, req.getMethod(), path);
+          }
+        }
+        checkRequiresCapability(viewData);
 
-          viewData = new ViewData(null, null);
+        while (viewData.view instanceof RestCollection<?, ?>) {
+          @SuppressWarnings("unchecked")
+          RestCollection<RestResource, RestResource> c =
+              (RestCollection<RestResource, RestResource>) viewData.view;
 
           if (path.isEmpty()) {
-            globals.quotaChecker.enforce(req);
-            if (rc instanceof NeedsParams) {
-              ((NeedsParams) rc).setParams(qp.params());
-            }
-
             if (isRead(req)) {
-              viewData = new ViewData(null, rc.list());
+              viewData = new ViewData(null, c.list());
             } else if (isPost(req)) {
+              // TODO: Here and on other collection methods: There is a bug that binds child views
+              // with pluginName="gerrit" instead of the real plugin name. This has never worked
+              // correctly and should be fixed where the binding gets created (DynamicMapProvider)
+              // and here.
               RestView<RestResource> restCollectionView =
-                  rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
+                  c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
+              if (restCollectionView != null) {
+                viewData = new ViewData(null, restCollectionView);
+              } else {
+                throw methodNotAllowed(req);
+              }
+            } else if (isDelete(req)) {
+              RestView<RestResource> restCollectionView =
+                  c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
               if (restCollectionView != null) {
                 viewData = new ViewData(null, restCollectionView);
               } else {
                 throw methodNotAllowed(req);
               }
             } else {
-              // DELETE on root collections is not supported
               throw methodNotAllowed(req);
             }
-          } else {
-            IdString id = path.remove(0);
-            try {
-              rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, rc, rsrc, id);
-              globals.quotaChecker.enforce(rsrc, req);
-              if (path.isEmpty()) {
-                checkPreconditions(req);
-              }
-            } catch (ResourceNotFoundException e) {
-              if (!path.isEmpty()) {
-                throw e;
-              }
-              globals.quotaChecker.enforce(req);
+            break;
+          }
+          IdString id = path.remove(0);
+          try {
+            rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, c, rsrc, id);
+            checkPreconditions(req);
+            viewData = new ViewData(null, null);
+          } catch (ResourceNotFoundException e) {
+            if (!path.isEmpty()) {
+              throw e;
+            }
 
-              if (isPost(req) || isPut(req)) {
-                RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
-                if (createView != null) {
-                  viewData = new ViewData(null, createView);
-                  path.add(id);
-                } else {
-                  throw e;
-                }
-              } else if (isDelete(req)) {
-                RestView<RestResource> deleteView =
-                    rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
-                if (deleteView != null) {
-                  viewData = new ViewData(null, deleteView);
-                  path.add(id);
-                } else {
-                  throw e;
-                }
+            if (isPost(req) || isPut(req)) {
+              RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
+              if (createView != null) {
+                viewData = new ViewData(viewData.pluginName, createView);
+                path.add(id);
               } else {
                 throw e;
               }
+            } else if (isDelete(req)) {
+              RestView<RestResource> deleteView =
+                  c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
+              if (deleteView != null) {
+                viewData = new ViewData(viewData.pluginName, deleteView);
+                path.add(id);
+              } else {
+                throw e;
+              }
+            } else {
+              throw e;
             }
-            if (viewData.view == null) {
-              viewData = view(rc, req.getMethod(), path);
-            }
+          }
+          if (viewData.view == null) {
+            viewData = view(c, req.getMethod(), path);
           }
           checkRequiresCapability(viewData);
+        }
 
-          while (viewData.view instanceof RestCollection<?, ?>) {
-            @SuppressWarnings("unchecked")
-            RestCollection<RestResource, RestResource> c =
-                (RestCollection<RestResource, RestResource>) viewData.view;
+        if (notModified(req, rsrc)) {
+          logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED);
+          res.sendError(SC_NOT_MODIFIED);
+          return;
+        }
 
-            if (path.isEmpty()) {
-              if (isRead(req)) {
-                viewData = new ViewData(null, c.list());
-              } else if (isPost(req)) {
-                // TODO: Here and on other collection methods: There is a bug that binds child views
-                // with pluginName="gerrit" instead of the real plugin name. This has never worked
-                // correctly and should be fixed where the binding gets created (DynamicMapProvider)
-                // and here.
-                RestView<RestResource> restCollectionView =
-                    c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
-                if (restCollectionView != null) {
-                  viewData = new ViewData(null, restCollectionView);
-                } else {
-                  throw methodNotAllowed(req);
-                }
-              } else if (isDelete(req)) {
-                RestView<RestResource> restCollectionView =
-                    c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
-                if (restCollectionView != null) {
-                  viewData = new ViewData(null, restCollectionView);
-                } else {
-                  throw methodNotAllowed(req);
-                }
-              } else {
-                throw methodNotAllowed(req);
-              }
-              break;
-            }
-            IdString id = path.remove(0);
-            try {
-              rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, c, rsrc, id);
-              checkPreconditions(req);
-              viewData = new ViewData(null, null);
-            } catch (ResourceNotFoundException e) {
-              if (!path.isEmpty()) {
-                throw e;
-              }
-
-              if (isPost(req) || isPut(req)) {
-                RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
-                if (createView != null) {
-                  viewData = new ViewData(viewData.pluginName, createView);
-                  path.add(id);
-                } else {
-                  throw e;
-                }
-              } else if (isDelete(req)) {
-                RestView<RestResource> deleteView =
-                    c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
-                if (deleteView != null) {
-                  viewData = new ViewData(viewData.pluginName, deleteView);
-                  path.add(id);
-                } else {
-                  throw e;
-                }
-              } else {
-                throw e;
-              }
-            }
-            if (viewData.view == null) {
-              viewData = view(c, req.getMethod(), path);
-            }
-            checkRequiresCapability(viewData);
-          }
-
-          if (notModified(req, rsrc)) {
-            logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED);
-            res.sendError(SC_NOT_MODIFIED);
+        try (DynamicOptions pluginOptions =
+            new DynamicOptions(globals.injector, globals.dynamicBeans)) {
+          if (!globals
+              .paramParser
+              .get()
+              .parse(viewData.view, pluginOptions, qp.params(), req, res)) {
             return;
           }
 
-          try (DynamicOptions pluginOptions =
-              new DynamicOptions(globals.injector, globals.dynamicBeans)) {
-            if (!globals
-                .paramParser
-                .get()
-                .parse(viewData.view, pluginOptions, qp.params(), req, res)) {
-              return;
-            }
+          if (viewData.view instanceof RestReadView<?> && isRead(req)) {
+            response =
+                invokeRestReadViewWithRetry(
+                    req, traceContext, viewData, (RestReadView<RestResource>) viewData.view, rsrc);
+          } else if (viewData.view instanceof RestModifyView<?, ?>) {
+            RestModifyView<RestResource, Object> m =
+                (RestModifyView<RestResource, Object>) viewData.view;
 
-            if (viewData.view instanceof RestReadView<?> && isRead(req)) {
-              response =
-                  invokeRestReadViewWithRetry(
-                      req,
-                      traceContext,
-                      viewData,
-                      (RestReadView<RestResource>) viewData.view,
-                      rsrc);
-            } else if (viewData.view instanceof RestModifyView<?, ?>) {
-              RestModifyView<RestResource, Object> m =
-                  (RestModifyView<RestResource, Object>) viewData.view;
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response =
+                invokeRestModifyViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, inputRequestBody);
 
-              Type type = inputType(m);
-              inputRequestBody = parseRequest(req, type);
-              response =
-                  invokeRestModifyViewWithRetry(
-                      req, traceContext, viewData, m, rsrc, inputRequestBody);
-
-              if (inputRequestBody instanceof RawInput) {
-                try (InputStream is = req.getInputStream()) {
-                  ServletUtils.consumeRequestBody(is);
-                }
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
               }
-            } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
-              RestCollectionCreateView<RestResource, RestResource, Object> m =
-                  (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
+            }
+          } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
+            RestCollectionCreateView<RestResource, RestResource, Object> m =
+                (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
 
-              Type type = inputType(m);
-              inputRequestBody = parseRequest(req, type);
-              response =
-                  invokeRestCollectionCreateViewWithRetry(
-                      req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
-              if (inputRequestBody instanceof RawInput) {
-                try (InputStream is = req.getInputStream()) {
-                  ServletUtils.consumeRequestBody(is);
-                }
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response =
+                invokeRestCollectionCreateViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
               }
-            } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
-              RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
-                  (RestCollectionDeleteMissingView<RestResource, RestResource, Object>)
-                      viewData.view;
+            }
+          } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
+            RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
+                (RestCollectionDeleteMissingView<RestResource, RestResource, Object>) viewData.view;
 
-              Type type = inputType(m);
-              inputRequestBody = parseRequest(req, type);
-              response =
-                  invokeRestCollectionDeleteMissingViewWithRetry(
-                      req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
-              if (inputRequestBody instanceof RawInput) {
-                try (InputStream is = req.getInputStream()) {
-                  ServletUtils.consumeRequestBody(is);
-                }
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response =
+                invokeRestCollectionDeleteMissingViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
               }
-            } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
-              RestCollectionModifyView<RestResource, RestResource, Object> m =
-                  (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
+            }
+          } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
+            RestCollectionModifyView<RestResource, RestResource, Object> m =
+                (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
 
-              Type type = inputType(m);
-              inputRequestBody = parseRequest(req, type);
-              response =
-                  invokeRestCollectionModifyViewWithRetry(
-                      req, traceContext, viewData, m, rsrc, inputRequestBody);
-              if (inputRequestBody instanceof RawInput) {
-                try (InputStream is = req.getInputStream()) {
-                  ServletUtils.consumeRequestBody(is);
-                }
+            Type type = inputType(m);
+            inputRequestBody = parseRequest(req, type);
+            response =
+                invokeRestCollectionModifyViewWithRetry(
+                    req, traceContext, viewData, m, rsrc, inputRequestBody);
+            if (inputRequestBody instanceof RawInput) {
+              try (InputStream is = req.getInputStream()) {
+                ServletUtils.consumeRequestBody(is);
               }
-            } else {
-              throw new ResourceNotFoundException();
-            }
-            String isUpdatedRefEnabled = req.getHeader(X_GERRIT_UPDATED_REF_ENABLED);
-            if (!Strings.isNullOrEmpty(isUpdatedRefEnabled)
-                && Boolean.valueOf(isUpdatedRefEnabled)) {
-              setXGerritUpdatedRefResponseHeaders(req, res);
-            }
-
-            if (response instanceof Response.Redirect) {
-              CacheHeaders.setNotCacheable(res);
-              String location = ((Response.Redirect) response).location();
-              res.sendRedirect(location);
-              logger.atFinest().log("REST call redirected to: %s", location);
-              return;
-            } else if (response instanceof Response.Accepted) {
-              CacheHeaders.setNotCacheable(res);
-              res.setStatus(response.statusCode());
-              res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
-              logger.atFinest().log("REST call succeeded: %d", response.statusCode());
-              return;
-            }
-
-            statusCode = response.statusCode();
-            response.headers().forEach((k, v) -> res.setHeader(k, v));
-            configureCaching(req, res, rsrc, response.caching());
-            res.setStatus(statusCode);
-            logger.atFinest().log("REST call succeeded: %d", statusCode);
-          }
-
-          if (response != Response.none()) {
-            Object value = Response.unwrap(response);
-            if (value instanceof BinaryResult) {
-              responseBytes = replyBinaryResult(req, res, (BinaryResult) value);
-            } else {
-              responseBytes = replyJson(req, res, false, qp.config(), value);
-            }
-          }
-        }
-      } catch (MalformedJsonException | JsonParseException e) {
-        cause = Optional.of(e);
-        logger.atFine().withCause(e).log("REST call failed on JSON parsing");
-        responseBytes =
-            replyError(
-                req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
-      } catch (BadRequestException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
-      } catch (AuthException e) {
-        cause = Optional.of(e);
-
-        StringBuilder messageBuilder = new StringBuilder(messageOr(e, "Forbidden"));
-        globals
-            .aclInfoController
-            .getAclInfoMessage()
-            .ifPresent(aclInfo -> messageBuilder.append("\n\n").append(aclInfo));
-
-        responseBytes =
-            replyError(
-                req, res, statusCode = SC_FORBIDDEN, messageBuilder.toString(), e.caching(), e);
-      } catch (AmbiguousViewException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
-      } catch (ResourceNotFoundException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
-      } catch (MethodNotAllowedException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req,
-                res,
-                statusCode = SC_METHOD_NOT_ALLOWED,
-                messageOr(e, "Method Not Allowed"),
-                e.caching(),
-                e);
-      } catch (ResourceConflictException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req, res, statusCode = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
-      } catch (PreconditionFailedException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req,
-                res,
-                statusCode = SC_PRECONDITION_FAILED,
-                messageOr(e, "Precondition Failed"),
-                e.caching(),
-                e);
-      } catch (UnprocessableEntityException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req,
-                res,
-                statusCode = SC_UNPROCESSABLE_ENTITY,
-                messageOr(e, "Unprocessable Entity"),
-                e.caching(),
-                e);
-      } catch (NotImplementedException e) {
-        cause = Optional.of(e);
-        logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
-        responseBytes =
-            replyError(
-                req, res, statusCode = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
-      } catch (QuotaException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(
-                req,
-                res,
-                statusCode = SC_TOO_MANY_REQUESTS,
-                messageOr(e, "Quota limit reached"),
-                e.caching(),
-                e);
-      } catch (InvalidDeadlineException e) {
-        cause = Optional.of(e);
-        responseBytes =
-            replyError(req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e);
-      } catch (Exception e) {
-        cause = Optional.of(e);
-
-        Optional<RequestCancelledException> requestCancelledException =
-            RequestCancelledException.getFromCausalChain(e);
-        if (requestCancelledException.isPresent()) {
-          RequestStateProvider.Reason cancellationReason =
-              requestCancelledException.get().getCancellationReason();
-          globals.cancellationMetrics.countCancelledRequest(
-              RequestInfo.RequestType.REST, requestUri, cancellationReason);
-          statusCode = getCancellationStatusCode(cancellationReason);
-          responseBytes =
-              replyError(
-                  req, res, statusCode, getCancellationMessage(requestCancelledException.get()), e);
-        } else {
-          statusCode = SC_INTERNAL_SERVER_ERROR;
-
-          Optional<ExceptionHook.Status> status = getStatus(e);
-          statusCode =
-              status.map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
-
-          if (res.isCommitted()) {
-            responseBytes = 0;
-            if (statusCode == SC_INTERNAL_SERVER_ERROR) {
-              logger.atSevere().withCause(e).log(
-                  "Error in %s %s, response already committed",
-                  req.getMethod(), uriForLogging(req));
-            } else {
-              logger.atWarning().log(
-                  "Response for %s %s already committed, wanted to set status %d",
-                  req.getMethod(), uriForLogging(req), statusCode);
             }
           } else {
-            res.reset();
-            TraceContext.getTraceIds().forEach(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+            throw new ResourceNotFoundException();
+          }
+          String isUpdatedRefEnabled = req.getHeader(X_GERRIT_UPDATED_REF_ENABLED);
+          if (!Strings.isNullOrEmpty(isUpdatedRefEnabled) && Boolean.valueOf(isUpdatedRefEnabled)) {
+            setXGerritUpdatedRefResponseHeaders(req, res);
+          }
 
-            if (status.isPresent()) {
-              responseBytes = reply(req, res, e, status.get(), getUserMessages(e));
-            } else {
-              responseBytes =
-                  replyInternalServerError(req, res, e, getViewName(viewData), getUserMessages(e));
-            }
+          if (response instanceof Response.Redirect) {
+            CacheHeaders.setNotCacheable(res);
+            String location = ((Response.Redirect) response).location();
+            res.sendRedirect(location);
+            logger.atFinest().log("REST call redirected to: %s", location);
+            return;
+          } else if (response instanceof Response.Accepted) {
+            CacheHeaders.setNotCacheable(res);
+            res.setStatus(response.statusCode());
+            res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
+            logger.atFinest().log("REST call succeeded: %d", response.statusCode());
+            return;
+          }
+
+          statusCode = response.statusCode();
+          response.headers().forEach((k, v) -> res.setHeader(k, v));
+          configureCaching(req, res, rsrc, response.caching());
+          res.setStatus(statusCode);
+          logger.atFinest().log("REST call succeeded: %d", statusCode);
+        }
+
+        if (response != Response.none()) {
+          Object value = Response.unwrap(response);
+          if (value instanceof BinaryResult) {
+            responseBytes = replyBinaryResult(req, res, (BinaryResult) value);
+          } else {
+            responseBytes = replyJson(req, res, false, qp.config(), value);
           }
         }
-      } finally {
-        String metric = getViewName(viewData);
-        String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none");
-        globals.metrics.count.increment(metric);
-        if (statusCode >= SC_BAD_REQUEST) {
-          globals.metrics.errorCount.increment(metric, statusCode, formattedCause);
-        }
-        if (responseBytes != -1) {
-          globals.metrics.responseBytes.record(metric, responseBytes);
-        }
-        globals.metrics.serverLatency.record(
-            metric, System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
-        globals.auditService.dispatch(
-            new ExtendedHttpAuditEvent(
-                sessionId,
-                currentUser,
-                req,
-                auditStartTs,
-                qp != null ? qp.params() : ImmutableListMultimap.of(),
-                inputRequestBody,
-                statusCode,
-                response,
-                rsrc,
-                viewData == null ? null : viewData.view));
       }
+    } catch (MalformedJsonException | JsonParseException e) {
+      cause = Optional.of(e);
+      logger.atFine().withCause(e).log("REST call failed on JSON parsing");
+      responseBytes =
+          replyError(
+              req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
+    } catch (BadRequestException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(
+              req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
+    } catch (AuthException e) {
+      cause = Optional.of(e);
+
+      StringBuilder messageBuilder = new StringBuilder(messageOr(e, "Forbidden"));
+      globals
+          .aclInfoController
+          .getAclInfoMessage()
+          .ifPresent(aclInfo -> messageBuilder.append("\n\n").append(aclInfo));
+
+      responseBytes =
+          replyError(
+              req, res, statusCode = SC_FORBIDDEN, messageBuilder.toString(), e.caching(), e);
+    } catch (AmbiguousViewException e) {
+      cause = Optional.of(e);
+      responseBytes = replyError(req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
+    } catch (ResourceNotFoundException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(
+              req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
+    } catch (MethodNotAllowedException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(
+              req,
+              res,
+              statusCode = SC_METHOD_NOT_ALLOWED,
+              messageOr(e, "Method Not Allowed"),
+              e.caching(),
+              e);
+    } catch (ResourceConflictException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(req, res, statusCode = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
+    } catch (PreconditionFailedException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(
+              req,
+              res,
+              statusCode = SC_PRECONDITION_FAILED,
+              messageOr(e, "Precondition Failed"),
+              e.caching(),
+              e);
+    } catch (UnprocessableEntityException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(
+              req,
+              res,
+              statusCode = SC_UNPROCESSABLE_ENTITY,
+              messageOr(e, "Unprocessable Entity"),
+              e.caching(),
+              e);
+    } catch (NotImplementedException e) {
+      cause = Optional.of(e);
+      logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
+      responseBytes =
+          replyError(req, res, statusCode = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
+    } catch (QuotaException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(
+              req,
+              res,
+              statusCode = SC_TOO_MANY_REQUESTS,
+              messageOr(e, "Quota limit reached"),
+              e.caching(),
+              e);
+    } catch (InvalidDeadlineException e) {
+      cause = Optional.of(e);
+      responseBytes =
+          replyError(req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e);
+    } catch (Exception e) {
+      cause = Optional.of(e);
+
+      Optional<RequestCancelledException> requestCancelledException =
+          RequestCancelledException.getFromCausalChain(e);
+      if (requestCancelledException.isPresent()) {
+        RequestStateProvider.Reason cancellationReason =
+            requestCancelledException.get().getCancellationReason();
+        globals.cancellationMetrics.countCancelledRequest(
+            RequestInfo.RequestType.REST, requestUri, cancellationReason);
+        statusCode = getCancellationStatusCode(cancellationReason);
+        responseBytes =
+            replyError(
+                req, res, statusCode, getCancellationMessage(requestCancelledException.get()), e);
+      } else {
+        statusCode = SC_INTERNAL_SERVER_ERROR;
+
+        Optional<ExceptionHook.Status> status = getStatus(e);
+        statusCode = status.map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
+
+        if (res.isCommitted()) {
+          responseBytes = 0;
+          if (statusCode == SC_INTERNAL_SERVER_ERROR) {
+            logger.atSevere().withCause(e).log(
+                "Error in %s %s, response already committed", req.getMethod(), uriForLogging(req));
+          } else {
+            logger.atWarning().log(
+                "Response for %s %s already committed, wanted to set status %d",
+                req.getMethod(), uriForLogging(req), statusCode);
+          }
+        } else {
+          res.reset();
+          TraceContext.getTraceIds().forEach(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+
+          if (status.isPresent()) {
+            responseBytes = reply(req, res, e, status.get(), getUserMessages(e));
+          } else {
+            responseBytes =
+                replyInternalServerError(req, res, e, getViewName(viewData), getUserMessages(e));
+          }
+        }
+      }
+    } finally {
+      String metric = getViewName(viewData);
+      String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none");
+      globals.metrics.count.increment(metric);
+      if (statusCode >= SC_BAD_REQUEST) {
+        globals.metrics.errorCount.increment(metric, statusCode, formattedCause);
+      }
+      if (responseBytes != -1) {
+        globals.metrics.responseBytes.record(metric, responseBytes);
+      }
+      globals.metrics.serverLatency.record(
+          metric, System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
+      globals.auditService.dispatch(
+          new ExtendedHttpAuditEvent(
+              sessionId,
+              currentUser,
+              req,
+              auditStartTs,
+              qp != null ? qp.params() : ImmutableListMultimap.of(),
+              inputRequestBody,
+              statusCode,
+              response,
+              rsrc,
+              viewData == null ? null : viewData.view));
     }
   }
 
@@ -1575,43 +1568,6 @@
     return parameterNames;
   }
 
-  private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
-    // There are 2 ways to enable tracing for REST calls:
-    // 1. by using the 'trace' or 'trace=<trace-id>' request parameter
-    // 2. by setting the 'X-Gerrit-Trace:' or 'X-Gerrit-Trace:<trace-id>' header
-    String traceValueFromHeader = req.getHeader(X_GERRIT_TRACE);
-    String traceValueFromRequestParam = req.getParameter(ParameterParser.TRACE_PARAMETER);
-    boolean forceLogging = traceValueFromHeader != null || traceValueFromRequestParam != null;
-
-    // Check whether no trace ID, one trace ID or 2 different trace IDs have been specified.
-    String traceId1;
-    String traceId2;
-    if (!Strings.isNullOrEmpty(traceValueFromHeader)) {
-      traceId1 = traceValueFromHeader;
-      if (!Strings.isNullOrEmpty(traceValueFromRequestParam)
-          && !traceValueFromHeader.equals(traceValueFromRequestParam)) {
-        traceId2 = traceValueFromRequestParam;
-      } else {
-        traceId2 = null;
-      }
-    } else {
-      traceId1 = Strings.emptyToNull(traceValueFromRequestParam);
-      traceId2 = null;
-    }
-
-    // Use the first trace ID to start tracing. If this trace ID is null, a trace ID will be
-    // generated.
-    TraceContext traceContext =
-        TraceContext.newTrace(
-            forceLogging, traceId1, (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId));
-    // If a second trace ID was specified, add a tag for it as well.
-    if (traceId2 != null) {
-      traceContext.addTag(RequestId.Type.TRACE_ID, traceId2);
-      res.addHeader(X_GERRIT_TRACE, traceId2);
-    }
-    return traceContext;
-  }
-
   private RequestInfo createRequestInfo(
       TraceContext traceContext, HttpServletRequest req, String requestUri, List<IdString> path) {
     RequestInfo.Builder requestInfo =
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 55e79f3..53f4af9 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -717,7 +717,7 @@
       } else if ("jar".equals(u.getProtocol())) {
         String p = u.getPath();
         try {
-          u = new URL(p.substring(0, p.indexOf('!')));
+          u = URI.create(p.substring(0, p.indexOf('!'))).toURL();
         } catch (MalformedURLException e) {
           FileNotFoundException fnfe = new FileNotFoundException("Not a valid jar file: " + u);
           fnfe.initCause(e);
diff --git a/java/com/google/gerrit/pgm/util/ProxyUtil.java b/java/com/google/gerrit/pgm/util/ProxyUtil.java
index c2c1141..c765f95 100644
--- a/java/com/google/gerrit/pgm/util/ProxyUtil.java
+++ b/java/com/google/gerrit/pgm/util/ProxyUtil.java
@@ -39,6 +39,7 @@
 
 import com.google.common.base.Strings;
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import org.eclipse.jgit.util.CachedAuthenticator;
 
@@ -59,7 +60,7 @@
       return;
     }
 
-    final URL u = new URL(!s.contains("://") ? "http://" + s : s);
+    final URL u = URI.create(!s.contains("://") ? "http://" + s : s).toURL();
     if (!"http".equals(u.getProtocol())) {
       throw new MalformedURLException("Invalid http_proxy: " + s + ": Only http supported.");
     }
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
index 3604d2b..501843c 100644
--- a/java/com/google/gerrit/server/ExceptionHook.java
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -138,8 +138,8 @@
    * <p>If multiple exception hooks return a value from this method, the value from exception hook
    * that is registered first is used.
    *
-   * <p>{@link #getUserMessages(Throwable, ImmutableSet<String>)} allows to define which additional
-   * messages should be included into the body of the HTTP response.
+   * <p>{@link #getUserMessages(Throwable, ImmutableSet)} allows to define which additional messages
+   * should be included into the body of the HTTP response.
    *
    * @param throwable throwable that was thrown while executing an operation
    * @return HTTP status that should be returned to the user, {@link Optional#empty()} if the
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index b069e39..d5cb87d 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -44,7 +44,7 @@
 import com.google.inject.util.Providers;
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
-import java.net.URL;
+import java.net.URI;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.util.Optional;
@@ -474,7 +474,7 @@
       String host;
       if (canonicalUrl.get() != null) {
         try {
-          host = new URL(canonicalUrl.get()).getHost();
+          host = URI.create(canonicalUrl.get()).toURL().getHost();
         } catch (MalformedURLException e) {
           host = SystemReader.getInstance().getHostname();
         }
diff --git a/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java b/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java
index b2e80d7..9dfd049 100644
--- a/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java
+++ b/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java
@@ -19,7 +19,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.SystemReader;
 
@@ -51,7 +51,7 @@
   private static String extractInstanceName(String canonicalUrl) {
     if (canonicalUrl != null) {
       try {
-        return new URL(canonicalUrl).getHost();
+        return URI.create(canonicalUrl).toURL().getHost();
       } catch (MalformedURLException e) {
         // Try something else.
       }
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index edb3bb3..a74e551 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -38,6 +38,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import org.eclipse.jgit.lib.Config;
 
@@ -203,7 +204,7 @@
       } else {
         String baseGerritUrl;
         if (gerritUrl != null) {
-          URL u = new URL(gerritUrl);
+          URL u = URI.create(gerritUrl).toURL();
           baseGerritUrl = u.getPath();
         } else {
           baseGerritUrl = "/";
diff --git a/java/com/google/gerrit/server/git/DelegateRefDatabase.java b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
index 3dbc881..a8b1bb9 100644
--- a/java/com/google/gerrit/server/git/DelegateRefDatabase.java
+++ b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
@@ -114,13 +114,13 @@
 
   @Override
   public ReflogReader getReflogReader(String refName) throws IOException {
-    return delegate.getReflogReader(refName);
+    return delegate.getRefDatabase().getReflogReader(refName);
   }
 
   @Override
   @NonNull
   public ReflogReader getReflogReader(@NonNull Ref ref) throws IOException {
-    return delegate.getReflogReader(ref);
+    return delegate.getRefDatabase().getReflogReader(ref);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
index 8cedc89..ce166ef 100644
--- a/java/com/google/gerrit/server/git/DelegateRepository.java
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -112,7 +112,7 @@
 
   @Override
   public ReflogReader getReflogReader(String refName) throws IOException {
-    return delegate.getReflogReader(refName);
+    return delegate.getRefDatabase().getReflogReader(refName);
   }
 
   @SuppressWarnings("rawtypes")
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 9696e12..392f2ae 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -74,7 +74,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -991,7 +991,7 @@
   private static String getGerritHost(String canonicalWebUrl) {
     if (canonicalWebUrl != null) {
       try {
-        return new URL(canonicalWebUrl).getHost();
+        return URI.create(canonicalWebUrl).toURL().getHost();
       } catch (MalformedURLException ignored) {
         logger.atWarning().log(
             "configured canonical web URL is invalid, using system default: %s",
diff --git a/java/com/google/gerrit/server/mail/EmailFactories.java b/java/com/google/gerrit/server/mail/EmailFactories.java
index 6b0bcf0..cb9d541 100644
--- a/java/com/google/gerrit/server/mail/EmailFactories.java
+++ b/java/com/google/gerrit/server/mail/EmailFactories.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.extensions.client.ChangeKind;
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index d0d380a..132fdc9 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -46,7 +46,7 @@
 import com.google.template.soy.data.SanitizedContent;
 import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -448,7 +448,7 @@
     Optional<String> gerritUrl = args.urlFormatter.get().getWebUrl();
     if (gerritUrl.isPresent()) {
       try {
-        return new URL(gerritUrl.get()).getHost();
+        return URI.create(gerritUrl.get()).toURL().getHost();
       } catch (MalformedURLException e) {
         // Try something else.
       }
diff --git a/java/com/google/gerrit/server/plugins/InstallPlugin.java b/java/com/google/gerrit/server/plugins/InstallPlugin.java
index a79a5a6..a1884db 100644
--- a/java/com/google/gerrit/server/plugins/InstallPlugin.java
+++ b/java/com/google/gerrit/server/plugins/InstallPlugin.java
@@ -32,7 +32,7 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.net.URL;
+import java.net.URI;
 import java.util.zip.ZipException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@@ -88,7 +88,7 @@
       return input.raw.getInputStream();
     }
     try {
-      return new URL(input.url).openStream();
+      return URI.create(input.url).toURL().openStream();
     } catch (IOException e) {
       throw new BadRequestException(e.getMessage());
     }
diff --git a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index 8794f66..4fec2ab 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -45,32 +45,35 @@
   public void start() {
     int cpus = Runtime.getRuntime().availableProcessors();
     if (config.getBoolean("cache", "projects", "loadOnStartup", false)) {
-      ExecutorService pool =
-          new LoggingContextAwareExecutorService(
-              new ScheduledThreadPoolExecutor(
-                  config.getInt("cache", "projects", "loadThreads", cpus),
-                  new ThreadFactoryBuilder().setNameFormat("ProjectCacheLoader-%d").build()));
       Thread scheduler =
           new Thread(
               () -> {
-                for (Project.NameKey name : cache.all()) {
-                  pool.execute(
-                      () -> {
-                        Optional<ProjectState> project = cache.get(name);
-                        if (!project.isPresent()) {
-                          throw new IllegalStateException(
-                              "race while traversing projects. got "
-                                  + name
-                                  + " when loading all projects, but can't load it now");
-                        }
-                      });
-                }
-                pool.shutdown();
-                try {
-                  pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
-                  logger.atInfo().log("Finished loading project cache");
-                } catch (InterruptedException e) {
-                  logger.atWarning().log("Interrupted while waiting for project cache to load");
+                try (ExecutorService pool =
+                    new LoggingContextAwareExecutorService(
+                        new ScheduledThreadPoolExecutor(
+                            config.getInt("cache", "projects", "loadThreads", cpus),
+                            new ThreadFactoryBuilder()
+                                .setNameFormat("ProjectCacheLoader-%d")
+                                .build()))) {
+                  for (Project.NameKey name : cache.all()) {
+                    pool.execute(
+                        () -> {
+                          Optional<ProjectState> project = cache.get(name);
+                          if (!project.isPresent()) {
+                            throw new IllegalStateException(
+                                "race while traversing projects. got "
+                                    + name
+                                    + " when loading all projects, but can't load it now");
+                          }
+                        });
+                  }
+                  pool.shutdown();
+                  try {
+                    pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+                    logger.atInfo().log("Finished loading project cache");
+                  } catch (InterruptedException e) {
+                    logger.atWarning().log("Interrupted while waiting for project cache to load");
+                  }
                 }
               });
       scheduler.setName("ProjectCacheWarmer");
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java b/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
index be1bced..a8e46b3 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
@@ -140,9 +140,9 @@
    *
    * <p>The method creates CommitModification by applying {@code fixReplacements} to the {@code
    * basePatchSetForFix}. If the {@code targetPatchSetForFix} is different from the {@code
-   * basePatchSetForFix}, CommitModification is created from the {@link PatchApplier.Result}, after
-   * applying the patch generated from {@code basePatchSetForFix} to the {@code
-   * targetPatchSetForFix}.
+   * basePatchSetForFix}, CommitModification is created from the {@link
+   * org.eclipse.jgit.patch.PatchApplier.Result}, after applying the patch generated from {@code
+   * basePatchSetForFix} to the {@code targetPatchSetForFix}.
    *
    * <p>Note: if there is a fix for a commit message and commit messages are different in {@code
    * basePatchSetForFix} and {@code targetPatchSetForFix}, the method can't move the fix to the
diff --git a/java/com/google/gerrit/server/restapi/project/GetReflog.java b/java/com/google/gerrit/server/restapi/project/GetReflog.java
index 967b3c5..6016abc 100644
--- a/java/com/google/gerrit/server/restapi/project/GetReflog.java
+++ b/java/com/google/gerrit/server/restapi/project/GetReflog.java
@@ -100,7 +100,7 @@
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       ReflogReader r;
       try {
-        r = repo.getReflogReader(rsrc.getRef());
+        r = repo.getRefDatabase().getReflogReader(rsrc.getRef());
       } catch (UnsupportedOperationException e) {
         String msg = "reflog not supported on repo " + rsrc.getNameKey().get();
         logger.atSevere().log("%s", msg);
diff --git a/java/com/google/gerrit/sshd/NoShell.java b/java/com/google/gerrit/sshd/NoShell.java
index ffac946..152b272 100644
--- a/java/com/google/gerrit/sshd/NoShell.java
+++ b/java/com/google/gerrit/sshd/NoShell.java
@@ -26,7 +26,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import org.apache.sshd.common.io.IoInputStream;
 import org.apache.sshd.common.io.IoOutputStream;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
@@ -209,7 +209,7 @@
       String url = urlProvider.get();
       if (url != null) {
         try {
-          return new URL(url).getHost();
+          return URI.create(url).toURL().getHost();
         } catch (MalformedURLException e) {
           // Ignored
         }
diff --git a/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index cfb47f7..5d2bb4b 100644
--- a/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -23,7 +23,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import java.nio.file.Files;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
@@ -74,7 +74,7 @@
       }
     } else {
       try {
-        data = new URL(source).openStream();
+        data = URI.create(source).toURL().openStream();
       } catch (MalformedURLException e) {
         throw die("invalid url " + source, e);
       } catch (IOException e) {
diff --git a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index 8395772..add21c2 100644
--- a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
 import org.apache.log4j.Level;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
@@ -82,7 +82,7 @@
     if (Strings.isNullOrEmpty(path)) {
       PropertyConfigurator.configure(Loader.getResource(LOG_CONFIGURATION));
     } else {
-      PropertyConfigurator.configure(new URL(path));
+      PropertyConfigurator.configure(URI.create(path).toURL());
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/GitRepositoryReferenceCountingManagerIT.java b/javatests/com/google/gerrit/acceptance/GitRepositoryReferenceCountingManagerIT.java
index 3108238..b7da25c 100644
--- a/javatests/com/google/gerrit/acceptance/GitRepositoryReferenceCountingManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/GitRepositoryReferenceCountingManagerIT.java
@@ -34,11 +34,11 @@
     }
   }
 
-  @Test(expected = AssertionError.class)
+  @Test()
   @SuppressWarnings("resource")
   public void shouldFailTestWhenRepositoryIsLeftOpen() throws Exception {
     Repository unused = repoManager.openRepository(project);
-    afterTest();
+    assertThrows(AssertionError.class, this::afterTest);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
index 7b21780..b0817fe 100644
--- a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -26,7 +26,7 @@
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.testing.FakeEmailSender;
-import java.net.URL;
+import java.net.URI;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
@@ -135,6 +135,6 @@
   // Each message-id must start with '<' and end with '>'. Also, it must contain no spaces and it
   // must contain a '@'.
   private String withPrefixAndSuffixForMessageId(String id) throws Exception {
-    return "<" + id + "@" + new URL(canonicalWebUrl.get()).getHost() + ">";
+    return "<" + id + "@" + URI.create(canonicalWebUrl.get()).toURL().getHost() + ">";
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
index b174d79..68d5a75 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
@@ -61,6 +61,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -1070,15 +1071,16 @@
       gApi.changes().id(changeToBeRebased2.get()).rebaseChain(rebaseInput);
 
       // The ref log for the patch set ref records the impersonated user aka the uploader.
-      ReflogEntry patchSetRefLogEntry1 = repo.getReflogReader(patchSetRef1).getLastEntry();
+      RefDatabase refDb = repo.getRefDatabase();
+      ReflogEntry patchSetRefLogEntry1 = refDb.getReflogReader(patchSetRef1).getLastEntry();
       assertThat(patchSetRefLogEntry1.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
-      ReflogEntry patchSetRefLogEntry2 = repo.getReflogReader(patchSetRef2).getLastEntry();
+      ReflogEntry patchSetRefLogEntry2 = refDb.getReflogReader(patchSetRef2).getLastEntry();
       assertThat(patchSetRefLogEntry2.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
 
       // The ref log for the change meta ref records the impersonated user aka the uploader.
-      ReflogEntry changeMetaRefLogEntry1 = repo.getReflogReader(changeMetaRef1).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry1 = refDb.getReflogReader(changeMetaRef1).getLastEntry();
       assertThat(changeMetaRefLogEntry1.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
-      ReflogEntry changeMetaRefLogEntry2 = repo.getReflogReader(changeMetaRef2).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry2 = refDb.getReflogReader(changeMetaRef2).getLastEntry();
       assertThat(changeMetaRefLogEntry2.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
     }
   }
@@ -1132,15 +1134,16 @@
       String combinedEmail = String.format("account-%s|account-%s@unknown", uploader1, uploader2);
 
       // The ref log for the patch set ref records the impersonated user aka the uploader.
-      ReflogEntry patchSetRefLogEntry1 = repo.getReflogReader(patchSetRef1).getLastEntry();
+      RefDatabase refDb = repo.getRefDatabase();
+      ReflogEntry patchSetRefLogEntry1 = refDb.getReflogReader(patchSetRef1).getLastEntry();
       assertThat(patchSetRefLogEntry1.getWho().getEmailAddress()).isEqualTo(combinedEmail);
-      ReflogEntry patchSetRefLogEntry2 = repo.getReflogReader(patchSetRef2).getLastEntry();
+      ReflogEntry patchSetRefLogEntry2 = refDb.getReflogReader(patchSetRef2).getLastEntry();
       assertThat(patchSetRefLogEntry2.getWho().getEmailAddress()).isEqualTo(combinedEmail);
 
       // The ref log for the change meta ref records the impersonated user aka the uploader.
-      ReflogEntry changeMetaRefLogEntry1 = repo.getReflogReader(changeMetaRef1).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry1 = refDb.getReflogReader(changeMetaRef1).getLastEntry();
       assertThat(changeMetaRefLogEntry1.getWho().getEmailAddress()).isEqualTo(combinedEmail);
-      ReflogEntry changeMetaRefLogEntry2 = repo.getReflogReader(changeMetaRef2).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry2 = refDb.getReflogReader(changeMetaRef2).getLastEntry();
       assertThat(changeMetaRefLogEntry2.getWho().getEmailAddress()).isEqualTo(combinedEmail);
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index c711f4e..c6d74d1 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -64,6 +64,7 @@
 import com.google.inject.Inject;
 import java.util.Collection;
 import java.util.Optional;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -1088,11 +1089,12 @@
       gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
 
       // The ref log for the patch set ref records the impersonated user aka the uploader.
-      ReflogEntry patchSetRefLogEntry = repo.getReflogReader(patchSetRef).getLastEntry();
+      RefDatabase refDb = repo.getRefDatabase();
+      ReflogEntry patchSetRefLogEntry = refDb.getReflogReader(patchSetRef).getLastEntry();
       assertThat(patchSetRefLogEntry.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
 
       // The ref log for the change meta ref records the impersonated user aka the uploader.
-      ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry = refDb.getReflogReader(changeMetaRef).getLastEntry();
       assertThat(changeMetaRefLogEntry.getWho().getEmailAddress()).isEqualTo(uploaderEmail);
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index b9bbd96..d7007b4 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -568,7 +568,7 @@
     assertThrows(AuthException.class, () -> gApi.groups().create(name("newGroup")));
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 083910f..70807c9 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -2075,7 +2075,7 @@
     }
   }
 
-  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
   // Instant
   @SuppressWarnings("JdkObsolete")
   private void assertPersonIdent(GitPerson gitPerson, PersonIdent expectedIdent) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 7de689d..5aac80d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -151,7 +151,8 @@
           .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
 
       // The ref log for the change meta ref records the impersonated user.
-      ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry =
+          repo.getRefDatabase().getReflogReader(changeMetaRef).getLastEntry();
       assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
           .isEqualTo(impersonatedUser.email());
     }
@@ -556,7 +557,7 @@
     // The ref log for the target branch records the impersonated user.
     try (Repository repo = repoManager.openRepository(project)) {
       ReflogEntry targetBranchRefLogEntry =
-          repo.getReflogReader("refs/heads/master").getLastEntry();
+          repo.getRefDatabase().getReflogReader("refs/heads/master").getLastEntry();
       assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
           .isEqualTo(impersonatedUser.email());
     }
@@ -592,13 +593,13 @@
     try (Repository repo = repoManager.openRepository(project)) {
       // The ref log for the patch set ref records the impersonated user.
       ReflogEntry patchSetRefLogEntry =
-          repo.getReflogReader(cd.currentPatchSet().refName()).getLastEntry();
+          repo.getRefDatabase().getReflogReader(cd.currentPatchSet().refName()).getLastEntry();
       assertThat(patchSetRefLogEntry.getWho().getEmailAddress())
           .isEqualTo(impersonatedUser.email());
 
       // The ref log for the target branch records the impersonated user.
       ReflogEntry targetBranchRefLogEntry =
-          repo.getReflogReader("refs/heads/master").getLastEntry();
+          repo.getRefDatabase().getReflogReader("refs/heads/master").getLastEntry();
       assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
           .isEqualTo(impersonatedUser.email());
     }
@@ -644,7 +645,8 @@
           .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
 
       // The ref log for the change meta ref records the impersonated user.
-      ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+      ReflogEntry changeMetaRefLogEntry =
+          repo.getRefDatabase().getReflogReader(changeMetaRef).getLastEntry();
       assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
           .isEqualTo(impersonatedUser.email());
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 9e9ad6b..a4cf221 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.entities.RefNames.REFS_DASHBOARDS;
 import static com.google.gerrit.server.restapi.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
-import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
 
 import com.google.common.collect.ImmutableList;
@@ -116,11 +115,6 @@
               .expectedResponseCode(SC_NOT_FOUND)
               .build(),
           RestCall.get("/projects/%s/branches/%s/mergeable"),
-          // The tests use DfsRepository which does not support getting the reflog.
-          RestCall.builder(GET, "/projects/%s/branches/%s/reflog")
-              .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
-              .expectedMessage("reflog not supported on")
-              .build(),
           RestCall.get("/projects/%s/branches/%s/validation-options"),
           RestCall.get("/projects/%s/branches/%s/suggest_reviewers"),
 
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 2bccc87..fb7a29a 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -54,7 +54,7 @@
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import java.net.URL;
+import java.net.URI;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Collection;
@@ -446,7 +446,8 @@
 
     // ensure the message header contains a valid message id.
     assertThat(((StringEmailHeader) message.headers().get("Message-ID")).getString())
-        .containsMatch("<someid-REJECTION-HTML@" + new URL(canonicalWebUrl.get()).getHost() + ">");
+        .containsMatch(
+            "<someid-REJECTION-HTML@" + URI.create(canonicalWebUrl.get()).toURL().getHost() + ">");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index 9093412..2d63f63 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -59,7 +59,7 @@
       }
 
       gApi.changes().id(id.get()).topic("foo");
-      ReflogEntry last = repo.getReflogReader(changeMetaRef(id)).getLastEntry();
+      ReflogEntry last = repo.getRefDatabase().getReflogReader(changeMetaRef(id)).getLastEntry();
       assertWithMessage("last RefLogEntry").that(last).isNotNull();
       assertThat(last.getComment()).isEqualTo("restapi.change.PutTopic");
     }
@@ -79,7 +79,7 @@
       }
 
       gApi.changes().id(id.get()).topic("foo");
-      ReflogEntry last = repo.getReflogReader(changeMetaRef(id)).getLastEntry();
+      ReflogEntry last = repo.getRefDatabase().getReflogReader(changeMetaRef(id)).getLastEntry();
       assertThat(last.getWho().getEmailAddress())
           .isEqualTo(admin.username() + "|account-" + admin.id() + "@unknown");
     }
@@ -98,7 +98,7 @@
       }
 
       gApi.changes().id(id.get()).topic("foo");
-      ReflogEntry last = repo.getReflogReader(changeMetaRef(id)).getLastEntry();
+      ReflogEntry last = repo.getRefDatabase().getReflogReader(changeMetaRef(id)).getLastEntry();
       assertThat(last.getWho().getEmailAddress()).isEqualTo(admin.email());
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java b/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
index 711fe05..b6b5dcc 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.WaitUtil.waitUntil;
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -126,7 +127,7 @@
     waitForEvent(() -> pollEventsContaining("ref-updated", "refs/draft-comments/").size() == 1);
   }
 
-  @Test(expected = InterruptedException.class)
+  @Test()
   @GerritConfig(name = "event.stream-events.enableRefUpdatedEvents", value = "true")
   @GerritConfig(name = "event.stream-events.enableBatchRefUpdatedEvents", value = "false")
   @GerritConfig(name = "event.stream-events.enableDraftCommentEvents", value = "false")
@@ -135,7 +136,12 @@
 
     draftReviewChange(PATCHSET_LEVEL, String.format("%s 1", TEST_REVIEW_DRAFT_COMMENT));
 
-    waitForEvent(() -> pollEventsContaining("ref-updated", "refs/draft-comments/").size() == 1);
+    assertThrows(
+        InterruptedException.class,
+        () -> {
+          waitForEvent(
+              () -> pollEventsContaining("ref-updated", "refs/draft-comments/").size() == 1);
+        });
   }
 
   @Test
@@ -151,7 +157,7 @@
         () -> pollEventsContaining("batch-ref-updated", "refs/draft-comments/").size() == 1);
   }
 
-  @Test(expected = InterruptedException.class)
+  @Test()
   @GerritConfig(name = "event.stream-events.enableBatchRefUpdatedEvents", value = "true")
   @GerritConfig(name = "event.stream-events.enableRefUpdatedEvents", value = "false")
   @GerritConfig(name = "event.stream-events.enableDraftCommentEvents", value = "false")
@@ -160,7 +166,12 @@
 
     draftReviewChange(PATCHSET_LEVEL, String.format("%s 1", TEST_REVIEW_DRAFT_COMMENT));
 
-    waitForEvent(() -> pollEventsContaining("ref-updated", "refs/draft-comments/").size() == 1);
+    assertThrows(
+        InterruptedException.class,
+        () -> {
+          waitForEvent(
+              () -> pollEventsContaining("ref-updated", "refs/draft-comments/").size() == 1);
+        });
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
index 75979a4..16d6a4a 100644
--- a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
+++ b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
@@ -265,7 +265,7 @@
     @Override
     public ReflogReader getReflogReader(String refName) throws IOException {
       checkIsOpen();
-      return repo.getReflogReader(refName);
+      return repo.getRefDatabase().getReflogReader(refName);
     }
 
     private void checkIsOpen() {
diff --git a/plugins/download-commands b/plugins/download-commands
index 09119a0..978e803 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 09119a065739a5921b7ad8eef75c996ae4043d8a
+Subproject commit 978e803c87416eb9e96236446b15b167017c0385
diff --git a/plugins/gitiles b/plugins/gitiles
index e6146ea..e5c66ff 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit e6146eafd28d3776470f86b0d675964d56695779
+Subproject commit e5c66ffd3af9fdbcc4f98fb7c594850bb5f513c7
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 2808425..ed7870e 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 2808425de3d91a319d7497e127af1336ca6692c0
+Subproject commit ed7870eb3c8b6e48511d0eb3bd54606927b46019
diff --git a/plugins/replication b/plugins/replication
index 729e562..a9f44a1 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 729e562946ae7357774c9e8572ef9052c15a6e9e
+Subproject commit a9f44a14c64efaa44afdf8f553a3b3211ef4342b
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index d205267..d3681a5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -822,10 +822,32 @@
         .header {
           align-items: center;
           background-color: var(--background-color-primary);
-          border-bottom: 1px solid var(--border-color);
+          border-bottom: 2px solid var(--border-color);
           display: flex;
           padding: var(--spacing-s) var(--spacing-l);
         }
+        .header.active {
+          border-color: var(--status-active);
+        }
+        .header.abandoned {
+          border-color: var(--status-abandoned);
+        }
+        .header.merged {
+          border-color: var(--status-merged);
+        }
+        .header.private {
+          border-color: var(--status-private);
+        }
+        .header.ready-to-submit {
+          border-color: var(--status-ready);
+        }
+        .header.wip {
+          border-color: var(--status-wip);
+        }
+        .header.merge-conflict,
+        .header.git-conflict {
+          border-color: var(--status-conflict);
+        }
         .header.editMode {
           background-color: var(--edit-mode-background-color);
         }
@@ -2375,6 +2397,10 @@
   // Private but used in tests.
   computeHeaderClass() {
     const classes = ['header'];
+    const status = this.computeChangeStatusChips()?.[0];
+    if (status) {
+      classes.push(status.toLowerCase());
+    }
     if (this.editMode) {
       classes.push('editMode');
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index 97fa267..00130fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -81,6 +81,7 @@
         }
         :host(.active) .chip {
           background-color: var(--status-active);
+          --status-text-color: black;
           color: var(--status-active);
         }
         :host(.ready-to-submit) .chip {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
index 2913fc8..40ce039 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
@@ -82,6 +82,15 @@
     /* Needs z-index to appear above wrapped content, since it's inserted
        into DOM before it. */
     z-index: 120;
+    position: absolute;
+  }
+  gr-diff-element gr-selection-action-box slot[invisible] {
+    visibility: hidden;
+  }
+  gr-diff-element gr-selection-action-box gr-tooltip {
+    position: absolute;
+    width: 22ch;
+    cursor: pointer;
   }
 `;
 
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
index 06c6768..9f08149 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -6,9 +6,8 @@
 import '../../../elements/shared/gr-tooltip/gr-tooltip';
 import {GrTooltip} from '../../../elements/shared/gr-tooltip/gr-tooltip';
 import {fire} from '../../../utils/event-util';
-import {css, html, LitElement} from 'lit';
+import {html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
-import {sharedStyles} from '../../../styles/shared-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -25,6 +24,10 @@
   @query('#tooltip')
   tooltip?: GrTooltip;
 
+  @state() private isSlotAssigned = false;
+
+  @query('slot') slotElement!: HTMLSlotElement;
+
   @property({type: Boolean})
   positionBelow = false;
 
@@ -43,32 +46,38 @@
     this.addEventListener('mousedown', e => this.handleMouseDown(e));
   }
 
-  static override get styles() {
-    return [
-      sharedStyles,
-      css`
-        :host {
-          cursor: pointer;
-          font-family: var(--font-family);
-          position: absolute;
-          width: 20ch;
-        }
-        gr-tooltip[invisible] {
-          visibility: hidden;
-        }
-      `,
-    ];
+  override render() {
+    // We create the gr-tooltip anyway even if the slot is assigned so that
+    // we reuse the logic for positioning the tooltip (in placeAbove/Below).
+    return html`
+      <slot
+        name="selectionActionBox"
+        ?invisible=${this.invisible}
+        @slotchange=${this.handleSlotChange}
+      >
+        <gr-tooltip
+          id="tooltip"
+          text=${this.hoverCardText}
+          ?position-below=${this.positionBelow}
+        ></gr-tooltip>
+      </slot>
+    `;
   }
 
-  override render() {
-    return html`
-      <gr-tooltip
-        id="tooltip"
-        ?invisible=${this.invisible}
-        text=${this.hoverCardText}
-        ?position-below=${this.positionBelow}
-      ></gr-tooltip>
-    `;
+  private handleSlotChange() {
+    const assignedNodes = this.slotElement.assignedNodes({flatten: true});
+    this.isSlotAssigned = assignedNodes.length > 0;
+  }
+
+  /**
+   * The browser API for handling selection does not (yet) work for selection
+   * across multiple shadow DOM elements. So we are rendering gr-diff components
+   * into the light DOM instead of the shadow DOM by overriding this method,
+   * which was the recommended workaround by the lit team.
+   * See also https://github.com/WICG/webcomponents/issues/79.
+   */
+  override createRenderRoot() {
+    return this;
   }
 
   // TODO(b/315277651): This is very similar in purpose to gr-tooltip-content.
@@ -133,6 +142,9 @@
 
   // visible for testing
   handleMouseDown(e: MouseEvent) {
+    if (this.isSlotAssigned) {
+      return;
+    }
     if (e.button !== 0) {
       return;
     } // 0 = main button
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
index 5cc8409..9155e53 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
@@ -32,14 +32,13 @@
   });
 
   test('renders', () => {
-    assert.shadowDom.equal(
-      element,
+    assertEqualIgnoreWhitespaceAndNewlines(
+      element.innerHTML,
       /* HTML */ `
-        <gr-tooltip
-          invisible
-          id="tooltip"
-          text="Press c to comment"
-        ></gr-tooltip>
+        <!---->
+        <slot name="selectionActionBox" invisible="">
+          <gr-tooltip id="tooltip" text="Press c to comment"></gr-tooltip>
+        </slot>
       `
     );
   });
@@ -111,10 +110,13 @@
     test('renders visible', async () => {
       await element.placeAbove(target);
       await element.updateComplete;
-      assert.shadowDom.equal(
-        element,
+      assertEqualIgnoreWhitespaceAndNewlines(
+        element.innerHTML,
         /* HTML */ `
-          <gr-tooltip id="tooltip" text="Press c to comment"></gr-tooltip>
+          <!---->
+          <slot name="selectionActionBox">
+            <gr-tooltip id="tooltip" text="Press c to comment"></gr-tooltip>
+          </slot>
         `
       );
     });
@@ -151,3 +153,21 @@
     });
   });
 });
+
+function assertEqualIgnoreWhitespaceAndNewlines(
+  actual: string,
+  expected: string
+): void {
+  const normalize = (str: string): string =>
+    str
+      .replace(/\r/g, '')
+      .replace(/\n/g, '')
+      .replace(/\s+/g, ' ')
+      .replace(/\s+>/g, '>')
+      .trim();
+  if (normalize(actual) !== normalize(expected)) {
+    throw new Error(`Assertion failed:
+    Actual: "${normalize(actual)}"
+    Expected: "${normalize(expected)}"`);
+  }
+}
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 2806936..97993c1 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -356,15 +356,15 @@
     --tag-brown: var(--brown-50);
 
     /* status colors */
-    --status-merged: var(--green-700);
+    --status-merged: var(--gray-700);
     --status-abandoned: var(--gray-700);
     --status-wip: #795548;
     --status-private: var(--purple-500);
     --status-conflict: var(--red-600);
     --status-revert: var(--gray-900);
     --status-revert-created: #e64a19;
-    --status-active: var(--blue-700);
-    --status-ready: var(--pink-800);
+    --status-active: var(--yellow-700);
+    --status-ready: var(--green-700);
     --status-custom: var(--purple-900);
 
     /* file status colors */
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 0387723..635d246 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -177,15 +177,15 @@
     --tag-brown: var(--brown-tonal);
 
     /* status colors */
-    --status-merged: var(--green-400);
+    --status-merged: #a4a4a4;
     --status-abandoned: var(--gray-300);
     --status-wip: #bcaaa4;
     --status-private: var(--purple-200);
     --status-conflict: var(--red-300);
     --status-revert: var(--gray-200);
     --status-revert-created: #ff8a65;
-    --status-active: var(--blue-400);
-    --status-ready: var(--pink-500);
+    --status-active: #f4ce5d;
+    --status-ready: #55c374;
     --status-custom: var(--purple-400);
 
     /* file status colors */