Merge branch 'stable-2.16'

* stable-2.16:
  ChangeCleanupConfig: Remove unnecessary usage of regex in URL replacement
  AbandonIT: Add tests for changeCleanup.abandonMessage
  UrlFormatter#getChangeViewUrl: Remove @Nullable and simplify default implementation
  UrlFormatter#getSettingsUrl: Annotate section parameter as @Nullable
  MergeUtil: Include project name in "Reviewed-On" URL
  ChangeCleanupConfig: Inject UrlFormatter via DynamicItem
  Adjust more classes to inject UrlFormatter via DynamicItem
  Set version to 2.15.12-SNAPSHOT
  Set version to 2.15.11
  Allow LFS-over-SSH created auth pass through ContainerAuthFilter
  Upgrade elasticsearch-rest-client to 6.6.1
  ElasticContainer: Bump the test server version to 5.6.15

Change-Id: Ie90d450ddb16d165ed2d5ec91964d35b735214dd
diff --git a/WORKSPACE b/WORKSPACE
index 189923b..ca29e49 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1058,8 +1058,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.6.0",
-    sha1 = "f0ce1ea819fedde731511b440b025e4fb5a2f5f7",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.6.1",
+    sha1 = "dc1c9284ffca28cd169fae2776c3956e90b76c00",
 )
 
 JACKSON_VERSION = "2.9.8"
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index b6fb46e..b6ecbc5 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -58,7 +59,7 @@
   @Singleton
   public static class Factory {
     private final Provider<InternalAccountQuery> accountQueryProvider;
-    private final UrlFormatter urlFormatter;
+    private final DynamicItem<UrlFormatter> urlFormatter;
     private final IdentifiedUser.GenericFactory userFactory;
     private final int maxTrustDepth;
     private final ImmutableMap<Long, Fingerprint> trusted;
@@ -68,7 +69,7 @@
         @GerritServerConfig Config cfg,
         Provider<InternalAccountQuery> accountQueryProvider,
         IdentifiedUser.GenericFactory userFactory,
-        UrlFormatter urlFormatter) {
+        DynamicItem<UrlFormatter> urlFormatter) {
       this.accountQueryProvider = accountQueryProvider;
       this.urlFormatter = urlFormatter;
       this.userFactory = userFactory;
@@ -101,7 +102,7 @@
   }
 
   private final Provider<InternalAccountQuery> accountQueryProvider;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final IdentifiedUser.GenericFactory userFactory;
 
   private IdentifiedUser expectedUser;
@@ -144,7 +145,7 @@
   private CheckResult checkIdsForExpectedUser(PGPPublicKey key) throws PGPException {
     Set<String> allowedUserIds = getAllowedUserIds(expectedUser);
     if (allowedUserIds.isEmpty()) {
-      Optional<String> settings = urlFormatter.getSettingsUrl("Identities");
+      Optional<String> settings = urlFormatter.get().getSettingsUrl("Identities");
       return CheckResult.bad(
           "No identities found for user"
               + (settings.isPresent() ? "; check " + settings.get() : ""));
diff --git a/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index ac66845..d13f2f6 100644
--- a/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -17,9 +17,12 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Strings.emptyToNull;
 import static com.google.common.net.HttpHeaders.AUTHORIZATION;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.CONTENTTYPE_VND_GIT_LFS_JSON;
+import static com.google.gerrit.httpd.GerritAuthModule.NOT_AUTHORIZED_LFS_URL_REGEX;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.AccessPath;
@@ -32,6 +35,7 @@
 import java.io.IOException;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.regex.Pattern;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -55,6 +59,9 @@
  */
 @Singleton
 class ContainerAuthFilter implements Filter {
+  private static final String LFS_AUTH_PREFIX = "Ssh: ";
+  private static final Pattern LFS_ENDPOINT = Pattern.compile(NOT_AUTHORIZED_LFS_URL_REGEX);
+
   private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final Config config;
@@ -93,6 +100,11 @@
   private boolean verify(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
     String username = RemoteUserUtil.getRemoteUser(req, loginHttpHeader);
     if (username == null) {
+      if (isLfsOverSshRequest(req)) {
+        // LFS-over-SSH auth request cannot be authorized by container
+        // therefore let it go through the filter
+        return true;
+      }
       rsp.sendError(SC_FORBIDDEN);
       return false;
     }
@@ -111,4 +123,12 @@
     ws.setAccessPathOk(AccessPath.REST_API, true);
     return true;
   }
+
+  private static boolean isLfsOverSshRequest(HttpServletRequest req) {
+    String hdr = req.getHeader(AUTHORIZATION);
+    return CONTENTTYPE_VND_GIT_LFS_JSON.equals(req.getContentType())
+        && !Strings.isNullOrEmpty(hdr)
+        && hdr.startsWith(LFS_AUTH_PREFIX)
+        && LFS_ENDPOINT.matcher(req.getRequestURI()).matches();
+  }
 }
diff --git a/java/com/google/gerrit/httpd/GerritAuthModule.java b/java/com/google/gerrit/httpd/GerritAuthModule.java
index c0ef207..253c220 100644
--- a/java/com/google/gerrit/httpd/GerritAuthModule.java
+++ b/java/com/google/gerrit/httpd/GerritAuthModule.java
@@ -24,7 +24,7 @@
 
 /** Configures filter for authenticating REST requests. */
 public class GerritAuthModule extends ServletModule {
-  private static final String NOT_AUTHORIZED_LFS_URL_REGEX = "^(?:(?!/a/))" + LFS_URL_WO_AUTH_REGEX;
+  static final String NOT_AUTHORIZED_LFS_URL_REGEX = "^(?:(?!/a/))" + LFS_URL_WO_AUTH_REGEX;
   private final AuthConfig authConfig;
 
   @Inject
diff --git a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
index f492247..f5c9fc2 100644
--- a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
+++ b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.config.ScheduleConfig.Schedule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -34,17 +35,19 @@
           + "\n"
           + "If this change is still wanted it should be restored.";
 
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final Optional<Schedule> schedule;
   private final long abandonAfter;
   private final boolean abandonIfMergeable;
   private final String abandonMessage;
 
   @Inject
-  ChangeCleanupConfig(@GerritServerConfig Config cfg, UrlFormatter urlFormatter) {
+  ChangeCleanupConfig(@GerritServerConfig Config cfg, DynamicItem<UrlFormatter> urlFormatter) {
+    this.urlFormatter = urlFormatter;
     schedule = ScheduleConfig.createSchedule(cfg, SECTION);
     abandonAfter = readAbandonAfter(cfg);
     abandonIfMergeable = cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true);
-    abandonMessage = readAbandonMessage(cfg, urlFormatter);
+    abandonMessage = readAbandonMessage(cfg);
   }
 
   private long readAbandonAfter(Config cfg) {
@@ -53,18 +56,9 @@
     return abandonAfter >= 0 ? abandonAfter : 0;
   }
 
-  private String readAbandonMessage(Config cfg, UrlFormatter urlFormatter) {
+  private String readAbandonMessage(Config cfg) {
     String abandonMessage = cfg.getString(SECTION, null, KEY_ABANDON_MESSAGE);
-    if (Strings.isNullOrEmpty(abandonMessage)) {
-      abandonMessage = DEFAULT_ABANDON_MESSAGE;
-    }
-
-    String docUrl = urlFormatter.getDocUrl("user-change-cleanup.html", "auto-abandon").orElse("");
-    if (!docUrl.isEmpty()) {
-      abandonMessage = abandonMessage.replaceAll("\\$\\{URL\\}", docUrl);
-    }
-
-    return abandonMessage;
+    return Strings.isNullOrEmpty(abandonMessage) ? DEFAULT_ABANDON_MESSAGE : abandonMessage;
   }
 
   public Optional<Schedule> getSchedule() {
@@ -80,6 +74,8 @@
   }
 
   public String getAbandonMessage() {
-    return abandonMessage;
+    String docUrl =
+        urlFormatter.get().getDocUrl("user-change-cleanup.html", "auto-abandon").orElse("");
+    return docUrl.isEmpty() ? abandonMessage : abandonMessage.replace("${URL}", docUrl);
   }
 }
diff --git a/java/com/google/gerrit/server/config/UrlFormatter.java b/java/com/google/gerrit/server/config/UrlFormatter.java
index 5cec1ac..066a3ca 100644
--- a/java/com/google/gerrit/server/config/UrlFormatter.java
+++ b/java/com/google/gerrit/server/config/UrlFormatter.java
@@ -40,16 +40,15 @@
   Optional<String> getWebUrl();
 
   /** Returns the URL for viewing a change. */
-  default Optional<String> getChangeViewUrl(@Nullable Project.NameKey project, Change.Id id) {
+  default Optional<String> getChangeViewUrl(Project.NameKey project, Change.Id id) {
 
     // In the PolyGerrit URL (contrary to REST URLs) there is no need to URL-escape strings, since
     // the /+/ separator unambiguously defines how to parse the path.
-    return getWebUrl()
-        .map(url -> url + "c/" + (project != null ? project.get() + "/+/" : "") + id.get());
+    return getWebUrl().map(url -> url + "c/" + project.get() + "/+/" + id.get());
   }
 
   /** Returns a URL pointing to a section of the settings page. */
-  default Optional<String> getSettingsUrl(String section) {
+  default Optional<String> getSettingsUrl(@Nullable String section) {
     return getWebUrl()
         .map(url -> url + "settings" + (Strings.isNullOrEmpty(section) ? "" : "#" + section));
   }
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 4103ce2..7904b28 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -83,7 +84,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final AccountCache accountCache;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final Emails emails;
   private final PatchListCache patchListCache;
   private final Provider<PersonIdent> myIdent;
@@ -97,7 +98,7 @@
   EventFactory(
       AccountCache accountCache,
       Emails emails,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       PatchListCache patchListCache,
       @GerritPersonIdent Provider<PersonIdent> myIdent,
       ChangeData.Factory changeDataFactory,
@@ -634,7 +635,7 @@
   /** Get a link to the change; null if the server doesn't know its own address. */
   private String getChangeUrl(Change change) {
     if (change != null) {
-      return urlFormatter.getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
+      return urlFormatter.get().getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
     }
     return null;
   }
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index 914f402..fc9abb4 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.inject.Inject;
@@ -28,10 +29,10 @@
   private static final int SUBJECT_CROP_RANGE = 10;
   private static final String NEW_CHANGE_INDICATOR = " [NEW]";
 
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
-  DefaultChangeReportFormatter(UrlFormatter urlFormatter) {
+  DefaultChangeReportFormatter(DynamicItem<UrlFormatter> urlFormatter) {
     this.urlFormatter = urlFormatter;
   }
 
@@ -50,7 +51,10 @@
     Change c = input.change();
     return String.format(
         "change %s closed",
-        urlFormatter.getChangeViewUrl(c.getProject(), c.getId()).orElse(c.getId().toString()));
+        urlFormatter
+            .get()
+            .getChangeViewUrl(c.getProject(), c.getId())
+            .orElse(c.getId().toString()));
   }
 
   protected String cropSubject(String subject) {
@@ -70,7 +74,7 @@
 
   protected String formatChangeUrl(Input input) {
     Change c = input.change();
-    Optional<String> changeUrl = urlFormatter.getChangeViewUrl(c.getProject(), c.getId());
+    Optional<String> changeUrl = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId());
     checkState(changeUrl.isPresent());
 
     StringBuilder m =
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 8599fbe..0d427e6 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -30,6 +30,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -159,7 +160,7 @@
   }
 
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final ApprovalsUtil approvalsUtil;
   private final ProjectState project;
   private final boolean useContentMerge;
@@ -170,7 +171,7 @@
   MergeUtil(
       @GerritServerConfig Config serverConfig,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       ApprovalsUtil approvalsUtil,
       PluggableCommitMessageGenerator commitMessageGenerator,
       @Assisted ProjectState project) {
@@ -188,7 +189,7 @@
   MergeUtil(
       @GerritServerConfig Config serverConfig,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       ApprovalsUtil approvalsUtil,
       @Assisted ProjectState project,
       PluggableCommitMessageGenerator commitMessageGenerator,
@@ -482,7 +483,7 @@
       msgbuf.append('\n');
     }
 
-    Optional<String> url = urlFormatter.getChangeViewUrl(null, c.getId());
+    Optional<String> url = urlFormatter.get().getChangeViewUrl(c.getProject(), c.getId());
     if (url.isPresent()) {
       if (!contains(footers, FooterConstants.REVIEWED_ON, url.get())) {
         msgbuf
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index e087325..79a915c 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -96,7 +97,7 @@
   private final CommentAdded commentAdded;
   private final ApprovalsUtil approvalsUtil;
   private final AccountCache accountCache;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
   public MailProcessor(
@@ -114,7 +115,7 @@
       ApprovalsUtil approvalsUtil,
       CommentAdded commentAdded,
       AccountCache accountCache,
-      UrlFormatter urlFormatter) {
+      DynamicItem<UrlFormatter> urlFormatter) {
     this.emails = emails;
     this.emailRejectionSender = emailRejectionSender;
     this.retryHelper = retryHelper;
@@ -240,7 +241,10 @@
       // If URL is not defined, we won't be able to parse line comments. We still attempt to get the
       // other ones.
       String changeUrl =
-          urlFormatter.getChangeViewUrl(cd.project(), cd.getId()).orElse("http://gerrit.invalid/");
+          urlFormatter
+              .get()
+              .getChangeViewUrl(cd.project(), cd.getId())
+              .orElse("http://gerrit.invalid/");
 
       List<MailComment> parsedComments;
       if (useHtmlParser(message)) {
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 22923c0..012d212 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -219,7 +219,10 @@
   /** Get a link to the change; null if the server doesn't know its own address. */
   @Nullable
   public String getChangeUrl() {
-    return args.urlFormatter.getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
+    return args.urlFormatter
+        .get()
+        .getChangeViewUrl(change.getProject(), change.getId())
+        .orElse(null);
   }
 
   public String getChangeMessageThreadId() {
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index df88a01..fe2f74b 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -67,7 +68,7 @@
   final AnonymousUser anonymousUser;
   final String anonymousCowardName;
   final PersonIdent gerritPersonIdent;
-  final UrlFormatter urlFormatter;
+  final DynamicItem<UrlFormatter> urlFormatter;
   final AllProjectsName allProjectsName;
   final List<String> sshAddresses;
   final SitePaths site;
@@ -100,7 +101,7 @@
       AnonymousUser anonymousUser,
       @AnonymousCowardName String anonymousCowardName,
       GerritPersonIdentProvider gerritPersonIdentProvider,
-      UrlFormatter urlFormatter,
+      DynamicItem<UrlFormatter> urlFormatter,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder queryBuilder,
       ChangeData.Factory changeDataFactory,
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 9c5f977..a6747b5 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -292,7 +292,7 @@
   }
 
   public String getGerritUrl() {
-    return args.urlFormatter.getWebUrl().orElse(null);
+    return args.urlFormatter.get().getWebUrl().orElse(null);
   }
 
   /** Set a header in the outgoing message. */
diff --git a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
index 8912e31..b33fcb5 100644
--- a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
+++ b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
@@ -43,7 +44,7 @@
 @Singleton
 public class ContributorAgreementsChecker {
 
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
   private final ProjectCache projectCache;
   private final Metrics metrics;
 
@@ -62,7 +63,7 @@
 
   @Inject
   ContributorAgreementsChecker(
-      UrlFormatter urlFormatter, ProjectCache projectCache, Metrics metrics) {
+      DynamicItem<UrlFormatter> urlFormatter, ProjectCache projectCache, Metrics metrics) {
     this.urlFormatter = urlFormatter;
     this.projectCache = projectCache;
     this.metrics = metrics;
@@ -129,7 +130,7 @@
           .append(iUser.getAccountId())
           .append(")");
 
-      msg.append(urlFormatter.getSettingsUrl("Agreements").orElse(""));
+      msg.append(urlFormatter.get().getSettingsUrl("Agreements").orElse(""));
       throw new AuthException(msg.toString());
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index a699e41..23115de 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -55,14 +56,14 @@
   private final boolean canGC;
   private final GarbageCollection.Factory garbageCollectionFactory;
   private final WorkQueue workQueue;
-  private final UrlFormatter urlFormatter;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
   GarbageCollect(
       GitRepositoryManager repoManager,
       GarbageCollection.Factory garbageCollectionFactory,
       WorkQueue workQueue,
-      UrlFormatter urlFormatter) {
+      DynamicItem<UrlFormatter> urlFormatter) {
     this.workQueue = workQueue;
     this.urlFormatter = urlFormatter;
     this.canGC = repoManager instanceof LocalDiskRepositoryManager;
@@ -99,7 +100,9 @@
     WorkQueue.Task<Void> task = (WorkQueue.Task<Void>) workQueue.getDefaultQueue().submit(job);
 
     Optional<String> url =
-        urlFormatter.getRestUrl("a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId()));
+        urlFormatter
+            .get()
+            .getRestUrl("a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId()));
     // We're in a HTTP handler, so must be present.
     checkState(url.isPresent());
     return Response.accepted(url.get());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index e655053..691dd94 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.AbandonUtil;
+import com.google.gerrit.server.config.ChangeCleanupConfig;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
@@ -46,6 +47,7 @@
 public class AbandonIT extends AbstractDaemonTest {
   @Inject private AbandonUtil abandonUtil;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ChangeCleanupConfig cleanupConfig;
 
   @Test
   public void abandon() throws Exception {
@@ -125,6 +127,31 @@
   }
 
   @Test
+  public void changeCleanupConfigDefaultAbandonMessage() throws Exception {
+    assertThat(cleanupConfig.getAbandonMessage())
+        .startsWith(
+            "Auto-Abandoned due to inactivity, see "
+                + canonicalWebUrl.get()
+                + "Documentation/user-change-cleanup.html#auto-abandon");
+  }
+
+  @Test
+  @GerritConfig(name = "changeCleanup.abandonMessage", value = "XX ${URL} XX")
+  public void changeCleanupConfigCustomAbandonMessageWithUrlReplacement() throws Exception {
+    assertThat(cleanupConfig.getAbandonMessage())
+        .isEqualTo(
+            "XX "
+                + canonicalWebUrl.get()
+                + "Documentation/user-change-cleanup.html#auto-abandon XX");
+  }
+
+  @Test
+  @GerritConfig(name = "changeCleanup.abandonMessage", value = "XX YYY XX")
+  public void changeCleanupConfigCustomAbandonMessageWithoutUrlReplacement() throws Exception {
+    assertThat(cleanupConfig.getAbandonMessage()).isEqualTo("XX YYY XX");
+  }
+
+  @Test
   public void abandonNotAllowedWithoutPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 88f090f..4e48165 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -2671,7 +2671,12 @@
     List<String> expectedFooters =
         Arrays.asList(
             "Change-Id: " + r2.getChangeId(),
-            "Reviewed-on: " + canonicalWebUrl.get() + "c/" + r2.getChange().getId(),
+            "Reviewed-on: "
+                + canonicalWebUrl.get()
+                + "c/"
+                + project.get()
+                + "/+/"
+                + r2.getChange().getId(),
             "Reviewed-by: Administrator <admin@example.com>",
             "Custom2: Administrator <admin@example.com>",
             "Tested-by: Administrator <admin@example.com>");
@@ -2706,7 +2711,12 @@
     List<String> expectedFooters =
         Arrays.asList(
             "Change-Id: " + change.getChangeId(),
-            "Reviewed-on: " + canonicalWebUrl.get() + "c/" + change.getChange().getId(),
+            "Reviewed-on: "
+                + canonicalWebUrl.get()
+                + "c/"
+                + project.get()
+                + "/+/"
+                + change.getChange().getId(),
             "Custom: refs/heads/master");
     assertThat(footers).containsExactlyElementsIn(expectedFooters);
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 1af517c..79170c4 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -37,7 +37,7 @@
   private static String getImageName(ElasticVersion version) {
     switch (version) {
       case V5_6:
-        return "docker.elastic.co/elasticsearch/elasticsearch:5.6.14";
+        return "docker.elastic.co/elasticsearch/elasticsearch:5.6.15";
       case V6_2:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4";
       case V6_3:
@@ -47,7 +47,7 @@
       case V6_5:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.4";
       case V6_6:
-        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.0";
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.1";
       case V7_0:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0-beta1";
     }