Merge branch 'stable-3.1'

* stable-3.1:
  Update git submodules
  Update git submodules
  OperatingSystemMXBeanFactory: Add a default constructor
  Remove unnecessary @SuppressWarnings("restriction")
  Set version to 2.16.19-SNAPSHOT
  Set version to 2.16.18
  Update git submodules
  Update git submodules
  Update git submodules
  Update git submodules
  Add mirror for downloading Bazel rules.
  Update git submodules
  Upgrade JGit to v5.6.1.202002131546-r-19-ga79c5b1f1
  Upgrade gwtjsonrpc to 1.12
  CommitValidators: Use ImmutableList.Builder instead of ImmutableList.of
  Update git submodules
  Update git submodules
  Update git submodules
  Always verify PolyGerrit if bazel related files are changed
  Always run all tests, if bazel-related files are changed
  Fix implementation plan link in Contributing page
  Add test that verifies 'visibleto' predicate for group
  Make cache disk stat metric computation optional
  ReceiveCommits: Log "update failed" at severe level
  Revert "Insert Change-Id at start of trailers"
  Revert "commit-msg: Remove obsolete comments"
  Remove obsolete UpgradeFrom2_0_x init step
  Update git submodules
  Update git submodules
  Add coverageRangeChanged to notify all related listeners
  Improve documentation of change refs
  Notify all coverage listeners when coverage data is available
  Remove duplicate test method removeAnonymousRead
  ChangeQueryBuilder: Use ChangeIsVisibleToPredicate.Factory
  ChangeQueryProcessor: Use ChangeIsVisibleToPredicate.Factory
  Make the build pipeline fail if cannot post Checks feedback
  Switch to using no-AOP guice distribution
  Bazel: Use canonical_id for artifacts cached by http_file
  Fix for Memory leak in gr-plugin-endpoints
  Documentation: Clarify how to log e2e http details
  Make legacy version of the commit-msg hook available
  Don't inject CurrentUser to ChangeIsVisibleToPredicate
  HttpLogoutServlet: Test redirections with canonicalWebUrl set
  commit-msg: Remove obsolete comments
  Upgrade testcontainers to 1.14.0
  Bump asm to version 7.2
  Upgrade guice to 4.2.3
  PostReview: Avoid multiple notifications for existing reviewers
  ChangeApi: Remove deprecated getEdit method
  ChangeApi: Remove useless @Deprecated annotation in NotImplemented
  Plugin API: Remove deprecated draft workflow methods
  ErrorProne: Enable ObjectToString check at ERROR severity
  Project: Add implementation of toString
  Allow HTTP {listen,canonicalWeb}Url in tests
  Account: Add implementation of toString
  LegacyChangeNoteWrite: Remove unused newIdent method
  Update git submodules
  PolyGerrit: Document commit-container extension endpoint
  e2e-tests: Make all current scenario names unique
  e2e-tests: Make http request name unique
  e2e-tests: Fix CloneUsingBothProtocols wait times
  e2e-tests: Create/delete ReplayRecordsFromFeeder project
  e2e-tests: Unhardcode ReplayRecordsFromFeeder data
  e2e-tests: Stabilize the ReplayRecordsFromFeeder scenario
  Remove obsolete FindBugs configuration
  Elasticsearch: Remove support for EOL 6.x versions
  REST: Allow to create annotated tag with only CREATE_TAG
  Upgrade recommended version of buildifier to 2.2.1
  Bump Bazel version to 3.0.0
  CreateRefControl: Pass CurrentUser to Reachable

Change-Id: I0d30560b4ee78393ef6f25166999529c88341273
diff --git a/.bazelversion b/.bazelversion
index ccbccc3..4a36342 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-2.2.0
+3.0.0
diff --git a/.settings/edu.umd.cs.findbugs.core.prefs b/.settings/edu.umd.cs.findbugs.core.prefs
deleted file mode 100644
index 4dfcf2d..0000000
--- a/.settings/edu.umd.cs.findbugs.core.prefs
+++ /dev/null
@@ -1,143 +0,0 @@
-#FindBugs User Preferences
-#Fri Mar 20 17:07:10 JST 2015
-cloud_id=edu.umd.cs.findbugs.cloud.doNothingCloud
-detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true
-detectorAtomicityProblem=AtomicityProblem|true
-detectorBadAppletConstructor=BadAppletConstructor|false
-detectorBadResultSetAccess=BadResultSetAccess|true
-detectorBadSyntaxForRegularExpression=BadSyntaxForRegularExpression|true
-detectorBadUseOfReturnValue=BadUseOfReturnValue|true
-detectorBadlyOverriddenAdapter=BadlyOverriddenAdapter|true
-detectorBooleanReturnNull=BooleanReturnNull|true
-detectorCallToUnsupportedMethod=CallToUnsupportedMethod|false
-detectorCheckExpectedWarnings=CheckExpectedWarnings|false
-detectorCheckImmutableAnnotation=CheckImmutableAnnotation|true
-detectorCheckRelaxingNullnessAnnotation=CheckRelaxingNullnessAnnotation|true
-detectorCheckTypeQualifiers=CheckTypeQualifiers|true
-detectorCloneIdiom=CloneIdiom|true
-detectorComparatorIdiom=ComparatorIdiom|true
-detectorConfusedInheritance=ConfusedInheritance|true
-detectorConfusionBetweenInheritedAndOuterMethod=ConfusionBetweenInheritedAndOuterMethod|true
-detectorCovariantArrayAssignment=CovariantArrayAssignment|false
-detectorCrossSiteScripting=CrossSiteScripting|true
-detectorDefaultEncodingDetector=DefaultEncodingDetector|true
-detectorDoInsideDoPrivileged=DoInsideDoPrivileged|true
-detectorDontCatchIllegalMonitorStateException=DontCatchIllegalMonitorStateException|true
-detectorDontIgnoreResultOfPutIfAbsent=DontIgnoreResultOfPutIfAbsent|true
-detectorDontUseEnum=DontUseEnum|true
-detectorDroppedException=DroppedException|true
-detectorDumbMethodInvocations=DumbMethodInvocations|true
-detectorDumbMethods=DumbMethods|true
-detectorDuplicateBranches=DuplicateBranches|true
-detectorEmptyZipFileEntry=EmptyZipFileEntry|false
-detectorEqualsOperandShouldHaveClassCompatibleWithThis=EqualsOperandShouldHaveClassCompatibleWithThis|true
-detectorExplicitSerialization=ExplicitSerialization|true
-detectorFinalizerNullsFields=FinalizerNullsFields|true
-detectorFindBadCast2=FindBadCast2|true
-detectorFindBadForLoop=FindBadForLoop|true
-detectorFindCircularDependencies=FindCircularDependencies|false
-detectorFindComparatorProblems=FindComparatorProblems|true
-detectorFindDeadLocalStores=FindDeadLocalStores|true
-detectorFindDoubleCheck=FindDoubleCheck|true
-detectorFindEmptySynchronizedBlock=FindEmptySynchronizedBlock|true
-detectorFindFieldSelfAssignment=FindFieldSelfAssignment|true
-detectorFindFinalizeInvocations=FindFinalizeInvocations|true
-detectorFindFloatEquality=FindFloatEquality|true
-detectorFindHEmismatch=FindHEmismatch|true
-detectorFindInconsistentSync2=FindInconsistentSync2|true
-detectorFindJSR166LockMonitorenter=FindJSR166LockMonitorenter|true
-detectorFindLocalSelfAssignment2=FindLocalSelfAssignment2|true
-detectorFindMaskedFields=FindMaskedFields|true
-detectorFindMismatchedWaitOrNotify=FindMismatchedWaitOrNotify|true
-detectorFindNakedNotify=FindNakedNotify|true
-detectorFindNonShortCircuit=FindNonShortCircuit|true
-detectorFindNullDeref=FindNullDeref|true
-detectorFindNullDerefsInvolvingNonShortCircuitEvaluation=FindNullDerefsInvolvingNonShortCircuitEvaluation|true
-detectorFindOpenStream=FindOpenStream|true
-detectorFindPuzzlers=FindPuzzlers|true
-detectorFindRefComparison=FindRefComparison|true
-detectorFindReturnRef=FindReturnRef|true
-detectorFindRoughConstants=FindRoughConstants|true
-detectorFindRunInvocations=FindRunInvocations|true
-detectorFindSelfComparison=FindSelfComparison|true
-detectorFindSelfComparison2=FindSelfComparison2|true
-detectorFindSleepWithLockHeld=FindSleepWithLockHeld|true
-detectorFindSpinLoop=FindSpinLoop|true
-detectorFindSqlInjection=FindSqlInjection|true
-detectorFindTwoLockWait=FindTwoLockWait|true
-detectorFindUncalledPrivateMethods=FindUncalledPrivateMethods|true
-detectorFindUnconditionalWait=FindUnconditionalWait|true
-detectorFindUninitializedGet=FindUninitializedGet|true
-detectorFindUnrelatedTypesInGenericContainer=FindUnrelatedTypesInGenericContainer|true
-detectorFindUnreleasedLock=FindUnreleasedLock|true
-detectorFindUnsatisfiedObligation=FindUnsatisfiedObligation|true
-detectorFindUnsyncGet=FindUnsyncGet|true
-detectorFindUseOfNonSerializableValue=FindUseOfNonSerializableValue|true
-detectorFindUselessControlFlow=FindUselessControlFlow|true
-detectorFindUselessObjects=FindUselessObjects|true
-detectorFormatStringChecker=FormatStringChecker|true
-detectorHugeSharedStringConstants=HugeSharedStringConstants|true
-detectorIDivResultCastToDouble=IDivResultCastToDouble|true
-detectorIncompatMask=IncompatMask|true
-detectorInconsistentAnnotations=InconsistentAnnotations|true
-detectorInefficientIndexOf=InefficientIndexOf|true
-detectorInefficientInitializationInsideLoop=InefficientInitializationInsideLoop|false
-detectorInefficientMemberAccess=InefficientMemberAccess|false
-detectorInefficientToArray=InefficientToArray|true
-detectorInfiniteLoop=InfiniteLoop|true
-detectorInfiniteRecursiveLoop=InfiniteRecursiveLoop|true
-detectorInheritanceUnsafeGetResource=InheritanceUnsafeGetResource|true
-detectorInitializationChain=InitializationChain|true
-detectorInitializeNonnullFieldsInConstructor=InitializeNonnullFieldsInConstructor|true
-detectorInstantiateStaticClass=InstantiateStaticClass|true
-detectorIntCast2LongAsInstant=IntCast2LongAsInstant|true
-detectorInvalidJUnitTest=InvalidJUnitTest|true
-detectorIteratorIdioms=IteratorIdioms|true
-detectorLazyInit=LazyInit|true
-detectorLoadOfKnownNullValue=LoadOfKnownNullValue|true
-detectorLostLoggerDueToWeakReference=LostLoggerDueToWeakReference|true
-detectorMethodReturnCheck=MethodReturnCheck|true
-detectorMultithreadedInstanceAccess=MultithreadedInstanceAccess|true
-detectorMutableEnum=MutableEnum|true
-detectorMutableLock=MutableLock|true
-detectorMutableStaticFields=MutableStaticFields|true
-detectorNaming=Naming|true
-detectorNoteUnconditionalParamDerefs=NoteUnconditionalParamDerefs|true
-detectorNumberConstructor=NumberConstructor|true
-detectorOptionalReturnNull=OptionalReturnNull|true
-detectorOverridingEqualsNotSymmetrical=OverridingEqualsNotSymmetrical|true
-detectorPreferZeroLengthArrays=PreferZeroLengthArrays|true
-detectorPublicSemaphores=PublicSemaphores|false
-detectorQuestionableBooleanAssignment=QuestionableBooleanAssignment|true
-detectorReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass=ReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass|true
-detectorReadReturnShouldBeChecked=ReadReturnShouldBeChecked|true
-detectorRedundantConditions=RedundantConditions|true
-detectorRedundantInterfaces=RedundantInterfaces|true
-detectorRepeatedConditionals=RepeatedConditionals|true
-detectorRuntimeExceptionCapture=RuntimeExceptionCapture|true
-detectorSerializableIdiom=SerializableIdiom|true
-detectorStartInConstructor=StartInConstructor|true
-detectorStaticCalendarDetector=StaticCalendarDetector|true
-detectorStringConcatenation=StringConcatenation|true
-detectorSuperfluousInstanceOf=SuperfluousInstanceOf|true
-detectorSuspiciousThreadInterrupted=SuspiciousThreadInterrupted|true
-detectorSwitchFallthrough=SwitchFallthrough|true
-detectorSynchronizationOnSharedBuiltinConstant=SynchronizationOnSharedBuiltinConstant|true
-detectorSynchronizeAndNullCheckField=SynchronizeAndNullCheckField|true
-detectorSynchronizeOnClassLiteralNotGetClass=SynchronizeOnClassLiteralNotGetClass|true
-detectorSynchronizingOnContentsOfFieldToProtectField=SynchronizingOnContentsOfFieldToProtectField|true
-detectorURLProblems=URLProblems|true
-detectorUncallableMethodOfAnonymousClass=UncallableMethodOfAnonymousClass|true
-detectorUnnecessaryMath=UnnecessaryMath|true
-detectorUnreadFields=UnreadFields|true
-detectorUselessSubclassMethod=UselessSubclassMethod|false
-detectorVarArgsProblems=VarArgsProblems|true
-detectorVolatileUsage=VolatileUsage|true
-detectorWaitInLoop=WaitInLoop|true
-detectorWrongMapIterator=WrongMapIterator|true
-detectorXMLFactoryBypass=XMLFactoryBypass|true
-detector_threshold=2
-effort=default
-filter_settings=Medium|BAD_PRACTICE,CORRECTNESS,EXPERIMENTAL,MALICIOUS_CODE,MT_CORRECTNESS,PERFORMANCE,SECURITY,STYLE|false|12
-filter_settings_neg=NOISE,I18N|
-run_at_full_build=false
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7dcd97e..3e36d69 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -706,6 +706,13 @@
 +
 Default is unset, no disk cache.
 
+[[cache.enableDiskStatMetrics]]cache.enableDiskStatMetrics::
++
+Whether to enable the computation of disk statistics of persistent caches.
+This computation is expensive and requires a long time on larger installations.
++
+By default, false.
+
 [[cache.h2CacheSize]]cache.h2CacheSize::
 +
 The size of the in-memory cache for each opened H2 cache database, in bytes.
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 850631f..fa3eb26 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -189,7 +189,9 @@
 
 The `src/test/resources/logback.xml` file
 link:http://logback.qos.ch/manual/configuration.html[configures,role=external,window=_blank]
-Gatling's logging level.
+Gatling's logging level. To quickly
+enable link:https://gatling.io/docs/current/general/debugging#logback[detailed logging] of `http`
+requests and responses, the `root level` can be set to `trace` in that file.
 
 === How to run using Docker
 
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 9dd58b8..bf6bc77 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -114,12 +114,15 @@
 
 [[change-ref]]
 When a commit is pushed for review, Gerrit stores it in a staging area
-which is a branch in the special `refs/changes/` namespace. A change
-ref has the format `refs/changes/XX/YYYY/ZZ` where `YYYY` is the
-numeric change number, `ZZ` is the patch set number and `XX` is the
-last two digits of the numeric change number, e.g.
-`refs/changes/20/884120/1`. Understanding the format of this ref is not
-required for working with Gerrit.
+which is a branch in the special `refs/changes/` namespace. Understanding
+the format of this ref is not required for working with Gerrit, but it
+is explained below.
+
+A change ref has the format `refs/changes/X/Y/Z` where `X` is the last
+two digits of the change number, `Y` is the entire change number, and `Z`
+is the patch set. For example, if the change number is
+link:https://gerrit-review.googlesource.com/c/gerrit/+/263270[263270],
+the ref would be `refs/changes/70/263270/2` for the second patch set.
 
 [[fetch-change]]
 Using the change ref git clients can fetch the corresponding commit,
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 84b1bc8..63c569a 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -922,18 +922,6 @@
 ----
 
 
-[[PublicDomain]]
-PublicDomain
-
-* guice:aopalliance
-
-[[PublicDomain_license]]
-----
-This software has been placed in the public domain by its author(s).
-
-----
-
-
 [[antlr]]
 antlr
 
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index fe5dc06..a8b3330 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -156,6 +156,22 @@
 The submit action, including the title and label, an instance of
 link:rest-api-changes.html#action-info[ActionInfo]
 
+=== commit-container
+The `commit-container` extension point adds content at the end of the commit
+message to the change view.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
 == Dynamic Plugin endpoints
 
 The following endpoints are available to plugins.
diff --git a/Jenkinsfile b/Jenkinsfile
index 5f93803..cafd654 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -90,16 +90,18 @@
     def changedFiles = queryChangedFiles(Globals.gerritUrl)
     def polygerritFiles = changedFiles.findAll { it.startsWith("polygerrit-ui") ||
         it.startsWith("lib/js") }
+    def bazelFiles = changedFiles.findAll { it == "WORKSPACE" || it.endsWith("BUILD") ||
+        it.endsWith(".bzl") }
 
     if(polygerritFiles.size() > 0) {
-        if(changedFiles.size() == polygerritFiles.size()) {
+        if(changedFiles.size() == polygerritFiles.size() && bazelFiles.isEmpty()) {
             println "Only PolyGerrit UI changes detected, skipping other test modes..."
             Builds.modes = ["polygerrit"]
         } else {
             println "PolyGerrit UI changes detected, adding 'polygerrit' validation..."
             Builds.modes += "polygerrit"
         }
-    } else if(changedFiles.contains("WORKSPACE")) {
+    } else if(!bazelFiles.isEmpty()) {
         println "WORKSPACE file changes detected, adding 'polygerrit' validation..."
         Builds.modes += "polygerrit"
     }
@@ -110,10 +112,10 @@
         stage("${buildName}/${mode}") {
             def slaveBuild = null
             for (int i = 1; i <= retryTimes; i++) {
+                postCheck(new GerritCheck(
+                    (buildName == "Gerrit-codestyle") ? "codestyle" : mode,
+                    new Build(currentBuild.getAbsoluteUrl(), null)))
                 try {
-                    postCheck(new GerritCheck(
-                        (buildName == "Gerrit-codestyle") ? "codestyle" : mode,
-                        new Build(currentBuild.getAbsoluteUrl(), null)))
                     slaveBuild = build job: "${buildName}", parameters: [
                         string(name: 'REFSPEC', value: "refs/changes/${env.BRANCH_NAME}"),
                         string(name: 'BRANCH', value: env.GERRIT_PATCHSET_REVISION),
diff --git a/WORKSPACE b/WORKSPACE
index 2c2631f..97fadaf 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -52,7 +52,10 @@
     name = "io_bazel_rules_closure",
     sha256 = "b9c2bc6ba377aa497eb7c31681d34404febf9d4e3c9c7d98ce0d78238a0af20f",
     strip_prefix = "rules_closure-0.31",
-    urls = ["https://github.com/davido/rules_closure/archive/V0.31.tar.gz"],
+    urls = [
+        "https://github.com/davido/rules_closure/archive/V0.31.tar.gz",
+        "https://gerrit-ci.gerritforge.com/lib/V0.31.tar.gz",
+    ],
 )
 
 http_archive(
@@ -153,10 +156,20 @@
 
 GUICE_VERS = "4.2.3"
 
-maven_jar(
-    name = "guice-library",
-    artifact = "com.google.inject:guice:" + GUICE_VERS,
-    sha1 = "2ea992d6d7bdcac7a43111a95d182a4c42eb5ff7",
+GUICE_LIBRARY_SHA256 = "5168f5e7383f978c1b4154ac777b78edd8ac214bb9f9afdb92921c8d156483d3"
+
+http_file(
+    name = "guice-library-no-aop",
+    canonical_id = "guice-library-no-aop-" + GUICE_VERS + ".jar-" + GUICE_LIBRARY_SHA256,
+    downloaded_file_path = "guice-library-no-aop.jar",
+    sha256 = GUICE_LIBRARY_SHA256,
+    urls = [
+        "https://repo1.maven.org/maven2/com/google/inject/guice/" +
+        GUICE_VERS +
+        "/guice-" +
+        GUICE_VERS +
+        "-no_aop.jar",
+    ],
 )
 
 maven_jar(
@@ -172,12 +185,6 @@
 )
 
 maven_jar(
-    name = "aopalliance",
-    artifact = "aopalliance:aopalliance:1.0",
-    sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
-)
-
-maven_jar(
     name = "javax_inject",
     artifact = "javax.inject:javax.inject:1",
     sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
@@ -257,14 +264,17 @@
     sha1 = "6000774d7f8412ced005a704188ced78beeed2bb",
 )
 
+CAFFEINE_GUAVA_SHA256 = "3a66ee3ec70971dee0bae6e56bda7b8742bc4bedd7489161bfbbaaf7137d89e1"
+
 # TODO(davido): Rename guava.jar to caffeine-guava.jar on fetch to prevent potential
 # naming collision between caffeine guava adapater and guava library itself.
 # Remove this renaming procedure, once this upstream issue is fixed:
 # https://github.com/ben-manes/caffeine/issues/364.
 http_file(
     name = "caffeine-guava-renamed",
+    canonical_id = "caffeine-guava-" + CAFFEINE_VERS + ".jar-" + CAFFEINE_GUAVA_SHA256,
     downloaded_file_path = "caffeine-guava-" + CAFFEINE_VERS + ".jar",
-    sha256 = "3a66ee3ec70971dee0bae6e56bda7b8742bc4bedd7489161bfbbaaf7137d89e1",
+    sha256 = CAFFEINE_GUAVA_SHA256,
     urls = [
         "https://repo1.maven.org/maven2/com/github/ben-manes/caffeine/guava/" +
         CAFFEINE_VERS +
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
index 86f9bf1..2389124 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
@@ -1,26 +1,10 @@
 [
   {
-    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "url": "ssh://admin@HOSTNAME:SSH_PORT/_PROJECT",
     "cmd": "clone"
   },
   {
-    "url": "ssh://admin@localhost:29418/loadtest-repo",
-    "cmd": "pull"
-  },
-  {
-    "url": "ssh://admin@localhost:29418/loadtest-repo",
-    "cmd": "push"
-  },
-  {
-    "url": "http://localhost:8080/loadtest-repo",
+    "url": "http://HOSTNAME:HTTP_PORT/_PROJECT",
     "cmd": "clone"
-  },
-  {
-    "url": "http://localhost:8080/loadtest-repo",
-    "cmd": "pull"
-  },
-  {
-    "url": "http://localhost:8080/loadtest-repo",
-    "cmd": "push"
   }
 ]
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
index 86a336d..4623586 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
@@ -28,7 +28,7 @@
     replaceKeyWith("_project", default, in)
   }
 
-  private val test: ScenarioBuilder = scenario(name)
+  private val test: ScenarioBuilder = scenario(unique)
       .feed(data)
       .exec(gitRequest)
 
@@ -40,11 +40,11 @@
       atOnceUsers(1)
     ),
     test.inject(
-      nothingFor(1 second),
+      nothingFor(2 seconds),
       constantUsersPerSec(1) during (2 seconds)
     ),
     deleteProject.test.inject(
-      nothingFor(3 second),
+      nothingFor(6 seconds),
       atOnceUsers(1)
     ),
   ).protocols(gitProtocol, httpProtocol)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
index 931ff02..b6027dd 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
@@ -26,7 +26,7 @@
     this.default = default
   }
 
-  val test: ScenarioBuilder = scenario(name)
+  val test: ScenarioBuilder = scenario(unique)
       .feed(data)
       .exec(httpRequest)
 
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
index bf13f83..f17fbba 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
@@ -26,7 +26,7 @@
     this.default = default
   }
 
-  val test: ScenarioBuilder = scenario(name)
+  val test: ScenarioBuilder = scenario(unique)
       .feed(data)
       .exec(httpRequest)
 
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
index d47af01..906633c 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
@@ -27,8 +27,9 @@
   private val path: String = pack.replaceAllLiterally(".", "/")
   protected val name: String = this.getClass.getSimpleName
   protected val resource: String = s"data/$path/$name.json"
+  protected val unique: String = name + "-" + this.hashCode()
 
-  protected val httpRequest: HttpRequestBuilder = http(name).post("${url}")
+  protected val httpRequest: HttpRequestBuilder = http(unique).post("${url}")
   protected val httpProtocol: HttpProtocolBuilder = http.basicAuth(
     conf.httpConfiguration.userName,
     conf.httpConfiguration.password)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
index 32df1b5..ff49b9a 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
@@ -21,21 +21,37 @@
 import scala.concurrent.duration._
 
 class ReplayRecordsFromFeeder extends GitSimulation {
-  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).circular
+  private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).circular
+  private val default: String = name
 
-  private val test: ScenarioBuilder = scenario(name)
-      .repeat(10000) {
+  override def replaceOverride(in: String): String = {
+    replaceKeyWith("_project", default, in)
+  }
+
+  private val test: ScenarioBuilder = scenario(unique)
+      .repeat(10) {
         feed(data)
             .exec(gitRequest)
       }
 
+  private val createProject = new CreateProject(default)
+  private val deleteProject = new DeleteProject(default)
+
   setUp(
+    createProject.test.inject(
+      atOnceUsers(1)
+    ),
     test.inject(
       nothingFor(4 seconds),
       atOnceUsers(10),
       rampUsers(10) during (5 seconds),
       constantUsersPerSec(20) during (15 seconds),
       constantUsersPerSec(20) during (15 seconds) randomized
-    )).protocols(gitProtocol)
-      .maxDuration(60 seconds)
+    ),
+    deleteProject.test.inject(
+      nothingFor(59 seconds),
+      atOnceUsers(1)
+    ),
+  ).protocols(gitProtocol, httpProtocol)
+      .maxDuration(61 seconds)
 }
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2afdad99..8df9518 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -20,9 +20,13 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
@@ -42,6 +46,7 @@
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
@@ -299,6 +304,7 @@
   @Inject private ProjectIndexCollection projectIndexes;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private SitePaths sitePaths;
+  @Inject private ProjectOperations projectOperations;
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
@@ -414,6 +420,9 @@
       baseConfig.setString("sshd", null, "listenAddress", "off");
     }
 
+    baseConfig.unset("gerrit", null, "canonicalWebUrl");
+    baseConfig.unset("httpd", null, "listenUrl");
+
     baseConfig.setInt("index", null, "batchThreads", -1);
 
     baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
@@ -987,6 +996,16 @@
     }
   }
 
+  protected void blockAnonymousRead() throws Exception {
+    String allRefs = RefNames.REFS + "*";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(allRefs).group(ANONYMOUS_USERS))
+        .add(allow(Permission.READ).ref(allRefs).group(REGISTERED_USERS))
+        .update();
+  }
+
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index b25dcc3..0ef6ad5 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -468,9 +468,13 @@
   private static void mergeTestConfig(Config cfg) {
     String forceEphemeralPort = String.format("%s:0", getLocalHost().getHostName());
     String url = "http://" + forceEphemeralPort + "/";
-    cfg.setString("gerrit", null, "canonicalWebUrl", url);
-    cfg.setString("httpd", null, "listenUrl", url);
 
+    if (cfg.getString("gerrit", null, "canonicalWebUrl") == null) {
+      cfg.setString("gerrit", null, "canonicalWebUrl", url);
+    }
+    if (cfg.getString("httpd", null, "listenUrl") == null) {
+      cfg.setString("httpd", null, "listenUrl", url);
+    }
     if (cfg.getString("sshd", null, "listenAddress") == null) {
       cfg.setString("sshd", null, "listenAddress", forceEphemeralPort);
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 2d023cf..82816fb 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,9 +18,6 @@
 import java.util.regex.Pattern;
 
 public enum ElasticVersion {
-  V6_2("6.2.*"),
-  V6_3("6.3.*"),
-  V6_4("6.4.*"),
   V6_5("6.5.*"),
   V6_6("6.6.*"),
   V6_7("6.7.*"),
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index fd3df97..cd3b27a 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -269,4 +269,9 @@
 
     public abstract Account build();
   }
+
+  @Override
+  public final String toString() {
+    return getName();
+  }
 }
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index ecef87d..867b14d 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -23,6 +23,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 
 /** Projects match a source code repository managed by Gerrit */
 public final class Project {
@@ -247,4 +248,9 @@
   public void setConfigRefState(String state) {
     configRefState = state;
   }
+
+  @Override
+  public String toString() {
+    return Optional.of(getName()).orElse("<null>");
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 284d8f6..8df5343 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
-import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RevertSubmissionInfo;
@@ -187,12 +186,6 @@
       EnumSet<ListChangesOption> listOptions, EnumSet<SubmittedTogetherOption> submitOptions)
       throws RestApiException;
 
-  /** Publishes a draft change. */
-  @Deprecated
-  default void publish() {
-    throw new UnsupportedOperationException("draft workflow is discontinued");
-  }
-
   /** Rebase the current revision of a change using default options. */
   default void rebase() throws RestApiException {
     rebase(new RebaseInput());
@@ -273,17 +266,6 @@
   }
 
   /**
-   * Retrieve change edit when exists.
-   *
-   * @deprecated Replaced by {@link ChangeApi#edit()} in combination with {@link
-   *     ChangeEditApi#get()}.
-   */
-  @Deprecated
-  default EditInfo getEdit() throws RestApiException {
-    return edit().get().orElse(null);
-  }
-
-  /**
    * Provides access to an API regarding the change edit of this change.
    *
    * @return a {@code ChangeEditApi} for the change edit of this change
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 7ae570f..ff9fb3c 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -38,11 +38,6 @@
 import java.util.Set;
 
 public interface RevisionApi {
-  @Deprecated
-  default void delete() {
-    throw new UnsupportedOperationException("draft workflow is discontinued");
-  }
-
   String description() throws RestApiException;
 
   void description(String description) throws RestApiException;
@@ -62,11 +57,6 @@
 
   BinaryResult submitPreview(String format) throws RestApiException;
 
-  @Deprecated
-  default void publish() {
-    throw new UnsupportedOperationException("draft workflow is discontinued");
-  }
-
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
 
   ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
diff --git a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
index 9befe16a..ef0ced6 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
@@ -20,10 +20,11 @@
 import java.lang.management.OperatingSystemMXBean;
 import java.util.Arrays;
 
-@SuppressWarnings("restriction")
 class OperatingSystemMXBeanFactory {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private OperatingSystemMXBeanFactory() {}
+
   static OperatingSystemMXBeanInterface create() {
     OperatingSystemMXBean sys = ManagementFactory.getOperatingSystemMXBean();
     if (sys instanceof UnixOperatingSystemMXBean) {
diff --git a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanUnixNative.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanUnixNative.java
index a7f5bba..fbde058 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanUnixNative.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanUnixNative.java
@@ -16,7 +16,6 @@
 
 import com.sun.management.UnixOperatingSystemMXBean;
 
-@SuppressWarnings("restriction")
 class OperatingSystemMXBeanUnixNative implements OperatingSystemMXBeanInterface {
   private final UnixOperatingSystemMXBean sys;
 
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 2d3cf2f..a831b8e 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -76,6 +76,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
 import com.google.gerrit.server.restapi.group.GroupModule;
 import com.google.gerrit.server.rules.DefaultSubmitRule;
 import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
@@ -167,6 +168,7 @@
     install(PureRevertCache.module());
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
+    factory(ChangeIsVisibleToPredicate.Factory.class);
     factory(ProjectState.Factory.class);
 
     // Submit rules
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index 5d30952..12194e7 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -25,10 +25,12 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Set;
+import org.eclipse.jgit.lib.Config;
 
 @Singleton
 public class CacheMetrics {
@@ -36,7 +38,8 @@
       Field.ofString("cache_name", Metadata.Builder::cacheName).build();
 
   @Inject
-  public CacheMetrics(MetricMaker metrics, DynamicMap<Cache<?, ?>> cacheMap) {
+  public CacheMetrics(
+      MetricMaker metrics, DynamicMap<Cache<?, ?>> cacheMap, @GerritServerConfig Config config) {
     CallbackMetric1<String, Long> memEnt =
         metrics.newCallbackMetric(
             "caches/memory_cached",
@@ -81,7 +84,8 @@
             memEnt.set(name, c.size());
             memHit.set(name, cstats.hitRate() * 100);
             memEvict.set(name, cstats.evictionCount());
-            if (c instanceof PersistentCache) {
+            if (c instanceof PersistentCache
+                && config.getBoolean("cache", "enableDiskStatMetrics", false)) {
               PersistentCache.DiskStats d = ((PersistentCache) c).diskStats();
               perDiskEnt.set(name, d.size());
               perDiskHit.set(name, hitRatio(d));
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 1f8075a..e447f2b 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -173,6 +173,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
 import com.google.gerrit.server.quota.QuotaEnforcer;
@@ -263,6 +264,7 @@
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeJson.AssistedFactory.class);
+    factory(ChangeIsVisibleToPredicate.Factory.class);
     factory(LabelsJson.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 72a3f07..7535f51 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -141,23 +141,25 @@
       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
       ProjectState projectState =
           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
-      return new CommitValidators(
-          ImmutableList.of(
-              new UploadMergesPermissionValidator(perm),
-              new ProjectStateValidationListener(projectState),
-              new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
-              new AuthorUploaderValidator(user, perm, urlFormatter.get()),
-              new FileCountValidator(repoManager, config),
-              new CommitterUploaderValidator(user, perm, urlFormatter.get()),
-              new SignedOffByValidator(user, perm, projectState),
+      ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
+      validators
+          .add(new UploadMergesPermissionValidator(perm))
+          .add(new ProjectStateValidationListener(projectState))
+          .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
+          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
+          .add(new FileCountValidator(repoManager, config))
+          .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()))
+          .add(new SignedOffByValidator(user, perm, projectState))
+          .add(
               new ChangeIdValidator(
-                  projectState, user, urlFormatter.get(), config, sshInfo, change),
-              new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
-              new BannedCommitsValidator(rejectCommits),
-              new PluginCommitValidationListener(pluginValidators, skipValidation),
-              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
-              new AccountCommitValidator(repoManager, allUsers, accountValidator),
-              new GroupCommitValidator(allUsers)));
+                  projectState, user, urlFormatter.get(), config, sshInfo, change))
+          .add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
+          .add(new BannedCommitsValidator(rejectCommits))
+          .add(new PluginCommitValidationListener(pluginValidators, skipValidation))
+          .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker))
+          .add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
+          .add(new GroupCommitValidator(allUsers));
+      return new CommitValidators(validators.build());
     }
 
     public CommitValidators forGerritCommits(
@@ -170,21 +172,23 @@
       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
       ProjectState projectState =
           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
-      return new CommitValidators(
-          ImmutableList.of(
-              new UploadMergesPermissionValidator(perm),
-              new ProjectStateValidationListener(projectState),
-              new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
-              new AuthorUploaderValidator(user, perm, urlFormatter.get()),
-              new FileCountValidator(repoManager, config),
-              new SignedOffByValidator(user, perm, projectState),
+      ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
+      validators
+          .add(new UploadMergesPermissionValidator(perm))
+          .add(new ProjectStateValidationListener(projectState))
+          .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
+          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
+          .add(new FileCountValidator(repoManager, config))
+          .add(new SignedOffByValidator(user, perm, projectState))
+          .add(
               new ChangeIdValidator(
-                  projectState, user, urlFormatter.get(), config, sshInfo, change),
-              new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
-              new PluginCommitValidationListener(pluginValidators),
-              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
-              new AccountCommitValidator(repoManager, allUsers, accountValidator),
-              new GroupCommitValidator(allUsers)));
+                  projectState, user, urlFormatter.get(), config, sshInfo, change))
+          .add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
+          .add(new PluginCommitValidationListener(pluginValidators))
+          .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker))
+          .add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
+          .add(new GroupCommitValidator(allUsers));
+      return new CommitValidators(validators.build());
     }
 
     public CommitValidators forMergedCommits(
@@ -205,12 +209,13 @@
       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
       ProjectState projectState =
           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
-      return new CommitValidators(
-          ImmutableList.of(
-              new UploadMergesPermissionValidator(perm),
-              new ProjectStateValidationListener(projectState),
-              new AuthorUploaderValidator(user, perm, urlFormatter.get()),
-              new CommitterUploaderValidator(user, perm, urlFormatter.get())));
+      ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
+      validators
+          .add(new UploadMergesPermissionValidator(perm))
+          .add(new ProjectStateValidationListener(projectState))
+          .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
+          .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()));
+      return new CommitValidators(validators.build());
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 5aa45db..22d332a 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -366,7 +366,7 @@
     }
   }
 
-  /** Users who have non-zero approval codes on the change. */
+  /** Users who were added as reviewers to this change. */
   protected void ccExistingReviewers() {
     if (!NotifyHandling.ALL.equals(notify.handling())
         && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 3c4af0b..69ac93e 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -29,6 +29,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
@@ -76,7 +77,7 @@
     PermissionBackend.ForRef perm = permissionBackend.user(user.get()).ref(branch);
     if (object instanceof RevCommit) {
       perm.check(RefPermission.CREATE);
-      checkCreateCommit(repo, (RevCommit) object, ps.getNameKey(), perm);
+      checkCreateCommit(user, repo, (RevCommit) object, ps.getNameKey(), perm);
     } else if (object instanceof RevTag) {
       RevTag tag = (RevTag) object;
       try (RevWalk rw = new RevWalk(repo)) {
@@ -96,7 +97,7 @@
 
       RevObject target = tag.getObject();
       if (target instanceof RevCommit) {
-        checkCreateCommit(repo, (RevCommit) target, ps.getNameKey(), perm);
+        checkCreateCommit(user, repo, (RevCommit) target, ps.getNameKey(), perm);
       } else {
         checkCreateRef(user, repo, branch, target);
       }
@@ -117,7 +118,11 @@
    * new commit to the repository.
    */
   private void checkCreateCommit(
-      Repository repo, RevCommit commit, Project.NameKey project, PermissionBackend.ForRef forRef)
+      Provider<? extends CurrentUser> user,
+      Repository repo,
+      RevCommit commit,
+      Project.NameKey project,
+      PermissionBackend.ForRef forRef)
       throws AuthException, PermissionBackendException, IOException {
     try {
       // If the user has update (push) permission, they can create the ref regardless
@@ -131,7 +136,8 @@
         project,
         repo,
         commit,
-        repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS))) {
+        repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS),
+        Optional.of(user.get()))) {
       // If the user has no push permissions, check whether the object is
       // merged into a branch or tag readable by this user. If so, they are
       // not effectively "pushing" more objects, so they can create the ref
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 57e9a7e..331b7da 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
@@ -28,6 +29,7 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -55,10 +57,20 @@
    */
   public boolean fromRefs(
       Project.NameKey project, Repository repo, RevCommit commit, List<Ref> refs) {
+    return fromRefs(project, repo, commit, refs, Optional.empty());
+  }
+
+  boolean fromRefs(
+      Project.NameKey project,
+      Repository repo,
+      RevCommit commit,
+      List<Ref> refs,
+      Optional<CurrentUser> optionalUserProvider) {
     try (RevWalk rw = new RevWalk(repo)) {
       Collection<Ref> filtered =
-          permissionBackend
-              .currentUser()
+          optionalUserProvider
+              .map(permissionBackend::user)
+              .orElse(permissionBackend.currentUser())
               .project(project)
               .filter(refs, repo, RefFilterOptions.defaults());
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 2e5c33b..077131b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -30,12 +30,17 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
 import java.util.Optional;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
 public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  public interface Factory {
+    ChangeIsVisibleToPredicate forUser(CurrentUser user);
+  }
+
   protected final ChangeNotes.Factory notesFactory;
   protected final CurrentUser user;
   protected final PermissionBackend permissionBackend;
@@ -45,10 +50,10 @@
   @Inject
   public ChangeIsVisibleToPredicate(
       ChangeNotes.Factory notesFactory,
-      CurrentUser user,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
-      Provider<AnonymousUser> anonymousUserProvider) {
+      Provider<AnonymousUser> anonymousUserProvider,
+      @Assisted CurrentUser user) {
     super(ChangeQueryBuilder.FIELD_VISIBLETO, IndexUtils.describe(user));
     this.notesFactory = notesFactory;
     this.user = user;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index f5cb458..400db67 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -49,7 +49,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.mail.Address;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -75,7 +74,6 @@
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -217,7 +215,6 @@
     final ChangeData.Factory changeDataFactory;
     final ChangeIndex index;
     final ChangeIndexRewriter rewriter;
-    final ChangeNotes.Factory notesFactory;
     final CommentsUtil commentsUtil;
     final ConflictsCache conflictsCache;
     final DynamicMap<ChangeHasOperandFactory> hasOperands;
@@ -233,7 +230,7 @@
     final StarredChangesUtil starredChangesUtil;
     final SubmitDryRun submitDryRun;
     final GroupMembers groupMembers;
-    final Provider<AnonymousUser> anonymousUserProvider;
+    final ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory;
     final OperatorAliasConfig operatorAliasConfig;
     final boolean indexMergeable;
     final HasOperandAliasConfig hasOperandAliasConfig;
@@ -250,7 +247,6 @@
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         PermissionBackend permissionBackend,
-        ChangeNotes.Factory notesFactory,
         ChangeData.Factory changeDataFactory,
         CommentsUtil commentsUtil,
         AccountResolver accountResolver,
@@ -268,10 +264,10 @@
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
         GroupMembers groupMembers,
-        Provider<AnonymousUser> anonymousUserProvider,
         OperatorAliasConfig operatorAliasConfig,
         @GerritServerConfig Config gerritConfig,
-        HasOperandAliasConfig hasOperandAliasConfig) {
+        HasOperandAliasConfig hasOperandAliasConfig,
+        ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory) {
       this(
           queryProvider,
           rewriter,
@@ -280,7 +276,6 @@
           userFactory,
           self,
           permissionBackend,
-          notesFactory,
           changeDataFactory,
           commentsUtil,
           accountResolver,
@@ -298,10 +293,10 @@
           starredChangesUtil,
           accountCache,
           groupMembers,
-          anonymousUserProvider,
           operatorAliasConfig,
           MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(),
-          hasOperandAliasConfig);
+          hasOperandAliasConfig,
+          changeIsVisbleToPredicateFactory);
     }
 
     private Arguments(
@@ -312,7 +307,6 @@
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         PermissionBackend permissionBackend,
-        ChangeNotes.Factory notesFactory,
         ChangeData.Factory changeDataFactory,
         CommentsUtil commentsUtil,
         AccountResolver accountResolver,
@@ -330,17 +324,16 @@
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
         GroupMembers groupMembers,
-        Provider<AnonymousUser> anonymousUserProvider,
         OperatorAliasConfig operatorAliasConfig,
         boolean indexMergeable,
-        HasOperandAliasConfig hasOperandAliasConfig) {
+        HasOperandAliasConfig hasOperandAliasConfig,
+        ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory) {
       this.queryProvider = queryProvider;
       this.rewriter = rewriter;
       this.opFactories = opFactories;
       this.userFactory = userFactory;
       this.self = self;
       this.permissionBackend = permissionBackend;
-      this.notesFactory = notesFactory;
       this.changeDataFactory = changeDataFactory;
       this.commentsUtil = commentsUtil;
       this.accountResolver = accountResolver;
@@ -359,7 +352,7 @@
       this.accountCache = accountCache;
       this.hasOperands = hasOperands;
       this.groupMembers = groupMembers;
-      this.anonymousUserProvider = anonymousUserProvider;
+      this.changeIsVisbleToPredicateFactory = changeIsVisbleToPredicateFactory;
       this.operatorAliasConfig = operatorAliasConfig;
       this.indexMergeable = indexMergeable;
       this.hasOperandAliasConfig = hasOperandAliasConfig;
@@ -374,7 +367,6 @@
           userFactory,
           Providers.of(otherUser),
           permissionBackend,
-          notesFactory,
           changeDataFactory,
           commentsUtil,
           accountResolver,
@@ -392,10 +384,10 @@
           starredChangesUtil,
           accountCache,
           groupMembers,
-          anonymousUserProvider,
           operatorAliasConfig,
           indexMergeable,
-          hasOperandAliasConfig);
+          hasOperandAliasConfig,
+          changeIsVisbleToPredicateFactory);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -1018,12 +1010,7 @@
   }
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
-    return new ChangeIsVisibleToPredicate(
-        args.notesFactory,
-        user,
-        args.permissionBackend,
-        args.projectCache,
-        args.anonymousUserProvider);
+    return args.changeIsVisbleToPredicateFactory.forUser(user);
   }
 
   public Predicate<ChangeData> isVisible() throws QueryParseException {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index f9263a9..40c0477 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryProcessor;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
@@ -40,9 +39,6 @@
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.HashMap;
@@ -58,11 +54,8 @@
 public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
     implements DynamicOptions.BeanReceiver, DynamicOptions.BeanProvider {
   private final Provider<CurrentUser> userProvider;
-  private final ChangeNotes.Factory notesFactory;
   private final ImmutableListMultimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin;
-  private final PermissionBackend permissionBackend;
-  private final ProjectCache projectCache;
-  private final Provider<AnonymousUser> anonymousUserProvider;
+  private final ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory;
   private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
 
   static {
@@ -80,11 +73,8 @@
       IndexConfig indexConfig,
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
-      ChangeNotes.Factory notesFactory,
       DynamicSet<ChangeAttributeFactory> attributeFactories,
-      PermissionBackend permissionBackend,
-      ProjectCache projectCache,
-      Provider<AnonymousUser> anonymousUserProvider) {
+      ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory) {
     super(
         metricMaker,
         ChangeSchemaDefinitions.INSTANCE,
@@ -94,10 +84,7 @@
         FIELD_LIMIT,
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
-    this.notesFactory = notesFactory;
-    this.permissionBackend = permissionBackend;
-    this.projectCache = projectCache;
-    this.anonymousUserProvider = anonymousUserProvider;
+    this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
 
     ImmutableListMultimap.Builder<String, ChangeAttributeFactory> factoriesBuilder =
         ImmutableListMultimap.builder();
@@ -144,14 +131,7 @@
   @Override
   protected Predicate<ChangeData> enforceVisibility(Predicate<ChangeData> pred) {
     return new AndChangeSource(
-        pred,
-        new ChangeIsVisibleToPredicate(
-            notesFactory,
-            userProvider.get(),
-            permissionBackend,
-            projectCache,
-            anonymousUserProvider),
-        start);
+        pred, changeIsVisibleToPredicateFactory.forUser(userProvider.get()), start);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 2037049..7008bb9 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -91,6 +91,7 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.AddReviewersEmail;
+import com.google.gerrit.server.change.AddReviewersOp.Result;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -426,10 +427,15 @@
       List<Address> toByEmail = new ArrayList<>();
       List<Address> ccByEmail = new ArrayList<>();
       for (ReviewerAddition addition : reviewerAdditions) {
-        if (addition.state() == ReviewerState.REVIEWER) {
+        Result reviewAdditionResult = addition.op.getResult();
+        if (addition.state() == ReviewerState.REVIEWER
+            && (!reviewAdditionResult.addedReviewers().isEmpty()
+                || !reviewAdditionResult.addedReviewersByEmail().isEmpty())) {
           to.addAll(addition.reviewers);
           toByEmail.addAll(addition.reviewersByEmail);
-        } else if (addition.state() == ReviewerState.CC) {
+        } else if (addition.state() == ReviewerState.CC
+            && (!reviewAdditionResult.addedCCs().isEmpty()
+                || !reviewAdditionResult.addedCCsByEmail().isEmpty())) {
           cc.addAll(addition.reviewers);
           ccByEmail.addAll(addition.reviewersByEmail);
         }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index b087f15..5cfb118 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -108,8 +108,11 @@
       boolean isSigned = isAnnotated && input.message.contains("-----BEGIN PGP SIGNATURE-----\n");
       if (isSigned) {
         throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\"");
-      } else if (isAnnotated && !check(perm, RefPermission.CREATE_TAG)) {
-        throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
+      } else if (isAnnotated) {
+        if (!check(perm, RefPermission.CREATE_TAG)) {
+          throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
+        }
+
       } else {
         perm.check(RefPermission.CREATE);
       }
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 8592dc7..b8b8b55 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -89,20 +89,20 @@
   private final Map<QueryKey, ImmutableList<ChangeData>> queryCache;
   private final Map<BranchNameKey, Optional<RevCommit>> heads;
   private final ProjectCache projectCache;
-  private final ChangeIsVisibleToPredicate changeIsVisibleToPredicate;
+  private final ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory;
 
   @Inject
   LocalMergeSuperSetComputation(
       PermissionBackend permissionBackend,
       Provider<InternalChangeQuery> queryProvider,
       ProjectCache projectCache,
-      ChangeIsVisibleToPredicate changeIsVisibleToPredicate) {
+      ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory) {
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.queryProvider = queryProvider;
     this.queryCache = new HashMap<>();
     this.heads = new HashMap<>();
-    this.changeIsVisibleToPredicate = changeIsVisibleToPredicate;
+    this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
   }
 
   @Override
@@ -150,7 +150,8 @@
           walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
       Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
 
-      ChangeSet partialSet = byCommitsOnBranchNotMerged(or, b, visibleHashes, nonVisibleHashes);
+      ChangeSet partialSet =
+          byCommitsOnBranchNotMerged(or, b, visibleHashes, nonVisibleHashes, user);
       Iterables.addAll(visibleChanges, partialSet.changes());
       Iterables.addAll(nonVisibleChanges, partialSet.nonVisibleChanges());
     }
@@ -207,13 +208,19 @@
   }
 
   private ChangeSet byCommitsOnBranchNotMerged(
-      OpenRepo or, BranchNameKey branch, Set<String> visibleHashes, Set<String> nonVisibleHashes)
+      OpenRepo or,
+      BranchNameKey branch,
+      Set<String> visibleHashes,
+      Set<String> nonVisibleHashes,
+      CurrentUser user)
       throws IOException {
     List<ChangeData> potentiallyVisibleChanges =
         byCommitsOnBranchNotMerged(or, branch, visibleHashes);
     List<ChangeData> invisibleChanges =
         new ArrayList<>(byCommitsOnBranchNotMerged(or, branch, nonVisibleHashes));
     List<ChangeData> visibleChanges = new ArrayList<>(potentiallyVisibleChanges.size());
+    ChangeIsVisibleToPredicate changeIsVisibleToPredicate =
+        changeIsVisibleToPredicateFactory.forUser(user);
     for (ChangeData cd : potentiallyVisibleChanges) {
       if (changeIsVisibleToPredicate.match(cd)) {
         visibleChanges.add(cd);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b8d20ab..47508bb 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -90,6 +90,7 @@
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -884,6 +885,99 @@
   }
 
   @Test
+  public void addExistingReviewersUsingPostReview() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    // First reviewer added to the change
+    ReviewInput input = new ReviewInput();
+    input.reviewers = new ArrayList<>(1);
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email();
+    input.reviewers.add(addReviewerInput);
+    gApi.changes().id(r.getChangeId()).current().review(input);
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message message = messages.get(0);
+    assertThat(message.rcpt()).containsExactly(user.getNameEmail());
+    assertMailReplyTo(message, admin.email());
+
+    sender.clear();
+
+    // Second reviewer and existing reviewer added to the change
+    ReviewInput input2 = new ReviewInput();
+    input2.reviewers = new ArrayList<>(2);
+    AddReviewerInput addReviewerInput2 = new AddReviewerInput();
+    addReviewerInput2.reviewer = user.email();
+    input2.reviewers.add(addReviewerInput2);
+    AddReviewerInput addReviewerInput3 = new AddReviewerInput();
+
+    TestAccount user2 = accountCreator.user2();
+    addReviewerInput3.reviewer = user2.email();
+    input2.reviewers.add(addReviewerInput3);
+
+    gApi.changes().id(r.getChangeId()).current().review(input2);
+    List<Message> messages2 = sender.getMessages();
+    assertThat(messages2).hasSize(1);
+    Message message2 = messages2.get(0);
+    assertThat(message2.rcpt()).containsExactly(user.getNameEmail(), user2.getNameEmail());
+    assertMailReplyTo(message, admin.email());
+
+    sender.clear();
+
+    // Existing reviewers re-added to the change: no notifications
+    ReviewInput input3 = new ReviewInput();
+    input3.reviewers = new ArrayList<>(2);
+    AddReviewerInput addReviewerInput4 = new AddReviewerInput();
+    addReviewerInput4.reviewer = user.email();
+    input3.reviewers.add(addReviewerInput4);
+    AddReviewerInput addReviewerInput5 = new AddReviewerInput();
+
+    addReviewerInput5.reviewer = user2.email();
+    input3.reviewers.add(addReviewerInput5);
+
+    gApi.changes().id(r.getChangeId()).current().review(input3);
+    List<Message> messages3 = sender.getMessages();
+    assertThat(messages3).isEmpty();
+  }
+
+  @Test
+  public void addExistingReviewersUsingAddReviewer() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    // First reviewer added to the change
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email();
+    gApi.changes().id(r.getChangeId()).addReviewer(addReviewerInput);
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message message = messages.get(0);
+    assertThat(message.rcpt()).containsExactly(user.getNameEmail());
+    assertMailReplyTo(message, admin.email());
+
+    sender.clear();
+
+    // Second reviewer added to the change
+    TestAccount user2 = accountCreator.user2();
+    AddReviewerInput addReviewerInput2 = new AddReviewerInput();
+    addReviewerInput2.reviewer = user2.email();
+    gApi.changes().id(r.getChangeId()).addReviewer(addReviewerInput2);
+    List<Message> messages2 = sender.getMessages();
+    assertThat(messages2).hasSize(1);
+    Message message2 = messages2.get(0);
+    assertThat(message2.rcpt()).containsExactly(user.getNameEmail(), user2.getNameEmail());
+    assertMailReplyTo(message2, admin.email());
+
+    sender.clear();
+
+    // Exiting reviewer re-added to the change: no notifications
+    AddReviewerInput addReviewerInput3 = new AddReviewerInput();
+    addReviewerInput3.reviewer = user2.email();
+    gApi.changes().id(r.getChangeId()).addReviewer(addReviewerInput3);
+    List<Message> messages3 = sender.getMessages();
+    assertThat(messages3).isEmpty();
+  }
+
+  @Test
   public void suggestAccounts() throws Exception {
     AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
     try (Registration registration =
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
similarity index 98%
rename from javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
rename to javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
index d3c0855..dcee118 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
@@ -22,7 +22,6 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -53,13 +52,18 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.junit.Before;
 import org.junit.Test;
 
-@NoHttpd
-public class SubmitOnPushIT extends AbstractDaemonTest {
+public abstract class AbstractSubmitOnPush extends AbstractDaemonTest {
   @Inject private ApprovalsUtil approvalsUtil;
   @Inject private ProjectOperations projectOperations;
 
+  @Before
+  public void blockAnonymous() throws Exception {
+    blockAnonymousRead();
+  }
+
   @Test
   public void submitOnPush() throws Exception {
     projectOperations
diff --git a/javatests/com/google/gerrit/acceptance/git/HttpSubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/HttpSubmitOnPushIT.java
new file mode 100644
index 0000000..3733415
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/HttpSubmitOnPushIT.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2020 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.acceptance.git;
+
+import com.google.gerrit.acceptance.GitUtil;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.Before;
+
+public class HttpSubmitOnPushIT extends AbstractSubmitOnPush {
+
+  @Before
+  public void cloneProjectOverHttp() throws Exception {
+    CredentialsProvider.setDefault(
+        new UsernamePasswordCredentialsProvider(admin.username(), admin.httpPassword()));
+    testRepo = GitUtil.cloneProject(project, admin.getHttpUrl(server) + "/a/" + project.get());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java b/javatests/com/google/gerrit/acceptance/git/SshSubmitOnPushIT.java
similarity index 61%
copy from javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
copy to javatests/com/google/gerrit/acceptance/git/SshSubmitOnPushIT.java
index 20d83a0..3a18257 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SshSubmitOnPushIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2020 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.
@@ -12,12 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest.project;
+package com.google.gerrit.acceptance.git;
 
-public class PushLightweightTagIT extends AbstractPushTag {
+import com.google.gerrit.acceptance.NoHttpd;
+import org.junit.Before;
 
-  @Override
-  protected TagType getTagType() {
-    return TagType.LIGHTWEIGHT;
+@NoHttpd
+public class SshSubmitOnPushIT extends AbstractSubmitOnPush {
+
+  @Before
+  public void cloneProjectOverSsh() throws Exception {
+    testRepo = cloneProject(project, admin);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractHttpPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractHttpPushTag.java
new file mode 100644
index 0000000..5e2af14
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractHttpPushTag.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2020 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.acceptance.rest.project;
+
+import com.google.gerrit.acceptance.GitUtil;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.Before;
+
+public abstract class AbstractHttpPushTag extends AbstractPushTag {
+
+  @Before
+  public void cloneProjectOverHttp() throws Exception {
+    // clone with user to avoid inherited tag permissions of admin user
+    CredentialsProvider.setDefault(
+        new UsernamePasswordCredentialsProvider(user.username(), user.httpPassword()));
+    testRepo = GitUtil.cloneProject(project, user.getHttpUrl(server) + "/" + project.get());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index 79494c5..d70d120 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -28,7 +28,6 @@
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.RefNames;
@@ -41,7 +40,6 @@
 import org.junit.Before;
 import org.junit.Test;
 
-@NoHttpd
 public abstract class AbstractPushTag extends AbstractDaemonTest {
   enum TagType {
     LIGHTWEIGHT(Permission.CREATE),
@@ -61,11 +59,9 @@
 
   @Before
   public void setUpTestEnvironment() throws Exception {
-    // clone with user to avoid inherited tag permissions of admin user
-    testRepo = cloneProject(project, user);
-
     initialHead = projectOperations.project(project).getHead("master");
     tagType = getTagType();
+    blockAnonymousRead();
   }
 
   protected abstract TagType getTagType();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractSshPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractSshPushTag.java
new file mode 100644
index 0000000..35297ae
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractSshPushTag.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2020 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.acceptance.rest.project;
+
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.UseSsh;
+import org.junit.Before;
+
+@NoHttpd
+@UseSsh
+public abstract class AbstractSshPushTag extends AbstractPushTag {
+  @Before
+  public void cloneProjectOverSsh() throws Exception {
+    // clone with user to avoid inherited tag permissions of admin user
+    testRepo = cloneProject(project, user);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index b50a12b..54ae5af 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -57,7 +57,9 @@
     name = "push_tag_util",
     testonly = True,
     srcs = [
+        "AbstractHttpPushTag.java",
         "AbstractPushTag.java",
+        "AbstractSshPushTag.java",
     ],
     deps = [
         "//java/com/google/gerrit/acceptance:lib",
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/HttpPushAnnotatedTagIT.java
similarity index 85%
rename from javatests/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java
rename to javatests/com/google/gerrit/acceptance/rest/project/HttpPushAnnotatedTagIT.java
index 24c8ed0..bb08f41 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/HttpPushAnnotatedTagIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-public class PushAnnotatedTagIT extends AbstractPushTag {
+public class HttpPushAnnotatedTagIT extends AbstractHttpPushTag {
 
   @Override
   protected TagType getTagType() {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/HttpPushLightweightTagIT.java
similarity index 85%
rename from javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
rename to javatests/com/google/gerrit/acceptance/rest/project/HttpPushLightweightTagIT.java
index 20d83a0..c89c332 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/HttpPushLightweightTagIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-public class PushLightweightTagIT extends AbstractPushTag {
+public class HttpPushLightweightTagIT extends AbstractHttpPushTag {
 
   @Override
   protected TagType getTagType() {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SshPushAnnotatedTagIT.java
similarity index 85%
copy from javatests/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java
copy to javatests/com/google/gerrit/acceptance/rest/project/SshPushAnnotatedTagIT.java
index 24c8ed0..df7636a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SshPushAnnotatedTagIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-public class PushAnnotatedTagIT extends AbstractPushTag {
+public class SshPushAnnotatedTagIT extends AbstractSshPushTag {
 
   @Override
   protected TagType getTagType() {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SshPushLightweightTagIT.java
similarity index 75%
copy from javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
copy to javatests/com/google/gerrit/acceptance/rest/project/SshPushLightweightTagIT.java
index 20d83a0..bcf7eb9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SshPushLightweightTagIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2020 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.
@@ -14,7 +14,12 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-public class PushLightweightTagIT extends AbstractPushTag {
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.UseSsh;
+
+@NoHttpd
+@UseSsh
+public class SshPushLightweightTagIT extends AbstractSshPushTag {
 
   @Override
   protected TagType getTagType() {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 3becb81..f5d2db4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -38,10 +38,13 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
+import java.util.Arrays;
 import java.util.List;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
@@ -66,6 +69,16 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  @Before
+  public void setupPermissions() throws Exception {
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      ProjectConfig cfg = u.getConfig();
+      removeAllBranchPermissions(
+          cfg, Permission.CREATE, Permission.CREATE_TAG, Permission.CREATE_SIGNED_TAG);
+      u.save();
+    }
+  }
+
   @Test
   public void listTagsOfNonExistingProject() throws Exception {
     assertThrows(
@@ -455,4 +468,10 @@
         .add(allow(Permission.CREATE_SIGNED_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
         .update();
   }
+
+  private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
+    cfg.getAccessSections().stream()
+        .filter(s -> s.getName().startsWith("refs/tags/"))
+        .forEach(s -> Arrays.stream(permissions).forEach(s::removePermission));
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/BUILD b/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
new file mode 100644
index 0000000..d1a64c0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_httpd",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java b/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
new file mode 100644
index 0000000..1dea800
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/httpd/HttpLogoutServletIT.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2020 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.acceptance.server.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpLogoutServletIT extends StandaloneSiteTest {
+  private static final String LOCALHOST = InetAddress.getLoopbackAddress().getHostName();
+
+  @ConfigSuite.Config
+  public static Config secondConfig() throws IOException {
+    Config cfg = new Config();
+    cfg.setString("auth", null, "logouturl", "/test-logout");
+    cfg.setString("gerrit", null, "canonicalWebUrl", "https://" + LOCALHOST + ":8443/");
+    cfg.setString("httpd", null, "listenUrl", "proxy-https://" + LOCALHOST + ":" + getFreePort());
+    return cfg;
+  }
+
+  @Inject @GerritServerConfig private Config gerritConfig;
+
+  private HttpClient httpClient;
+
+  @Before
+  public void setUp() {
+    httpClient = HttpClientBuilder.create().disableRedirectHandling().build();
+  }
+
+  @Test
+  public void shouldHonourCanonicalWebUrlProxyWhenRedirectAfterLogout() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+
+      URIish listenUrl = new URIish(gerritConfig.getString("httpd", null, "listenUrl"));
+      URIish canonicalWebUrl =
+          new URIish(gerritConfig.getString("gerrit", null, "canonicalWebUrl"));
+
+      String logoutPath =
+          Optional.ofNullable(baseConfig.getString("auth", null, "logouturl")).orElse("/");
+
+      HttpGet getLogout = new HttpGet("/logout");
+      getLogout.addHeader("X-Forwarded-Host", canonicalWebUrl.getHost());
+      getLogout.addHeader("X-Forwarded-Port", "" + canonicalWebUrl.getPort());
+      getLogout.addHeader("X-Forwarded-Proto", canonicalWebUrl.getScheme());
+
+      HttpResponse logoutResponse =
+          httpClient.execute(new HttpHost(listenUrl.getHost(), listenUrl.getPort()), getLogout);
+
+      assertThat(logoutResponse.getStatusLine().getStatusCode())
+          .isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY);
+      assertThat(getLocationHeaderURIish(logoutResponse))
+          .containsExactly(canonicalWebUrl.setPath(logoutPath));
+    }
+  }
+
+  private List<URIish> getLocationHeaderURIish(HttpResponse logoutResponse) {
+    return Arrays.stream(logoutResponse.getHeaders("Location"))
+        .map(h -> h.getValue())
+        .map(HttpLogoutServletIT::unsafeNewURIish)
+        .filter(u -> u.isPresent())
+        .map(u -> u.get())
+        .collect(Collectors.toList());
+  }
+
+  private static Optional<URIish> unsafeNewURIish(String uri) {
+    try {
+      return Optional.of(new URIish(uri));
+    } catch (URISyntaxException e) {
+      return Optional.empty();
+    }
+  }
+
+  private static int getFreePort() throws IOException {
+    try (ServerSocket s = new ServerSocket(0)) {
+      return s.getLocalPort();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 879c7c5..e4b45ed 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -38,12 +38,6 @@
 
   private static String getImageName(ElasticVersion version) {
     switch (version) {
-      case V6_2:
-        return "blacktop/elasticsearch:6.2.4";
-      case V6_3:
-        return "blacktop/elasticsearch:6.3.2";
-      case V6_4:
-        return "blacktop/elasticsearch:6.4.3";
       case V6_5:
         return "blacktop/elasticsearch:6.5.4";
       case V6_6:
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 20c8a17..e36ff0b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -22,15 +22,6 @@
 public class ElasticVersionTest {
   @Test
   public void supportedVersion() throws Exception {
-    assertThat(ElasticVersion.forVersion("6.2.0")).isEqualTo(ElasticVersion.V6_2);
-    assertThat(ElasticVersion.forVersion("6.2.4")).isEqualTo(ElasticVersion.V6_2);
-
-    assertThat(ElasticVersion.forVersion("6.3.0")).isEqualTo(ElasticVersion.V6_3);
-    assertThat(ElasticVersion.forVersion("6.3.2")).isEqualTo(ElasticVersion.V6_3);
-
-    assertThat(ElasticVersion.forVersion("6.4.0")).isEqualTo(ElasticVersion.V6_4);
-    assertThat(ElasticVersion.forVersion("6.4.1")).isEqualTo(ElasticVersion.V6_4);
-
     assertThat(ElasticVersion.forVersion("6.5.0")).isEqualTo(ElasticVersion.V6_5);
     assertThat(ElasticVersion.forVersion("6.5.1")).isEqualTo(ElasticVersion.V6_5);
 
@@ -79,9 +70,6 @@
 
   @Test
   public void atLeastMinorVersion() throws Exception {
-    assertThat(ElasticVersion.V6_2.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
-    assertThat(ElasticVersion.V6_3.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
-    assertThat(ElasticVersion.V6_4.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V6_5.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V6_6.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V6_7.isAtLeastMinorVersion(ElasticVersion.V6_7)).isTrue();
@@ -97,9 +85,6 @@
 
   @Test
   public void version6OrLater() throws Exception {
-    assertThat(ElasticVersion.V6_2.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V6_3.isV6OrLater()).isTrue();
-    assertThat(ElasticVersion.V6_4.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_5.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_6.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_7.isV6OrLater()).isTrue();
@@ -115,9 +100,6 @@
 
   @Test
   public void version7OrLater() throws Exception {
-    assertThat(ElasticVersion.V6_2.isV7OrLater()).isFalse();
-    assertThat(ElasticVersion.V6_3.isV7OrLater()).isFalse();
-    assertThat(ElasticVersion.V6_4.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_5.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_6.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_7.isV7OrLater()).isFalse();
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 7f133ce..a936d28 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -44,7 +44,6 @@
             null,
             null,
             null,
-            null,
             indexes,
             null,
             null,
@@ -53,8 +52,8 @@
             null,
             null,
             null,
-            null,
             new Config(),
+            null,
             null));
   }
 
diff --git a/lib/LICENSE-PublicDomain b/lib/LICENSE-PublicDomain
deleted file mode 100644
index 8a71ce0..0000000
--- a/lib/LICENSE-PublicDomain
+++ /dev/null
@@ -1 +0,0 @@
-This software has been placed in the public domain by its author(s).
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index f73984b..14179d6 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -1,4 +1,9 @@
-load("@rules_java//java:defs.bzl", "java_library")
+load("@rules_java//java:defs.bzl", "java_import", "java_library")
+
+java_import(
+    name = "guice-library-no-aop",
+    jars = ["@guice-library-no-aop//file"],
+)
 
 java_library(
     name = "guice",
@@ -14,8 +19,7 @@
     name = "guice-library",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
-    exports = ["@guice-library//jar"],
-    runtime_deps = ["aopalliance"],
+    exports = [":guice-library-no-aop"],
 )
 
 java_library(
@@ -35,12 +39,6 @@
 )
 
 java_library(
-    name = "aopalliance",
-    data = ["//lib:LICENSE-PublicDomain"],
-    exports = ["@aopalliance//jar"],
-)
-
-java_library(
     name = "javax_inject",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
diff --git a/resources/com/google/gerrit/server/commit-msg_test.sh b/resources/com/google/gerrit/server/commit-msg_test.sh
index c016a89..d797be3 100755
--- a/resources/com/google/gerrit/server/commit-msg_test.sh
+++ b/resources/com/google/gerrit/server/commit-msg_test.sh
@@ -63,9 +63,9 @@
 @@ -38,6 +38,7 @@
  context
  line
-
+ 
 +hello, world
-
+ 
  context
  line
 EOF
@@ -111,7 +111,7 @@
 }
 
 # Change-Id goes after existing trailers.
-function test_at_start {
+function test_at_end {
   cat << EOF > input
 bla bla
 
@@ -119,16 +119,16 @@
 EOF
 
   ${hook} input || fail "failed hook execution"
-  result=$(git interpret-trailers --parse input | head -1 | grep ^Change-Id)
+  result=$(tail -1 input | grep ^Change-Id)
   if [[ -z "${result}" ]] ; then
     echo "after: "
     cat input
 
-    fail "did not find Change-Id at start"
+    fail "did not find Change-Id at end"
   fi
 }
 
-function test_dash_at_start {
+function test_dash_at_end {
   if [[ ! -x /bin/dash ]] ; then
     echo "/bin/dash not installed; skipping dash test."
     return
@@ -142,12 +142,12 @@
 
   /bin/dash ${hook} input || fail "failed hook execution"
 
-  result=$(git interpret-trailers --parse input | head -1 | grep ^Change-Id)
+  result=$(tail -1 input | grep ^Change-Id)
   if [[ -z "${result}" ]] ; then
     echo "after: "
     cat input
 
-    fail "did not find Change-Id at start"
+    fail "did not find Change-Id at end"
   fi
 }
 
diff --git a/resources/com/google/gerrit/server/tools/root/TOC b/resources/com/google/gerrit/server/tools/root/TOC
index 647b5aa..7a48c64 100644
--- a/resources/com/google/gerrit/server/tools/root/TOC
+++ b/resources/com/google/gerrit/server/tools/root/TOC
@@ -5,4 +5,6 @@
 
 755 hooks/commit-msg
 
+755 hooks/commit-msg-legacy
+
 755 scripts/reposize.sh
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 2b1a2fc..2901232 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -50,7 +50,7 @@
 
 # Avoid the --in-place option which only appeared in Git 2.8
 # Avoid the --if-exists option which only appeared in Git 2.15
-if ! git -c trailer.ifexists=doNothing interpret-trailers --where start \
+if ! git -c trailer.ifexists=doNothing interpret-trailers \
       --trailer "Change-Id: I${random}" < "$1" > "${dest}" ; then
   echo "cannot insert change-id line in $1"
   exit 1
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg-legacy b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg-legacy
new file mode 100755
index 0000000..4c64559
--- /dev/null
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg-legacy
@@ -0,0 +1,193 @@
+#!/bin/sh
+#
+# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
+#
+# Copyright (C) 2009 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.
+#
+
+unset GREP_OPTIONS
+
+CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test|Feature|Fixes|Fixed"
+MSG="$1"
+
+# Check for, and add if missing, a unique Change-Id
+#
+add_ChangeId() {
+	clean_message=`sed -e '
+		/^diff --git .*/{
+			s///
+			q
+		}
+		/^Signed-off-by:/d
+		/^#/d
+	' "$MSG" | git stripspace`
+	if test -z "$clean_message"
+	then
+		return
+	fi
+
+	# Do not add Change-Id to temp commits
+	if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!'
+	then
+		return
+	fi
+
+	if test "false" = "`git config --bool --get gerrit.createChangeId`"
+	then
+		return
+	fi
+
+	# Does Change-Id: already exist? if so, exit (no change).
+	if grep -i '^Change-Id:' "$MSG" >/dev/null
+	then
+		return
+	fi
+
+	id=`_gen_ChangeId`
+	T="$MSG.tmp.$$"
+	AWK=awk
+	if [ -x /usr/xpg4/bin/awk ]; then
+		# Solaris AWK is just too broken
+		AWK=/usr/xpg4/bin/awk
+	fi
+
+	# Get core.commentChar from git config or use default symbol
+	commentChar=`git config --get core.commentChar`
+	commentChar=${commentChar:-#}
+
+	# How this works:
+	# - parse the commit message as (textLine+ blankLine*)*
+	# - assume textLine+ to be a footer until proven otherwise
+	# - exception: the first block is not footer (as it is the title)
+	# - read textLine+ into a variable
+	# - then count blankLines
+	# - once the next textLine appears, print textLine+ blankLine* as these
+	#   aren't footer
+	# - in END, the last textLine+ block is available for footer parsing
+	$AWK '
+	BEGIN {
+		if (match(ENVIRON["OS"], "Windows")) {
+			RS="\r?\n" # Required on recent Cygwin
+		}
+		# while we start with the assumption that textLine+
+		# is a footer, the first block is not.
+		isFooter = 0
+		footerComment = 0
+		blankLines = 0
+	}
+
+	# Skip lines starting with commentChar without any spaces before it.
+	/^'"$commentChar"'/ { next }
+
+	# Skip the line starting with the diff command and everything after it,
+	# up to the end of the file, assuming it is only patch data.
+	# If more than one line before the diff was empty, strip all but one.
+	/^diff --git / {
+		blankLines = 0
+		while (getline) { }
+		next
+	}
+
+	# Count blank lines outside footer comments
+	/^$/ && (footerComment == 0) {
+		blankLines++
+		next
+	}
+
+	# Catch footer comment
+	/^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) {
+		footerComment = 1
+	}
+
+	/]$/ && (footerComment == 1) {
+		footerComment = 2
+	}
+
+	# We have a non-blank line after blank lines. Handle this.
+	(blankLines > 0) {
+		print lines
+		for (i = 0; i < blankLines; i++) {
+			print ""
+		}
+
+		lines = ""
+		blankLines = 0
+		isFooter = 1
+		footerComment = 0
+	}
+
+	# Detect that the current block is not the footer
+	(footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) {
+		isFooter = 0
+	}
+
+	{
+		# We need this information about the current last comment line
+		if (footerComment == 2) {
+			footerComment = 0
+		}
+		if (lines != "") {
+			lines = lines "\n";
+		}
+		lines = lines $0
+	}
+
+	# Footer handling:
+	# If the last block is considered a footer, splice in the Change-Id at the
+	# right place.
+	# Look for the right place to inject Change-Id by considering
+	# CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first,
+	# then Change-Id, then everything else (eg. Signed-off-by:).
+	#
+	# Otherwise just print the last block, a new line and the Change-Id as a
+	# block of its own.
+	END {
+		unprinted = 1
+		if (isFooter == 0) {
+			print lines "\n"
+			lines = ""
+		}
+		changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):"
+		numlines = split(lines, footer, "\n")
+		for (line = 1; line <= numlines; line++) {
+			if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) {
+				unprinted = 0
+				print "Change-Id: I'"$id"'"
+			}
+			print footer[line]
+		}
+		if (unprinted) {
+			print "Change-Id: I'"$id"'"
+		}
+	}' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
+}
+_gen_ChangeIdInput() {
+	echo "tree `git write-tree`"
+	if parent=`git rev-parse "HEAD^0" 2>/dev/null`
+	then
+		echo "parent $parent"
+	fi
+	echo "author `git var GIT_AUTHOR_IDENT`"
+	echo "committer `git var GIT_COMMITTER_IDENT`"
+	echo
+	printf '%s' "$clean_message"
+}
+_gen_ChangeId() {
+	_gen_ChangeIdInput |
+	git hash-object -t commit --stdin
+}
+
+
+add_ChangeId
diff --git a/tools/BUILD b/tools/BUILD
index 7f890b1..5159177 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -70,6 +70,7 @@
         "-Xep:NullableConstructor:ERROR",
         "-Xep:NullablePrimitive:ERROR",
         "-Xep:NullableVoid:ERROR",
+        "-Xep:ObjectToString:ERROR",
         "-Xep:OperatorPrecedence:ERROR",
         "-Xep:OverridesGuiceInjectableMethod:ERROR",
         "-Xep:PreconditionsInvalidPlaceholder:ERROR",
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 14b5109..7de7ec9 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -120,18 +120,18 @@
         sha1 = "dc13ae4faca6df981fc7aeb5a522d9db446d5d50",
     )
 
-    TESTCONTAINERS_VERSION = "1.13.0"
+    TESTCONTAINERS_VERSION = "1.14.0"
 
     maven_jar(
         name = "testcontainers",
         artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-        sha1 = "c92d1094d2b227e881f66bf09872c46d91ce9ac5",
+        sha1 = "c0d6aea93f4f7ff4b0d559e31308340eaa398798",
     )
 
     maven_jar(
         name = "testcontainers-elasticsearch",
         artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-        sha1 = "09353bd960b3f0d26ba7652ae57bb4ac46d043a2",
+        sha1 = "6df7bc7cb5e99c6d9528ea28dd16dbb042b1beec",
     )
 
     maven_jar(