Merge branch 'stable-2.11' into stable-2.12

* stable-2.11:
  Upgrade JGit to 4.5.5.201812240535-r

Change-Id: I2f3bce590b9ddbd3a66ae070c125680ea8243eca
diff --git a/.buckconfig b/.buckconfig
index e4a19f1..51318f3 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -1,5 +1,4 @@
 [alias]
-  all = //:all
   api = //:api
   api_deploy = //tools/maven:api_deploy
   api_install = //tools/maven:api_install
@@ -9,9 +8,11 @@
   docs = //Documentation:searchfree
   firefox = //:firefox
   gerrit = //:gerrit
+  headless = //:headless
   release = //:release
   safari = //:safari
   soyc = //gerrit-gwtui:ui_soyc
+  soyc_r = //gerrit-gwtui:ui_soyc_r
   withdocs = //:withdocs
 
 [buildfile]
@@ -25,4 +26,4 @@
 
 [cache]
   mode = dir
-  dir = ~/.gerritcodereview/buck-cache/cache
+  dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
diff --git a/.buckversion b/.buckversion
index 9c09744..9daac2c 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-79d36de9f5284f6e833cca81867d6088a25685fb
+1b03b4313b91b634bd604fc3487a05f877e59dee
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1f149cf
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+# http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
index c30cee6..32a1826 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,5 +19,7 @@
 /local.properties
 *.pyc
 /gwt-unitCache
+.DS_Store
 *.swp
 *.asc
+/bin/
diff --git a/.mailmap b/.mailmap
index 75aea08..c8e2f82 100644
--- a/.mailmap
+++ b/.mailmap
@@ -1,21 +1,43 @@
-Adrian Görler <adrian.goerler@sap.com>        Adrian Goerler <adrian.goerler@sap.com>
-Alex Ryazantsev <alex.ryazantsev@gmail.com>   alex <alex.ryazantsev@gmail.com>
-Alex Ryazantsev <alex.ryazantsev@gmail.com>   alex.ryazantsev <alex.ryazantsev@gmail.com>
-Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com> carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
-Deniz Türkoglu <deniz@spotify.com>            Deniz Turkoglu <deniz@spotify.com>
-Deniz Türkoglu <deniz@spotify.com>            Deniz Türkoglu <deniz@spotify.com>
-Edwin Kempin <edwin.kempin@sap.com>           Edwin Kempin <edwin.kempin@gmail.com>
-Hugo Arès <hugo.ares@ericsson.com>            Hugo Ares <hugo.ares@ericsson.com>
-Jason Huntley <jhuntley@houghtonassociates.com> jhuntley <jhuntley@houghtonassociates.com>
-Johan Björk <jbjoerk@gmail.com>               Johan Bjork <phb@spotify.com>
+Adrian Görler <adrian.goerler@sap.com>                                                      Adrian Goerler <adrian.goerler@sap.com>
+Ahaan Ugale <ahaanugale@gmail.com>                                                          <augale@codeaurora.org>
+Alex Blewitt <alex.blewitt@gmail.com>                                                       <alex.blewitt@gs.com>
+Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex <alex.ryazantsev@gmail.com>
+Alex Ryazantsev <alex.ryazantsev@gmail.com>                                                 alex.ryazantsev <alex.ryazantsev@gmail.com>
+Brad Larson <bklarson@gmail.com>                                                            <brad.larson@garmin.com>
+Bruce Zu <bruce.zu@sonymobile.com>                                                          <bruce.zu@sonyericsson.com>
+Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com>                           carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
+David Ostrovsky <david@ostrovsky.org>                                                       <d.ostrovsky@gmx.de>
+Deniz Türkoglu <deniz@spotify.com>                                                          Deniz Türkoglu <deniz@spotify.com>
+Deniz Türkoglu <deniz@spotify.com>                                                          Deniz Turkoglu <deniz@spotify.com>
+Edwin Kempin <edwin.kempin@sap.com>                                                         Edwin Kempin <edwin.kempin@gmail.com>
+Eryk Szymanski <eryksz@gmail.com>                                                           <eryksz@google.com>
+Fredrik Luthander <fredrik.luthander@sonymobile.com>                                        <fredrik@gandaraj.com>
+Fredrik Luthander <fredrik.luthander@sonymobile.com>                                        <fredrik.luthander@sonyericsson.com>
+Gustaf Lundh <gustaf.lundh@sonymobile.com>                                                  <gustaf.lundh@sonyericsson.com>
+Hugo Arès <hugo.ares@ericsson.com>                                                          Hugo Ares <hugo.ares@ericsson.com>
+Jason Huntley <jhuntley@houghtonassociates.com>                                             jhuntley <jhuntley@houghtonassociates.com>
+Jiří Engelthaler <EngyCZ@gmail.com>                                                         <engycz@gmail.com>
+Joe Onorato <onoratoj@gmail.com>                                                            <joeo@android.com>
+Johan Björk <jbjoerk@gmail.com>                                                             Johan Bjork <phb@spotify.com>
 Lincoln Oliveira Campos Do Nascimento <lincoln.oliveiracamposdonascimento@sonyericsson.com> lincoln <lincoln.oliveiracamposdonascimento@sonyericsson.com>
-Mônica Dionísio <monica.dionisio@sonyericsson.com> monica.dionisio <monica.dionisio@sonyericsson.com>
-Peter Jönsson <peter.joensson@gmail.com>      Peter Jönsson <peter.joensson@gmail.com>
-Rafael Rabelo Silva <rafael.rabelosilva@sonyericsson.com> rafael.rabelosilva <rafael.rabelosilva@sonyericsson.com>
-Saša Živkov <sasa.zivkov@sap.com>             Sasa Zivkov <sasa.zivkov@sap.com>
-Saša Živkov <sasa.zivkov@sap.com>             Saša Živkov <zivkov@gmail.com>
-Shawn Pearce <sop@google.com>                 Shawn O. Pearce <sop@google.com>
-Tomas Westling <thomas.westling@sonyericsson.com> thomas.westling <thomas.westling@sonyericsson.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjolin <ulrik.sjolin@gmail.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
+Luca Milanesio <luca.milanesio@gmail.com>                                                   <luca@gitent-scm.com>
+Magnus Bäck <baeck@google.com>                                                              <magnus.back@sonyericsson.com>
+Mark Derricutt <mark.derricutt@smxemail.com>                                                <mark@talios.com>
+Martin Fick <mfick@codeaurora.org>                                                          <mogulguy10@gmail.com>
+Martin Fick <mfick@codeaurora.org>                                                          <mogulguy@yahoo.com>
+Michael Zhou <moz@google.com>                                                               <zhoumotongxue008@gmail.com>
+Mônica Dionísio <monica.dionisio@sonyericsson.com>                                          monica.dionisio <monica.dionisio@sonyericsson.com>
+Nasser Grainawi <nasser@grainawi.org>                                                       <nasser@codeaurora.org>
+Nasser Grainawi <nasser@grainawi.org>                                                       <nasserg@quicinc.com>
+Peter Jönsson <peter.joensson@gmail.com>                                                    Peter Jönsson <peter.joensson@gmail.com>
+Rafael Rabelo Silva <rafael.rabelosilva@sonyericsson.com>                                   rafael.rabelosilva <rafael.rabelosilva@sonyericsson.com>
+Richard Möhn <richard.moehn@posteo.de>                                                      <richard.moehn@fu-berlin.de>
+Saša Živkov <sasa.zivkov@sap.com>                                                           Sasa Zivkov <sasa.zivkov@sap.com>
+Saša Živkov <sasa.zivkov@sap.com>                                                           Saša Živkov <zivkov@gmail.com>
+Shawn Pearce <sop@google.com>                                                               Shawn O. Pearce <sop@google.com>
+Tomas Westling <thomas.westling@sonyericsson.com>                                           thomas.westling <thomas.westling@sonyericsson.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>                                                <ulrik.sjolin@gmail.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>                                                Ulrik Sjolin <ulrik.sjolin@gmail.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>                                                Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>                                                Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
+Zalán Blénessy <zalanb@axis.com>                                                            Zalan Blenessy <zalanb@axis.com>
diff --git a/.settings/edu.umd.cs.findbugs.core.prefs b/.settings/edu.umd.cs.findbugs.core.prefs
new file mode 100644
index 0000000..4dfcf2d
--- /dev/null
+++ b/.settings/edu.umd.cs.findbugs.core.prefs
@@ -0,0 +1,143 @@
+#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/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 0fa494d..8f5678f 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -5,9 +5,17 @@
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
 org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
 org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
 org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.doc.comment.support=enabled
 org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
 org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
 org.eclipse.jdt.core.compiler.problem.deadCode=warning
@@ -16,7 +24,8 @@
 org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
 org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
 org.eclipse.jdt.core.compiler.problem.emptyStatement=warning
-org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning
 org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
 org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
 org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
@@ -28,12 +37,25 @@
 org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
 org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
 org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private
 org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
 org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
 org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
 org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
 org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
 org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag
+org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=protected
 org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning
 org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
 org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
@@ -77,6 +99,7 @@
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
 org.eclipse.jdt.core.compiler.problem.unusedImport=warning
 org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
 org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
@@ -93,6 +116,7 @@
 org.eclipse.jdt.core.compiler.source=1.7
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
@@ -103,15 +127,18 @@
 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
 org.eclipse.jdt.core.formatter.blank_lines_after_package=1
 org.eclipse.jdt.core.formatter.blank_lines_before_field=0
@@ -131,6 +158,7 @@
 org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
 org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
@@ -147,10 +175,16 @@
 org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
 org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
 org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
 org.eclipse.jdt.core.formatter.compact_else_if=true
 org.eclipse.jdt.core.formatter.continuation_indentation=2
 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
 org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
 org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
 org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
 org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
@@ -162,10 +196,16 @@
 org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
 org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
 org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
 org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
 org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
 org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
@@ -213,6 +253,7 @@
 org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
 org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
 org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
@@ -231,12 +272,14 @@
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
 org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
 org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
 org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
 org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
@@ -260,6 +303,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
 org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
@@ -287,6 +331,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
@@ -315,6 +360,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
 org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
 org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
 org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
@@ -324,6 +370,7 @@
 org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
 org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
 org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
 org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
@@ -347,5 +394,9 @@
 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
 org.eclipse.jdt.core.formatter.tabulation.char=space
 org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
 org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
 org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
+org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
index d4218a5..d990610 100644
--- a/.settings/org.eclipse.jdt.ui.prefs
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -1,10 +1,9 @@
-#Wed Jul 29 11:31:38 PDT 2009
 eclipse.preferences.version=1
 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
 formatter_profile=_Google Format
-formatter_settings_version=11
+formatter_settings_version=12
 org.eclipse.jdt.ui.ignorelowercasenames=true
-org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax;
 org.eclipse.jdt.ui.ondemandthreshold=99
 org.eclipse.jdt.ui.staticondemandthreshold=99
 org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
diff --git a/BUCK b/BUCK
index 2cd3fa8..c986874 100644
--- a/BUCK
+++ b/BUCK
@@ -1,13 +1,17 @@
 include_defs('//tools/build.defs')
 
 gerrit_war(name = 'gerrit')
+gerrit_war(name = 'headless', ui = None)
 gerrit_war(name = 'chrome',   ui = 'ui_chrome')
 gerrit_war(name = 'firefox',  ui = 'ui_firefox')
 gerrit_war(name = 'safari',   ui = 'ui_safari')
 gerrit_war(name = 'withdocs', docs = True)
-gerrit_war(name = 'release',  docs = True, context = ['//plugins:core'],  visibility = ['//tools/maven:'])
+gerrit_war(name = 'release',  ui = 'ui_optdbg_r', docs = True, context = ['//plugins:core'],  visibility = ['//tools/maven:'])
 
 API_DEPS = [
+  '//gerrit-acceptance-framework:acceptance-framework',
+  '//gerrit-acceptance-framework:acceptance-framework-src',
+  '//gerrit-acceptance-framework:acceptance-framework-javadoc',
   '//gerrit-extension-api:extension-api',
   '//gerrit-extension-api:extension-api-src',
   '//gerrit-extension-api:extension-api-javadoc',
@@ -19,22 +23,7 @@
   '//gerrit-plugin-gwtui:gwtui-api-javadoc',
 ]
 
-genrule(
+zip_file(
   name = 'api',
-  cmd = ';'.join(
-    ['cd $TMP'] +
-    ['ln -s $(location %s) .' % n for n in API_DEPS] +
-    ['zip -q0 $OUT *']),
-  deps = API_DEPS,
-  out = 'api.zip',
-)
-
-genrule(
-  name = 'all',
-  cmd = 'echo done >$OUT',
-  deps = [
-    ':api',
-    ':release',
-  ],
-  out = '__fake.all__',
+  srcs = API_DEPS,
 )
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 48a6525..126bf1f 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -3,7 +3,8 @@
 include_defs('//tools/git.defs')
 
 DOC_DIR = 'Documentation'
-MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+JSUI = '//gerrit-gwtui:ui_module'
+MAIN = '//gerrit-pgm:pgm'
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
 
 genasciidoc(
@@ -29,11 +30,22 @@
 
 genrule(
   name = 'licenses.txt',
-  cmd = '$(exe :gen_licenses) >$OUT',
-  deps = [':gen_licenses'] + MAIN,
+  cmd = '$(exe :gen_licenses) --asciidoc '
+    + '--classpath $(classpath %s) ' % MAIN
+    + '--classpath $(classpath %s) ' % JSUI
+    + MAIN + ' ' + JSUI + ' >$OUT',
   out = 'licenses.txt',
 )
 
+# Required by Google for gerrit-review.
+genrule(
+  name = 'js_licenses.txt',
+  cmd = '$(exe :gen_licenses) --partial '
+    + '--classpath $(classpath %s) ' % JSUI
+    + JSUI + ' >$OUT',
+  out = 'js_licenses.txt',
+)
+
 genrule(
   name = 'doc.css',
   srcs = ['doc.css.in'],
diff --git a/Documentation/Makefile b/Documentation/Makefile
deleted file mode 100644
index aed9e90..0000000
--- a/Documentation/Makefile
+++ /dev/null
@@ -1,50 +0,0 @@
-# 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.
-
-ASCIIDOC       ?= asciidoc
-ASCIIDOC_EXTRA ?=
-ASCIIDOC_VER   ?= 8.6.3
-
-all: html
-
-clean:
-	rm -f *.html
-
-ASCIIDOC_EXE := $(shell which $(ASCIIDOC))
-ifeq ($(wildcard $(ASCIIDOC_EXE)),)
-  $(error $(ASCIIDOC) must be available)
-else
-  ASCIIDOC_OK := $(shell expr `asciidoc --version | cut -f2 -d' '` \>= $(ASCIIDOC_VER))
-  ifeq ($(ASCIIDOC_OK),0)
-    $(error $(ASCIIDOC) version $(ASCIIDOC_VER) or higher is required)
-  endif
-endif
-
-DOC_HTML := $(patsubst %.txt,%.html,$(wildcard *.txt))
-REVISION := $(shell git describe HEAD | sed s/^v//)
-
-html: $(DOC_HTML)
-
-$(DOC_HTML): %.html : %.txt
-	@echo "FORMAT $@"
-	@rm -f $@+ $@
-	@$(ASCIIDOC) -a toc \
-		-a data-uri \
-		-a 'revision=$(REVISION)' \
-		-a 'newline=\n' \
-		-b xhtml11 \
-		-f asciidoc.conf \
-		$(ASCIIDOC_EXTRA) \
-		-o $@+ $<
-	@mv $@+ $@
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index acd33c0..0758c5c 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1244,6 +1244,22 @@
 a replication task or a user initiated task such as an upload-pack or
 receive-pack.
 
+[[capability_maintainServer]]
+=== Maintain Server
+
+Allow basic, constrained server maintenance tasks, such as flushing caches and
+reindexing changes. Does not grant arbitrary database access, read/write, or
+ACL management permissions, as might the
+<<capability_administrateServer,administrate server capability>>.
+
+Implies the following capabilities:
+
+* <<capability_flushCaches,Flush Caches>>
+* <<capability_kill,Kill Task>>
+* <<capability_runGC,Run Garbage Collection>>
+* <<capability_viewCaches,View Caches>>
+* <<capability_viewQueue,View Queue>>
+
 [[capability_modifyAccount]]
 === Modify Account
 
diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf
deleted file mode 100644
index 2fe6213..0000000
--- a/Documentation/asciidoc.conf
+++ /dev/null
@@ -1,29 +0,0 @@
-[attributes]
-asterisk=&#42;
-plus=&#43;
-caret=&#94;
-startsb=&#91;
-endsb=&#93;
-tilde=&#126;
-
-[specialsections]
-GERRIT=gerrituplink
-
-[gerrituplink]
-<hr style="
-  height: 2px;
-  color: silver;
-  margin-top: 1.2em;
-  margin-bottom: 0.5em;
-">
-
-[macros]
-(?u)^(?P<name>get)::(?P<target>\S*?)$=#
-
-[get-blockmacro]
-<a id="{target}" onmousedown="javascript:
-  var i =  document.URL.lastIndexOf('/Documentation/');
-  var url = document.URL.substring(0, i) + '{target}';
-  document.getElementById('{target}').href = url;">
-    GET {target} HTTP/1.0
-</a>
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 31b4538..d1108b5 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -21,7 +21,7 @@
   [--empty-commit]
   [--max-object-size-limit <N>]
   [--plugin-config <PARAM> ...]
-  { <NAME> | --name <NAME> }
+  { <NAME> }
 --
 
 == DESCRIPTION
@@ -49,11 +49,6 @@
 	Required; name of the new project to create.  If name ends
 	with `.git` the suffix will be automatically removed.
 
---name::
--n::
-	Deprecated alias for the <NAME> argument. This option may
-	be removed in a future release.
-
 --branch::
 -b::
 	Name of the initial branch(es) in the newly created project.
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index d93d47c..aa9790d 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -21,9 +21,16 @@
 If no options are supplied, defaults to `--all`.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group,
-or in a group that have been granted
-link:access-control.html#capability_flushCaches[the 'Flush Caches' global capability].
+
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_flushCaches[Flush Caches] (any cache
+  except "web_sessions")
+* link:access-control.html#capability_maintainServer[Maintain Server] (any cache
+  including "web_sessions")
+* link:access-control.html#capability_administrateServer[Administrate Server]
+  (any cache including "web_sessions")
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-gc.txt b/Documentation/cmd-gc.txt
index a890f1b..b7388a1 100644
--- a/Documentation/cmd-gc.txt
+++ b/Documentation/cmd-gc.txt
@@ -8,6 +8,7 @@
 'ssh' -p <port> <host> 'gerrit gc'
   [--all]
   [--show-progress]
+  [--aggressive]
   <NAME> ...
 --
 
@@ -44,6 +45,9 @@
 --show-progress::
 	If specified progress information is shown.
 
+--aggressive::
+	If an aggressive garbage collection should be done.
+
 == EXAMPLES
 
 Run the Git garbage collection for the projects 'myProject' and
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 665ff8d..90212fb 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -75,6 +75,9 @@
 link:cmd-review.html[gerrit review]::
 	Verify, approve and/or submit a patch set from the command line.
 
+link:cmd-set-head.html[gerrit set-head]::
+	Change the HEAD reference of a project.
+
 link:cmd-set-reviewers.html[gerrit set-reviewers]::
 	Add or remove reviewers on a change.
 
@@ -117,7 +120,7 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
-link:cmd-index-index.html[gerrit index activate]::
+link:cmd-index-activate.html[gerrit index activate]::
 	Activate the latest index version available.
 
 link:cmd-index-start.html[gerrit index start]::
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 70a695e..0590337 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -128,6 +128,12 @@
 	or invalid value) and votes that are not permitted for the user are
 	silently ignored.
 
+--strict-labels::
+	Require ability to vote on all specified labels before reviewing change.
+	If the vote is invalid (invalid label or invalid name), the vote is not
+	permitted for the user, or the vote is on an outdated or closed patch set,
+	return an error instead of silently discarding the vote.
+
 == ACCESS
 Any user who has configured an SSH key.
 
diff --git a/Documentation/cmd-set-head.txt b/Documentation/cmd-set-head.txt
new file mode 100644
index 0000000..d74caaa
--- /dev/null
+++ b/Documentation/cmd-set-head.txt
@@ -0,0 +1,45 @@
+= gerrit set-head
+
+== NAME
+gerrit set-head - Change a project's HEAD.
+
+== SYNOPSIS
+--
+'ssh' -p <port> <host> 'gerrit set-head' <NAME>
+  --new-head <REF>
+--
+
+== DESCRIPTION
+Modifies a given project's HEAD reference.
+
+The command is argument-safe, that is, if no argument is given the
+previous settings are kept intact.
+
+== ACCESS
+Caller must be an owner of the given project.
+
+== SCRIPTING
+This command is intended to be used in scripts.
+
+== OPTIONS
+<NAME>::
+    Required; name of the project to change the HEAD. If name ends
+    with `.git` the suffix will be automatically removed.
+
+--new-head::
+    Required; name of the ref that should be set as new HEAD. The
+    'refs/heads/' prefix can be omitted.
+
+== EXAMPLES
+Change HEAD of project `example` to `stable-2.11` branch:
+
+====
+    $ ssh -p 29418 review.example.com gerrit set-head example --new-head stable-2.11
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index dd79d8b..5d6ab20 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -30,14 +30,17 @@
 	Width of the output table.
 
 == ACCESS
-The caller must be a member of a group that is granted the
-link:access-control.html#capability_viewCaches[View Caches] capability
-or the link:access-control.html#capability_administrateServer[
-Administrate Server] capability.
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_viewCaches[View Caches]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 The summary information about SSH, threads, tasks, memory and JVM are
 only printed out if the caller is a member of a group that is granted
 the link:access-control.html#capability_administrateServer[Administrate
+Server] or link:access-control.html#capability_maintainServer[Maintain
 Server] capability.
 
 == SCRIPTING
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index a694fb3..81eb174 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -40,6 +40,7 @@
 
 Start::
 	Time (local to the server) that this connection started.
+	Only valid for MINA backend.
 
 Idle::
 	Time since the last data transfer on this connection.
@@ -47,6 +48,7 @@
 	connection keep-alive, but also an encrypted keep alive
 	higher up in the SSH protocol stack.  That higher keep
 	alive resets the idle timer, about once a minute.
+	Only valid for MINA backend.
 
 User::
 	The username of the account that is authenticated on this
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index c754f35..dcdbb07 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -153,6 +153,19 @@
 eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
 created.
 
+=== Project Created
+
+Sent when a new project has been created.
+
+type:: "project-created"
+
+projectName:: The created project name
+
+projectHead:: The created project head name
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
 === Merge Failed
 
 Sent when a change has failed to be merged into the git repository.
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index 624135c..4c8d04a 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -29,7 +29,6 @@
 ====
   [contributor-agreement "Individual"]
     description = If you are going to be contributing code on your own, this is the one you want. You can sign this one online.
-    requireContactInformation = true
     agreementUrl = static/cla_individual.html
     autoVerify = group CLA Accepted - Individual
     accepted = group CLA Accepted - Individual
@@ -52,11 +51,6 @@
 Short text describing the contributor agreement. This text will appear
 when the user selects an agreement.
 
-[[contributor-agreement.name.requireContactInformation]]contributor-agreement.<name>.requireContactInformation::
-+
-True if the user must provide contact information when signing a
-contributor agreement. Default is false.
-
 [[contributor-agreement.name.agreementUrl]]contributor-agreement.<name>.agreementUrl::
 +
 An absolute URL or a relative path to an HTML file containing the text
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
deleted file mode 100644
index e0795be..0000000
--- a/Documentation/config-contact.txt
+++ /dev/null
@@ -1,213 +0,0 @@
-= Gerrit Code Review - Contact Information
-
-To help ensure contributor privacy, but still support gathering of
-contributor agreements as necessary, Gerrit encrypts all offline
-contact information gathered from users.  This data is shipped to
-another server, typically at a different location, to make it more
-difficult for an attacker to obtain.
-
-This feature is optional.  If the crypto APIs aren't installed
-and the `contactstore.url` setting in `gerrit.config` is not set,
-Gerrit will not collect contact information from users.
-
-
-== Setup
-
-Ensure Bouncy Castle Crypto API is available in the web application's
-CLASSPATH (e.g. in `'JETTY_HOME'/lib/plus` for Jetty).  Gerrit needs
-both `bcprov-jdk\*-*.jar` and `bcpg-jdk\*-*.jar` to be provided
-for the contact encryption to work.
-
-* link:http://www.bouncycastle.org/latest_releases.html[Bouncy Castle Crypto API]
-
-Ensure a proper JCE policy file is installed.  By default most
-JRE installations forbid the use of a strong key, resulting in
-SecurityException messages when trying to encrypt the contact data.
-You need to obtain a strong JCE policy file and install it by hand.
-Look for the 'Unlimited Strength Jurisdiction Policy' download.
-
-* link:http://java.sun.com/javase/downloads/index.jsp[Java SE Downloads]
-
-Create a public/private key pair for contact data handling.
-Generate the keys on a protected system, where the resulting
-private key is unlikely to fall into the wrong hands.
-
-====
-  gpg --gen-key
-====
-
-Select to use a `DSA and Elgamal` key type, as the public key will
-be used for data encryption.
-
-The information chosen for name, email and comment fields can be
-anything reasonable which would identify the contact store of this
-Gerrit instance.  It is probably a good idea to not use a real
-person's name here, but instead some sort of organizational role.
-The actual values chosen don't matter later, and are only to help
-document the purpose of the key.
-
-Choose a fairly long expiration period, such as 20 years.  For most
-Gerrit instances, contact data will be written once, and rarely,
-if ever, read back.
-
-Export the public key for Gerrit to use during encryption.  The
-public key must be stored in a file called `contact_information.pub`
-and reside inside of the `site_config` directory.  Armoring it
-during export makes it easier to transport between systems, as
-you can easily copy-and-paste the text.  Gerrit can read both the
-armored and unarmored formats.
-
-====
-  gpg --export --armor KEYEMAIL >$site_path/etc/contact_information.pub
-====
-
-Consider storing the private key with some sort of key escrow
-service within your organization.  Without the private key it
-is impossible to recover contact records.
-
-Install a contact store implementation somewhere to receive
-the contact records.  To be really paranoid, Gerrit always
-ships the data to another HTTP server, preferably over HTTPS.
-Existing open-source server implementations can be found in the
-gerrit-contactstore project.
-
-* link:https://code.google.com/p/gerrit/source/checkout?repo=contactstore[gerrit-contactstore]
-
-Configure `'$site_path'/etc/gerrit.config` with the contact store's
-URL (in `contactstore.url`), and if needed, APPSEC value (in
-`contactstore.appsec`):
-
-====
-  git config --file $site_path/etc/gerrit.config appsec.url https://...
-  git config --file $site_path/etc/gerrit.config appsec.appsec sekret
-====
-
-
-== Contact Store Protocol
-
-To implement a new contact store, the following details are useful.
-
-Gerrit connects to the contact store by sending a standard
-`application/x-www-form-urlencoded` within an HTTP POST request
-sent to the store URL (the exact URL that is in contactstore.url)
-with the following form fields in the body:
-
-* APPSEC
-+
-A shared secret "password" that should be known only to Gerrit
-and the contact store.  The contact store should test this value to
-deter spamming of the contact store by outside parties.  Gerrit reads
-this from contactstore.appsec.
-
-* account_id
-+
-Unique account_id value from the Gerrit database for the account
-the contact information belongs to.  Base 10 integer.
-
-* email
-+
-Preferred email address of the account.  May facilitate lookups in
-the contact store at a future date.  May be omitted or the empty
-string if the user hasn't chosen a preferred email.
-
-* filed
-+
-Seconds since the UNIX epoch of when the contact information
-was filed.  May be omitted or the empty string if Gerrit
-doesn't think the supplied contact information is valid enough.
-
-* data
-+
-Encrypted account data as an armored ASCII blob.  This is usually
-several KB of text data as a single string, with embedded newlines
-to break the lines at about 70-75 characters per line.  Data can
-be decoded using GnuPG with the correct private key.
-
-Upon successful store, the contact store application should respond
-with HTTP status code `200` and a body consisting only of `OK`
-(or `OK\n`).  Any other response code or body is considered to be
-a failure by Gerrit.
-
-Using `https://` for the store URL is *highly* encouraged, as it
-prevents man-in-the-middle attacks from reading the shared secret
-APPSEC token, or messing with the data field.
-
-=== Data Format
-
-Once decrypted the `data` field looks something like the following:
-
-----
-Account-Id: 1001240
-Date: 2009-02-23 20:32:32.852 UTC
-Full-Name: John Doe
-Preferred-Email: jdoe@example.com
-Identity: jd15@some-isp.com
-Identity: jdoe@example.com <http://jdoe.blogger.com/>
-Address:
-	123 Any Street
-	Any Town, Somewhere
-Country: USA
-Phone-Number: +1 (555) 555-1212
-Fax-Number: 555.1200
-----
-
-The fields are as follows:
-
-* `Account-Id`
-+
-Value of the `account_id` field in the metadata database.  This is
-a unique key for this account, and links all data records to it.
-
-* `Date`
-+
-Date and time of when this contact record was submitted by the user.
-Written in an ISO formatted date/time string (`YYYY-MM-DD hh:mm:ss`),
-in the UTC timezone.
-
-* `Full-Name`
-+
-The `full_name` field of the account record when the user submitted
-the contact information.  This should be the user's given name and
-family name.
-
-* `Preferred-Email`
-+
-The `preferred_email` field of the account record when the user
-submitted the contact information.  This should be one of the emails
-listed in the `Identity` field.
-
-* `Identity`
-+
-This field occurs once for each `account_external_id` record
-in the database for this account.  The email address is listed,
-and if the user is using OpenID authentication, the OpenID claimed
-identity follows in brackets (`<...>`).  Identity lines without an
-OpenID identity are usually created by sending an email containing
-a unique hyperlink that the user must visit to setup the identity.
-
-* `Address`
-+
-Free form text, as entered by the user.  This should describe some
-location that physical documents could be sent to, but it is not
-verified, so users can enter pretty much anything here.  Each line
-is prefixed with a single TAB character, but is otherwise exactly
-as entered.
-
-* `Country`
-+
-Free form text, as entered by the user.  This should be some sort
-of country name or ISO country abbreviation, but it is not verified,
-so it can be pretty much anything.
-
-* `Phone-Number`, `Fax-Number`
-+
-Free form text, as entered by the user.  The format here can be
-anything, and as the example shows, may not even be consistent in
-the same record.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index e65861a..bc64d7f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -237,6 +237,13 @@
 +
 Default is -1, permitting infinite time between authentications.
 
+[[auth.registerEmailPrivateKey]]auth.registerEmailPrivateKey::
++
+Private key to use when generating an email verification token.
++
+If not set, a random key is generated when running the
+link:pgm-init.html[site initialization].
+
 [[auth.maxRegisterEmailTokenAge]]auth.maxRegisterEmailTokenAge::
 +
 Time in seconds before an email verification token sent to a user in
@@ -430,8 +437,9 @@
 [[auth.gitBasicAuth]]auth.gitBasicAuth::
 +
 If true then Git over HTTP and HTTP/S traffic is authenticated using
-standard BasicAuth and the credentials are validated using the same
-auth method as configured for the Gerrit Web UI.
+standard BasicAuth and the credentials are validated against the randomly
+generated HTTP password or against LDAP when it is configured as Gerrit
+Web UI authentication method.
 +
 This parameter affects git over HTTP traffic and access to the REST
 API. If set to false then Gerrit will authenticate through DIGEST
@@ -474,6 +482,16 @@
 +
 Default is true.
 
+[[auth.allowRegisterNewEmail]]auth.allowRegisterNewEmail::
++
+Whether users are allowed to register new email addresses.
++
+In addition for the HTTP authentication type
+link:#auth.httpemailheader[auth.httpemailheader] must *not* be set to
+enable registration of new email addresses.
++
+By default, true.
+
 [[cache]]
 === Section cache
 
@@ -862,6 +880,13 @@
 +
 Default is "Submit".
 
+[[change.submitLabelWithParents]]change.submitLabelWithParents::
++
+Label name for the submit button if the change has parents which will
+be submitted together with this change.
++
+Default is "Submit including parents".
+
 [[change.submitTooltip]]change.submitTooltip::
 +
 Tooltip for the submit button.  Variables available for replacement
@@ -871,6 +896,44 @@
 +
 Default is "Submit patch set ${patchSet} into ${branch}".
 
+[[change.submitTooltipAncestors]]change.submitTooltipAncestors::
++
+Tooltip for the submit button if there are ancestors which would
+also be submitted by submitting the change. Additionally to the variables
+as in link:#change.submitTooltip[change.submitTooltip], there is the
+variable `${submitSize}` indicating the number of changes which are
+submitted.
++
+Default is "Submit all ${topicSize} changes of the same topic (${submitSize}
+changes including ancestors and other changes related by topic)".
+
+[[change.submitWholeTopic]]change.submitWholeTopic (*Experimental*)::
++
+Determines if the submit button submits the whole topic instead of
+just the current change.
++
+Default is false.
+
+[[change.submitTopicLabel]]change.submitTopicLabel::
++
+If `change.submitWholeTopic` is set and a change has a topic,
+the label name for the submit button is given here instead of
+the configuration `change.submitLabel`.
++
+Defaults to "Submit whole topic"
+
+[[change.submitTopicTooltip]]change.submitTopicTooltip::
++
+If `change.submitWholeTopic` is configuerd to true and a change has a
+topic, this configuration determines the tooltip for the submit button
+instead of `change.submitTooltip`. The variable `${topicSize}` is available
+for the number of changes in the same topic to be submitted. The number of
+all changes to be submitted is in the variable `${submitSize}`.
++
+Defaults to "Submit all ${topicSize} changes of the same topic
+(${submitSize} changes including ancestors and other
+changes related by topic)".
+
 [[change.replyLabel]]change.replyLabel::
 +
 Label name for the reply button. In the user interface an ellipsis (…)
@@ -886,6 +949,82 @@
 Default is "Reply and score". In the user interface it becomes "Reply
 and score (Shortcut: a)".
 
+[[changeCleanup]]
+=== Section changeCleanup
+
+This section allows to configure change cleanups and schedules them to
+run periodically.
+
+[[changeCleanup.abandonAfter]]changeCleanup.abandonAfter::
++
+Period of inactivity after which open changes should be abandoned
+automatically.
++
+By default `0`, never abandon open changes.
++
+[WARNING] Auto-Abandoning changes may confuse/annoy users. When
+enabling this, make sure to choose a reasonably large grace period and
+inform users in advance.
++
+The following suffixes are supported to define the time unit:
++
+* `d, day, days`
+* `w, week, weeks` (`1 week` is treated as `7 days`)
+* `mon, month, months` (`1 month` is treated as `30 days`)
+* `y, year, years` (`1 year` is treated as `365 days`)
+
+[[changeCleanup.abandonIfMergeable]]changeCleanup.abandonIfMergeable::
++
+Whether changes which are mergeable should be auto-abandoned.
++
+By default `true`.
+
+[[changeCleanup.abandonMessage]]changeCleanup.abandonMessage::
++
+Change message that should be posted when a change is abandoned.
++
+'${URL}' can be used as a placeholder for the Gerrit web URL.
++
+By default "Auto-Abandoned due to inactivity, see
+${URL}Documentation/user-change-cleanup.html#auto-abandon\n\n
+If this change is still wanted it should be restored.".
+
+[[changeCleanup.startTime]]changeCleanup.startTime::
++
+Start time to define the first execution of the change cleanups.
+If the configured `'changeCleanup.interval'` is shorter than
+`'changeCleanup.startTime - now'` the start time will be preponed by
+the maximum integral multiple of `'changeCleanup.interval'` so that the
+start time is still in the future.
++
+----
+<day of week> <hours>:<minutes>
+or
+<hours>:<minutes>
+
+<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
+<hours>       : 00-23
+<minutes>     : 0-59
+----
+
+
+[[changeCleanup.interval]]changeCleanup.interval::
++
+Interval for periodic repetition of triggering the change cleanups.
+The interval must be larger than zero. The following suffixes are supported
+to define the time unit for the interval:
++
+* `s, sec, second, seconds`
+* `m, min, minute, minutes`
+* `h, hr, hour, hours`
+* `d, day, days`
+* `w, week, weeks` (`1 week` is treated as `7 days`)
+* `mon, month, months` (`1 month` is treated as `30 days`)
+* `y, year, years` (`1 year` is treated as `365 days`)
+
+link:#schedule-examples[Schedule examples] can be found in the
+link:#gc[gc] section.
+
 [[changeMerge]]
 === Section changeMerge
 
@@ -937,7 +1076,7 @@
 ----
 [commentlink "changeid"]
   match = (I[0-9a-f]{8,40})
-  link = "#q,$1"
+  link = "#/q/$1"
 
 [commentlink "bugzilla"]
   match = "(bug\\s+#?)(\\d+)"
@@ -1011,22 +1150,6 @@
 link:rest-api-projects.html#get-config[REST API].
 
 
-[[contactstore]]
-=== Section contactstore
-
-[[contactstore.url]]contactstore.url::
-+
-URL of the web based contact store Gerrit will send any offline
-contact information to when it collects the data from users as part
-of a contributor agreement.
-+
-See link:config-contact.html[Contact Information].
-
-[[contactstore.appsec]]contactstore.appsec::
-+
-Shared secret of the web based contact store.
-
-
 [[container]]
 === Section container
 
@@ -1193,7 +1316,7 @@
 [[core.useRecursiveMerge]]core.useRecursiveMerge::
 +
 Use JGit's recursive merger for three-way merges. This only affects
-projects configured to automatically resolve conflicts.
+projects that allow content merges.
 +
 As explained in this
 link:http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html[
@@ -1223,21 +1346,37 @@
 used to automatically create correct database.driver and database.url
 values to open the connection.
 +
-* `POSTGRESQL`
+* `DB2`
 +
-Connect to a PostgreSQL database server.
+Connect to a DB2 database server.
++
+* `DERBY`
++
+Connect to an Apache Derby database server.
 +
 * `H2`
 +
 Connect to a local embedded H2 database.
 +
+* `JDBC`
++
+Connect using a JDBC driver class name and URL.
++
+* `MAXDB`
++
+Connect to an SAP MaxDb database server.
++
 * `MYSQL`
 +
 Connect to a MySQL database server.
 +
-* `JDBC`
+* `ORACLE`
 +
-Connect using a JDBC driver class name and URL.
+Connect to an Oracle database server.
++
+* `POSTGRESQL`
++
+Connect to a PostgreSQL database server.
 
 +
 If not specified, database.driver and database.url are used as-is,
@@ -1430,10 +1569,35 @@
 If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
 downloads are allowed.
 
+[[download.checkForHiddenChangeRefs]]download.checkForHiddenChangeRefs::
++
+Whether the download commands should be adapted when the change refs
+are hidden.
++
+Git has a configuration option to hide refs from the initial
+advertisement (`uploadpack.hideRefs`). This option can be used to hide
+the change refs from the client. As consequence fetching changes by
+change ref does not work anymore. However by setting
+`uploadpack.allowTipSha1InWant` to `true` fetching changes by commit ID
+is possible. If `download.checkForHiddenChangeRefs` is set to `true`
+the git download commands use the commit ID instead of the change ref
+when a project is configured like this.
++
+Example git configuration on a project:
++
+----
+[uploadpack]
+  hideRefs = refs/changes/
+  hideRefs = refs/cache-automerge/
+  allowTipSha1InWant = true
+----
++
+By default `false`.
+
 [[download.archive]]download.archive::
 +
 Specifies which archive formats, if any, should be offered on the change
-screen:
+screen and supported for `git-upload-archive` operation:
 +
 ----
 [download]
@@ -1441,11 +1605,17 @@
   archive = tbz2
   archive = tgz
   archive = txz
+  archive = zip
 ----
 
 If `download.archive` is not specified defaults to all archive
 commands. Set to `off` or empty string to disable.
 
+Zip is not supported because it may be interpreted by a Java plugin as a
+valid JAR file, whose code would have access to cookies on the domain.
+For this reason `zip` format is always excluded from formats offered
+through the `Download` drop down or accessible in the REST API.
+
 [[gc]]
 === Section gc
 
@@ -1453,6 +1623,15 @@
 to run periodically. It will be triggered and executed sequentially for all
 projects.
 
+[[gc.aggressive]]gc.aggressive::
++
+Determines if scheduled garbage collections and garbage collections triggered
+through Web-UI should run in aggressive mode or not. Aggressive garbage
+collections are more expensive but may lead to significantly smaller
+repositories.
++
+Valid values are "true" and "false," default is "false".
+
 [[gc.startTime]]gc.startTime::
 +
 Start time to define the first execution of the git garbage collection.
@@ -1485,6 +1664,7 @@
 * `mon, month, months` (`1 month` is treated as `30 days`)
 * `y, year, years` (`1 year` is treated as `365 days`)
 
+[[schedule-examples]]
 Examples::
 +
 ----
@@ -1559,6 +1739,28 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.docUrl]]gerrit.docUrl::
++
+Optional base URL for documentation, under which one can find
+"index.html", "rest-api.html", etc. Used as the base for the fixed set
+of links in the "Documentation" tab. A slash is implicitly appended.
+(For finer control over the top menu, consider writing a
+link:dev-plugins.html#top-menu-extensions[plugin].)
++
+If unset or empty, the documentation tab will only be shown if
+`/Documentation/index.html` can be reached by the browser at app load
+time.
+
+[[gerrit.editGpgKeys]]gerrit.editGpgKeys::
++
+If enabled and server-side signed push validation is also
+link:#receive.enableSignedPush[enabled], enable the
+link:rest-api-accounts.html#list-gpg-keys[REST API endpoints] and web UI
+for editing GPG keys. If disabled, GPG keys can only be added by
+administrators with direct git access to All-Users.
++
+Defaults to true.
+
 [[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
 +
 Optional command to install the `commit-msg` hook. Typically of the
@@ -1723,13 +1925,13 @@
 +
 Valid values are the characters '*', '(' and ')'.
 
-[[gitweb.linkDrafts]]gitweb.urlEncode::
+[[gitweb.urlEncode]]gitweb.urlEncode::
 +
 Whether or not Gerrit should encode the generated viewer URL.
 +
 Gerrit composes the viewer URL using information about the project, branch, file
 or commit of the target object to be displayed. Typically viewers such as CGit
-and GitWeb do need those parts to be encoded, including the '/' in project's name,
+and gitweb do need those parts to be encoded, including the '/' in project's name,
 for being correctly parsed.
 However other viewers could instead require an unencoded URL (e.g. GitHub web
 based viewer)
@@ -1805,6 +2007,11 @@
 Optional filename for the hashtags changed hook, if not specified then
 `hashtags-changed` will be used.
 
+[[hooks.projectCreatedHook]]hooks.projectCreatedHook::
++
+Optional filename for the project created hook, if not specified then
+`project-created` will be used.
+
 [[hooks.mergeFailedHook]]hooks.mergeFailedHook::
 +
 Optional filename for the merge failed hook, if not specified then
@@ -2042,7 +2249,7 @@
 thread pool waiting for a worker thread to become available.
 0 sets the queue size to the Integer.MAX_VALUE.
 +
-By default 50.
+By default 200.
 
 [[httpd.maxWait]]httpd.maxWait::
 +
@@ -2136,19 +2343,17 @@
 +
 A link:http://lucene.apache.org/[Lucene] index is used.
 +
-* `SOLR`
-+
-A link:https://cwiki.apache.org/confluence/display/solr/SolrCloud[
-SolrCloud] index is used.
 
 +
 By default, `LUCENE`.
 
 [[index.threads]]index.threads::
 +
-Number of threads to use for indexing in normal interactive operations.
+Number of threads to use for indexing in normal interactive operations. Setting
+it to 0 disables the dedicated thread pool and indexing will be done in the same
+thread as the operation.
 +
-Defaults to 1 if not set, or set to a negative value (unless
+Defaults to 0 if not set, or set to a negative value (unless
 link:#changeMerge.interactiveThreadPoolSize[changeMerge.interactiveThreadPoolSize]
 is iset).
 
@@ -2161,6 +2366,49 @@
 CPUs as returned by the JVM (unless
 link:#changeMerge.threadPoolSize[changeMerge.threadPoolSize] is set).
 
+[[index.onlineUpgrade]]index.onlineUpgrade::
++
+Whether to upgrade to new index schema versions while the server is
+running. This is recommended as it prevents additional downtime during
+Gerrit version upgrades (avoiding the need for an offline reindex step
+using Reindex), but can add additional server load during the upgrade.
++
+If set to false, there is no way to upgrade the index schema to take
+advantage of new search features without restarting the server.
++
+Defaults to true.
+
+[[index.maxLimit]]index.maxLimit::
++
+Maximum limit to allow for search queries. Requesting results above this
+limit will truncate the list (but will still set `_more_changes` on
+result lists). Set to 0 for no limit.
++
+Defaults to no limit.
+
+[[index.maxPages]]index.maxPages::
++
+Maximum number of pages of search results to allow, as index
+implementations may have to scan through large numbers of skipped
+results when searching with an offset. Requesting results starting past
+this threshold times the requested limit will result in an error. Set to
+0 for no limit.
++
+Defaults to no limit.
+
+[[index.maxTerms]]index.maxTerms::
++
+Maximum number of leaf terms to allow in a query. Too-large queries may
+perform poorly, so setting this option causes query parsing to fail fast
+before attempting to send them to the secondary index. Should this limit
+be reached, database is used instead of index as applicable.
++
+When the index type is `LUCENE`, also sets the maximum number of clauses
+permitted per BooleanQuery. This is so that all enforced query limits
+are the same.
++
+Defaults to 1024.
+
 ==== Lucene configuration
 
 Open and closed changes are indexed in separate indexes named
@@ -2168,14 +2416,6 @@
 
 The following settings are only used when the index type is `LUCENE`.
 
-[[index.defaultMaxClauseCount]]index.defaultMaxClauseCount::
-+
-Only used when the type is `LUCENE`.
-+
-Sets the maximum number of clauses permitted per BooleanQuery.
-+
-Defaults to 1024.
-
 [[index.name.ramBufferSize]]index.name.ramBufferSize::
 +
 Determines the amount of RAM that may be used for buffering added documents
@@ -2219,7 +2459,6 @@
 ----
 [index]
   type = LUCENE
-  defaultMaxClauseCount = 2048
 
 [index "changes_open"]
   ramBufferSize = 60 m
@@ -2230,17 +2469,6 @@
   maxBufferedDocs = 500
 ----
 
-==== Solr configuration
-
-Open and closed changes are indexed in separate indexes named
-'changes_open' and 'changes_closed' respectively.
-
-The following settings are only used when the index type is `SOLR`.
-
-[[index.url]]index.url::
-+
-URL of the index server.
-
 [[ldap]]
 === Section ldap
 
@@ -2414,7 +2642,10 @@
 example `${userPrincipalName.localPart}` would provide only 'user'.
 +
 If set, users will be unable to modify their SSH username field, as
-Gerrit will populate it only from the LDAP data.
+Gerrit will populate it only from the LDAP data. Note that once the
+username has been set it cannot be changed, therefore it is
+recommended not to make changes to this setting that would cause the
+value to differ, as this will prevent users from logging in.
 +
 Default is `uid` for RFC 2307 servers,
 and `${sAMAccountName.toLowerCase}` for Active Directory.
@@ -2592,6 +2823,22 @@
   javaOptions = -Dcom.sun.jndi.ldap.connect.pool.timeout=300000
 ----
 
+[[log]]
+=== Section log
+
+[[log.jsonLogging]]log.jsonLogging::
++
+If set to true, enables error logging in JSON format (file name: "logs/error_log.json").
++
+Defaults to false.
+
+[[log.textLogging]]log.textLogging::
++
+If set to true, enables error logging in regular plain text format. Can only be disabled
+if `jsonLogging` is enabled.
++
+Defaults to true.
+
 [[mimetype]]
 === Section mimetype
 
@@ -2668,22 +2915,63 @@
 and SSH.  If set to true Administrators can install new plugins
 remotely, or disable existing plugins.  Defaults to false.
 
+[[plugins.jsLoadTimeout]]plugins.jsLoadTimeout::
++
+Set the timeout value for loading JavaScript plugins in Gerrit UI.
+Values can be specified using standard time unit abbreviations ('ms',
+'sec', 'min', etc.).
++
+Default is 5 seconds. Negative values will be converted to 0.
 
 [[receive]]
 === Section receive
 
-This section is used to set who can execute the 'receive-pack' and
-to limit the maximum Git object size that 'receive-pack' will accept.
-'receive-pack' is what runs on the server during a user's push or
-repo upload command. It also contains some advanced options for tuning the
-behavior of Gerrit's 'receive-pack' mechanism.
+This section is used to configure behavior of the 'receive-pack'
+handler, which responds to 'git push' requests.
 
-----
-[receive]
-  allowGroup = GROUP_ALLOWED_TO_EXECUTE
-  allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE
-  maxObjectSizeLimit = 40 m
-----
+[[receive.allowGroup]]receive.allowGroup::
++
+Name of the groups of users that are allowed to execute
+'receive-pack' on the server. One or more groups can be set.
++
+If no groups are added, any user will be allowed to execute
+'receive-pack' on the server.
+
+[[receive.certNonceSeed]]receive.certNonceSeed::
++
+If set to a non-empty value and server-side signed push validation is
+link:#receive.enableSignedPush[enabled], use this value as the seed to
+the HMAC SHA-1 nonce generator. If unset, a 64-byte random seed will be
+generated at server startup.
++
+As this is used as the seed of a cryptographic algorithm, it is
+recommended to be placed in link:#secure-config[`secure.config`].
++
+Defaults to unset.
+
+[[receive.certNonceSlop]]receive.certNonceSlop::
++
+When validating the nonce passed as part of the signed push protocol,
+accept valid nonces up to this many seconds old. This allows
+certificate verification to work over HTTP where there is a lag between
+the HTTP response providing the nonce to sign and the next request
+containing the signed nonce. This can be significant on large
+repositories, since the lag also includes the time to count objects on
+the client.
++
+Default is 5 minutes.
+
+[[receive.changeUpdateThreads]]receive.changeUpdateThreads::
++
+Number of threads to perform change creation or patch set updates
+concurrently. Each thread uses its own database connection from
+the database connection pool, and if all threads are busy then
+main receive thread will also perform a change creation or patch
+set update.
++
+Defaults to 1, using only the main receive thread. This feature is for
+databases with very high latency that can benefit from concurrent
+operations when multiple changes are impacted at once.
 
 [[receive.checkMagicRefs]]receive.checkMagicRefs::
 +
@@ -2713,13 +3001,30 @@
 +
 Default is true.
 
-[[receive.allowGroup]]receive.allowGroup::
+[[receive.enableSignedPush]]receive.enableSignedPush::
 +
-Name of the groups of users that are allowed to execute
-'receive-pack' on the server. One or more groups can be set.
+If true, server-side signed push validation is enabled.
 +
-If no groups are added, any user will be allowed to execute
-'receive-pack' on the server.
+When a client pushes with `git push --signed`, this ensures that the
+push certificate is valid and signed with a valid public key stored in
+the `refs/meta/gpg-keys` branch of `All-Users`.
++
+Defaults to false.
+
+[[receive.maxBatchChanges]]receive.maxBatchChanges::
++
+The maximum number of changes that Gerrit allows to be pushed
+in a batch for review. When this number is exceeded Gerrit rejects
+the push with an error message.
++
+May be overridden for certain groups by specifying a limit in the
+link:access-control.html#capability_batchChangesLimit['Batch Changes Limit']
+global capability.
++
+This setting can be used to prevent users from uploading large
+number of changes for review by mistake.
++
+Default is zero, no limit.
 
 [[receive.maxObjectSizeLimit]]receive.maxObjectSizeLimit::
 +
@@ -2740,20 +3045,13 @@
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
 
-[[receive.maxBatchChanges]]receive.maxBatchChanges::
+[[receive.maxTrustDepth]]receive.maxTrustDepth::
 +
-The maximum number of changes that Gerrit allows to be pushed
-in a batch for review. When this number is exceeded Gerrit rejects
-the push with an error message.
+If signed push validation is link:#receive.enableSignedPush[enabled],
+set to the maximum depth to search when checking if a key is
+link:#receive.trustedKey[trusted].
 +
-May be overridden for certain groups by specifying a limit in the
-link:access-control.html#capability_batchChangesLimit['Batch Changes Limit']
-global capability.
-+
-This setting can be used to prevent users from uploading large
-number of changes for review by mistake.
-+
-Default is zero, no limit.
+Default is 0, meaning only explicitly trusted keys are allowed.
 
 [[receive.threadPoolSize]]receive.threadPoolSize::
 +
@@ -2762,18 +3060,6 @@
 +
 Defaults to the number of available CPUs according to the Java runtime.
 
-[[receive.changeUpdateThreads]]receive.changeUpdateThreads::
-+
-Number of threads to perform change creation or patch set updates
-concurrently. Each thread uses its own database connection from
-the database connection pool, and if all threads are busy then
-main receive thread will also perform a change creation or patch
-set update.
-+
-Defaults to 1, using only the main receive thread. This feature is for
-databases with very high latency that can benefit from concurrent
-operations when multiple changes are impacted at once.
-
 [[receive.timeout]]receive.timeout::
 +
 Overall timeout on the time taken to process the change data in
@@ -2782,9 +3068,28 @@
 be specified using standard time unit abbreviations ('ms', 'sec',
 'min', etc.).
 +
-Default is 2 minutes. If no unit is specified, milliseconds
+Default is 4 minutes. If no unit is specified, milliseconds
 is assumed.
 
+[[receive.trustedKey]]receive.trustedKey::
++
+List of GPG key fingerprints that should be considered trust roots by
+the server when signed push validation is
+link:#receive.enableSignedPush[enabled]. A key is trusted by the server
+if it is either in this list, or a path of trust signatures leads from
+the key to a configured trust root. The maximum length of the path is
+determined by link:#receive.maxTrustDepth[`receive.maxTrustDepth`].
++
+Key fingerprints can be displayed with `gpg --list-keys
+--with-fingerprint`.
++
+Trust signatures can be added to a key using the `tsign` command to
+link:https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html[
+`gpg --edit-key`], after which the signed key should be re-uploaded.
++
+If no keys are specified, web-of-trust checks are disabled. This is the
+default behavior.
+
 
 [[repository]]
 === Section repository
@@ -2799,9 +3104,22 @@
   ownerGroup = Registered Users
 ----
 
-[NOTE]
-Currently only the repository name `*` is supported.
-This is a wildcard designating all repositories.
+The only matching patterns supported are exact match or wildcard matching which
+can be specified by ending the name with a `*`. If a project matches more than one
+repository configuration, then the configuration from the more precise match
+will be used. In the following example, the default submit type for a project
+named `project/plugins/a` would be `CHERRY_PICK`.
+
+----
+[repository "project/*"]
+  defaultSubmitType = MERGE_IF_NECESSARY
+[repository "project/plugins/*"]
+  defaultSubmitType = CHERRY_PICK
+----
+
+[NOTE] All properties are used from the matching repository configuration. In
+the previous example, all properties will be used from `project/plugins/\*`
+section and no properties will be inherited nor overridden from `project/*`.
 
 [[repository.name.defaultSubmitType]]repository.<name>.defaultSubmitType::
 +
@@ -2919,7 +3237,7 @@
 <<user.name,user.name>> and <<user.email,user.email>>, or guessed
 from the local operating system.
 +
-* 'Code Review' `<`'review'`@`'example.com'`>`
+* `Code Review <review@example.com>`
 +
 If set to a name and email address in brackets, Gerrit will use
 this name and email address for any messages, overriding the name
@@ -3036,6 +3354,23 @@
 [[sshd]]
 === Section sshd
 
+[[sshd.enableCompression]]sshd.enableCompression::
++
+In the general case, we want to disable transparent compression, since
+the majority of our data transfer is highly compressed Git pack files
+and we cannot make them any smaller than they already are.
++
+However, if there are CPU in abundance and the server is reachable
+through slow networks, gits with huge amount of refs can benefit from
+SSH-compression since git does not compress the ref announcement during
+handshake.
++
+Compression can be especially useful when Gerrit slaves are being used
+for the larger clones and fetches and the master server mostly takes
+small receive-packs.
++
+By default, `false`.
+
 [[sshd.backend]]sshd.backend::
 +
 Starting from version 0.9.0 Apache SSHD project added support for NIO2
@@ -3049,12 +3384,12 @@
 Specifies the local addresses the internal SSHD should listen
 for connections on.  The following forms may be used to specify
 an address.  In any form, `:'port'` may be omitted to use the
-default of 29418.
+default of `29418`.
 +
-* 'hostname':'port' (for example `review.example.com:29418`)
-* 'IPv4':'port' (for example `10.0.0.1:29418`)
-* ['IPv6']:'port' (for example `[ff02::1]:29418`)
-* +*:'port'+ (for example `+*:29418+`)
+* `'hostname':'port'` (for example `review.example.com:29418`)
+* `'IPv4':'port'` (for example `10.0.0.1:29418`)
+* `['IPv6']:'port'` (for example `[ff02::1]:29418`)
+* `+*:'port'+` (for example `+*:29418+`)
 
 +
 --
@@ -3063,7 +3398,7 @@
 
 To disable the internal SSHD, set listenAddress to `off`.
 
-By default, *:29418.
+By default, `*:29418`.
 --
 
 [[sshd.advertisedAddress]]sshd.advertisedAddress::
@@ -3074,16 +3409,16 @@
 22. The following forms may be used to specify an address.  In any
 form, `:'port'` may be omitted to use the default SSH port of 22.
 
-* 'hostname':'port' (for example `review.example.com:22`)
-* 'IPv4':'port' (for example `10.0.0.1:29418`)
-* ['IPv6']:'port' (for example `[ff02::1]:29418`)
+* `'hostname':'port'` (for example `review.example.com:22`)
+* `'IPv4':'port'` (for example `10.0.0.1:29418`)
+* `['IPv6']:'port'` (for example `[ff02::1]:29418`)
 
 +
 --
 If multiple values are supplied, the daemon will advertise all
 of them.
 
-By default, sshd.listenAddress.
+By default uses the value of `sshd.listenAddress`.
 --
 
 [[sshd.tcpKeepAlive]]sshd.tcpKeepAlive::
@@ -3093,7 +3428,7 @@
 +
 Only effective when `sshd.backend` is set to `MINA`.
 +
-By default, true.
+By default, `true`.
 
 [[sshd.threads]]sshd.threads::
 +
@@ -3119,7 +3454,7 @@
 than the total number of threads allocated in sshd.threads, then the
 value of sshd.threads is increased to accommodate the requested value.
 +
-By default, 0.
+By default is 1 on single core node, 2 otherwise.
 
 [[sshd.streamThreads]]sshd.streamThreads::
 +
@@ -3194,8 +3529,8 @@
 to the default ciphers, cipher names starting with `-` are removed
 from the default cipher set.
 +
-Supported ciphers: aes128-cbc, aes128-cbc, aes256-cbc, blowfish-cbc,
-3des-cbc, none.
+Supported ciphers: `aes128-cbc`, `aes128-cbc`, `aes256-cbc`, `blowfish-cbc`,
+`3des-cbc`, `none`.
 +
 By default, all supported ciphers except `none` are available.
 
@@ -3207,7 +3542,8 @@
 are enabled in addition to the default MACs, MAC names starting with
 `-` are removed from the default MACs.
 +
-Supported MACs: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96.
+Supported MACs: `hmac-md5`, `hmac-md5-96`, `hmac-sha1`, `hmac-sha1-96`,
+`hmac-sha2-256`, `hmac-sha2-512`.
 +
 By default, all supported MACs are available.
 
@@ -3247,7 +3583,7 @@
 `log4j.appender` with the name `sshd_log` can be configured to overwrite
 programmatic configuration.
 +
-By default, true.
+By default, `true`.
 
 [[sshd.rekeyBytesLimit]]sshd.rekeyBytesLimit::
 +
@@ -3256,7 +3592,7 @@
 +
 By default, 1073741824 (bytes, 1GB).
 +
-The rekeyBytesLimit cannot be set to lower than 32.
+The `rekeyBytesLimit` cannot be set to lower than 32.
 
 [[sshd.rekeyTimeLimit]]sshd.rekeyTimeLimit::
 +
@@ -3295,7 +3631,9 @@
 
 [[suggest.fullTextSearch]]suggest.fullTextSearch::
 +
-If 'true' the reviewer completion suggestions will be based on a full text search.
+If `true` the reviewer completion suggestions will be based on a full text search.
++
+By default `false`.
 
 [[suggest.from]]suggest.from::
 +
@@ -3512,6 +3850,58 @@
 If no groups are added, any user will be allowed to execute
 'upload-pack' on the server.
 
+[[urlAlias]]
+=== Section urlAlias
+
+URL aliases define regular expressions for URL tokens that are mapped
+to target URL tokens.
+
+Each URL alias must be specified in its own subsection. The subsection
+name should be a descriptive name. It must be unique, but is not
+interpreted in any way.
+
+The URL aliases are applied in no particular order. The first matching
+URL alias is used and further matches are ignored.
+
+URL aliases can be used to map plugin screens into the Gerrit URL
+namespace, or to replace Gerrit screens by plugin screens.
+
+Example:
+
+----
+[urlAlias "MyPluginScreen"]
+  match = /myscreen/(.*)
+  token = /x/myplugin/myscreen/$1
+[urlAlias "MyChangeScreen"]
+  match = /c/(.*)
+  token = /x/myplugin/c/$1
+----
+
+[[urlAlias.match]]urlAlias.match::
++
+A regular expression for a URL token.
++
+The matched URL token is replaced by `urlAlias.token`.
+
+[[urlAlias.token]]urlAlias.token::
++
+The target URL token.
++
+It can contain placeholders for the groups matched by the
+`urlAlias.match` regular expression: `$1` for the first matched group,
+`$2` for the second matched group, etc.
+
+[[submodule]]
+=== Section submodule
+
+[[submodule.verbosesuperprojectupdate]]submodule.verboseSuperprojectUpdate
++
+When using link:user-submodules.html#automatic_update[automatic superproject updates]
+this option will determine if the submodule commit messages are included into
+the commit message of the superproject update.
++
+By default this is true.
+
 
 [[user]]
 === Section user
@@ -3541,7 +3931,7 @@
 By default "Anonymous Coward" is used.
 
 
-== File `etc/secure.config`
+== [[secure.config]]File `etc/secure.config`
 The optional file `'$site_path'/etc/secure.config` overrides (or
 supplements) the settings supplied by `'$site_path'/etc/gerrit.config`.
 The file should be readable only by the daemon process and can be
@@ -3552,7 +3942,6 @@
 ----
 [auth]
   registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
-  restTokenPrivateKey = 7e40PzCjlUKOnXATvcBNXH6oyiu+r0dFk2c=
 
 [database]
   username = webuser
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index 2311184..63eaffd 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -67,12 +67,12 @@
 
 For the external configuration, gitweb runs under the control of an
 external web server, and Gerrit access controls are not enforced. Gerrit
-provides configuration parameters for integration with GitWeb.
+provides configuration parameters for integration with gitweb.
 
 [[linuxGitWeb]]
 ==== Linux Installation
 
-===== Install GitWeb
+===== Install Gitweb
 
 On Ubuntu:
 
@@ -86,7 +86,7 @@
   $ yum install gitweb
 ====
 
-===== Configure GitWeb
+===== Configure Gitweb
 
 
 Update `/etc/gitweb.conf`, add the public GIT repositories:
@@ -161,7 +161,7 @@
 [[WindowsGitWeb]]
 ==== Windows Installation
 
-Instructions are available for installing the GitWeb module distributed with
+Instructions are available for installing the gitweb module distributed with
 MsysGit:
 
 link:https://github.com/msysgit/msysgit/wiki/GitWeb[GitWeb]
@@ -176,7 +176,7 @@
 tech note useful for configuring Apache Service to run under another account.
 You must grant the new account link:http://technet.microsoft.com/en-us/library/cc794944(WS.10).aspx["run as service"] permission:
 
-The GitWeb version in msysgit is missing several important and required
+The gitweb version in msysgit is missing several important and required
 perl modules, including CGI.pm. The perl included with the msysgit distro 1.7.8
 is broken.. The link:http://groups.google.com/group/msysgit/browse_thread/thread/ba3501f1f0ed95af[unicore folder is missing along with utf8_heavy.pl and CGI.pm]. You can
 verify by checking for perl modules. From an msys console, execute the
@@ -207,7 +207,7 @@
 
 copy the contents of lib into `msysgit/lib/perl5/5.8.8` and overwrite existing files.
 
-==== Enable GitWeb Integration
+==== Enable Gitweb Integration
 
 To enable the external gitweb integration, set
 link:config-gerrit.html#gitweb.url[gitweb.url] with the URL of your
@@ -219,7 +219,7 @@
 namespace is available.
 
 ----
-$ git config -f $site_path/etc/gerrit.config gitweb.cgi $PATH_TO_GITWEB/gitweb.cgi
+$ git config -f $site_path/etc/gerrit.config --unset gitweb.cgi
 $ git config -f $site_path/etc/gerrit.config gitweb.url https://gitweb.corporation.com
 ----
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index ce908b9..3068cc5 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -110,6 +110,14 @@
   ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
 ====
 
+=== project-created
+
+Called whenever a project has been created.
+
+====
+  project-created --project <project name> --head <head name>
+====
+
 === reviewer-added
 
 Called whenever a reviewer is added to a change.
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index ce49d31..a40c5f3 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -243,7 +243,7 @@
 This is the case if the change was rebased onto a different parent.
 This can be used to enable sticky approvals, reducing turn-around for
 trivial rebases prior to submitting a change.
-It is recommended to enable this for the Code-Review label.
+For the pre-installed Code-Review label this is enabled by default.
 Defaults to false.
 
 [[label_copyAllScoresIfNoCodeChange]]
@@ -255,7 +255,8 @@
 the commit message is different. This can be used to enable sticky
 approvals on labels that only depend on the code, reducing turn-around
 if only the commit message is changed prior to submitting a change.
-It is recommended to enable this for the Verified label if enabled.
+For the Verified label that is installed by the link:pgm-init.html[init]
+site program this is enabled by default.
 Defaults to false.
 
 [[label_copyAllScoresIfNoChange]]
diff --git a/Documentation/config-login-register.txt b/Documentation/config-login-register.txt
index 76f47ed..2e775b4 100644
--- a/Documentation/config-login-register.txt
+++ b/Documentation/config-login-register.txt
@@ -89,7 +89,8 @@
 
 * Real name (visible name in Gerrit)
 * Register your email (it must be confirmed later)
-* Select a username with which to communicate with Gerrit over ssh+git
+* Select a username with which to communicate with Gerrit over ssh+git. Note
+that once saved, the username cannot be changed.
 
 * The server will ask you for an RSA public key.
 That's the key we generated above, and it's time to make sure that Gerrit knows
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 62d0219..da213a8 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -28,6 +28,12 @@
 to a change being abandoned.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
 `ChangeFooter.vm`.
 
+=== AddKey.vm
+
+The `AddKey.vm` template will determine the contents of the email related to
+SSH and GPG keys being added to a user account. This notification is not sent
+when the key is administratively added to another user account.
+
 === ChangeFooter.vm
 
 The `ChangeFooter.vm` template will determine the contents of the footer
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 3f84bda..ca95099 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -102,6 +102,16 @@
 link:https://gerrit.googlesource.com/plugins/reviewnotes/+doc/master/src/main/resources/Documentation/about.md[
 Documentation]
 
+[[review-strategy]]
+=== review-strategy
+
+This plugin allows users to configure different review strategies.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/review-strategy[
+Project] |
+link:https://gerrit.googlesource.com/plugins/review-strategy/+/master/src/main/resources/Documentation/about.md[
+Documentation]
+
 [[singleusergroup]]
 === singleusergroup
 
@@ -118,9 +128,12 @@
 these plugins.
 
 The Gerrit Project doesn't provide binaries for these plugins, but
-there are some public services, like the
-link:https://ci.gerritforge.com/[CI Server from GerritForge], that
-offer the download of ready plugin jars.
+there are some public services that offer the download of pre-built
+plugin jars:
+
+* link:https://gerrit-ci.gerritforge.com[CI Server from GerritForge]
+* link:http://builds.quelltextlich.at/gerrit/nightly/index.html[
+  CI Server from Quelltextlich]
 
 The following list gives an overview about available plugins, but the
 list may not be complete. You may discover more plugins on
@@ -166,7 +179,7 @@
 
 This plugin allows the rendering of Git repository branch network in a
 graphical HTML5 Canvas. It is mainly intended to be used as a
-"project link" in a GitWeb configuration or by other Gerrit GWT UI
+"project link" in a gitweb configuration or by other Gerrit GWT UI
 plugins to be plugged elsewhere in Gerrit.
 
 link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/branch-network[
@@ -367,6 +380,19 @@
 https://gerrit.googlesource.com/plugins/javamelody/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[labelui]]
+=== labelui
+
+The labelui plugin adds a user preference that allows users to choose a
+table control to render the labels/approvals on the change screen
+(similar to how labels/approvals were rendered on the old change
+screen).
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/labelui[
+Project] |
+link:https://gerrit.googlesource.com/plugins/labelui/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
 [[menuextender]]
 === menuextender
 
@@ -396,6 +422,25 @@
 link:https://gerrit.googlesource.com/plugins/motd/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[oauth-authentication-provider]]
+=== OAuth authentication provider
+This plugin enables Gerrit to use OAuth2 protocol for authentication.
+Two different OAuth providers are supported:
+
+* GitHub
+* Google
+
+https://github.com/davido/gerrit-oauth-provider[Project] |
+https://github.com/davido/gerrit-oauth-provider/wiki/Getting-Started[Configuration]
+
+[[owners]]
+=== owners
+This plugin provides a Prolog predicate `add_owner_approval/3` that
+appends `label('Owner-Approval', need(_))` to a provided list.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/owners[Project] |
+link:https://gerrit.googlesource.com/plugins/owners/+/refs/heads/master/README.md[Documentation]
+
 [[project-download-commands]]
 === project-download-commands
 
@@ -428,6 +473,31 @@
 link:https://gerrit.googlesource.com/plugins/quota/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[ref-protection]]
+=== ref-protection
+
+A plugin that protects against commits being lost.
+
+Backups of deleted or non-fast-forward updated refs are created under the
+`refs/backups/` namespace.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/ref-protection[
+Project] |
+link:https://gerrit.googlesource.com/plugins/ref-protection/+/refs/heads/stable-2.11/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[reparent]]
+=== reparent
+
+A plugin that provides project reparenting as a self-service for project owners.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/reparent[
+Project] |
+link:https://gerrit.googlesource.com/plugins/reparent/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/reparent/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
 [[reviewers]]
 === reviewers
 
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 276117b..7d03681 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -154,6 +154,27 @@
 +
 The default value for this is true, false disables the checks.
 
+[[receive.enableSignedPush]]receive.enableSignedPush::
++
+Controls whether server-side signed push validation is enabled on the
+project. Only has an effect if signed push validation is enabled on the
+server; see the link:config-gerrit.html#receive.enableSignedPush[global
+configuration] for details.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
+[[receive.requireSignedPush]]receive.requireSignedPush::
++
+Controls whether server-side signed push validation is required on the
+project. Only has an effect if signed push validation is enabled on the
+server, and link:#receive.enableSignedPush is set on the project. See
+the link:config-gerrit.html#receive.enableSignedPush[global
+configuration] for details.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 7f30b14..eff777b 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -48,6 +48,8 @@
 	  <Proxy *>
 	    Order deny,allow
 	    Allow from all
+	    # Use following line instead of the previous two on Apache >= 2.4
+	    # Require all granted
 	  </Proxy>
 
 	  AllowEncodedSlashes On
@@ -97,7 +99,7 @@
 	  listen 80;
 	  server_name review.example.com;
 
-	  location /r/ {
+	  location ^~ /r/ {
 	    proxy_pass        http://127.0.0.1:8081;
 	    proxy_set_header  X-Forwarded-For $remote_addr;
 	    proxy_set_header  Host $host;
diff --git a/Documentation/config-themes.txt b/Documentation/config-themes.txt
index 5c3a448..b165e37 100644
--- a/Documentation/config-themes.txt
+++ b/Documentation/config-themes.txt
@@ -133,9 +133,6 @@
 block in the header or footer will execute before Gerrit has defined
 the function and is ready to register the hook callback.
 
-The function `gerrit_addHistoryHook` is deprecated and may be
-removed in a future release.
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index 4f854fb..d720287 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -18,6 +18,21 @@
 
 If this option interests you, you might want to consider link:install-quick.html[the quick guide].
 
+[[createdb_derby]]
+=== Apache Derby
+
+If Derby is selected, Gerrit will automatically set up the embedded Derby
+database as backend so no set up or configuration is necessary.
+
+Currently only support for embedded mode is added. There are two other
+deployment options for Apache Derby that can be added later:
+
+* link:http://db.apache.org/derby/papers/DerbyTut/ns_intro.html#Network+Server+Options[
+Derby Network Server (standalone mode)]
+
+* link:http://db.apache.org/derby/papers/DerbyTut/ns_intro.html#Embedded+Server[
+Embedded Server (hybrid mode)]
+
 [[createdb_postgres]]
 === PostgreSQL
 
@@ -146,6 +161,51 @@
 Visit SAP MaxDB's link:http://maxdb.sap.com/documentation/[documentation] for further
 information regarding using SAP MaxDB.
 
+[[createdb_db2]]
+=== DB2
+
+IBM DB2 is a supported database for running Gerrit Code Review. However it is
+recommended only for environments where you intend to run Gerrit on an existing
+DB2 installation to reduce administrative overhead.
+
+Create a system wide user for the Gerrit application, and grant the user
+full rights on the newly created database:
+
+----
+  db2 => create database gerrit
+  db2 => connect to gerrit
+  db2 => grant connect,accessctrl,dataaccess,dbadm,secadm on database to gerrit2;
+----
+
+JDBC driver db2jcc4.jar and db2jcc_license_cu.jar must be obtained
+from your DB2 distribution. Gerrit initialization process tries to copy
+it from a known location:
+
+----
+/opt/ibm/db2/V10.5/java/db2jcc4.jar
+/opt/ibm/db2/V10.5/java/db2jcc_license_cu.jar
+----
+
+If these files cannot be located at this place, then an alternative location
+can be provided during init step execution.
+
+Sample database section in $site_path/etc/gerrit.config:
+
+----
+[database]
+        type = db2
+        database = gerrit
+        hostname = localhost
+        username = gerrit2
+        port = 50001
+----
+
+Sample database section in $site_path/etc/secure.config:
+
+----
+[database]
+        password = secret_pasword
+----
 
 GERRIT
 ------
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 2eb478c..ec8515f 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -7,7 +7,7 @@
 
 There is currently no binary distribution of Buck, so it has to be manually
 built and installed.  Apache Ant is required.  Currently only Linux and Mac
-OS are supported.  Buck requires Python version 2.7 to be installed.
+OS are supported.
 
 Clone the git and build it:
 
@@ -50,6 +50,10 @@
 script from `./scripts/buck_completion.bash` in the buck project.  Refer
 to the script's header comments for installation instructions.
 
+== Prerequisites
+
+Buck requires Python version 2.7 to be installed. The Maven download toolchain
+requires `curl` to be installed.
 
 [[eclipse]]
 == Eclipse Integration
@@ -63,17 +67,7 @@
   tools/eclipse/project.py
 ----
 
-In Eclipse, choose 'Import existing project' and select the `gerrit` project
-from the current working directory.
-
-Expand the `gerrit` project, right-click on the `buck-out` folder, select
-'Properties', and then under 'Attributes' check 'Derived'.
-
-Note that if you make any changes in the project configuration
-that get saved to the `.project` file, for example adding Resource
-Filters on a folder, they will be overwritten the next time you run
-`tools/eclipse/project.py`.
-
+and then follow the link:dev-eclipse.html#setup[setup instructions].
 
 === Refreshing the Classpath
 
@@ -112,10 +106,24 @@
 The output executable WAR will be placed in:
 
 ----
-  buck-out/gen/gerrit.war
+  buck-out/gen/gerrit/gerrit.war
 ----
 
 
+=== Headless Mode
+
+To build Gerrit in headless mode, i.e. without the GWT Web UI:
+
+----
+  buck build headless
+----
+
+The output executable WAR will be placed in:
+
+----
+  buck-out/gen/headless/headless.war
+----
+
 === Extension and Plugin API JAR Files
 
 To build the extension, plugin and GWT API JAR files:
@@ -129,8 +137,8 @@
 
 ----
   buck-out/gen/gerrit-plugin-api/plugin-api.jar
+  buck-out/gen/gerrit-plugin-api/plugin-api-javadoc/plugin-api-javadoc.jar
   buck-out/gen/gerrit-plugin-api/plugin-api-src.jar
-  buck-out/gen/gerrit-plugin-api/plugin-api-javadoc.jar
 ----
 
 Install {extension,plugin,gwt}-api to the local maven repository:
@@ -162,7 +170,7 @@
 The JAR files will also be packaged in:
 
 ----
-  buck-out/gen/plugins/core.zip
+  buck-out/gen/plugins/core/core.zip
 ----
 
 To build a specific plugin:
@@ -216,7 +224,7 @@
 The html files will also be bundled into `searchfree.zip` in this location:
 
 ----
-  buck-out/gen/Documentation/searchfree.zip
+  buck-out/gen/Documentation/searchfree/searchfree.zip
 ----
 
 To build the executable WAR with the documentation included:
@@ -228,7 +236,7 @@
 The WAR file will be placed in:
 
 ----
-  buck-out/gen/withdocs.war
+  buck-out/gen/withdocs/withdocs.war
 ----
 
 [[soyc]]
@@ -264,54 +272,47 @@
 The output release WAR will be placed in:
 
 ----
-  buck-out/gen/release.war
-----
-
-[[all]]
-=== Combined build target
-
-To build release and api targets, a combined build target is provided:
-
-----
-  buck build all
+  buck-out/gen/release/release.war
 ----
 
 [[tests]]
 == Running Unit Tests
 
-To run all tests including acceptance tests:
+To run all tests including acceptance tests (but not flaky tests):
 
 ----
-  buck test
+  buck test --exclude flaky
 ----
 
-To exclude slow tests:
+To exclude flaky and slow tests:
 
 ----
-  buck test --all --exclude slow
+  buck test --exclude flaky slow
 ----
 
-To include a specific group of acceptance tests:
+To run only a specific group of acceptance tests:
 
 ----
-  buck test --all --include api
+  buck test --include api
 ----
 
 The following groups of tests are currently supported:
 
+* acceptance
 * api
 * edit
+* flaky
 * git
 * pgm
 * rest
 * server
 * ssh
+* slow
 
-To run a specific test, e.g. the acceptance test
-`com.google.gerrit.acceptance.git.HttpPushForReviewIT`:
+To run a specific test group, e.g. the rest-account test group:
 
 ----
-  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:HttpPushForReviewIT
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
 ----
 
 To create test coverage report:
@@ -370,62 +371,6 @@
  )
 ----
 
-== Building against unpublished JARs, that change frequently
-
-If a dependent Gerrit library is undergoing active development it must be
-recompiled and the change must be reflected in the Buck build process. For
-example testing Gerrit against changed JGit snapshot version. After building
-JGit library, the artifacts are created in local Maven build directory, e. g.:
-
-----
-  mvn package
-  /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar
-  /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT-sources.jar
-----
-
-If as usual, installation of the build artifacts takes place in local maven
-repository, then the Buck build must fetch them from there with normal
-`download_file.py` process. Disadvantage of this approach is that Buck cache
-invalidation must occur to refresh the artifacts after next
-change-compile-install round trip.
-
-To shorten that workflow and take the installation of the artifacts to the
-local Maven repository and fetching it again from there out of the picture,
-`local_jar()` method is used instead of `maven_jar()`:
-
-[source,python]
-----
- local_jar(
-   name = 'jgit',
-   jar = '/home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar',
-   src = '/home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT-sources.jar',
-   deps = [':ewah']
- )
-----
-
-This creates a symlink to the Buck targets direct against artifacts in
-another project's Maven target directory:
-
-----
-  buck-out/gen/lib/jgit/jgit.jar ->
-  /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar
-----
-
-After `buck clean` and `buck build lib/jgit:jgit` the symbolic link that was
-created the first time is lost due to Buck's caching mechanism. This means that
-when a new version of the local artifact is deployed (by running `mvn package`
-in the JGit project in the example above), Buck is not aware of it, because it
-still has a stale version of it in its cache.
-
-To solve this problem and re-create the symbolic link, you don't need to wipe out
-the entire Buck cache. Just rebuilding the target with the `--no-cache` option
-does the job:
-
-----
-  buck clean
-  buck build --no-cache lib/jgit:jgit
-----
-
 == Building against artifacts from custom Maven repositories
 
 To build against custom Maven repositories, two modes of operations are
@@ -489,7 +434,7 @@
 === Cleaning The Buck Cache
 
 The cache for the Gerrit Code Review project is located in
-`~/.gerritcodereview/buck-cache/cache`.
+`~/.gerritcodereview/buck-cache/locally-built-artifacts`.
 
 The Buck cache should never need to be manually deleted. If you find yourself
 deleting the Buck cache regularly, then it is likely that there is something
@@ -498,11 +443,12 @@
 If you really do need to clean the cache manually, then:
 
 ----
- rm -rf ~/.gerritcodereview/buck-cache/cache
+ rm -rf ~/.gerritcodereview/buck-cache/locally-built-artifacts
 ----
 
-Note that the root `buck-cache` folder should not be deleted as this is where
-downloaded artifacts are stored.
+Note that the root `buck-cache` folder should not be deleted as it also contains
+the `downloaded-artifacts` directory, which holds the artifacts that got
+downloaded (not built locally).
 
 [[buck-daemon]]
 === Using Buck daemon
@@ -593,29 +539,41 @@
 needs to be repeated, the unit test cache for that test must be removed first:
 
 ----
-  rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/.AddRemoveGroupMembersIT/
+  rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/.rest-account/
 ----
 
 After clearing the cache, the test can be run again:
 
 ----
-  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
-  TESTING //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
-  PASS  14,9s  8 Passed   0 Failed   com.google.gerrit.acceptance.rest.group.AddRemoveGroupMembersIT
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
+  [-] TESTING...FINISHED 12,3s (12 PASS/0 FAIL)
+  RESULTS FOR //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
+  PASS     970ms  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.CapabilitiesIT
+  PASS     999ms  1 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.EditPreferencesIT
+  PASS      1,2s  1 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.GetAccountDetailIT
+  PASS     951ms  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.GetAccountIT
+  PASS      6,4s  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.GetDiffPreferencesIT
+  PASS      1,2s  4 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.PutUsernameIT
   TESTS PASSED
 ----
 
 An alternative approach is to use Buck's `--filters` (`-f`) option:
 
 ----
-  buck test -f 'com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT'
-  TESTING SELECTED TESTS
-  PASS  14,5s  6 Passed   0 Failed   com.google.gerrit.acceptance.rest.change.SubmitByMergeAlwaysIT
+  buck test -f 'com.google.gerrit.acceptance.rest.account.CapabilitiesIT'
+  Using buckd.
+  [-] PROCESSING BUCK FILES...FINISHED 1,0s [100%]
+  [-] BUILDING...FINISHED 2,8s [100%] (334/701 JOBS, 110 UPDATED, 5,1% CACHE MISS)
+  [-] TESTING...FINISHED 9,2s (6 PASS/0 FAIL)
+  RESULTS FOR SELECTED TESTS
+  PASS      8,0s  2 Passed   0 Skipped   0 Failed   com.google.gerrit.acceptance.rest.account.CapabilitiesIT
+  PASS    <100ms  4 Passed   0 Skipped   0 Failed   //tools:util_test
   TESTS PASSED
 ----
 
 When this option is used, the cache is disabled per design and doesn't need to
-be explicitly deleted.
+be explicitly deleted. Note, that this is a known issue, that python tests are
+always executed.
 
 Note that when this option is used, the whole unit test cache is dropped, so
 repeating the
@@ -633,6 +591,64 @@
 buck test --no-results-cache
 ----
 
+== Upgrading Buck
+
+The following tests should be executed, when Buck version is upgraded:
+
+* buck build release
+* buck build api_install
+* buck test
+* buck build gerrit, change some sources in gerrit-server project,
+  repeat buck build gerrit and verify that gerrit.war was updated
+* install and verify new gerrit site
+* upgrade and verify existing gerrit site
+* reindex existing gerrit site
+* verify that tools/eclipse/project.py produces sane Eclipse project
+* verify that tools/eclipse/project.py --src generates sources as well
+* verify that unit test execution from Eclipse works
+* verify that daemon started from Eclipse works
+* verify that GWT SDM debug session started from Eclipse works
+
+== Known issues and bugs
+
+=== Symbolic links and `watchman`
+
+`Buck` with activated `Watchman` has currently a
+[known bug](https://github.com/facebook/buck/issues/341) related to
+symbolic links. The symbolic links are used very often with external
+plugins, that are linked per symbolic link to the plugins directory.
+With this use case Buck is failing to rebuild the plugin artefact
+after it was built. All attempts to convince Buck to rebuild will fail.
+The only known way to recover is to weep out `buck-out` directory. The
+better workaround is to avoid using Watchman in this specific use case.
+Watchman can either be de-installed or disabled. See
+link:#buck-daemon[Using Buck daemon] section above how to temporarily
+disable `buckd`.
+
+=== Re-triggering rule execution
+
+There is no way to re-trigger custom rules with side effects, like
+`api_{deploy|install}`. This is a `genrule()` that depends on Java sources
+and is deploying the Plugin API through custom Python script to the local or
+remote Maven repositories. When for some reasons the deployment was undone,
+there is no supported way to re-trigger the execution of `api_{deploy|install}`
+targets. That's because `--no-cache` option will ignore the `Buck` cache, but
+there is no way to ignore `buck-out` directory. To overcome this Buck's design
+limitation new `tools/maven/api.py` script was added, that always re-triggers
+installation or deployment of Plugin API to local or Central Maven repository.
+
+```
+  tools/maven/api.py {deploy|install}
+```
+
+Dry run mode is also supported:
+
+```
+  tools/maven/api.py -n {deploy|install}
+```
+
+With this script the deployment would re-trigger on every invocation.
+
 == Troubleshooting Buck
 
 In some cases problems with Buck itself need to be investigated. See for example
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 72d7ddf..2d96b84 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -78,7 +78,8 @@
   * Followed by one or more explanatory paragraphs
   * Use the present tense (fix instead of fixed)
   * Use the past tense when describing the status before this commit
-  * Include a `Bug: Issue <#>` line if fixing a Gerrit issue
+  * Include a `Bug: Issue <#>` line if fixing a Gerrit issue, or a
+    `Feature: Issue <#>` line if implementing a feature request.
   * Include a `Change-Id` line
 
 === Setting up Vim for Git commit message
@@ -334,6 +335,8 @@
 plugins that have a dependency on GWT.
 * Update the GWT version in the archetype metadata in the
 `gerrit-plugin-gwt-archetype`.
+* Update the version of `gwt-maven-plugin` in the example pom.xml file in
+link:dev-plugins.html[dev-plugins].
 * Update to the same GWT version in the `gwtjsonrpc` project, and release a
 new version.
 
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 905b5f1..bdd2a68 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -66,7 +66,7 @@
 on a J2EE servlet container and an SQL database.
 
 * link:http://video.google.com/videoplay?docid=-8502904076440714866[Mondrian Code Review On The Web]
-* link:http://code.google.com/p/rietveld/[Rietveld - Code Review for Subversion]
+* link:https://github.com/rietveld-codereview/rietveld[Rietveld - Code Review for Subversion]
 * link:http://eagain.net/gitweb/?p=gitosis.git;a=blob;f=README.rst;hb=HEAD[Gitosis README]
 * link:http://source.android.com/[Android Open Source Project]
 
@@ -166,7 +166,7 @@
 requires that the OpenID provider selected by a user must be
 online and operating in order to authenticate that user.
 
-* link:http://code.google.com/webtoolkit/[Google Web Toolkit (GWT)]
+* link:http://www.gwtproject.org/[Google Web Toolkit (GWT)]
 * link:http://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html[Git Repository Format]
 * link:http://www.postgresql.org/about/[About PostgreSQL]
 * link:http://openid.net/developers/specs/[OpenID Specifications]
@@ -183,9 +183,9 @@
 
 Gerrit is developed as a self-hosting open source project:
 
-* link:http://code.google.com/p/gerrit/[Project Homepage]
-* link:http://code.google.com/p/gerrit/downloads/list[Release Versions]
-* link:http://code.google.com/p/gerrit/source/checkout[Source]
+* link:https://www.gerritcodereview.com/[Project Homepage]
+* link:https://www.gerritcodereview.com/download/index.html[Release Versions]
+* link:https://gerrit.googlesource.com/gerrit[Source]
 * link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking]
 * link:https://review.source.android.com/[Change Review]
 
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index b3525a1..b8d01e8 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -7,6 +7,35 @@
 runtime debugging environment.
 
 
+[[setup]]
+== Project Setup
+
+In your Eclipse installation's `eclipse.ini` file, add the following line in
+the `vmargs` section:
+
+----
+  -DmaxCompiledUnitsAtOnce=10000
+----
+
+Without this setting, annotation processing does not work reliably and the
+build is likely to fail with errors like:
+
+----
+  Could not write generated class ... javax.annotation.processing.FilerException: Source file already created
+----
+
+In Eclipse, choose 'Import existing project' and select the `gerrit` project
+from the current working directory.
+
+Expand the `gerrit` project, right-click on the `buck-out` folder, select
+'Properties', and then under 'Attributes' check 'Derived'.
+
+Note that if you make any changes in the project configuration
+that get saved to the `.project` file, for example adding Resource
+Filters on a folder, they will be overwritten the next time you run
+`tools/eclipse/project.py`.
+
+
 [[Formatting]]
 == Code Formatter Settings
 
@@ -79,6 +108,26 @@
 recompiling.
 * To reflect your changes in the debug session, click `Dev Mode On` then `Compile`.
 
+
+=== Running GWT Debug Mode for Gerrit plugins
+
+A Gerrit plugin can expose GWT module and its implementation can be inspected
+in the SDM debug session.
+
+`codeserver` needs two additional inputs to expose the plugin module in the SDM
+debug session: the module name and the source folder location. For example the
+module name and source folder of `cookbook-plugin` should be added in the local
+copy of the `gerrit_gwt_debug` configuration:
+
+----
+  com.googlesource.gerrit.plugins.cookbook.HelloForm \
+  -src ${resource_loc:/gerrit}/plugins/cookbook-plugin/src/main/java \
+  -- --console-log [...]
+----
+
+After doing that, both the Gerrit core and plugin GWT modules can be activated
+during SDM (debug session)[http://imgur.com/HFXZ5No].
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 828ea32..254f511 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -36,7 +36,7 @@
 ----
 mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
     -DarchetypeArtifactId=gerrit-plugin-archetype \
-    -DarchetypeVersion=2.11.5 \
+    -DarchetypeVersion=2.12.9-SNAPSHOT \
     -DgroupId=com.googlesource.gerrit.plugins.testplugin \
     -DartifactId=testplugin
 ----
@@ -283,6 +283,7 @@
     private final ConsoleUI ui;
     private final AllProjectsConfig allProjectsConfig;
 
+    @Inject
     public MyInitStep(@PluginName String pluginName, ConsoleUI ui,
         AllProjectsConfig allProjectsConfig) {
       this.pluginName = pluginName;
@@ -404,6 +405,10 @@
 +
 Publication of usage data
 
+* `com.google.gerrit.extensions.events.GarbageCollectorListener`:
++
+Garbage collection ran on a project
+
 [[stream-events]]
 == Sending Events to the Events Stream
 
@@ -762,6 +767,17 @@
 can be notified when this configuration parameter is updated on a
 project.
 
+[[configuring-groups]]
+=== Referencing groups in `project.config`
+
+Plugins can refer to groups so that when they are renamed, the project
+config will also be updated in this section. The proper format to use is
+the string representation of a GroupReference, as shown below.
+
+----
+Group[group_name / group_uuid]
+----
+
 [[project-specific-configuration]]
 == Project Specific Configuration in own config file
 
@@ -958,6 +974,108 @@
 [[ui_extension]]
 == UI Extension
 
+[[panels]]
+=== Panels
+
+GWT plugins can contribute panels to Gerrit screens.
+
+Gerrit screens define extension points where plugins can add GWT
+panels with custom controls:
+
+* Change Screen:
+** `GerritUiExtensionPoint.CHANGE_SCREEN_HEADER`:
++
+Panel will be shown in the header bar to the right of the change
+status.
+
+** `GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS`:
++
+Panel will be shown in the header bar on the right side of the buttons.
+
+** `GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS`:
++
+Panel will be shown in the header bar on the right side of the pop down
+buttons.
+
+** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK`:
++
+Panel will be shown below the change info block.
+
+** The following parameters are provided:
+*** `GerritUiExtensionPoint.Key.CHANGE_INFO`:
++
+The link:rest-api-changes.html#change-info[ChangeInfo] entity for the
+current change.
+
+* Project Info Screen:
+** `GerritUiExtensionPoint.PROJECT_INFO_SCREEN_TOP`:
++
+Panel will be shown at the top of the screen.
+
+** `GerritUiExtensionPoint.PROJECT_INFO_SCREEN_BOTTOM`:
++
+Panel will be shown at the bottom of the screen.
+
+** The following parameters are provided:
+*** `GerritUiExtensionPoint.Key.PROJECT_NAME`:
++
+The name of the project.
+
+* User Password Screen:
+** `GerritUiExtensionPoint.PASSWORD_SCREEN_BOTTOM`:
++
+Panel will be shown at the bottom of the screen.
+
+** The following parameters are provided:
+*** `GerritUiExtensionPoint.Key.ACCOUNT_INFO`:
++
+The link:rest-api-accounts.html#account-info[AccountInfo] entity for
+the current user.
+
+* User Preferences Screen:
+** `GerritUiExtensionPoint.PREFERENCES_SCREEN_BOTTOM`:
++
+Panel will be shown at the bottom of the screen.
+
+** The following parameters are provided:
+*** `GerritUiExtensionPoint.Key.ACCOUNT_INFO`:
++
+The link:rest-api-accounts.html#account-info[AccountInfo] entity for
+the current user.
+
+* User Profile Screen:
+** `GerritUiExtensionPoint.PROFILE_SCREEN_BOTTOM`:
++
+Panel will be shown at the bottom of the screen below the grid with the
+profile data.
+
+** The following parameters are provided:
+*** `GerritUiExtensionPoint.Key.ACCOUNT_INFO`:
++
+The link:rest-api-accounts.html#account-info[AccountInfo] entity for
+the current user.
+
+Example panel:
+[source,java]
+----
+public class MyPlugin extends PluginEntryPoint {
+  @Override
+  public void onPluginLoad() {
+    Plugin.get().panel(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+        new Panel.EntryPoint() {
+          @Override
+          public void onLoad(Panel panel) {
+            panel.setWidget(new InlineLabel("My Panel for change "
+                + panel.getInt(GerritUiExtensionPoint.Key.CHANGE_ID, -1));
+          }
+        });
+  }
+}
+----
+
+[[actions]]
+=== Actions
+
 Plugins can contribute UI actions on core Gerrit pages. This is useful
 for workflow customization or exposing plugin functionality through the
 UI in addition to SSH commands and the REST API.
@@ -1519,7 +1637,7 @@
 <plugin>
   <groupId>org.codehaus.mojo</groupId>
   <artifactId>gwt-maven-plugin</artifactId>
-  <version>2.5.1</version>
+  <version>2.7.0</version>
   <configuration>
     <module>com.googlesource.gerrit.plugins.myplugin.HelloPlugin</module>
     <disableClassMetadata>true</disableClassMetadata>
@@ -1657,6 +1775,40 @@
 }
 ----
 
+[[user-settings-screen]]
+== Add User Settings Screen
+
+A link:#gwt_ui_extension[GWT plugin] can implement a user settings
+screen that is integrated into the Gerrit user settings menu.
+
+Example settings screen:
+[source,java]
+----
+public class MyPlugin extends PluginEntryPoint {
+  @Override
+  public void onPluginLoad() {
+    Plugin.get().settingsScreen("my-preferences", "My Preferences",
+        new Screen.EntryPoint() {
+          @Override
+          public void onLoad(Screen screen) {
+            screen.setPageTitle("Settings");
+            screen.add(new InlineLabel("My Preferences"));
+            screen.show();
+          }
+    });
+  }
+}
+----
+
+By defining an link:config-gerrit.html#urlAlias[urlAlias] Gerrit
+administrators can map plugin screens into the Gerrit URL namespace or
+even replace Gerrit screens by plugin screens.
+
+Plugins may also programatically add URL aliases in the preferences of
+of a user. This way certain screens can be replaced for certain users.
+E.g. the plugin may offer a user preferences setting for choosing a
+screen that then sets/unsets a URL alias for the user.
+
 [[settings-screen]]
 == Plugin Settings Screen
 
@@ -1717,17 +1869,18 @@
 [[data-directory]]
 == Data Directory
 
-Plugins can request a data directory with a `@PluginData` File
-dependency. A data directory will be created automatically by the
-server in `$site_path/data/$plugin_name` and passed to the plugin.
+Plugins can request a data directory with a `@PluginData` Path (or File,
+deprecated) dependency. A data directory will be created automatically
+by the server in `$site_path/data/$plugin_name` and passed to the
+plugin.
 
 Plugins can use this to store any data they want.
 
 [source,java]
 ----
 @Inject
-MyType(@PluginData java.io.File myDir) {
-  new FileInputStream(new File(myDir, "my.config"));
+MyType(@PluginData java.nio.file.Path myDir) {
+  this.in = Files.newInputStream(myDir.resolve("my.config"));
 }
 ----
 
@@ -1762,15 +1915,29 @@
 [[download-commands]]
 == Download Commands
 
-Gerrit offers commands for downloading changes using different
-download schemes (e.g. for downloading via different network
-protocols). Plugins can contribute download schemes and download
-commands by implementing
-`com.google.gerrit.extensions.config.DownloadScheme` and
-`com.google.gerrit.extensions.config.DownloadCommand`.
+Gerrit offers commands for downloading changes and cloning projects
+using different download schemes (e.g. for downloading via different
+network protocols). Plugins can contribute download schemes, download
+commands and clone commands by implementing
+`com.google.gerrit.extensions.config.DownloadScheme`,
+`com.google.gerrit.extensions.config.DownloadCommand` and
+`com.google.gerrit.extensions.config.CloneCommand`.
 
-The download schemes and download commands which are used most often
-are provided by the Gerrit core plugin `download-commands`.
+The download schemes, download commands and clone commands which are
+used most often are provided by the Gerrit core plugin
+`download-commands`.
+
+[[included-in]]
+== Included In
+
+For merged changes the link:user-review-ui.html#included-in[Included In]
+drop-down panel shows the branches and tags in which the change is
+included.
+
+Plugins can add additional systems in which the change can be included
+by implementing `com.google.gerrit.extensions.config.ExternalIncludedIn`,
+e.g. a plugin can provide a list of servers on which the change was
+deployed.
 
 [[links-to-external-tools]]
 == Links To External Tools
@@ -1831,8 +1998,8 @@
 prefix is configurable by setting the `Gerrit-HttpDocumentationPrefix`
 attribute.
 
-Documentation may be written in
-link:http://daringfireball.net/projects/markdown/[Markdown] style
+Documentation may be written in the Markdown flavor
+link:https://github.com/sirthias/pegdown[pegdown]
 if the file name ends with `.md`. Gerrit will automatically convert
 Markdown to HTML if accessed with extension `.html`.
 
@@ -1933,6 +2100,40 @@
 Disabled plugins can be re-enabled using the
 link:cmd-plugin-enable.html[plugin enable] command.
 
+== Known issues and bugs
+
+=== Error handling in UI when using the REST API
+
+When a plugin invokes a REST endpoint in the UI, it provides an
+`AsyncCallback` to handle the result. At the moment the
+`onFailure(Throwable)` of the callback is never invoked, even if there
+is an error. Errors are always handled by the Gerrit core UI which
+shows the error dialog. This means currently plugins cannot do any
+error handling and e.g. ignore expected errors.
+
+In the following example the REST endpoint would return '404 Not Found'
+if there is no HTTP password and the Gerrit core UI would display an
+error dialog for this. However having no HTTP password is not an error
+and the plugin may like to handle this case.
+
+[source,java]
+----
+new RestApi("accounts").id("self").view("password.http")
+    .get(new AsyncCallback<NativeString>() {
+
+  @Override
+  public void onSuccess(NativeString httpPassword) {
+    // TODO
+  }
+
+  @Override
+  public void onFailure(Throwable caught) {
+    // never invoked
+  }
+});
+----
+
+
 == SEE ALSO
 
 * link:js-api.html[JavaScript API]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index f59f2fc..4a5c0bd 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -56,6 +56,17 @@
 Eclipse integration and then open it as Eclipse project in IDEA.
 You need the Eclipse plugin activated in IntelliJ IDEA.
 
+Once you start compiling using both buck and your Gerrit project in
+IDEA, you will likely need to mark the below directories as generated
+sources roots. You can do so using the IDEA "Project" view. In the
+context menu of each one of these, use "Mark Directory As" to mark
+them as "Generated Sources Root":
+
+----
+  __auto_value_tests_gen__
+  __httpd_gen__
+  __server_gen__
+----
 
 == Mac OS X
 
@@ -76,7 +87,7 @@
 testing site for development use:
 
 ----
-  java -jar buck-out/gen/gerrit.war init -d ../gerrit_testsite
+  java -jar buck-out/gen/gerrit/gerrit.war init -d ../gerrit_testsite
 ----
 
 Accept defaults by pressing Enter until 'init' completes, or add
@@ -119,7 +130,7 @@
 copying to the test site:
 
 ----
-  java -jar buck-out/gen/gerrit.war daemon -d ../gerrit_testsite
+  java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite
 ----
 
 === Running the Daemon with Gerrit Inspector
@@ -138,7 +149,7 @@
 command used to launch the daemon:
 
 ----
-  java -jar buck-out/gen/gerrit.war daemon -d ../gerrit_testsite -s
+  java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite -s
 ----
 
 Gerrit Inspector examines Java libraries first, then loads
@@ -165,7 +176,7 @@
 command line.  If the daemon is not currently running:
 
 ----
-  java -jar buck-out/gen/gerrit.war gsql -d ../gerrit_testsite
+  java -jar buck-out/gen/gerrit/gerrit.war gsql -d ../gerrit_testsite
 ----
 
 Or, if it is running and the database is in use, connect over SSH
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index f3397a4..b2c5358 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -10,12 +10,11 @@
 To be able to publish artifacts to Maven Central some preparations must
 be done:
 
-* Create a Sonatype account as described in the
-link:https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide#SonatypeOSSMavenRepositoryUsageGuide-2.Signup[
-Sonatype OSS Maven Repository Usage Guide].
+* Create an account on
+link:https://issues.sonatype.org/secure/Signup!default.jspa[Sonatype's Jira].
 +
 Sonatype is the company that runs Maven Central and you need a Sonatype
-account for uploading artifacts to Maven Central.
+account to be able to upload artifacts to Maven Central.
 
 * Configure your Sonatype user and password in `~/.m2/settings.xml`:
 +
@@ -75,7 +74,7 @@
 
 Gerrit Subproject Artifacts are stored on
 link:https://developers.google.com/storage/[Google Cloud Storage].
-Via the link:https://code.google.com/apis/console/?noredirect[API Console] the
+Via the link:https://console.developers.google.com/project/164060093628[Developers Console] the
 Gerrit maintainers have access to the `Gerrit Code Review` project.
 This projects host several buckets for storing Gerrit artifacts:
 
@@ -90,13 +89,14 @@
 
 To upload artifacts to a bucket the user must authenticate with a
 username and password. The username and password need to be retrieved
-from the link:https://code.google.com/apis/console/?noredirect[API Console]:
+from the link:https://console.developers.google.com/project/164060093628[
+Google Developers Console]:
 
-* Go to the `Gerrit Code Review` project
-* In the menu on the left select `Google Cloud Storage` >
-`Interoperable Access`
-* Use the `Access Key` as username
-* Click under `Secret` on the `Show` button to find the password
+* In the menu on the left select `Storage` -> `Cloud Storage` >
+> `Storage access`
+* Select the `Interoperability` tab
+* If no keys are listed under `Interoperable storage access keys`, select "Create a new key"
+* Use the `Access Key` as username, and `Secret` as the password
 
 To make the username and password known to Maven, they must be
 configured in the `~/.m2/settings.xml` file.
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 3d3104c..3157214 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -84,8 +84,8 @@
 . link:#subproject[Release Subprojects]
 . link:#build-gerrit[Build the Gerrit Release]
 . link:#publish-gerrit[Publish the Gerrit Release]
-.. link:#extension-and-plugin-api[Publish the Extension and Plugin API Jars]
-.. link:#publish-gerrit-war[Publish the Gerrit WAR (with Core Plugins)]
+.. link:#publish-to-maven-central[Publish the Gerrit artifacts to Maven Central]
+.. link:#publish-to-google-storage[Publish the Gerrit WAR to Google Storage]
 .. link:#push-stable[Push the Stable Branch]
 .. link:#push-tag[Push the Release Tag]
 .. link:#upload-documentation[Upload the Documentation]
@@ -157,7 +157,8 @@
 * Build the Gerrit WAR and API JARs
 +
 ----
-  buck build release
+  buck clean
+  buck build --no-cache release
   buck build api_install
 ----
 
@@ -167,15 +168,6 @@
 [[publish-gerrit]]
 === Publish the Gerrit Release
 
-[[publish-gerrit-war]]
-==== Publish the Gerrit WAR (with Core Plugins)
-
-* Upload the WAR to the Google Cloud Storage
-
-** go to https://console.developers.google.com/project/164060093628/storage/gerrit-releases/
-** make sure you are signed in with your Gmail account
-** manually upload the Gerrit WAR file by using the `Upload` button
-
 [[publish-to-maven-central]]
 ==== Publish the Gerrit artifacts to Maven Central
 
@@ -230,10 +222,6 @@
 link:https://oss.sonatype.org/[Sonatype Nexus Server].
 
 * Verify the staging repository
-+
-How to do this is described in the
-link:https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide#SonatypeOSSMavenRepositoryUsageGuide-8.a.1.ClosingaStagingRepository[
-Sonatype OSS Maven Repository Usage Guide].
 
 ** Go to the link:https://oss.sonatype.org/[Sonatype Nexus Server] and
 sign in with your Sonatype credentials.
@@ -305,6 +293,13 @@
 ** Select `com.google.gerrit` as `Project`.
 
 
+[[publish-to-google-storage]]
+==== Publish the Gerrit WAR to the Google Cloud Storage
+
+* go to https://console.developers.google.com/project/164060093628/storage/gerrit-releases/
+* make sure you are signed in with your Gmail account
+* manually upload the Gerrit WAR file by using the `Upload` button
+
 [[push-stable]]
 ==== Push the Stable Branch
 
@@ -352,18 +347,13 @@
 
 * Upload the html files manually via web browser to the
 link:https://console.developers.google.com/project/164060093628/storage/gerrit-documentation/[
-gerrit-documentation] storage bucket. The `gerrit-documentation`
-storage bucket is accessible via the
-link:https://cloud.google.com/console[Google Developers Console].
+gerrit-documentation] storage bucket.
 
 [[update-links]]
-==== Update Google Code project links
+==== Update homepage links
 
-* Go to http://code.google.com/p/gerrit/admin
-* Update the documentation link in the `Resources` section of the
-Description text, and in the `Links` section.
-* Add a link to the new release notes in the `News` section of the
-Description text
+Upload a change on the link:https://gerrit-review.googlesource.com/#/admin/projects/homepage[
+homepage project] to change the version numbers to the new version.
 
 [[update-issues]]
 ==== Update the Issues
diff --git a/Documentation/doc.css.in b/Documentation/doc.css.in
index 6be89f6..429e81c 100644
--- a/Documentation/doc.css.in
+++ b/Documentation/doc.css.in
@@ -17,6 +17,10 @@
   border-bottom: 2px solid silver;
 }
 
+h1 {
+  margin-top: 1.5em;
+}
+
 p {
   margin: 0.5em 0 0.5em 0;
 }
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index af7cd88..db3480b 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,6 +17,7 @@
 
 from __future__ import print_function
 
+import argparse
 from collections import defaultdict, deque
 from os import chdir, path
 import re
@@ -24,7 +25,13 @@
 from subprocess import Popen, PIPE
 from sys import stdout, stderr
 
-MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+parser = argparse.ArgumentParser()
+parser.add_argument('--asciidoc', action='store_true')
+parser.add_argument('--partial', action='store_true')
+parser.add_argument('--classpath', action='append')
+parser.add_argument('targets', nargs='+')
+args = parser.parse_args()
+
 KNOWN_PROVIDED_DEPS = [
   '//lib/bouncycastle:bcpg',
   '//lib/bouncycastle:bcpkix',
@@ -35,14 +42,25 @@
   graph = defaultdict(list)
   while not path.isfile('.buckconfig'):
     chdir('..')
+  # TODO(davido): use passed in classpath from Buck instead
   p = Popen(
-    ['buck', 'audit', 'classpath', '--dot'] + MAIN,
+    ['buck', 'audit', 'classpath', '--dot'] + args.targets,
     stdout = PIPE)
   for line in p.stdout:
     m = re.search(r'"(//.*?)" -> "(//.*?)";', line)
     if not m:
       continue
     target, dep = m.group(1), m.group(2)
+    if args.partial:
+      if dep == '//lib/codemirror:js_minifier':
+        if target == '//lib/codemirror:js':
+          continue
+        if target.startswith('//lib/codemirror:mode_'):
+          continue
+      if target == '//gerrit-gwtui:ui_module' and \
+         dep == '//gerrit-gwtexpui:CSS':
+        continue
+
     # Dependencies included in provided_deps set are contained in audit
     # classpath and must be sorted out. That's safe thing to do because
     # they are not included in the final artifact.
@@ -60,7 +78,7 @@
 graph = parse_graph()
 licenses = defaultdict(set)
 
-queue = deque(MAIN)
+queue = deque(args.targets)
 while queue:
   target = queue.popleft()
   for dep in graph[target]:
@@ -70,7 +88,8 @@
   queue.extend(graph[target])
 used = sorted(licenses.keys())
 
-print("""\
+if args.asciidoc:
+  print("""\
 Gerrit Code Review - Licenses
 =============================
 
@@ -122,26 +141,33 @@
 for n in used:
   libs = sorted(licenses[n])
   name = n[len('//lib:LICENSE-'):]
-  print()
-  print('[[%s]]' % name.replace('.', '_'))
-  print(name)
-  print('~' * len(name))
-  print()
+  if args.asciidoc:
+    print()
+    print('[[%s]]' % name.replace('.', '_'))
+    print(name)
+    print('~' * len(name))
+    print()
+  else:
+    print()
+    print(name)
+    print('--')
   for d in libs:
     if d.startswith('//lib:') or d.startswith('//lib/'):
       p = d[len('//lib:'):]
     else:
       p = d[d.index(':')+1:].lower()
     print('* ' + p)
-  print()
-  print('[[license]]')
-  print('[verse]')
-  print('--')
+  if args.asciidoc:
+    print()
+    print('[[license]]')
+    print('[verse]')
+    print('--')
   with open(n[2:].replace(':', '/')) as fd:
     copyfileobj(fd, stdout)
   print('--')
 
-print("""
+if args.asciidoc:
+  print("""
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 9b477ae..63e3be6 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -7,23 +7,21 @@
 .. link:intro-project-owner.html[Project Owner Guide]
 .. link:http://source.android.com/submit-patches/workflow[Default Android Workflow] (external)
 . Web
-.. Registering a new Gerrit account
 .. link:user-review-ui.html[Reviewing Changes]
 .. link:user-search.html[Searching Changes]
 .. link:user-inline-edit.html[Manipulating Changes in Browser]
 .. link:user-notify.html[Subscribing to Email Notifications]
 . SSH
-.. SSH connection details
+.. link:user-upload.html#ssh[SSH connection details]
 .. link:cmd-index.html[Command Line Tools]
 . Git
-.. Git connection details
 .. Commands, scenarios
 ... link:user-upload.html[Uploading Changes]
 ... link:error-messages.html[Error Messages]
 .. Changes
 ... link:user-changeid.html[Change-Id Lines]
 ... link:user-signedoffby.html[Signed-off-by Lines]
-.. Patch sets
+... link:user-change-cleanup.html[Change Cleanup]
 
 == Project Management
 . link:project-configuration.html[Project Configuration]
@@ -31,13 +29,13 @@
 .. link:config-labels.html[Review Labels]
 .. link:config-project-config.html[Access Controls Configuration Format]
 . Multi-project management
-.. Submodules
-.. Repo
+.. link:user-submodules.html[Submodules]
+.. link:https://source.android.com/source/using-repo.html[Repo] (external)
 . Prolog rules
 .. link:prolog-cookbook.html[Prolog Cookbook]
 .. link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
 . link:user-submodules.html[Subscribing to Git Submodules]
-. Project sunset
+. link:intro-project-owner.html#project-deletion[Project deletion]
 
 == Customization and Integration
 . link:user-dashboards.html[Dashboards]
@@ -52,15 +50,9 @@
 == Server Administration
 . link:install.html[Installation Guide]
 . link:config-gerrit.html[System Settings]
-. Backup
-. Performance tuning
-.. link:cmd-index.html[Command Line Tools]
-.. Reading show-caches efficiently
-.. How to read stats from the JVM
-. High availability
-. Replication
+. link:cmd-index.html[Command Line Tools]
+. link:config-plugins.html#replication[Replication]
 . link:config-plugins.html[Plugins]
-. link:config-contact.html[User Contact Information]
 . link:config-reverseproxy.html[Reverse Proxy]
 . link:config-auto-site-initialization.html[Automatic Site Initialization on Startup]
 . link:pgm-index.html[Server Side Administrative Tools]
@@ -76,7 +68,6 @@
 .. link:dev-build-plugins.html[Building Gerrit plugins]
 .. link:js-api.html[JavaScript Plugin API]
 .. link:config-validation.html[Validation Interfaces]
-. Documentation formatting guide for contributions
 . link:dev-design.html[System Design]
 . link:i18n-readme.html[i18n Support]
 
@@ -86,11 +77,11 @@
 
 == Resources
 * link:licenses.html[Licenses and Notices]
-* link:http://code.google.com/p/gerrit/[Homepage]
-* link:http://gerrit-releases.storage.googleapis.com/index.html[Downloads]
+* link:https://www.gerritcodereview.com/[Homepage]
+* link:https://www.gerritcodereview.com/download/index.html[Downloads]
 * link:https://bugs.chromium.org/p/gerrit/issues/list[Issue Tracking]
-* link:http://code.google.com/p/gerrit/source/checkout[Source Code]
-* link:http://code.google.com/p/gerrit/wiki/Background[A History of Gerrit Code Review]
+* link:https://gerrit.googlesource.com/gerrit[Source Code]
+* link:https://www.gerritcodereview.com/about.md[A History of Gerrit Code Review]
 
 SEARCHBOX
 ---------
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index f4c12a9..2623256 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -58,11 +58,11 @@
 
 You can choose from different versions to download from here:
 
-* http://code.google.com/p/gerrit/downloads/list[A list of releases available]
+* https://www.gerritcodereview.com/download/index.html[A list of releases available]
 
 This tutorial is based on version 2.2.2, and you can download that from this link
 
-* http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.war[Link to the 2.2.2 war archive]
+* https://www.gerritcodereview.com/download/gerrit-2.2.2.war[Link to the 2.2.2 war archive]
 
 
 [[initialization]]
@@ -121,7 +121,7 @@
 This is done via the SSH port:
 
 ----
-  user@host:~$ ssh -p 29418 user@localhost gerrit create-project --empty-commit --name demo-project
+  user@host:~$ ssh -p 29418 user@localhost gerrit create-project demo-project --empty-commit
   user@host:~$
 ----
 
@@ -134,7 +134,7 @@
 First you have to create the project.  This is done via the SSH port:
 
 ----
-  user@host:~$ ssh -p 29418 user@localhost gerrit create-project --name demo-project
+  user@host:~$ ssh -p 29418 user@localhost gerrit create-project demo-project
   user@host:~$
 ----
 
diff --git a/Documentation/install.txt b/Documentation/install.txt
index df2f0dd..3f7d1c1 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -45,7 +45,7 @@
 == Download Gerrit
 
 Current and past binary releases of Gerrit can be obtained from
-the link:https://gerrit-releases.storage.googleapis.com/index.html[
+the link:https://www.gerritcodereview.com/download/index.html[
 Gerrit Releases site].
 
 Download any current `*.war` package. The war will be referred to as
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index f5d5277..dfffe57 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -63,7 +63,7 @@
 commit message is provided you can also see from the history why the
 access rights were modified.
 
-If a Git browser such as GitWeb is configured for the Gerrit server you
+If a Git browser such as gitweb is configured for the Gerrit server you
 can find a link to the history of the `project.config` file in the
 Web UI. Otherwise you may inspect the history locally. If you have
 cloned the repository you can do this by executing the following
@@ -238,7 +238,7 @@
 === Submit Type
 
 An important decision for a project is the choice of the submit type
-and the content merge setting (aka `Automatically resolve conflicts`).
+and the content merge setting (see the `Allow content merges` option).
 The link:project-configuration.html#submit_type[submit type] is the method
 Gerrit uses to submit a change to the project. The submit type defines
 what Gerrit should do on submit of a change if the destination branch
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 4677307..b9bdad0 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -29,6 +29,9 @@
 
 * link:http://eclipse.org/mylyn/[Mylyn Gerrit Connector]: Gerrit
   integration with Mylyn
+* link:https://github.com/uwolfer/gerrit-intellij-plugin[Gerrit
+  IntelliJ Plugin]: Gerrit integration with the
+  link:http://www.jetbrains.com/idea/[IntelliJ Platform]
 * link:https://play.google.com/store/apps/details?id=com.jbirdvegas.mgerrit[
   mGerrit]: Android client for Gerrit
 * link:https://github.com/stackforge/gertty[Gertty]: Console-based
@@ -560,11 +563,23 @@
 auto-merge on push] to benefit from the automatic merge/rebase on
 server side while pushing directly into the repository.
 
+[[user-refs]]
+== User Refs
+
+User configuration data such as link:#preferences[preferences] is
+stored in the `All-Users` project under a per-user ref.  The user's
+ref is based on the user's account id which is an integer.  The user
+refs are sharded by the last two digits (`+nn+`) in the refname,
+leading to refs of the format `+refs/users/nn/accountid+`.
+
 [[preferences]]
 == Preferences
 
 There are several options to control the rendering in the Gerrit web UI.
 Users can configure their preferences under `Settings` > `Preferences`.
+The user's preferences are stored in a `git config` style file named
+`preferences.config` under the link:#user-refs[user's ref] in the
+`All-Users` project.
 
 The following preferences can be configured:
 
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 43e4336..03ff5a5 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -60,11 +60,25 @@
   a string, otherwise the result is a JavaScript object or array,
   as described in the relevant REST API documentation.
 
+[[self_getServerInfo]]
+=== self.getServerInfo()
+Returns the server's link:rest-api-config.html#server-info[ServerInfo]
+data.
+
 [[self_getCurrentUser]]
 === self.getCurrentUser()
 Returns the currently signed in user's AccountInfo data; empty account
 data if no user is currently signed in.
 
+[[Gerrit_getUserPreferences]]
+=== Gerrit.getUserPreferences()
+Returns the preferences of the currently signed in user; the default
+preferences if no user is currently signed in.
+
+[[Gerrit_refreshUserPreferences]]
+=== Gerrit.refreshUserPreferences()
+Refreshes the preferences of the current user.
+
 [[self_getPluginName]]
 === self.getPluginName()
 Returns the name this plugin was installed as by the server
diff --git a/Documentation/json.txt b/Documentation/json.txt
index feef1a1..32fa472 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -43,9 +43,6 @@
 
   DRAFT;; Change is a draft change that only consists of draft patchsets.
 
-  SUBMITTED;; Change has been submitted and is in the merge queue.
-  It may be waiting for one or more dependencies.
-
   MERGED;; Change has been merged to its branch.
 
   ABANDONED;; Change was abandoned by its owner or administrator.
@@ -156,7 +153,7 @@
 
 newRev:: The new value the ref was updated to.
 
-refName:: Ref name within project.
+refName:: Full ref name within project.
 
 project:: Project path in Gerrit.
 
diff --git a/Documentation/man/.gitignore b/Documentation/man/.gitignore
deleted file mode 100644
index 7993a55..0000000
--- a/Documentation/man/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-gerrit*
diff --git a/Documentation/man/Makefile b/Documentation/man/Makefile
deleted file mode 100644
index 945f215..0000000
--- a/Documentation/man/Makefile
+++ /dev/null
@@ -1,64 +0,0 @@
-# Copyright (C) 2013 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.
-
-A2X ?= a2x
-
-all: man
-
-clean:
-	@rm -f gerrit-*
-
-CMD_CORE_SSH_CMD := \
-cmd-ban-commit.txt     \
-cmd-create-account.txt \
-cmd-create-group.txt   \
-cmd-create-project.txt \
-cmd-flush-caches.txt   \
-cmd-gc.txt             \
-cmd-gsql.txt           \
-cmd-ls-groups.txt      \
-cmd-ls-projects.txt    \
-cmd-ls-user-refs.txt   \
-cmd-query.txt          \
-cmd-rename-group.txt   \
-cmd-review.txt         \
-cmd-set-account.txt    \
-cmd-set-members.txt    \
-cmd-set-project-parent.txt \
-cmd-set-project.txt    \
-cmd-set-reviewers.txt  \
-cmd-show-caches.txt    \
-cmd-show-connections.txt   \
-cmd-show-queue.txt     \
-cmd-stream-events.txt  \
-cmd-test-submit-rule.txt   \
-cmd-version.txt
-
-GERRIT_CORE_SSH_CMD := $(patsubst cmd-%,gerrit-%,$(CMD_CORE_SSH_CMD))
-DOC_MAN := $(patsubst %.txt,%.1,$(GERRIT_CORE_SSH_CMD))
-
-man: $(GERRIT_CORE_SSH_CMD) $(DOC_MAN)
-
-$(GERRIT_CORE_SSH_CMD) : gerrit-%.txt : ../cmd-%.txt
-	@cp $< $@
-
-$(DOC_MAN) : %.1 : %.txt
-	@echo "creating man page for $@ ..."
-	@rm -f $@
-	@$(eval TITLE := $(join $(basename $<),\(1\)))
-	@$(eval SEPERATOR := $(shell echo $(TITLE) | sed 's/./=/g'))
-	@sed -i -re '1s/^.*$//$(TITLE)/' $<
-	@sed -i -re '2s/^=.*/$(SEPERATOR)/' $<
-	@sed -i -re '6s/^gerrit\s+(\w)/gerrit-\1/' $<
-	@$(A2X) --doctype manpage --format manpage $<
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 39cd70d..6aa3a74 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -11,6 +11,7 @@
 	[--no-auto-start]
 	[--list-plugins]
 	[--install-plugin=<PLUGIN_NAME>]
+        [--dev]
 --
 
 == DESCRIPTION
@@ -51,6 +52,10 @@
 	This option may be supplied more than once to install multiple
 	plugins.
 
+--dev::
+	Install in developer mode. Default configuration settings are
+	chosen to run the Gerrit server as a developer.
+
 == CONTEXT
 This command can only be run on a server which has direct
 connectivity to the metadata database, and local access to the
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
index e1d8e8b..bf09e0c 100644
--- a/Documentation/pgm-reindex.txt
+++ b/Documentation/pgm-reindex.txt
@@ -24,9 +24,6 @@
 --verbose::
 	Output debug information for each change.
 
---dry-run::
-	Dry run.  Don't write anything to index.
-
 == CONTEXT
 The secondary index must be enabled. See
 link:config-gerrit.html#index.type[index.type].
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 1cecabf..6d7c6d0 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -114,7 +114,7 @@
 the same file has also been changed on the other side of the merge.
 
 [[content_merge]]
-If `Automatically resolve conflicts` is enabled, Gerrit will try
+If `Allow content merges` is enabled, Gerrit will try
 to do a content merge when a path conflict occurs.
 
 [[project-state]]
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index b15c283..b53da4b 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -56,7 +56,7 @@
 examples.
 
 == Prolog in Gerrit
-Gerrit uses its own link:https://code.google.com/p/prolog-cafe/[fork] of the
+Gerrit uses its own link:https://gerrit.googlesource.com/prolog-cafe/[fork] of the
 original link:http://kaminari.istc.kobe-u.ac.jp/PrologCafe/[prolog-cafe]
 project. Gerrit embeds the prolog-cafe library and can interpret Prolog programs
 at runtime.
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 8deee62..fec4a58 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index a510e8a..5459306 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -117,6 +117,36 @@
   }
 ----
 
+[[get-detail]]
+=== Get Account Details
+--
+'GET /accounts/link:#account-id[\{account-id\}]/detail'
+--
+
+Retrieves the details of an account as an link:#account-detail-info[
+AccountDetailInfo] entity.
+
+.Request
+----
+  GET /accounts/self/detail HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "registered_on": "2015-07-23 07:01:09.296000000",
+    "_account_id": 1000096,
+    "name": "John Doe",
+    "email": "john.doe@example.com",
+    "username": "john"
+  }
+----
+
 [[get-account-name]]
 === Get Account Name
 --
@@ -223,6 +253,30 @@
 
 If the account does not have a username the response is "`404 Not Found`".
 
+[[set-username]]
+=== Set Username
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/username'
+--
+
+The new username must be provided in the request body inside
+a link:#username-input[UsernameInput] entity.
+
+Once set, the username cannot be changed or deleted. If attempted this
+fails with "`405 Method Not Allowed`".
+
+.Request
+----
+  PUT /accounts/self/username HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "username": "jdoe"
+  }
+----
+
+As response the new username is returned.
+
 [[get-active]]
 === Get Active
 --
@@ -633,6 +687,148 @@
   HTTP/1.1 204 No Content
 ----
 
+[[list-gpg-keys]]
+=== List GPG Keys
+--
+'GET /accounts/link:#account-id[\{account-id\}]/gpgkeys'
+--
+
+Returns the GPG keys of an account.
+
+.Request
+----
+  GET /accounts/self/gpgkeys HTTP/1.0
+----
+
+As a response, the GPG keys of the account are returned as a map of
+link:#gpg-key-info[GpgKeyInfo] entities, keyed by ID.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "AFC8A49B": {
+      "fingerprint": "0192 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B",
+      "user_ids": [
+        "John Doe \u003cjohn.doe@example.com\u003e"
+      ],
+      "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0",
+      "status": "TRUSTED",
+      "problems": [],
+    },
+  }
+----
+
+[[get-gpg-key]]
+=== Get GPG Key
+--
+'GET /accounts/link:#account-id[\{account-id\}]/gpgkeys/link:#gpg-key-id[\{gpg-key-id\}]'
+--
+
+Retrieves a GPG key of a user.
+
+.Request
+----
+  GET /accounts/self/gpgkeys/AFC8A49B HTTP/1.0
+----
+
+As a response, a link:#gpg-key-info[GpgKeyInfo] entity is returned that
+describes the GPG key.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "AFC8A49B",
+    "fingerprint": "0192 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B",
+    "user_ids": [
+      "John Doe \u003cjohn.doe@example.com\u003e"
+    ],
+    "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0",
+    "status": "TRUSTED",
+    "problems": [],
+  }
+----
+
+[[add-delete-gpg-keys]]
+=== Add/Delete GPG Keys
+--
+'POST /accounts/link:#account-id[\{account-id\}]/gpgkeys'
+--
+
+Add or delete one or more GPG keys for a user.
+
+The changes must be provided in the request body as a
+link:#gpg-keys-input[GpgKeysInput] entity. Each new GPG key is provided in
+ASCII armored format, and must contain a self-signed certification
+matching a registered email or other identity of the user.
+
+.Request
+----
+  POST /accounts/link:#account-id[\{account-id\}]/gpgkeys
+  Content-Type: application/json
+
+  {
+    "add": [
+      "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0yZO5AQ0E\nVdSk1wEIALUycrH2HK9zQYdR/KJo1yJJuaextLWsYYn881yDQo/p06U5vXOZ28lG\nAq/Xs96woVZPbgME6FyQzhf20Z2sbr+5bNo3OcEKaKX3Eo/sWwSJ7bXbGLDxMf4S\netfY1WDC+4rTqE30JuC++nQviPRdCcZf0AEgM6TxVhYEMVYwV787YO1IH62EBICM\nSkIONOfnusNZ4Skgjq9OzakOOpROZ4tki5cH/5oSDgdcaGPy1CFDpL9fG6er2zzk\nsw3qCbraqZrrlgpinWcAduiao67U/dV18O6OjYzrt33fTKZ0+bXhk1h1gloC21MQ\nya0CXlnfR/FOQhvuK0RlbR3cMfhZQscAEQEAAYkBHwQYAQIACQUCVdSk1wIbDAAK\nCRCTUJ5Lr8ikm8+QB/4uE+AlvFQFh9W8koPdfk7CJF7wdgZZ2NDtktvLL71WuMK8\nPOmf9f5JtcLCX4iJxGzcWogAR5ed20NgUoHUg7jn9Xm3fvP+kiqL6WqPhjazd89h\nk06v9hPE65kp4wb0fQqDrtWfP1lFGuh77rQgISt3Y4QutDl49vXS183JAfGPxFxx\n8FgGcfNwL2LVObvqCA0WLqeIrQVbniBPFGocE3yA/0W9BB/xtolpKfgMMsqGRMeu\n9oIsNxB2oE61OsqjUtGsnKQi8k5CZbhJaql4S89vwS+efK0R+mo+0N55b0XxRlCS\nfaURgAcjarQzJnG0hUps2GNO/+nM7UyyJAGfHlh5\n=EdXO\n-----END PGP PUBLIC KEY BLOCK-----\n"
+    ],
+    "delete": [
+      "DEADBEEF",
+    ]
+  }'
+----
+
+As a response, the modified GPG keys are returned as a map of
+link:#gpg-key-info[GpgKeyInfo] entities, keyed by ID. Deleted keys are
+represented by an empty object.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "AFC8A49B": {
+      "fingerprint": "0192 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B",
+      "user_ids": [
+        "John Doe \u003cjohn.doe@example.com\u003e"
+      ],
+      "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: BCPG v1.52\n\nmQENBFXUpNcBCACv4paCiyKxZ0EcKy8VaWVNkJlNebRBiyw9WxU85wPOq5Gz/3GT\nRQwKqeY0SxVdQT8VNBw2sBe2m6eqcfZ2iKmesSlbXMe15DA7k8Bg4zEpQ0tXNG1L\nhceZDVQ1Xk06T2sgkunaiPsXi82nwN3UWYtDXxX4is5e6xBNL48Jgz4lbqo6+8D5\nvsVYiYMx4AwRkJyt/oA3IZAtSlY8Yd445nY14VPcnsGRwGWTLyZv9gxKHRUppVhQ\nE3o6ePXKEVgmONnQ4CjqmkGwWZvjMF2EPtAxvQLAuFa8Hqtkq5cgfgVkv/Vrcln4\nnQZVoMm3a3f5ODii2tQzNh6+7LL1bpqAmVEtABEBAAG0H0pvaG4gRG9lIDxqb2hu\nLmRvZUBleGFtcGxlLmNvbT6JATgEEwECACIFAlXUpNcCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheAAAoJEJNQnkuvyKSbfjoH/2OcSQOu1kJ20ndjhgY2yNChm7gd\ntU7TEBbB0TsLeazkrrLtKvrpW5+CRe07ZAG9HOtp3DikwAyrhSxhlYgVsQDhgB8q\nG0tYiZtQ88YyYrncCQ4hwknrcWXVW9bK3V4ZauxzPv3ADSloyR9tMURw5iHCIeL5\nfIw/pLvA3RjPMx4Sfow/bqRCUELua39prGw5Tv8a2ZRFbj2sgP5j8lUFegyJPQ4z\ntJhe6zZvKOzvIyxHO8llLmdrImsXRL9eqroWGs0VYqe6baQpY6xpSjbYK0J5HYcg\nTO+/u80JI+ROTMHE6unGp5Pgh/xIz6Wd34E0lWL1eOyNfGiPLyRWn1d0"
+      "status": "TRUSTED",
+      "problems": [],
+    }
+    "DEADBEEF": {}
+  }
+----
+
+[[delete-gpg-key]]
+=== Delete GPG Key
+--
+'DELETE /accounts/link:#account-id[\{account-id\}]/gpgkeys/link:#gpg-key-id[\{gpg-key-id\}]'
+--
+
+Deletes a GPG key of a user.
+
+.Request
+----
+  DELETE /accounts/self/gpgkeys/AFC8A49B HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[list-account-capabilities]]
 === List Account Capabilities
 --
@@ -1063,9 +1259,10 @@
   {
     "context": 10,
     "theme": "DEFAULT",
-    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
+    "cursor_blink_rate": 500,
     "show_tabs": true,
     "show_whitespace_errors": true,
     "syntax_highlighting": true,
@@ -1086,15 +1283,16 @@
 
 .Request
 ----
-  GET /a/accounts/self/preferences.diff HTTP/1.0
+  PUT /a/accounts/self/preferences.diff HTTP/1.0
   Content-Type: application/json; charset=UTF-8
 
   {
     "context": 10,
     "theme": "ECLIPSE",
-    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
+    "cursor_blink_rate": 500,
     "show_line_endings": true,
     "show_tabs": true,
     "show_whitespace_errors": true,
@@ -1116,7 +1314,7 @@
   {
     "context": 10,
     "theme": "ECLIPSE",
-    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
     "show_line_endings": true,
@@ -1127,6 +1325,84 @@
   }
 ----
 
+[[get-edit-preferences]]
+=== Get Edit Preferences
+--
+'GET /accounts/link:#account-id[\{account-id\}]/preferences.edit'
+--
+
+Retrieves the edit preferences of a user.
+
+.Request
+----
+  GET /a/accounts/self/preferences.edit HTTP/1.0
+----
+
+As result the edit preferences of the user are returned as a
+link:#edit-preferences-info[EditPreferencesInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "theme": "ECLIPSE",
+    "key_map_type": "VIM",
+    "tab_size": 4,
+    "line_length": 80,
+    "cursor_blink_rate": 530,
+    "hide_top_menu": true,
+    "show_whitespace_errors": true,
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "line_wrapping": false,
+    "auto_close_brackets": true
+  }
+----
+
+[[set-edit-preferences]]
+=== Set Edit Preferences
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/preferences.edit'
+--
+
+Sets the edit preferences of a user.
+
+The new edit preferences must be provided in the request body as a
+link:#edit-preferences-info[EditPreferencesInfo] entity.
+
+.Request
+----
+  PUT /a/accounts/self/preferences.edit HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "theme": "ECLIPSE",
+    "key_map_type": "VIM",
+    "tab_size": 4,
+    "line_length": 80,
+    "cursor_blink_rate": 530,
+    "hide_top_menu": true,
+    "show_tabs": true,
+    "show_whitespace_errors": true,
+    "syntax_highlighting": true,
+    "hide_line_numbers": true,
+    "match_brackets": true,
+    "line_wrapping": false,
+    "auto_close_brackets": true
+  }
+----
+
+The response is "`204 No Content`"
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[get-starred-changes]]
 === Get Starred Changes
 --
@@ -1241,10 +1517,32 @@
 === \{ssh-key-id\}
 The sequence number of the SSH key.
 
+[[gpg-key-id]]
+=== \{gpg-key-id\}
+A GPG key identifier, either the 8-character hex key reported by
+`gpg --list-keys`, or the 40-character hex fingerprint (whitespace is
+ignored) reported by `gpg --list-keys --with-fingerprint`.
+
 
 [[json-entities]]
 == JSON Entities
 
+[[account-detail-info]]
+=== AccountDetailInfo
+The `AccountDetailInfo` entity contains detailled information about an
+account.
+
+`AccountDetailInfo` has the same fields as link:#account-info[
+AccountInfo]. In addition `AccountDetailInfo` has the following fields:
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name          ||Description
+|`registered_on`     ||
+The link:rest-api.html#timestamp[timestamp] of when the account was
+registered.
+|=================================
+
 [[account-info]]
 === AccountInfo
 The `AccountInfo` entity contains information about an account.
@@ -1327,6 +1625,9 @@
 capability.
 |`killTask`          |not set if `false`|Whether the user has the
 link:access-control.html#capability_kill[Kill Task] capability.
+|`maintainServer`    |not set if `false`|Whether the user has the
+link:access-control.html#capability_maintainServer[Maintain
+Server] capability.
 |`priority`          |not set if `INTERACTIVE`|The name of the thread
 pool used by the user, see link:access-control.html#capability_priority[
 Priority] capability.
@@ -1372,12 +1673,15 @@
 |`ignore_whitespace`           ||
 Whether whitespace changes should be ignored and if yes, which
 whitespace changes should be ignored. +
-Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
-`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+Allowed values are `IGNORE_NONE`, `IGNORE_TRAILING`,
+`IGNORE_LEADING_AND_TRAILING`, `IGNORE_ALL`.
 |`intraline_difference`        |not set if `false`|
 Whether intraline differences should be highlighted.
 |`line_length`                 ||
 Number of characters that should be displayed in one line.
+|`cursor_blink_rate`           ||
+Half-period in milliseconds used for cursor blinking.
+Setting it to 0 disables cursor blinking.
 |`manual_review`               |not set if `false`|
 Whether the 'Reviewed' flag should not be set automatically on a patch
 when it is viewed.
@@ -1399,7 +1703,7 @@
 |`syntax_highlighting`         |not set if `false`|
 Whether syntax highlighting should be enabled.
 |`hide_top_menu`               |not set if `false`|
-If true the top menu header and site header is hidden.
+If true the top menu header and site header are hidden.
 |`auto_hide_diff_table_header` |not set if `false`|
 If true the diff table header is automatically hidden when
 scrolling down more than half of a page.
@@ -1410,6 +1714,10 @@
 |'hide_empty_pane'             |not set if `false`|
 Whether empty panes should be hidden. The left pane is empty when a
 file was added; the right pane is empty when a file was deleted.
+|`match_brackets`              |not set if `false`|
+Whether matching brackets should be highlighted.
+|`line_wrapping`               |not set if `false`|
+Whether to enable line wrapping or not.
 |===========================================
 
 [[diff-preferences-input]]
@@ -1428,8 +1736,8 @@
 |`ignore_whitespace`           |optional|
 Whether whitespace changes should be ignored and if yes, which
 whitespace changes should be ignored. +
-Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
-`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+Allowed values are `IGNORE_NONE`, `IGNORE_TRAILING`,
+`IGNORE_LEADING_AND_TRAILING`, `IGNORE_ALL`.
 |`intraline_difference`        |optional|
 Whether intraline differences should be highlighted.
 |`line_length`                 |optional|
@@ -1463,6 +1771,48 @@
 True if the line numbers should be hidden.
 |`tab_size`                    |optional|
 Number of spaces that should be used to display one tab.
+|`line_wrapping`               |optional|
+Whether to enable line wrapping or not.
+|===========================================
+
+[[edit-preferences-info]]
+=== EditPreferencesInfo
+The `EditPreferencesInfo` entity contains information about the edit
+preferences of a user.
+
+[options="header",cols="1,^1,5"]
+|===========================================
+|Field Name                    ||Description
+|`theme`                       ||
+The CodeMirror theme. Currently only a subset of light and dark
+CodeMirror themes are supported. Light themes `DEFAULT`, `ECLIPSE`,
+`ELEGANT`, `NEAT`. Dark themes `MIDNIGHT`, `NIGHT`, `TWILIGHT`.
+|`key_map_type`                ||
+The CodeMirror key map. Currently only a subset of key maps are
+supported: `DEFAULT`, `EMACS`, `VIM`.
+|`tab_size`                    ||
+Number of spaces that should be used to display one tab.
+|`line_length`                 ||
+Number of characters that should be displayed per line.
+|`cursor_blink_rate`           ||
+Half-period in milliseconds used for cursor blinking.
+Setting it to 0 disables cursor blinking.
+|`hide_top_menu`               |not set if `false`|
+If true the top menu header and site header is hidden.
+|`show_tabs`                   |not set if `false`|
+Whether tabs should be shown.
+|`show_whitespace_errors`      |not set if `false`|
+Whether whitespace errors should be shown.
+|`syntax_highlighting`         |not set if `false`|
+Whether syntax highlighting should be enabled.
+|`hide_line_numbers`           |not set if `false`|
+Whether line numbers should be hidden.
+|`match_brackets`              |not set if `false`|
+Whether matching brackets should be highlighted.
+|`line_wrapping`               |not set if `false`|
+Whether to enable line wrapping or not.
+|`auto_close_brackets`         |not set if `false`|
+Whether brackets and quotes should be auto-closed during typing.
 |===========================================
 
 [[email-info]]
@@ -1504,6 +1854,41 @@
 confirmation.
 |==============================
 
+[[gpg-key-info]]
+=== GpgKeyInfo
+The `GpgKeyInfo` entity contains information about a GPG public key.
+
+[options="header",cols="1,^1,5"]
+|========================
+|Field Name   ||Description
+|`id`         |Not set in map context|The 8-char hex GPG key ID.
+|`fingerprint`|Not set for deleted keys|The 40-char (plus spaces) hex GPG key fingerprint.
+|`user_ids`   |Not set for deleted keys|
+link:https://tools.ietf.org/html/rfc4880#section-5.11[OpenPGP User IDs]
+associated with the public key.
+|`key`        |Not set for deleted keys|ASCII armored public key material.
+|`status`     |Not set for deleted keys|
+The result of server-side checks on the key; one of `BAD`, `OK`, or `TRUSTED`.
+`BAD` keys have serious problems and should not be used. If a key is `OK,
+inspecting only that key found no problems, but the system does not fully trust
+the key's origin. A `TRUSTED` key is valid, and the system knows enough about
+the key and its origin to trust it.
+|`problems`   |Not set for deleted keys|
+A list of human-readable problem strings found in the course of checking whether
+the key is valid and trusted.
+|========================
+
+[[gpg-keys-input]]
+=== GpgKeysInput
+The `GpgKeysInput` entity contains information for adding/deleting GPG keys.
+
+[options="header",cols="1,6"]
+|========================
+|Field Name|Description
+|`add`     |List of ASCII armored public key strings to add.
+|`delete`  |List of link:#gpg-key-id[`\{gpg-key-id\}`]s to delete.
+|========================
+
 [[http-password-input]]
 === HttpPasswordInput
 The `HttpPasswordInput` entity contains information for setting/generating
@@ -1526,42 +1911,50 @@
 The `PreferencesInfo` entity contains information about a user's preferences.
 
 [options="header",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`changes_per_page`               ||
+|============================================
+|Field Name                     ||Description
+|`changes_per_page`             ||
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
-|`show_site_header`   |not set if `false`|
+|`show_site_header`             |not set if `false`|
 Whether the site header should be shown.
-|`use_flash_clipboard`     |not set if `false`|
+|`use_flash_clipboard`          |not set if `false`|
 Whether to use the flash clipboard widget.
-|`download_scheme`      ||
-The type of download URL the user prefers to use.
-|`download_command`     ||
+|`download_scheme`              ||
+The type of download URL the user prefers to use. May be any key from
+the `schemes` map in
+link:rest-api-config.html#download-info[DownloadInfo].
+|`download_command`             ||
 The type of download command the user prefers to use.
-|`copy_self_on_email`       |not set if `false`|
+|`copy_self_on_email`           |not set if `false`|
 Whether to CC me on comments I write.
-|`date_format`         ||
+|`date_format`                  ||
 The format to display the date in.
 Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
-|`time_format`     ||
+|`time_format`                  ||
 The format to display the time in.
 Allowed values are `HHMM_12`, `HHMM_24`.
-|`relative_date_in_change_table`  |not set if `false`|
+|`relative_date_in_change_table`|not set if `false`|
 Whether to show relative dates in the changes table.
-|`size_bar_in_change_table`      |not set if `false`|
+|`size_bar_in_change_table`     |not set if `false`|
 Whether to show the change sizes as colored bars in the change table.
-|`legacycid_in_change_table`      |not set if `false`|
+|`legacycid_in_change_table`    |not set if `false`|
 Whether to show change number in the change table.
-|`mute_common_path_prefixes` |not set if `false`|
+|`mute_common_path_prefixes`    |not set if `false`|
 Whether to mute common path prefixes in file names in the file table.
-|`review_category_strategy`   ||
+|`review_category_strategy`     ||
 The strategy used to displayed info in the review category column.
 Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
-|`diff_view`     ||
+|`diff_view`                    ||
 The type of diff view to show.
 Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
-|=====================================
+|`my`                           ||
+The menu items of the `MY` top menu as a list of
+link:rest-api-config.html#top-menu-item-info[TopMenuItemInfo] entities.
+|`url_aliases`                  |optional|
+A map of URL path pairs, where the first URL path is an alias for the
+second URL path.
+|============================================
 
 [[preferences-input]]
 === PreferencesInput
@@ -1569,42 +1962,48 @@
 user preferences. Fields which are not set will not be updated.
 
 [options="header",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`changes_per_page`               |optional|
+|============================================
+|Field Name                     ||Description
+|`changes_per_page`             |optional|
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
-|`show_site_header`   |optional|
+|`show_site_header`             |optional|
 Whether the site header should be shown.
-|`use_flash_clipboard`     |optional|
+|`use_flash_clipboard`          |optional|
 Whether to use the flash clipboard widget.
-|`download_scheme`      |optional|
+|`download_scheme`              |optional|
 The type of download URL the user prefers to use.
-|`download_command`     |optional|
+|`download_command`             |optional|
 The type of download command the user prefers to use.
-|`copy_self_on_email`       |optional|
+|`copy_self_on_email`           |optional|
 Whether to CC me on comments I write.
-|`date_format`         |optional|
+|`date_format`                  |optional|
 The format to display the date in.
 Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
-|`time_format`     |optional|
+|`time_format`                  |optional|
 The format to display the time in.
 Allowed values are `HHMM_12`, `HHMM_24`.
-|`relative_date_in_change_table`  |optional|
+|`relative_date_in_change_table`|optional|
 Whether to show relative dates in the changes table.
-|`size_bar_in_change_table`      |optional|
+|`size_bar_in_change_table`     |optional|
 Whether to show the change sizes as colored bars in the change table.
-|`legacycid_in_change_table`      |optional|
+|`legacycid_in_change_table`    |optional|
 Whether to show change number in the change table.
-|`mute_common_path_prefixes` |optional|
+|`mute_common_path_prefixes`    |optional|
 Whether to mute common path prefixes in file names in the file table.
-|`review_category_strategy`   |optional|
+|`review_category_strategy`     |optional|
 The strategy used to displayed info in the review category column.
 Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
-|`diff_view`     |optional|
+|`diff_view`                    |optional|
 The type of diff view to show.
 Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
-|=====================================
+|`my`                           |optional|
+The menu items of the `MY` top menu as a list of
+link:rest-api-config.html#top-menu-item-info[TopMenuItemInfo] entities.
+|`url_aliases`                  |optional|
+A map of URL path pairs, where the first URL path is an alias for the
+second URL path.
+|============================================
 
 [[query-limit-info]]
 === QueryLimitInfo
@@ -1634,6 +2033,17 @@
 |`valid`         ||Whether the SSH key is valid.
 |=============================
 
+[[username-input]]
+=== UsernameInput
+The `UsernameInput` entity contains information for setting the
+username for an account.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`username` |The new username of the account.
+|=======================
+
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 51c3a65..aa9417b 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -224,20 +224,13 @@
 * `ALL_REVISIONS`: describe all revisions, not just current.
 --
 
-[[download_commands]]
+[[download-commands]]
 --
 * `DOWNLOAD_COMMANDS`: include the `commands` field in the
   link:#fetch-info[FetchInfo] for revisions. Only valid when the
   `CURRENT_REVISION` or `ALL_REVISIONS` option is selected.
 --
 
-[[draft_comments]]
---
-* `DRAFT_COMMENTS`: include the `has_draft_comments` field for
-  revisions. Only valid when the `CURRENT_REVISION` or `ALL_REVISIONS`
-  option is selected.
---
-
 [[current-commit]]
 --
 * `CURRENT_COMMIT`: parse and output all header fields from the
@@ -294,8 +287,13 @@
 
 [[reviewed]]
 --
-* `REVIEWED`: include the `reviewed` field if the caller is
-  authenticated and has commented on the current revision.
+* `REVIEWED`: include the `reviewed` field if all of the following are
+  true:
+  * the change is open
+  * the caller is authenticated
+  * the caller has commented on the change more recently than the last update
+    from the change owner, i.e. this change would show up in the results of
+    link:user-search.html#reviewedby[reviewedby:self].
 --
 
 [[web-links]]
@@ -310,6 +308,21 @@
 * `CHECK`: include potential problems with the change.
 --
 
+[[commit-footers]]
+--
+* `COMMIT_FOOTERS`: include the full commit message with
+  Gerrit-specific commit footers in the
+  link:#revision-info[RevisionInfo].
+--
+
+[[push-certificates]]
+--
+* `PUSH_CERTIFICATES`: include push certificate information in the
+  link:#revision-info[RevisionInfo]. Ignored if signed push is not
+  link:config-gerrit.html#receive.enableSignedPush[enabled] on the
+  server.
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -400,29 +413,36 @@
           },
           "files": {
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
-              "lines_deleted": 8
+              "lines_deleted": 8,
+              "size_delta": -412
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
-              "lines_inserted": 1
+              "lines_inserted": 1,
+              "size_delta": 23
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
               "lines_inserted": 11,
-              "lines_deleted": 19
+              "lines_deleted": 19,
+              "size_delta": -298
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
               "lines_inserted": 23,
-              "lines_deleted": 20
+              "lines_deleted": 20,
+              "size_delta": 132
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
               "status": "D",
-              "lines_deleted": 139
+              "lines_deleted": 139,
+              "size_delta": -5512
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
               "status": "A",
-              "lines_inserted": 204
+              "lines_inserted": 204,
+              "size_delta": 8345
             },
             "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
-              "lines_deleted": 9
+              "lines_deleted": 9,
+              "size_delta": -343
             }
           }
         }
@@ -711,9 +731,6 @@
 
 Deletes the topic of a change.
 
-The request body does not need to include a link:#topic-input[
-TopicInput] entity if no review comment is added.
-
 Please note that some proxies prohibit request bodies for DELETE
 requests. In this case, if you want to specify a commit message, use
 link:#set-topic[PUT] to delete the topic.
@@ -1061,6 +1078,272 @@
   blocked by Verified
 ----
 
+[[submitted-together]]
+=== Changes submitted together
+--
+'GET /changes/link:#change-id[\{change-id\}]/submitted_together'
+--
+
+Returns a list of all changes which are submitted when
+link:#submit-change[\{submit\}] is called for this change,
+including the current change itself.
+
+An empty list is returned if this change will be submitted
+by itself (no other changes).
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/submitted_together HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+----
+
+The return value is a list of changes in the same format as in
+link:#list-changes[\{listing changes\}] with the options
+link:#labels[\{LABELS\}], link:#detailed-labels[\{DETAILED_LABELS\}],
+link:#current-revision[\{CURRENT_REVISION\}],
+link:#current-commit[\{CURRENT_COMMIT\}] set.
+The list consists of:
+
+* The given change.
+* If link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
+  is enabled, include all open changes with the same topic.
+* For each change whose submit type is not CHERRY_PICK, include unmerged
+  ancestors targeting the same branch.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+)]}'
+[
+  {
+    "id": "gerrit~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14",
+    "project": "gerrit",
+    "branch": "master",
+    "hashtags": [],
+    "change_id": "I1ffe09a505e25f15ce1521bcfb222e51e62c2a14",
+    "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes",
+    "status": "NEW",
+    "created": "2015-05-01 15:39:57.979000000",
+    "updated": "2015-05-20 19:25:21.592000000",
+    "mergeable": true,
+    "insertions": 303,
+    "deletions": 210,
+    "_number": 1779,
+    "owner": {
+      "_account_id": 1000000
+    },
+    "labels": {
+      "Code-Review": {
+        "approved": {
+          "_account_id": 1000000
+        },
+        "all": [
+          {
+            "value": 2,
+            "date": "2015-05-20 19:25:21.592000000",
+            "_account_id": 1000000
+          }
+        ],
+        "values": {
+          "-2": "This shall not be merged",
+          "-1": "I would prefer this is not merged as is",
+          " 0": "No score",
+          "+1": "Looks good to me, but someone else must approve",
+          "+2": "Looks good to me, approved"
+        },
+        "default_value": 0
+      },
+      "Verified": {
+        "approved": {
+          "_account_id": 1000000
+        },
+        "all": [
+          {
+            "value": 1,
+            "date": "2015-05-20 19:25:21.592000000",
+            "_account_id": 1000000
+          }
+        ],
+        "values": {
+          "-1": "Fails",
+          " 0": "No score",
+          "+1": "Verified"
+        },
+        "default_value": 0
+      }
+    },
+    "permitted_labels": {
+      "Code-Review": [
+        "-2",
+        "-1",
+        " 0",
+        "+1",
+        "+2"
+      ],
+      "Verified": [
+        "-1",
+        " 0",
+        "+1"
+      ]
+    },
+    "removable_reviewers": [
+      {
+        "_account_id": 1000000
+      }
+    ],
+    "current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
+    "revisions": {
+      "9adb9f4c7b40eeee0646e235de818d09164d7379": {
+        "_number": 1,
+        "created": "2015-05-01 15:39:57.979000000",
+        "uploader": {
+          "_account_id": 1000000
+        },
+        "ref": "refs/changes/79/1779/1",
+        "fetch": {},
+        "commit": {
+          "parents": [
+            {
+              "commit": "2d3176497a2747faed075f163707e57d9f961a1c",
+              "subject": "Merge changes from topic \u0027submodule-subscription-tests-and-fixes-3\u0027"
+            }
+          ],
+          "author": {
+            "name": "Stefan Beller",
+            "email": "sbeller@google.com",
+            "date": "2015-04-29 21:36:52.000000000",
+            "tz": -420
+          },
+          "committer": {
+            "name": "Stefan Beller",
+            "email": "sbeller@google.com",
+            "date": "2015-05-01 00:11:16.000000000",
+            "tz": -420
+          },
+          "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes",
+          "message": "ChangeMergeQueue: Rewrite such that it works on set of changes\n\nChangeMergeQueue used to work on branches rather than sets of changes.\nThis change is a first step to merge sets of changes (e.g. grouped by a\ntopic and `changes.submitWholeTopic` enabled) in an atomic fashion.\nThis change doesn\u0027t aim to implement these changes, but only as a step\ntowards it.\n\nMergeOp keeps its functionality and behavior as is. A new class\nMergeOpMapper is introduced which will map the set of changes to\nthe set of branches. Additionally the MergeOpMapper is also\nresponsible for the threading done right now, which was part of\nthe ChangeMergeQueue before.\n\nChange-Id: I1ffe09a505e25f15ce1521bcfb222e51e62c2a14\n"
+        }
+      }
+    }
+  },
+  {
+    "id": "gerrit~master~I7fe807e63792b3d26776fd1422e5e790a5697e22",
+    "project": "gerrit",
+    "branch": "master",
+    "hashtags": [],
+    "change_id": "I7fe807e63792b3d26776fd1422e5e790a5697e22",
+    "subject": "AbstractSubmoduleSubscription: Split up createSubscription",
+    "status": "NEW",
+    "created": "2015-05-01 15:39:57.979000000",
+    "updated": "2015-05-20 19:25:21.546000000",
+    "mergeable": true,
+    "insertions": 15,
+    "deletions": 6,
+    "_number": 1780,
+    "owner": {
+      "_account_id": 1000000
+    },
+    "labels": {
+      "Code-Review": {
+        "approved": {
+          "_account_id": 1000000
+        },
+        "all": [
+          {
+            "value": 2,
+            "date": "2015-05-20 19:25:21.546000000",
+            "_account_id": 1000000
+          }
+        ],
+        "values": {
+          "-2": "This shall not be merged",
+          "-1": "I would prefer this is not merged as is",
+          " 0": "No score",
+          "+1": "Looks good to me, but someone else must approve",
+          "+2": "Looks good to me, approved"
+        },
+        "default_value": 0
+      },
+      "Verified": {
+        "approved": {
+          "_account_id": 1000000
+        },
+        "all": [
+          {
+            "value": 1,
+            "date": "2015-05-20 19:25:21.546000000",
+            "_account_id": 1000000
+          }
+        ],
+        "values": {
+          "-1": "Fails",
+          " 0": "No score",
+          "+1": "Verified"
+        },
+        "default_value": 0
+      }
+    },
+    "permitted_labels": {
+      "Code-Review": [
+        "-2",
+        "-1",
+        " 0",
+        "+1",
+        "+2"
+      ],
+      "Verified": [
+        "-1",
+        " 0",
+        "+1"
+      ]
+    },
+    "removable_reviewers": [
+      {
+        "_account_id": 1000000
+      }
+    ],
+    "current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
+    "revisions": {
+      "1bd7c12a38854a2c6de426feec28800623f492c4": {
+        "_number": 1,
+        "created": "2015-05-01 15:39:57.979000000",
+        "uploader": {
+          "_account_id": 1000000
+        },
+        "ref": "refs/changes/80/1780/1",
+        "fetch": {},
+        "commit": {
+          "parents": [
+            {
+              "commit": "9adb9f4c7b40eeee0646e235de818d09164d7379",
+              "subject": "ChangeMergeQueue: Rewrite such that it works on set of changes"
+            }
+          ],
+          "author": {
+            "name": "Stefan Beller",
+            "email": "sbeller@google.com",
+            "date": "2015-04-25 00:11:59.000000000",
+            "tz": -420
+          },
+          "committer": {
+            "name": "Stefan Beller",
+            "email": "sbeller@google.com",
+            "date": "2015-05-01 00:11:16.000000000",
+            "tz": -420
+          },
+          "subject": "AbstractSubmoduleSubscription: Split up createSubscription",
+          "message": "AbstractSubmoduleSubscription: Split up createSubscription\n\nLater we want to have subscriptions to more submodules, so we need to\nfind a way to add more submodule entries into the file. By splitting up\nthe createSubscription() method, that is very easy by using the\naddSubmoduleSubscription method multiple times.\n\nChange-Id: I7fe807e63792b3d26776fd1422e5e790a5697e22\n"
+        }
+      }
+    }
+  }
+]
+----
+
+
 [[publish-draft-change]]
 === Publish Draft Change
 --
@@ -1146,6 +1429,109 @@
   HTTP/1.1 204 No Content
 ----
 
+[[list-change-comments]]
+=== List Change Comments
+--
+'GET /changes/link:#change-id[\{change-id\}]/comments'
+--
+
+Lists the published comments of all revisions of the change.
+
+Returns a map of file paths to lists of link:#comment-info[CommentInfo]
+entries. The entries in the map are sorted by file path, and the
+comments for each path are sorted by patch set number. Each comment has
+the `patch_set` and `author` fields set.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/comments HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "patch_set": 1,
+        "id": "TvcXrmjM",
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000"
+        "author": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        }
+      },
+      {
+        "patch_set": 2,
+        "id": "TveXwFiA",
+        "line": 49,
+        "in_reply_to": "TfYX-Iuo",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000"
+        "author": {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        }
+      }
+    ]
+  }
+----
+
+[[list-change-drafts]]
+=== List Change Drafts
+--
+'GET /changes/link:#change-id[\{change-id\}]/drafts'
+--
+
+Lists the draft comments of all revisions of the change that belong to
+the calling user.
+
+Returns a map of file paths to lists of link:#comment-info[CommentInfo]
+entries. The entries in the map are sorted by file path, and the
+comments for each path are sorted by patch set number. Each comment has
+the `patch_set` field set, and no `author`.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/drafts HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "patch_set": 1,
+        "id": "TvcXrmjM",
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000"
+      },
+      {
+        "patch_set": 2,
+        "id": "TveXwFiA",
+        "line": 49,
+        "in_reply_to": "TfYX-Iuo",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000"
+      }
+    ]
+  }
+----
+
 [[check-change]]
 === Check change
 --
@@ -1206,6 +1592,9 @@
 /check], and additionally fixes any problems that can be fixed
 automatically. The returned field values reflect any fixes.
 
+Some fixes have options controlling their behavior, which can be set in the
+link:#fix-input[FixInput] entity body.
+
 Only the change owner, a project owner, or an administrator may fix changes.
 
 .Request
@@ -1859,6 +2248,7 @@
 
   )]}'
   {
+    "commit": "674ac754f91e64a0efb8087e59a176484bd534d1",
     "parents": [
       {
         "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
@@ -2105,6 +2495,7 @@
         "_change_number": 58478,
         "_revision_number": 2,
         "_current_revision_number": 2
+        "status": "NEW"
       },
       {
         "change_id": "I5e4fc08ce34d33c090c9e0bf320de1b17309f774",
@@ -2126,6 +2517,7 @@
         "_change_number": 58081,
         "_revision_number": 10,
         "_current_revision_number": 10
+        "status": "NEW"
       }
     ]
   }
@@ -2303,18 +2695,10 @@
 
 Submits a revision.
 
-The request body only needs to include a link:#submit-input[
-SubmitInput] entity if the request should wait for the merge to
-complete.
-
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/submit HTTP/1.0
   Content-Type: application/json; charset=UTF-8
-
-  {
-    "wait_for_merge": true
-  }
 ----
 
 As response a link:#submit-info[SubmitInfo] entity is returned that
@@ -2583,7 +2967,7 @@
 ----
 
 [[list-drafts]]
-=== List Drafts
+=== List Revision Drafts
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/'
 --
@@ -2591,9 +2975,8 @@
 Lists the draft comments of a revision that belong to the calling
 user.
 
-As result a map is returned that maps the file path to a list of
-link:#comment-info[CommentInfo] entries. The entries in the map are
-sorted by file path.
+Returns a map of file paths to lists of link:#comment-info[CommentInfo]
+entries. The entries in the map are sorted by file path.
 
 .Request
 ----
@@ -2762,7 +3145,7 @@
 ----
 
 [[list-comments]]
-=== List Comments
+=== List Revision Comments
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/'
 --
@@ -2880,11 +3263,13 @@
   {
     "/COMMIT_MSG": {
       "status": "A",
-      "lines_inserted": 7
+      "lines_inserted": 7,
+      "size_delta": 551
     },
     "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
       "lines_inserted": 5,
-      "lines_deleted": 3
+      "lines_deleted": 3,
+      "size_delta": 98
     }
   }
 ----
@@ -2952,6 +3337,63 @@
 `application/json` the content is returned as JSON string and
 `X-FYI-Content-Encoding` is set to `json`.
 
+[[get-safe-content]]
+=== Download Content
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/download'
+--
+
+Downloads the content of a file from a certain revision, in a safe format
+that poses no risk for inadvertent execution of untrusted code.
+
+If the content type is defined as safe, the binary file content is returned
+verbatim. If the content type is not safe, the file is stored inside a ZIP
+file, containing a single entry with a random, unpredictable name having the
+same base and suffix as the true filename. The ZIP file is returned in
+verbatim binary form.
+
+See link:config-gerrit.html#mimetype.name.safe[Gerrit config documentation]
+for information about safe file type configuration.
+
+The HTTP resource Content-Type is dependent on the file type: the
+applicable type for safe files, or "application/zip" for unsafe files.
+
+The optional, integer-valued `parent` parameter can be specified to request
+the named file from a parent commit of the specified revision. The value is
+the 1-based index of the parent's position in the commit object. If the
+parameter is omitted or the value non-positive, the patch set is referenced.
+
+Filenames are decorated with a suffix of `_new` for the current patch,
+`_old` for the only parent, or `_oldN` for the Nth parent of many.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/website%2Freleases%2Flogo.png/safe_content HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment; filename="logo.png"
+  Content-Type: image/png
+
+  `[binary data for logo.png]`
+----
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/safe_content?suffix=new HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: Content-Disposition:attachment; filename="RefControl_new-931cdb73ae9d97eb500a3533455b055d90b99944.java.zip"
+  Content-Type:application/zip
+
+  `[binary ZIP archive containing a single file, "RefControl_new-cb218df1337df48a0e7ab30a49a8067ac7321881.java"]`
+----
+
 [[get-diff]]
 === Get Diff
 --
@@ -3387,8 +3829,7 @@
 |`subject`            ||
 The subject of the change (header line of the commit message).
 |`status`             ||
-The status of the change (`NEW`, `SUBMITTED`, `MERGED`, `ABANDONED`,
-`DRAFT`).
+The status of the change (`NEW`, `MERGED`, `ABANDONED`, `DRAFT`).
 |`created`            ||
 The link:rest-api.html#timestamp[timestamp] of when the change was
 created.
@@ -3491,6 +3932,9 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
+|`patch_set`   |optional|
+The patch set number for the comment; only set in contexts where +
+comments may be returned for multiple patch sets.
 |`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
 The path of the file for which the inline comment was done. +
@@ -3573,25 +4017,28 @@
 === CommitInfo
 The `CommitInfo` entity contains information about a commit.
 
-[options="header",cols="1,6"]
-|==========================
-|Field Name    |Description
-|`commit`      |The commit ID.
-|`parents`     |
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`commit`      |Optional|
+The commit ID. Not set if included in a link:#revision-info[
+RevisionInfo] entity that is contained in a map which has the commit ID
+as key.
+|`parents`     ||
 The parent commits of this commit as a list of
 link:#commit-info[CommitInfo] entities. In each parent
 only the `commit` and `subject` fields are populated.
-|`author`      |The author of the commit as a
+|`author`      ||The author of the commit as a
 link:#git-person-info[GitPersonInfo] entity.
-|`committer`   |The committer of the commit as a
+|`committer`   ||The committer of the commit as a
 link:#git-person-info[GitPersonInfo] entity.
-|`subject`     |
+|`subject`     ||
 The subject of the commit (header line of the commit message).
-|`message`     |The commit message.
+|`message`     ||The commit message.
 |`web_links`   |optional|
 Links to the commit in external sites as a list of
 link:#web-link-info[WebLinkInfo] entities.
-|==========================
+|===========================
 
 [[diff-content]]
 === DiffContent
@@ -3739,7 +4186,7 @@
 |`commands`    |optional|
 The download commands for this patch set as a map that maps the command
 names to the commands. +
-Only set if link:#download_commands[download commands] are requested.
+Only set if link:#download-commands[download commands] are requested.
 |==========================
 
 [[file-info]]
@@ -3763,8 +4210,25 @@
 |`lines_deleted` |optional|
 Number of deleted lines. +
 Not set for binary files or if no lines were deleted.
+|`size_delta`    ||
+Number of bytes by which the file size increased/decreased.
 |=============================
 
+[[fix-input]]
+=== FixInput
+The `FixInput` entity contains options for fixing commits using the
+link:#fix-change[fix change] endpoint.
+
+[options="header",cols="1,6"]
+|==========================
+|Field Name                          |Description
+|`delete_patch_set_if_commit_missing`|If true, delete patch sets from the
+database if they refer to missing commit options.
+|`expect_merged_as`                  |If set, check that the change is
+merged into the destination branch as this exact SHA-1. If not, insert
+a new patch set referring to this commit.
+|==========================
+
 [[git-person-info]]
 === GitPersonInfo
 The `GitPersonInfo` entity contains information about the
@@ -3797,14 +4261,17 @@
 The `IncludedInInfo` entity contains information about the branches a
 change was merged into and tags it was tagged with.
 
-[options="header",cols="1,6"]
-|==========================
-|Field Name |Description
-|`branches` | The list of branches this change was merged into.
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`branches`||The list of branches this change was merged into.
 Each branch is listed without the 'refs/head/' prefix.
-|`tags`     | The list of tags this change was tagged with.
+|`tags`    ||The list of tags this change was tagged with.
 Each tag is listed without the 'refs/tags/' prefix.
-|==========================
+|`external`|optional|A map that maps a name to a list of external
+systems that include this change, e.g. a list of servers on which this
+change is deployed.
+|=======================
 
 [[label-info]]
 === LabelInfo
@@ -3901,6 +4368,23 @@
 outcome of the fix.
 |===========================
 
+[[push-certificate-info]]
+=== PushCertificateInfo
+The `PushCertificateInfo` entity contains information about a push
+certificate provided when the user pushed for review with `git push
+--signed HEAD:refs/for/<branch>`. Only used when signed push is
+link:config-gerrit.html#receive.enableSignedPush[enabled] on the server.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name|Description
+|`certificate`|Signed certificate payload and GPG signature block.
+|`key`        |
+Information about the key that signed the push, along with any problems
+found while checking the signature or the key itself, as a
+link:rest-api-accounts.html#gpg-key-info[GpgKeyInfo] entity.
+|===========================
+
 [[rebase-input]]
 === RebaseInput
 The `RebaseInput` entity contains information for changing parent when rebasing.
@@ -3931,6 +4415,8 @@
 |`_change_number`          |optional|The change number.
 |`_revision_number`        |optional|The revision number.
 |`_current_revision_number`|optional|The current revision number.
+|`status`                  |optional|The status of the change. The status of
+the change is one of (`NEW`, `MERGED`, `ABANDONED`, `DRAFT`).
 |===========================
 
 [[related-changes-info]]
@@ -3990,34 +4476,38 @@
 
 [options="header",cols="1,^1,5"]
 |============================
-|Field Name     ||Description
-|`message`      |optional|
+|Field Name               ||Description
+|`message`                |optional|
 The message to be added as review comment.
-|`labels`       |optional|
+|`labels`                 |optional|
 The votes that should be added to the revision as a map that maps the
 label names to the voting values.
-|`comments`     |optional|
+|`comments`               |optional|
 The comments that should be added as a map that maps a file path to a
 list of link:#comment-input[CommentInput] entities.
-|`strict_labels`|`true` if not set|
+|`strict_labels`          |`true` if not set|
 Whether all labels are required to be within the user's permitted ranges
 based on access controls. +
 If `true`, attempting to use a label not granted to the user will fail
 the entire modify operation early. +
 If `false`, the operation will execute anyway, but the proposed labels
 will be modified to be the "best" value allowed by the access controls.
-|`drafts`      |optional|
+|`drafts`                 |optional|
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
 input. +
-Allowed values are `DELETE`, `PUBLISH` and `KEEP`. +
+Allowed values are `DELETE`, `PUBLISH`, `PUBLISH_ALL_REVISIONS` and
+`KEEP`. All values except `PUBLISH_ALL_REVISIONS` operate only on drafts
+for a single revision. +
 If not set, the default is `DELETE`.
-|`notify`      |optional|
+|`notify`                 |optional|
 Notify handling that defines to whom email notifications should be sent
 after the review is stored. +
 Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
 If not set, the default is `ALL`.
-|`on_behalf_of`|optional|
+|`omit_duplicate_comments`|optional|
+If `true`, comments with the same content at the same place will be omitted.
+|`on_behalf_of`           |optional|
 link:rest-api-accounts.html#account-id[\{account-id\}] the review
 should be posted on behalf of. To use this option the caller must
 have been granted `labelAs-NAME` permission for all keys of labels.
@@ -4073,9 +4563,6 @@
 |===========================
 |Field Name    ||Description
 |`draft`       |not set if `false`|Whether the patch set is a draft.
-|`has_draft_comments`       |not set if `false`|Whether the patch
-set has one or more draft comments by the calling user. Only set if
-link:#draft_comments[DRAFT_COMMENTS] option is requested.
 |`_number`     ||The patch set number.
 |`created`     ||
 The link:rest-api.html#timestamp[timestamp] of when the patch set was
@@ -4105,6 +4592,18 @@
 |`reviewed`     |optional|
 Indicates whether the caller is authenticated and has commented on the
 current revision. Only set if link:#reviewed[REVIEWED] option is requested.
+|`messageWithFooter` |optional|
+If the link:#commit-footers[COMMIT_FOOTERS] option is requested and
+this is the current patch set, contains the full commit message with
+Gerrit-specific commit footers, as if this revision were submitted
+using the link:project-configuration.html#cherry_pick[Cherry Pick]
+submit type.
+|`push_certificate` |optional|
+If the link:#push-certificates[PUSH_CERTIFICATES] option is requested,
+contains the push certificate provided by the user when uploading this
+patch set as a link:#push-certificate-info[PushCertificateInfo] entity.
+This field is always set if the option is requested; if no push
+certificate was provided, it is set to an empty object.
 |===========================
 
 [[rule-input]]
@@ -4129,15 +4628,15 @@
 The `SubmitInfo` entity contains information about the change status
 after submitting.
 
-[options="header",cols="1,6"]
+[options="header",cols="1,^1,5"]
 |==========================
-|Field Name    |Description
-|`status`      |
-The status of the change after submitting, can be `MERGED` or
-`SUBMITTED`. +
-If `wait_for_merge` in the link:#submit-input[SubmitInput] was set to
-`false` the returned status is `SUBMITTED` and the caller can't know
-whether the change could be merged successfully.
+|Field Name    ||Description
+|`status`      ||
+The status of the change after submitting is `MERGED`.
++
+As `wait_for_merge` in the link:#submit-input[SubmitInput] is deprecated and
+the request always waits for the merge to be completed, you can expect
+`MERGED` to be returned here.
 |`on_behalf_of`|optional|
 The link:rest-api-accounts.html#account-id[\{account-id\}] of the user on
 whose behalf the action should be done. To use this option the caller must
@@ -4156,11 +4655,8 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name      ||Description
-|`wait_for_merge`|`false` if not set|
-Whether the request should wait for the merge to complete. +
-If `false` the request returns immediately after the change has been
-added to the merge queue and the caller can't know whether the change
-could be merged successfully.
+|`wait_for_merge`|Deprecated, always `true`|
+Whether the request should wait for the merge to complete.
 |===========================
 
 [[submit-record]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 0ee6966..b1b795c 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -30,6 +30,129 @@
   "2.7"
 ----
 
+[[get-info]]
+=== Get Server Info
+--
+'GET /config/server/info'
+--
+
+Returns the information about the Gerrit server configuration.
+
+.Request
+----
+  GET /config/server/info HTTP/1.0
+----
+
+As result a link:#server-info[ServerInfo] entity is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "auth": {
+      "auth_type": "LDAP",
+      "editable_account_fields": [
+        "FULL_NAME",
+        "REGISTER_NEW_EMAIL"
+      ]
+    },
+    "download": {
+      "schemes": {
+        "anonymous http": {
+          "url": "http://gerrithost:8080/${project}",
+          "commands": {
+            "Checkout": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
+            "Format Patch": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+            "Pull": "git pull http://gerrithost:8080/${project} ${ref}",
+            "Cherry Pick": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
+          },
+          "clone_commands": {
+            "Clone": "git clone http://gerrithost:8080/${project}",
+            "Clone with commit-msg hook": "git clone http://gerrithost:8080/${project} \u0026\u0026 scp -p -P 29418 jdoe@gerrithost:hooks/commit-msg ${project}/.git/hooks/"
+          }
+        },
+        "http": {
+          "url": "http://jdoe@gerrithost:8080/${project}",
+          "is_auth_required": true,
+          "is_auth_supported": true,
+          "commands": {
+            "Checkout": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
+            "Format Patch": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+            "Pull": "git pull http://jdoe@gerrithost:8080/${project} ${ref}",
+            "Cherry Pick": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
+          },
+          "clone_commands": {
+            "Clone": "git clone http://jdoe@gerrithost:8080/${project}",
+            "Clone with commit-msg hook": "git clone http://jdoe@gerrithost:8080/${project} \u0026\u0026 scp -p -P 29418 jdoe@gerrithost:hooks/commit-msg ${project}/.git/hooks/"
+          }
+        },
+        "ssh": {
+          "url": "ssh://jdoe@gerrithost:29418/${project}",
+          "is_auth_required": true,
+          "is_auth_supported": true,
+          "commands": {
+            "Checkout": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
+            "Format Patch": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+            "Pull": "git pull ssh://jdoe@gerrithost:29418/${project} ${ref}",
+            "Cherry Pick": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
+          },
+          "clone_commands": {
+            "Clone": "git clone ssh://jdoe@gerrithost:29418/${project}",
+            "Clone with commit-msg hook": "git clone ssh://jdoe@gerrithost:29418/${project} \u0026\u0026 scp -p -P 29418 jdoe@gerrithost:hooks/commit-msg ${project}/.git/hooks/"
+          }
+        }
+      },
+      "archives": [
+        "tgz",
+        "tar",
+        "tbz2",
+        "txz"
+      ]
+    },
+    "gerrit": {
+      "all_projects": "All-Projects",
+      "all_users": "All-Users"
+    },
+    "sshd": {},
+    "suggest": {
+      "from": 0
+    },
+    "user": {
+      "anonymous_coward_name": "Anonymous Coward"
+    }
+  }
+----
+
+[[confirm-email]]
+=== Confirm Email
+--
+'PUT /config/server/email.confirm'
+--
+
+Confirms that the user owns an email address.
+
+The email token must be provided in the request body inside
+an link:#email-confirmation-input[EmailConfirmationInput] entity.
+
+.Request
+----
+  PUT /config/server/email.confirm HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "token": "Enim+QNbAo6TV8Hur8WwoUypI6apG7qBPvF+bw==$MTAwMDAwNDp0ZXN0QHRlc3QuZGU="
+  }
+----
+
+The response is "`204 No Content`".
+
+If the token is invalid or if it's the token of another user the
+request fails and the response is "`422 Unprocessable Entity`".
+
+
 [[list-caches]]
 === List Caches
 --
@@ -38,10 +161,12 @@
 
 Lists the caches of the server. Caches defined by plugins are included.
 
-The caller must be a member of a group that is granted the
-link:access-control.html#capability_viewCaches[View Caches] capability
-or the link:access-control.html#capability_administrateServer[
-Administrate Server] capability.
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_viewCaches[View Caches]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 As result a map of link:#cache-info[CacheInfo] entities is returned.
 
@@ -396,10 +521,12 @@
 
 Retrieves information about a cache.
 
-The caller must be a member of a group that is granted the
-link:access-control.html#capability_viewCaches[View Caches] capability
-or the link:access-control.html#capability_administrateServer[
-Administrate Server] capability.
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_viewCaches[View Caches]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 As result a link:#cache-info[CacheInfo] entity is returned.
 
@@ -435,15 +562,15 @@
 
 Flushes a cache.
 
-The caller must be a member of a group that is granted the
-link:access-control.html#capability_flushCaches[Flush Caches] capability
-or the link:access-control.html#capability_administrateServer[
-Administrate Server] capability.
+The caller must be a member of a group that is granted one of the
+following capabilities:
 
-The "web_sessions" cache can only be flushed if the caller is member of
-a group that is granted the
-link:access-control.html#capability_administrateServer[Administrate
-Server] capability.
+* link:access-control.html#capability_flushCaches[Flush Caches] (any cache
+  except "web_sessions")
+* link:access-control.html#capability_maintainServer[Maintain Server] (any cache
+  including "web_sessions")
+* link:access-control.html#capability_administrateServer[Administrate Server]
+  (any cache including "web_sessions")
 
 .Request
 ----
@@ -652,10 +779,12 @@
 is associated with. Tasks operating on other projects, or that do not
 have a specific project, are hidden.
 
-Members of a group that is granted the
-link:access-control.html#capability_viewQueue[View Queue] capability or
-the link:access-control.html#capability_administrateServer[Administrate
-Server] capability can see all tasks.
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_viewQueue[View Queue]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 As result a list of link:#task-info[TaskInfo] entities is returned.
 
@@ -704,10 +833,12 @@
 is associated with. Tasks operating on other projects, or that do not
 have a specific project, are hidden.
 
-Members of a group that is granted the
-link:access-control.html#capability_viewQueue[View Queue] capability or
-the link:access-control.html#capability_administrateServer[Administrate
-Server] capability can see all tasks.
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_viewQueue[View Queue]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 As result a link:#task-info[TaskInfo] entity is returned.
 
@@ -740,19 +871,23 @@
 Kills a task from the background work queue that the Gerrit daemon
 is currently performing, or will perform in the near future.
 
-The caller must be a member of a group that is granted the
-link:access-control.html#capability_kill[Kill Task] capability
-or the link:access-control.html#capability_administrateServer[
-Administrate Server] capability.
+The caller must be a member of a group that is granted one of the
+following capabilities:
+
+* link:access-control.html#capability_kill[Kill Task]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 End-users may see a task only if they can also see the project the task
 is associated with. Tasks operating on other projects, or that do not
 have a specific project, are hidden.
 
-Members of a group that is granted the
-link:access-control.html#capability_viewQueue[View Queue] capability or
-the link:access-control.html#capability_administrateServer[Administrate
-Server] capability can see all tasks.
+Members of a group granted one of the following capabilities may view
+all tasks:
+
+* link:access-control.html#capability_viewQueue[View Queue]
+* link:access-control.html#capability_maintainServer[Maintain Server]
+* link:access-control.html#capability_administrateServer[Administrate Server]
 
 .Request
 ----
@@ -822,6 +957,59 @@
 [[json-entities]]
 == JSON Entities
 
+[[auth-info]]
+=== AuthInfo
+The `AuthInfo` entity contains information about the authentication
+configuration of the Gerrit server.
+
+[options="header",cols="1,^1,5"]
+|==========================================
+|Field Name                   ||Description
+|`type`                       ||
+The link:config-gerrit.html#auth.type[authentication type] that is
+configured on the server. Can be `OPENID`, `OPENID_SSO`, `OAUTH`,
+`HTTP`, `HTTP_LDAP`, `CLIENT_SSL_CERT_LDAP`, `LDAP`, `LDAP_BIND`,
+`CUSTOM_EXTENSION` or `DEVELOPMENT_BECOME_ANY_ACCOUNT`.
+|`use_contributor_agreements` |not set if `false`|
+Whether link:config-gerrit.html#auth.contributorAgreements[contributor
+agreements] are required.
+|`editable_account_fields`    ||
+List of account fields that are editable. Possible values are
+`FULL_NAME`, `USER_NAME` and `REGISTER_NEW_EMAIL`.
+|`login_url`                  |optional|
+The link:config-gerrit.html#auth.loginUrl[login URL]. Only set if
+link:config-gerrit.html#auth.type[authentication type] is `HTTP` or
+`HTTP_LDAP`.
+|`login_text`                 |optional|
+The link:config-gerrit.html#auth.loginText[login text]. Only set if
+link:config-gerrit.html#auth.type[authentication type] is `HTTP` or
+`HTTP_LDAP`.
+|`switch_account_url`         |optional|
+The link:config-gerrit.html#auth.switchAccountUrl[URL to switch
+accounts].
+|`register_url`               |optional|
+The link:config-gerrit.html#auth.registerUrl[register URL]. Only set if
+link:config-gerrit.html#auth.type[authentication type] is `LDAP`,
+`LDAP_BIND` or `CUSTOM_EXTENSION`.
+|`register_text`              |optional|
+The link:config-gerrit.html#auth.registerText[register text]. Only set
+if link:config-gerrit.html#auth.type[authentication type] is `LDAP`,
+`LDAP_BIND` or `CUSTOM_EXTENSION`.
+|`edit_full_name_url`         |optional|
+The link:config-gerrit.html#auth.editFullNameUrl[URL to edit the full
+name]. Only set if link:config-gerrit.html#auth.type[authentication
+type] is `LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
+|`http_password_url`          |optional|
+The link:config-gerrit.html#auth.httpPasswordUrl[URL to obtain an HTTP
+password]. Only set if link:config-gerrit.html#auth.type[authentication
+type] is `CUSTOM_EXTENSION`.
+|`is_git_basic_auth`          |optional, not set if `false`|
+Whether link:config-gerrit.html#auth.gitBasicAuth[basic authentication
+is used for Git over HTTP/HTTPS]. Only set if
+link:config-gerrit.html#auth.type[authentication type] is is `LDAP` or
+`LDAP_BIND`.
+|==========================================
+
 [[cache-info]]
 === CacheInfo
 The `CacheInfo` entity contains information about a cache.
@@ -878,6 +1066,98 @@
 |`name`               |capability name
 |=================================
 
+[[change-config-info]]
+=== ChangeConfigInfo
+The `ChangeConfigInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#change[change]
+section.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name           ||Description
+|`allow_drafts`       |not set if `false`|
+link:config-gerrit.html#change.allowDrafts[Whether draft workflow is
+allowed].
+|`large_change`       ||
+link:config-gerrit.html#change.largeChange[Number of changed lines from
+which on a change is considered as a large change].
+|`reply_label`        ||
+link:config-gerrit.html#change.replyTooltip[Label name for the reply
+button].
+|`reply_tooltip`      ||
+link:config-gerrit.html#change.replyTooltip[Tooltip for the reply
+button].
+|`update_delay`       ||
+link:config-gerrit.html#change.updateDelay[How often in seconds the web
+interface should poll for updates to the currently open change].
+|`submit_whole_topic` ||
+link:config-gerrit.html#change.submitWholeTopic[A configuration if
+the whole topic is submitted].
+|=============================
+
+[[download-info]]
+=== DownloadInfo
+The `DownloadInfo` entity contains information about supported download
+options.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`schemes`  |
+The supported download schemes as a map which maps the scheme name to a
+of link:#download-scheme-info[DownloadSchemeInfo] entity.
+|`archives` |
+List of supported archive formats. Possible values are `tgz`, `tar`,
+`tbz2` and `txz`.
+|=======================
+
+[[download-scheme-info]]
+=== DownloadSchemeInfo
+The `DownloadSchemeInfo` entity contains information about a supported
+download scheme and its commands.
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name          ||Description
+|`url`               ||
+The URL of the download scheme, where '${project}' is used as
+placeholder for the project name.
+|`is_auth_required`  |not set if `false`|
+Whether this download scheme requires authentication.
+|`is_auth_supported` |not set if `false`|
+Whether this download scheme supports authentication.
+|`commands`          ||
+Download commands as a map which maps the command name to the download
+command. In the download command '${project}' is used as
+placeholder for the project name, and '${ref}' is used as
+placeholder for the (change) ref.
+
+Empty, if accessed anonymously and the download scheme requires
+authentication.
+|`clone_commands`    ||
+Clone commands as a map which maps the command name to the clone
+command. In the clone command '${project}' is used as
+placeholder for the project name and '${project-base-name}' as name
+for the project base name (e.g. for a project 'foo/bar' '${project}'
+is a placeholder for 'foo/bar' and '${project-base-name}' is a
+placeholder for 'bar').
+
+Empty, if accessed anonymously and the download scheme requires
+authentication.
+|=================================
+
+[[email-confirmation-input]]
+=== EmailConfirmationInput
+The `EmailConfirmationInput` entity contains information for confirming
+an email address.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`token`    |
+The token that was sent by mail to a newly registered email address.
+|=======================
+
 [[entries-info]]
 === EntriesInfo
 The `EntriesInfo` entity contains information about the entries in a
@@ -896,6 +1176,80 @@
 `g`: gigabytes). Only set for disk caches.
 |==================================
 
+[[gerrit-info]]
+=== GerritInfo
+The `GerritInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#gerrit[gerrit] section.
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name          ||Description
+|`all_projects_name` ||
+Name of the link:config-gerrit.html#gerrit.allProjects[root project].
+|`all_users_name`    ||
+Name of the link:config-gerrit.html#gerrit.allUsers[project in which
+meta data of all users is stored].
+|`doc_url`           |optional|
+Custom base URL where Gerrit server documentation is located.
+(Documentation may still be available at /Documentation relative to the
+Gerrit base path even if this value is unset.)
+|`edit_gpg_keys`     |not set if `false`|
+Whether to enable the web UI for editing GPG keys.
+|`report_bug_url`    |optional|
+link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
+|`report_bug_text`   |optional, not set if default|
+link:config-gerrit.html#gerrit.reportBugText[Display text for report
+bugs link].
+|=================================
+
+[[git-web-info]]
+=== GitwebInfo
+The `GitwebInfo` entity contains information about the
+link:config-gerrit.html#gitweb[gitweb] configuration.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`url`      |
+The link:config-gerrit.html#gitweb.url[gitweb base URL].
+|`type`     |
+The link:config-gerrit.html#gitweb.type[gitweb type] as
+link:#git-web-type-info[GitwebTypeInfo] entity.
+|=======================
+
+[[git-web-type-info]]
+=== GitwebTypeInfo
+The `GitwebTypeInfo` entity contains information about the
+link:config-gerrit.html#gitweb[gitweb] configuration.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name      ||Description
+|`name`          ||
+The link:config-gerrit.html#gitweb.linkname[gitweb link name].
+|`revision`      |optional|
+The link:config-gerrit.html#gitweb.revision[gitweb revision pattern].
+|`project`       |optional|
+The link:config-gerrit.html#gitweb.project[gitweb project pattern].
+|`branch`        |optional|
+The link:config-gerrit.html#gitweb.branch[gitweb branch pattern].
+|`root_tree`     |optional|
+The link:config-gerrit.html#gitweb.roottree[gitweb root tree pattern].
+|`file`          |optional|
+The link:config-gerrit.html#gitweb.file[gitweb file pattern].
+|`file_history`  |optional|
+The link:config-gerrit.html#gitweb.filehistory[gitweb file history
+pattern].
+|`path_separator`||
+The link:config-gerrit.html#gitweb.pathSeparator[gitweb path separator].
+|`link_drafts`   |optional|
+link:config-gerrit.html#gitweb.linkDrafts[Whether Gerrit should provide
+links to gitweb on draft patch set.]
+|`url_encode`    |optional|
+link:config-gerrit.html#gitweb.urlEncode[Whether Gerrit should encode
+the generated viewer URL.]
+|=============================
+
 [[hit-ration-info]]
 === HitRatioInfo
 The `HitRatioInfo` entity contains information about the hit ratio of a
@@ -958,6 +1312,106 @@
 The number of open files.
 |============================
 
+[[plugin-config-info]]
+=== PluginConfigInfo
+The `PluginConfigInfo` entity contains information about Gerrit
+extensions by plugins.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`has_avatars` |not set if `false`|
+Whether an avatar provider is registered.
+|===========================
+
+[[receive-info]]
+=== ReceiveInfo
+The `ReceiveInfo` entity contains information about the configuration
+of git-receive-pack behavior on the server.
+
+[options="header",cols="1,^1,5"]
+|=======================================
+|Field Name        ||Description
+|`enableSignedPush`|optional|
+Whether signed push validation support is enabled on the server; see the
+link:config-gerrit.html#receive.certNonceSeed[global configuration] for
+details.
+|=======================================
+
+[[server-info]]
+=== ServerInfo
+The `ServerInfo` entity contains information about the configuration of
+the Gerrit server.
+
+[options="header",cols="1,^1,5"]
+|=======================================
+|Field Name                ||Description
+|`auth`                    ||
+Information about the authentication configuration as
+link:#auth-info[AuthInfo] entity.
+|`change`                  ||
+Information about the configuration from the
+link:config-gerrit.html#change[change] section as
+link:#change-config-info[ChangeConfigInfo] entity.
+|`download`                ||
+Information about the configured download options as
+link:#download-info[DownloadInfo] entity.
+information about Gerrit
+|`gerrit`                  ||
+Information about the configuration from the
+link:config-gerrit.html#gerrit[gerrit] section as link:#gerrit-info[
+GerritInfo] entity.
+|`gitweb `                 |optional|
+Information about the link:config-gerrit.html#gitweb[gitweb]
+|`plugin `                 ||
+Information about Gerrit extensions by plugins as
+link:#plugin-config-info[PluginConfigInfo] entity.
+|`receive`                 |optional|
+Information about the receive-pack configuration as a
+link:#receive-info[ReceiveInfo] entity.
+|`sshd`                    |optional|
+Information about the configuration from the
+link:config-gerrit.html#sshd[sshd] section as link:#sshd-info[SshdInfo]
+entity. Not set if SSHD is disabled.
+|`suggest`                 ||
+Information about the configuration from the
+link:config-gerrit.html#suggest[suggest] section as link:#suggest-info[
+SuggestInfo] entity.
+|`url_aliases`             |optional|
+A map of URL aliases, where a regular expression for an URL token is
+mapped to a target URL token. The target URL token can contain
+placeholders for the groups matched by the regular expression: `$1` for
+the first matched group, `$2` for the second matched group, etc.
+|`user`                    ||
+Information about the configuration from the
+link:config-gerrit.html#user[user] section as link:#user-config-info[
+UserConfigInfo] entity.
+|=======================================
+
+[[sshd-info]]
+=== SshdInfo
+The `SshdInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#sshd[sshd]
+section.
+
+This entity doesn't contain any data, but the presence of this (empty)
+entity in the link:#server-info[ServerInfo] entity means that SSHD is
+enabled on the server.
+
+[[suggest-info]]
+=== SuggestInfo
+The `SuggestInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#suggest[suggest]
+section.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`from`     |
+The link:config-gerrit.html#suggest.from[number of characters] that a
+user must have typed before suggestions are provided.
+|=======================
+
 [[summary-info]]
 === SummaryInfo
 The `SummaryInfo` entity contains information about the current state
@@ -1069,6 +1523,21 @@
 |`id`       |optional|The `id` attribute of the menu item link.
 |========================
 
+[[user-config-info]]
+=== UserConfigInfo
+The `UserConfigInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#user[user] section.
+
+[options="header",cols="1,6"]
+|====================================
+|Field Name              |Description
+|`anonymous_coward_name` |
+link:config-gerrit.html#user.anonymousCoward[Username] that is
+displayed in the Gerrit Web UI and in e-mail notifications if the full
+name of the user is not set.
+|====================================
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index fbd3ba5..e0df4ca 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -172,6 +172,46 @@
   GET /groups/?n=25&S=50 HTTP/1.0
 ----
 
+[[suggest-group]]
+==== Suggest Group
+The `suggest` option indicates a user-entered string that
+should be auto-completed to group names.
+If this option is set and `n` is not set, then `n` defaults to 10.
+
+When using this option,
+the `project` or `p` option can be used to name the current project,
+to allow context-dependent suggestions.
+
+Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`,
+or `S`.
+(Attempts to use one of those options combined with `suggest` will
+error out.)
+
+.Request
+----
+  GET /groups/?suggest=ad&p=All-Projects HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "Administrators": {
+      "url": "#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b",
+      "options": {},
+      "description": "Gerrit Site Administrators",
+      "group_id": 1,
+      "owner": "Administrators",
+      "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b",
+      "id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b"
+    }
+  }
+----
+
 [[get-group]]
 === Get Group
 --
@@ -592,6 +632,89 @@
   }
 ----
 
+[[get-audit-log]]
+=== Get Audit Log
+--
+'GET /groups/link:#group-id[\{group-id\}]/log.audit'
+--
+
+Gets the audit log of a Gerrit internal group.
+
+.Request
+----
+  GET /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/log.audit HTTP/1.0
+----
+
+As response a list of link:#group-audit-event-info[GroupAuditEventInfo]
+entities is returned that describes the audit events of the group. The
+returned audit events are sorted by date in reverse order so that the
+newest audit event comes first.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "member": {
+        "url": "#/admin/groups/uuid-fdda826a0815859ab48d22a05a43472f0f55f89a",
+        "options": {},
+        "group_id": 3,
+        "owner": "Administrators",
+        "owner_id": "e56678641565e7f59dd5c6878f5bcbc842bf150a",
+        "id": "fdda826a0815859ab48d22a05a43472f0f55f89a",
+        "name": "MyGroup"
+      },
+      "type": "REMOVE_GROUP",
+      "user": {
+        "_account_id": 1000000,
+        "name": "Administrator",
+        "email": "admin@example.com",
+        "username": "admin"
+      },
+      "date": "2015-07-03 09:22:26.348000000"
+    },
+    {
+      "member": {
+        "url": "#/admin/groups/uuid-fdda826a0815859ab48d22a05a43472f0f55f89a",
+        "options": {},
+        "group_id": 3,
+        "owner": "Administrators",
+        "owner_id": "e56678641565e7f59dd5c6878f5bcbc842bf150a",
+        "id": "fdda826a0815859ab48d22a05a43472f0f55f89a",
+        "name": "MyGroup"
+      },
+      "type": "ADD_GROUP",
+      "user": {
+        "_account_id": 1000000,
+        "name": "Administrator",
+        "email": "admin@example.com",
+        "username": "admin"
+      },
+      "date": "2015-07-03 08:43:36.592000000"
+    },
+    {
+      "member": {
+        "_account_id": 1000000,
+        "name": "Administrator",
+        "email": "admin@example.com",
+        "username": "admin"
+      },
+      "type": "ADD_USER",
+      "user": {
+        "_account_id": 1000001,
+        "name": "John Doe",
+        "email": "john.doe@example.com",
+        "username": "jdoe"
+      },
+      "date": "2015-07-01 13:36:36.602000000"
+    }
+  ]
+----
+
 [[group-member-endpoints]]
 == Group Member Endpoints
 
@@ -1108,6 +1231,38 @@
 [[json-entities]]
 == JSON Entities
 
+[[group-audit-event-info]]
+=== GroupAuditEventInfo
+The `GroupAuditEventInfo` entity contains information about an audit
+event of a group.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`member`  |
+The group member that is added/removed. If `type` is `ADD_USER` or
+`REMOVE_USER` the member is returned as detailed
+link:rest-api-accounts.html#account-info[AccountInfo] entity, if `type`
+is `ADD_GROUP` or `REMOVE_GROUP` the member is returned as
+link:#group-info[GroupInfo] entity.
+|`type`    |
+The event type, can be: `ADD_USER`, `REMOVE_USER`, `ADD_GROUP` or
+`REMOVE_GROUP`.
+
+`ADD_USER`: A user was added as member to the group.
+
+`REMOVE_USER`: A user member was removed from the group.
+
+`ADD_GROUP`: A group was included as member in the group.
+
+`REMOVE_GROUP`: An included group was removed from the group.
+|`user`    |
+The user that did the add/remove as detailed
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`date`    |
+The timestamp of the event.
+|======================
+
 [[group-info]]
 === GroupInfo
 The `GroupInfo` entity contains information about a group. This can be
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 3baaaa8..986ccd8 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -731,6 +731,8 @@
     "use_content_merge": "INHERIT",
     "use_signed_off_by": "INHERIT",
     "create_new_change_for_all_not_in_target": "INHERIT",
+    "enable_signed_push": "INHERIT",
+    "require_signed_push": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -774,6 +776,16 @@
       "configured_value": "TRUE",
       "inherited_value": true
     },
+    "enable_signed_push": {
+      "value": true,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
+    "require_signed_push": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "max_object_size_limit": {
       "value": "10m",
       "configured_value": "10m",
@@ -1447,6 +1459,80 @@
   ]
 ----
 
+[[tag-options]]
+==== Tag Options
+
+Limit(n)::
+Limit the number of tags to be included in the results.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?n=2 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    }
+  ]
+----
+
+Skip(s)::
+Skip the given number of tags from the beginning of the list.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?n=2&s=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    },
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    }
+  ]
+----
+
+
 [[get-tag]]
 === Get Tag
 --
@@ -1902,6 +1988,14 @@
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
 to a branch or tag.
+|`enable_signed_push`|
+optional, not set if signed push is disabled|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+signed push validation is enabled on the project.
+|`require_signed_push`|
+optional, not set if signed push is disabled
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+signed push validation is required on the project.
 |`max_object_size_limit`     ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
@@ -2116,6 +2210,8 @@
 |Field Name      ||Description
 |`show_progress` |`false` if not set|
 Whether progress information should be shown.
+|`aggressive`    |`false` if not set|
+Whether an aggressive garbage collection should be done.
 |=============================
 
 [[head-input]]
diff --git a/Documentation/user-change-cleanup.txt b/Documentation/user-change-cleanup.txt
new file mode 100644
index 0000000..e569f38
--- /dev/null
+++ b/Documentation/user-change-cleanup.txt
@@ -0,0 +1,28 @@
+= Gerrit Code Review - Change Cleanup
+
+Gerrit administrators may configure
+link:config-gerrit.html#changeCleanup[change cleanups] that are
+executed periodically.
+
+[[auto-abandon]]
+== Auto-Abandon
+
+This cleanup job automatically abandons open changes that have been
+inactive for a defined time.
+
+Abandoning old inactive changes has the following advantages:
+
+* it signals change authors that changes are considered outdated
+* it keeps dashboards clean
+* it reduces the load on the server (for open changes the mergeability
+  flag is recomputed whenever a change is merged)
+
+If a change is still wanted it can be restored by clicking on the
+`Restore` button.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index 49821af..05932df 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -44,10 +44,9 @@
 While in edit mode, it is possible to add new files to the change by clicking
 the 'Add...' button at the top of the file list.
 
-Files can be removed from the change, or restored, by clicking the icon to the
-left of the file name. Reverting a file in the change is also supported and is
-achieved in two steps: remove file from the change and restore the file in the
-change.
+File changes can be reverted or files can be removed from the change or
+deleted files can be restored, by clicking the icons to the left of the file
+name.
 
 To switch from edit mode back to review mode, click the 'Done Editing' button.
 
@@ -147,13 +146,18 @@
 granted the link:access-control.html#capability_accessDatabase[accessDatabase]
 global capability are able to access change edit refs.
 
+[[search-for-change-edits]]
+
+To search change edits from the UI the link:user-search.html#has[has:edit]
+predicate can be used.
+
+Alternatively change edits can be accessed through "My => Edits" dashboard.
+
 [[not-implemented-features]]
 == Not Implemented Features
 
-* [PENDING CHANGE]
-The inline editor uses settings decided from the user's diff preferences, but those
-preferences are only modifiable from the side-by-side diff screen. It should be possible
-to open the preferences also from within the editor.
+* Support default configuration options for inline editor that an
+administrator has set in `refs/users/default:preferences.config` file.
 
 * Allow to rename files that are already contained in the change (from the file table).
 The same rename file dialog can be used with preselected and disabled original file
@@ -174,9 +178,6 @@
 ** "save-when-file-was-changed" or
 ** "close-when-no-changes"
 
-* Allow to activate different key maps, supported by CM: Emacs, Sublime, Vim. Load key
-maps dynamically. Currently default mode is used.
-
 * Implement conflict resolution during rebase of change edit using inline edit
 feature by creating new edit on top of current patch set with auto merge content
 
diff --git a/Documentation/user-named-destinations.txt b/Documentation/user-named-destinations.txt
new file mode 100644
index 0000000..1b6f143
--- /dev/null
+++ b/Documentation/user-named-destinations.txt
@@ -0,0 +1,32 @@
+= Gerrit Code Review - Named Destinations
+
+[[user-named-destinations]]
+== User Named Destinations
+It is possible to define named destination sets on a user level.
+To do this, define the named destination sets in files named after
+each destination set in the `destinations` directory of the user's
+account ref in the `All-Users` project.  The user's account ref is
+based on the user's account id which is an integer.  The account
+refs are sharded by the last two digits (`+nn+`) in the refname,
+leading to refs of the format `+refs/users/nn/accountid+`.  The
+user's destination files are a 2 column tab delimited file.  Each
+row in a destination file represents a single destination in the
+named set.  The left column represents the ref of the destination,
+and the right column represents the project of the destination.
+
+Example destination file named `destinations/myreviews`:
+
+----
+# Ref            	Project
+#
+refs/heads/master	gerrit
+refs/heads/stable-2.11	gerrit
+refs/heads/master	plugins/cookbook-plugin
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-named-queries.txt b/Documentation/user-named-queries.txt
new file mode 100644
index 0000000..e79b3da
--- /dev/null
+++ b/Documentation/user-named-queries.txt
@@ -0,0 +1,28 @@
+= Gerrit Code Review - Named Queries
+
+[[user-named-queries]]
+== User Named Queries
+It is possible to define named queries on a user level. To do
+this, define the named queries in the `queries` file under the
+link:intro-user.html#user-refs[user's ref] in the `All-Users` project.  The
+user's queries file is a 2 column tab delimited file.  The left
+column represents the name of the query, and the right column
+represents the query expression represented by the name.
+
+Example queries file:
+
+----
+# Name         	Query
+#
+selfapproved   	owner:self label:code-review+2,user=self
+blocked        	label:code-review-2 OR label:verified-1
+# Note below how to reference your own named queries in other named queries
+ready          	label:code-review+2 label:verified+1 -query:blocked status:open
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index dddbadc..859765c 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -53,18 +53,6 @@
 +
 The change has all necessary approvals and may be submitted.
 
-- [[submitted-merge-pending]]`Submitted, Merge Pending`:
-+
-The change was submitted and was added to the merge queue.
-+
-The change stays in the merge queue if it depends on a change that is
-still in review. In this case it will get automatically merged when all
-dependency changes have been merged.
-+
-This status can also mean that the change depends on an abandoned
-change or on an outdated patch set of another change. In this case you
-may want to rebase the change.
-
 - [[merged]]`Merged`:
 +
 The change was successfully merged into the destination branch.
@@ -94,7 +82,7 @@
 displayed with a copy-to-clipboard icon that allows the ID to be copied
 into the clipboard.
 
-If a Git web browser, such as GitWeb or Gitiles, is configured, there
+If a Git web browser, such as gitweb or Gitiles, is configured, there
 is also a link to the commit in the Git web browser.
 
 image::images/user-review-ui-change-screen-commit-info.png[width=800, link="images/user-review-ui-change-screen-commit-info.png"]
@@ -198,10 +186,6 @@
 The `Submit` button is available if the change is submittable and
 the link:access-control.html#category_submit[Submit] access right is
 assigned.
-+
-It is also possible to submit changes that have merge conflicts. This
-allows to do the conflict resolution for a change series in a single
-merge commit and submit the changes in reverse order.
 
 ** [[abandon]]`Abandon`:
 +
@@ -408,9 +392,6 @@
 switch between them. The patch sets are sorted in descending order so
 that the current patch set is always on top.
 
-Patch sets that have unpublished draft comments are marked by a comment
-icon.
-
 Draft patch sets are marked with `DRAFT`.
 
 image::images/user-review-ui-change-screen-patch-set-list.png[width=800, link="images/user-review-ui-change-screen-patch-set-list.png"]
@@ -539,12 +520,18 @@
 
 ** [[closed-ancestor]]Black Dot:
 +
-Indicates a merged ancestor, e.g. the commit was directly pushed into
+Indicates a closed ancestor, e.g. the commit was directly pushed into
 the repository bypassing code review, or the ancestor change was
 reviewed and submitted on another branch. The latter may indicate that
 the user has accidentally pushed the commit to the wrong branch, e.g.
 the commit was done on `branch-a`, but was then pushed to
 `refs/for/branch-b`.
+A black dot is also present if the change was abandoned.
+
+** [[closed-ancestor-abandoned]]Strikethrough Subject:
++
+When the commit is abandoned, its subject line will be striked
+through.
 
 +
 image::images/user-review-ui-change-screen-related-changes-indicators.png[width=800, link="images/user-review-ui-change-screen-related-changes-indicators.png"]
@@ -568,6 +555,17 @@
 +
 image::images/user-review-ui-change-screen-same-topic.png[width=800, link="images/user-review-ui-change-screen-same-topic.png"]
 
+- [[submitted-together]]`Submitted Together`:
++
+This tab page shows changes that will be submitted together with the
+currently viewed change, when clicking the submit button. It includes
+ancestors of the current patch set.
++
+This may include changes and its ancestors with the same topic if
+`change.submitWholeTopic` is enabled. Only open changes with the
+same topic are included in the list.
++
+
 - [[cherry-picks]]`Cherry-Picks`:
 +
 This tab page shows changes with the same link:user-changeid.html[
@@ -1089,6 +1087,15 @@
 +
 Large files that exceed 4000 lines will not be fully rendered.
 
+- [[line-wrapping]]`Line Wrapping`:
++
+Controls weather to enable line wrapping or not.
++
+If `false` is selected then line wrapping is disabled.
+This is the default option.
++
+If `true` is selected then line wrapping is enabled.
+
 [[keyboard-shortcuts]]
 == Keyboard Shortcuts
 
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3de45d2..b2b3614 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -88,6 +88,12 @@
 as a legacy numerical 'ID' such as 15183, or a newer style Change-Id
 that was scraped out of the commit message.
 
+[[destination]]
+destination:'NAME'::
++
+Changes which match the current user's destination named 'NAME'.
+(see link:user-named-destinations.html[Named Destinations]).
+
 [[owner]]
 owner:'USER', o:'USER'::
 +
@@ -99,6 +105,12 @@
 +
 Changes originally submitted by a user in 'GROUP'.
 
+[[query]]
+query:'NAME'::
++
+Changes which match the current user's query named 'NAME'
+(see link:user-named-queries.html[Named Queries]).
+
 [[reviewer]]
 reviewer:'USER', r:'USER'::
 +
@@ -147,18 +159,23 @@
 link:http://www.brics.dk/automaton/[dk.brics.automaton
 library] is used for evaluation of such patterns.
 
-[[topic]]
-topic:'TOPIC'::
+[[intopic]]
+intopic:'TOPIC'::
 +
-Changes whose designated topic at upload was 'TOPIC'.  This is
-often combined with 'branch:' and 'project:' operators to select
-all related changes in a series.
+Changes whose designated topic contains 'TOPIC', using a full-text search.
 +
 If 'TOPIC' starts with `^` it matches topic names by regular
 expression patterns.  The
 link:http://www.brics.dk/automaton/[dk.brics.automaton
 library] is used for evaluation of such patterns.
 
+[[topic]]
+topic:'TOPIC'::
++
+Changes whose designated topic matches 'TOPIC' exactly.  This is
+often combined with 'branch:' and 'project:' operators to select
+all related changes in a series.
+
 [[ref]]
 ref:'REF'::
 +
@@ -240,6 +257,10 @@
 Same as 'is:starred', true if the change has been starred by the
 current user.
 
+has:edit::
++
+True if the change has inline edit created by the current user.
+
 [[is]]
 is:starred::
 +
@@ -253,8 +274,8 @@
 
 is:reviewed::
 +
-True if there is at least one non-zero score on the change, in any
-approval category, by any user.
+True if any user has commented on the change more recently than the
+last update (comment or patch set) from the change owner.
 
 is:owner::
 +
@@ -268,7 +289,7 @@
 
 is:open, is:pending::
 +
-True if the change is either open or submitted, merge pending.
+True if the change is open.
 
 is:draft::
 +
@@ -278,7 +299,7 @@
 +
 True if the change is either merged or abandoned.
 
-is:submitted, is:merged, is:abandoned::
+is:merged, is:abandoned::
 +
 Same as <<status,status:'STATE'>>.
 
@@ -291,17 +312,13 @@
 [[status]]
 status:open, status:pending::
 +
-True if the change state is either 'review in progress' or 'submitted,
-merge pending'.
+True if the change state is 'review in progress'.
 
 status:reviewed::
 +
-Same as 'is:reviewed', matches if there is at least one non-zero
-score on the change, in any approval category, by any user.
-
-status:submitted::
-+
-Change has been submitted, but is waiting for a dependency.
+Same as 'is:reviewed', matches if any user has commented on the change
+more recently than the last update (comment or patch set) from the
+change owner.
 
 status:closed::
 +
@@ -327,6 +344,38 @@
 Valid relations are >=, >, <=, <, or no relation, which will match if the
 number of lines is exactly equal.
 
+[[commentby]]
+commentby:'USER'::
++
+Changes containing a top-level or inline comment by 'USER'. The special
+case of `commentby:self` will find changes where the caller has
+commented.
+
+[[from]]
+from:'USER'::
++
+Changes containing a top-level or inline comment by 'USER', or owned by
+'USER'. Equivalent to `(owner:USER OR commentby:USER)`.
+
+[[reviewedby]]
+reviewedby:'USER'::
++
+Changes where 'USER' has commented on the change more recently than the
+last update (comment or patch set) from the change owner.
+
+[[author]]
+author:'AUTHOR'::
++
+Changes where 'AUTHOR' is the author of the current patch set. 'AUTHOR' may be
+the author's exact email address, or part of the name or email address.
+
+[[committer]]
+committer:'COMMITTER'::
++
+Changes where 'COMMITTER' is the committer of the current patch set.
+'COMMITTER' may be the committer's exact email address, or part of the name or
+email address.
+
 
 == Argument Quoting
 
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 8411595..151ac71 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -97,6 +97,7 @@
 gitlinks and a .gitmodules file with all required info) and if so,
 a new submodule subscription is registered.
 
+[[automatic_update]]
 == Automatic Update of Superprojects
 
 After a superproject is subscribed to a submodule, it is not
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b8de6e2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,79 @@
+# Gerrit Code Review
+
+[Gerrit](https://www.gerritcodereview.com) is a code review and project
+management tool for Git based projects.
+
+## Objective
+
+Gerrit makes reviews easier by showing changes in a side-by-side display,
+and allowing inline comments to be added by any reviewer.
+
+Gerrit simplifies Git based project maintainership by permitting any
+authorized user to submit changes to the master Git repository, rather
+than requiring all approved changes to be merged in by hand by the project
+maintainer.
+
+## Documentation
+
+For information about how to install and use Gerrit, refer to
+[the documentation](https://gerrit-review.googlesource.com/Documentation/index.html).
+
+## Source
+
+Our canonical Git repository is located on [googlesource.com](https://gerrit.googlesource.com/gerrit).
+There is a mirror of the repository on [Github](https://github.com/gerrit-review/gerrit).
+
+## Reporting bugs
+
+Please report bugs on the [issue tracker](https://bugs.chromium.org/p/gerrit/issues/list).
+
+## Contribute
+
+Gerrit is the work of hundreds of contributors. We appreciate your help!
+
+Please read the [contribution guidelines](https://gerrit.googlesource.com/gerrit/+/master/SUBMITTING_PATCHES).
+
+Note that we do not accept Pull Requests via the Github mirror.
+
+## Getting in contact
+
+The IRC channel on freenode is #gerrit. An archive is available at:
+[echelog.com](http://echelog.com/logs/browse/gerrit).
+
+The Developer Mailing list is [repo-discuss on Google Groups](https://groups.google.com/forum/#!forum/repo-discuss).
+
+## License
+
+Gerrit is provided under the Apache License 2.0.
+
+## Build
+
+Install [Buck](http://facebook.github.io/buck/setup/install.html) and run the following:
+
+        git clone --recursive https://gerrit.googlesource.com/gerrit
+        cd gerrit && buck build release
+
+## Install binary packages (Deb/Rpm)
+
+The instruction how to configure GerritForge/BinTray repositories is
+[here](http://gitenterprise.me/2015/02/27/gerrit-2-10-rpm-and-debian-packages-available)
+
+On Debian/Ubuntu run:
+
+        apt-get update & apt-get install gerrit=<version>-<release>
+
+_NOTE: release is a counter that starts with 1 and indicates the number of packages that have
+been released with the same version of the software._
+
+On CentOS/RedHat run:
+
+        yum clean all && yum install gerrit-<version>[-<release>]
+
+_NOTE: release is optional. Last released package of the version is installed if the release
+number is omitted._
+
+## Events
+
+- November 7-8 2015: Gerrit User Conference, Mountain View. ([Register](http://goo.gl/forms/fifi2YQTc7)).
+- November 9-13 2015: Gerrit Hackathon, Mountain View. (Invitation Only).
+- March 2016: Gerrit Hackathon, Berlin. (Details to be confirmed).
diff --git a/ReleaseNotes/ReleaseNotes-2.0.10.txt b/ReleaseNotes/ReleaseNotes-2.0.10.txt
index 6fd7087..695be4f 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.10.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.10 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
 New Features
diff --git a/ReleaseNotes/ReleaseNotes-2.0.11.txt b/ReleaseNotes/ReleaseNotes-2.0.11.txt
index 0767873..62f2a18 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.11.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.11 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains a schema change.*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.12.txt b/ReleaseNotes/ReleaseNotes-2.0.12.txt
index c82bab0..eb28e2e 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.12.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.12 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains a schema change.*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.13.txt b/ReleaseNotes/ReleaseNotes-2.0.13.txt
index da84013..8ec13a8 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.13.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.13.1 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains a major configuration change.*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.14.txt b/ReleaseNotes/ReleaseNotes-2.0.14.txt
index 77ae3ba..de58035 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.14.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.14.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.14.1 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains a schema change* (since 2.0.13)
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.15.txt b/ReleaseNotes/ReleaseNotes-2.0.15.txt
index 7de1f76..a87cba1 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.15.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.15.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.15 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.16.txt b/ReleaseNotes/ReleaseNotes-2.0.16.txt
index 5dedd8c..4d0252d 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.16.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.16.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.16 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.17.txt b/ReleaseNotes/ReleaseNotes-2.0.17.txt
index cf556cb..493a64b 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.17.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.17.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.17 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.18.txt b/ReleaseNotes/ReleaseNotes-2.0.18.txt
index 18c5abe..df635d9 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.18.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.18.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.18 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Important Notices
 -----------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.19.txt b/ReleaseNotes/ReleaseNotes-2.0.19.txt
index 6c33e6b..0e114c8 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.19.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.19.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.19.2 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Important Notices
 -----------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.2.txt b/ReleaseNotes/ReleaseNotes-2.0.2.txt
index 473cf3d..b2d5b98 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.2 is now available for download:
 
-link:http://code.google.com/p/gerrit/[http://code.google.com/p/gerrit/]
+link:https://www.gerritcodereview.com/[https://www.gerritcodereview.com/]
 
 Important Notes
 ---------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.20.txt b/ReleaseNotes/ReleaseNotes-2.0.20.txt
index d46f74d..527de8e 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.20.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.20.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.20 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.21.txt b/ReleaseNotes/ReleaseNotes-2.0.21.txt
index 053ec89..34ab581 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.21.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.21.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.21 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
 Schema Change
diff --git a/ReleaseNotes/ReleaseNotes-2.0.22.txt b/ReleaseNotes/ReleaseNotes-2.0.22.txt
index 3a35421..faaca81 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.22.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.22.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.22 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.23.txt b/ReleaseNotes/ReleaseNotes-2.0.23.txt
index db841cc..16488d4 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.23.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.23.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.23 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.24.txt b/ReleaseNotes/ReleaseNotes-2.0.24.txt
index 7e0a617..1f08582 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.24.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.24.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.24 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
 Schema Change
diff --git a/ReleaseNotes/ReleaseNotes-2.0.3.txt b/ReleaseNotes/ReleaseNotes-2.0.3.txt
index 848f739..6bf3510 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.3.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.3 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 I would like to express a big thank you to Brad Larson for diving into
 Gerrit and coming up with the implementation for  "Add reviewer to an
diff --git a/ReleaseNotes/ReleaseNotes-2.0.4.txt b/ReleaseNotes/ReleaseNotes-2.0.4.txt
index 4869233..fec2425 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.4.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.4 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains a schema change.*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.5.txt b/ReleaseNotes/ReleaseNotes-2.0.5.txt
index 78389fc..70116d3 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.5.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.5 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 WARNING: This version contains a schema change.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.6.txt b/ReleaseNotes/ReleaseNotes-2.0.6.txt
index f9444cc..9d0af33 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.6.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.6 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 New Features
 ------------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.7.txt b/ReleaseNotes/ReleaseNotes-2.0.7.txt
index 3e70233..afc7784 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.7.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.7 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Of note is the WAR file doubled in size.  This is due to the switch to openid4java for the OpenID relying party implementation, as it is more compliant to the OpenID 2.0 draft standard than the prior relying party, dyuproject.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.8.txt b/ReleaseNotes/ReleaseNotes-2.0.8.txt
index 07429cc..4b2d10a5 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.8.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.8 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains a schema change.*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.9.txt b/ReleaseNotes/ReleaseNotes-2.0.9.txt
index e9fed5f..d2a9196 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.9.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.0.9 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 *WARNING: This version contains schema changes.*
 
diff --git a/ReleaseNotes/ReleaseNotes-2.1.1.txt b/ReleaseNotes/ReleaseNotes-2.1.1.txt
index 5f02146..9d795b6 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.1.1 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.10.txt b/ReleaseNotes/ReleaseNotes-2.1.10.txt
index 9d43bf4..5464267 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.10.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.1.9.html[2.1.9].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.1.10.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.1.10.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.10.war[https://www.gerritcodereview.com/download/gerrit-2.1.10.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.1.txt b/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
index 3d42558..ae5d912 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.2.1 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
index ccf25d6..6565833 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.2.2 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.3.txt b/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
index d551ad8..3cfbdd1 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.3.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.2.3 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.4.txt b/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
index f96c74a..5e863f7 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.4.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.2.4 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 New Features
 ------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.5.txt b/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
index 1cd5ae8..6e9a49e 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.5.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.2.5 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.txt
index ca69e86..e0d8c12 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.2 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.3.txt b/ReleaseNotes/ReleaseNotes-2.1.3.txt
index 44b6b56..f4faf32 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.3.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.3 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.4.txt b/ReleaseNotes/ReleaseNotes-2.1.4.txt
index 639229a..3e25163 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.4.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.4 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.5.txt b/ReleaseNotes/ReleaseNotes-2.1.5.txt
index b55aedf..4934223 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.5.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.5 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.5.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.5.war[https://www.gerritcodereview.com/download/gerrit-2.1.5.war]
 
 This is primarily a bug fix release to 2.1.4, but some additional
 new features were included so its named 2.1.5 rather than 2.1.4.1.
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.1.txt b/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
index d531a6c..a490c0a 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.6.1 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.6.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.6.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.6.1.war[https://www.gerritcodereview.com/download/gerrit-2.1.6.1.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.txt b/ReleaseNotes/ReleaseNotes-2.1.6.txt
index ce65f1a..520b2a6 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.6 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.6.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.6.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.6.war[https://www.gerritcodereview.com/download/gerrit-2.1.6.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
index 802f1c7..fdd7725 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.7.2 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.7.2.war[https://www.gerritcodereview.com/download/gerrit-2.1.7.2.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.txt b/ReleaseNotes/ReleaseNotes-2.1.7.txt
index b12713c..5123279 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.7 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.7.war[https://www.gerritcodereview.com/download/gerrit-2.1.7.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.8.txt b/ReleaseNotes/ReleaseNotes-2.1.8.txt
index fcfd225..476e312 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.8.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1.8 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.8.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.8.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.8.war[https://www.gerritcodereview.com/download/gerrit-2.1.8.war]
 
 New Features
 ------------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.9.txt b/ReleaseNotes/ReleaseNotes-2.1.9.txt
index 728d7cc..2efc5b6 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.9.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.1.8.html[2.1.8].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.1.9.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.1.9.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.1.9.war[https://www.gerritcodereview.com/download/gerrit-2.1.9.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.1.txt b/ReleaseNotes/ReleaseNotes-2.1.txt
index e1a27b0..127ab09 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.1 is now available in the usual location:
 
-link:http://code.google.com/p/gerrit/downloads/list[http://code.google.com/p/gerrit/downloads/list]
+link:https://www.gerritcodereview.com/download/index.html[https://www.gerritcodereview.com/download/index.html]
 
 
 New site_path Layout
diff --git a/ReleaseNotes/ReleaseNotes-2.10.1.txt b/ReleaseNotes/ReleaseNotes-2.10.1.txt
index df70b64..3065492 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.1.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.10.html[2.10].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.10.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.10.1.war[
+https://www.gerritcodereview.com/download/gerrit-2.10.1.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.10.2.txt b/ReleaseNotes/ReleaseNotes-2.10.2.txt
index 2ca5505..ac7c866 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.2.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.10.1.html[2.10.1].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.2.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.10.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.10.2.war[
+https://www.gerritcodereview.com/download/gerrit-2.10.2.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
index 2b90a6d..39312eb 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.1.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.10.3.html[2.10.3].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.10.3.1.war[
+https://www.gerritcodereview.com/download/gerrit-2.10.3.1.war]
 
 The 2.10.3 release packaged wrong version of the core plugins due to a bug
 in our buck build scripts. This version fixes this issue.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.txt b/ReleaseNotes/ReleaseNotes-2.10.3.txt
index 052840d..f7a69c3 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.txt
@@ -2,8 +2,8 @@
 ===============================
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.10.3.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.10.3.war[
+https://www.gerritcodereview.com/download/gerrit-2.10.3.war]
 
 Important Notes
 ---------------
diff --git a/ReleaseNotes/ReleaseNotes-2.10.4.txt b/ReleaseNotes/ReleaseNotes-2.10.4.txt
index e221549..c16e7e9 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.4.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.10.3.1.html[2.10.3.1].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.4.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.10.4.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.10.4.war[
+https://www.gerritcodereview.com/download/gerrit-2.10.4.war]
 
 New Features
 ------------
diff --git a/ReleaseNotes/ReleaseNotes-2.10.txt b/ReleaseNotes/ReleaseNotes-2.10.txt
index 18d0f34..f6bd951 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.txt
@@ -4,8 +4,8 @@
 
 Gerrit 2.10 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.10.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.10.war[
+https://www.gerritcodereview.com/download/gerrit-2.10.war]
 
 Gerrit 2.10 includes the bug fixes done with
 link:ReleaseNotes-2.9.1.html[Gerrit 2.9.1],
diff --git a/ReleaseNotes/ReleaseNotes-2.11.1.txt b/ReleaseNotes/ReleaseNotes-2.11.1.txt
index a070b86..be19fc5 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.1.txt
@@ -3,8 +3,8 @@
 
 Gerrit 2.11.1 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.11.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.11.1.war[
+https://www.gerritcodereview.com/download/gerrit-2.11.1.war]
 
 Gerrit 2.11.1 includes the bug fixes done with
 link:ReleaseNotes-2.10.4.html[Gerrit 2.10.4] and
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
index 90519dc..44c5398 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -4,8 +4,8 @@
 
 Gerrit 2.11 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.11.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.11.war[
+https://www.gerritcodereview.com/download/gerrit-2.11.war]
 
 Gerrit 2.11 includes the bug fixes done with
 link:ReleaseNotes-2.10.1.html[Gerrit 2.10.1],
diff --git a/ReleaseNotes/ReleaseNotes-2.12.1.txt b/ReleaseNotes/ReleaseNotes-2.12.1.txt
new file mode 100644
index 0000000..f49de7d
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -0,0 +1,236 @@
+Release notes for Gerrit 2.12.1
+===============================
+
+Gerrit 2.12.1 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war]
+
+Gerrit 2.12.1 includes the bug fixes done with
+link:ReleaseNotes-2.11.6.html[Gerrit 2.11.6] and
+link:ReleaseNotes-2.11.7.html[Gerrit 2.11.7]. These bug fixes are *not*
+listed in these release notes.
+
+Schema Upgrade
+--------------
+
+*WARNING:* This version includes a manual schema upgrade when upgrading
+from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, this manual step is not
+necessary and should be omitted.
+
+
+Bug Fixes
+---------
+
+General
+~~~~~~~
+
+* Fix column type for signed push certificates.
++
+The column type `VARCHAR(255)` was too small, preventing some PGP push
+certificates from being stored.
+
+* Add the `DRAFT_COMMENTS` option to the list changes REST API endpoint
+and mark it as deprecated.
++
+It was removed in version 2.12 because it's not needed any more by the UI,
+but this caused failures for clients that still use it.
++
+Now it is added back, although it does not do anything and is marked as
+deprecated.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3669[Issue 3669]:
+Fix schema migration when migrating to 2.12.x directly from a version
+earlier than 2.11.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3733[Issue 3733]:
+Correctly detect symlinked log directory on startup.
++
+If `$site_path/logs` was a symlink, the server would not start.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3871[Issue 3871]:
+Throw an explicit exception when failing to load a change from the database.
++
+If a change could not be loaded from the database, for example if it was
+manually removed from the changes table but references to it were remaining
+in other tables, a null change was returned which would then lead to an
+'Internal Server Error' that was difficult to track down. Now an error is
+raised earlier which will help administrators to find the root cause.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3743[Issue 3743]:
+Use submitter identity as committer when using 'Rebase if Necessary' merge
+strategy.
++
+When submitting a change that required rebase, the committer was being
+set to 'Gerrit Code Review' instead of the name of the submitter.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3758[Issue 3758]:
+Fix serving of static resources when deployed in application container.
++
+When deployed in a container, for example Tomcat, it was not possible to
+load the UI because static content could not be loaded from the WAR file.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3790[Issue 3790]:
+When deployed in a container, for example Tomcat, the 'Documentation' menu
+was missing.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3786[Issue 3786]:
+Fix SQL statement syntax in schema migration.
++
+An extra semicolon was preventing migration from 2.11.x to 2.12 when using
+an Oracle database.
+
+* Send email using email queue instead of the default queue.
++
+Some emails sent asynchronously were already being sent using that queue
+but some were not. This was confusing for a gerrit administrator because
+if there is a build up of `send-email` tasks in the queue, he would
+think that increasing `sendemail.threadPoolSize` would help but it did not
+because some of the email were sent using the default queue which is
+configurable using `execution.defaultThreadPoolSize`.
+
+* Fix XSRF token cookie to honor `auth.cookieSecure` setting.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3767[Issue 3767]:
+Fix replication of first patch set for new changes.
++
+When new changes were pushed from the command line, the first patch
+set did not get replicated to destinations.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3771[Issue 3771]:
+Remove `index.defaultMaxClauseCount` configuration option.
++
+When `index.maxTerms` was either not set (thus no limit) or set to a value
+higher than `index.defaultMaxClauseCount` it was possible that viewing the
+related changes tab could cause a 'Too many clauses' error for changes that
+have a lot of related changes.
++
+The `index.defaultMaxClauseCount` configuration option is removed, and the
+existing `index.maxTerms` is reused. The default value of `index.maxTerms`
+is reduced from 'no limit' to 1024.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
+Explicitly set parent project to 'All-Projects' when a project is created
+without giving the parent.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3948[Issue 3948]:
+Fix submit of project parent updates on `refs/meta/config`.
++
+When submitting a change on `refs/meta/config` to update a project's parent,
+the error 'The change must be submitted by a Gerrit administrator' was being
+displayed even when the submitter was an admin. The submit was successful
+when clicking 'Submit' a second time.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3811[Issue 3811]:
+Fix submittability of merge commits that resolve merge conflicts.
++
+If a series of changes contained a change that conflicted with the destination
+branch, but the conflict was solved by a merge commit at the tip of the
+series, the series was not submittable.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3883[Issue 3883]:
+Respect the `core.commentchar` setting from `.gitconfig` in `commit-msg` hook.
+
+UI
+~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3894[Issue 3894]:
+Fix display of 'Related changes' after change is rebased in web UI:
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3071[Issue 3071]:
+Fix display of submodule differences in side-by-side view.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3718[Issue 3718]:
+Hide avatar images when no avatars are available.
++
+The UI was showing a transparent empty image with a border.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3731[Issue 3731]:
+Fix syntax higlighting of tcl files.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3863[Issue 3863]:
+Fix display of active row marker in tag list.
++
+Clicking on one of the rows would cause the tag name to disappear.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1207[Issue 1207]:
+Fix keyboard shortcuts for non-US keyboards on side-by-side diff screen.
++
+The forward/backward navigation keys `[` and `]` only worked on keyboards where
+these characters could be typed without using any modifier key (like CTRL, ALT,
+etc..).
++
+Note that the problem still exists on the unified diff screen.
+
+* Improve tooltip on 'Submit' button when 'Submit whole topic' is enabled
+and the topic can't be submitted due to some changes not being ready.
+
+Plugins
+~~~~~~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]:
+Fix repeated reloading of plugins when running on OpenJDK 8.
++
+OpenJDK 8 uses nanotime precision for file modification time on systems that
+are POSIX 2008 compatible. This leads to precision incompatibility when
+comparing the plugin's JAR file timestamp, resulting in the plugin being
+reloaded every minute.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3741[Issue 3741]:
+Fix handling of merge validation exceptions emitted by plugins.
++
+If a plugin raised an exception, it was reported to the user as 'Change is
+new', rather than 'Missing dependency'.
+
+* Allow plugins to get the caller in merge validation requests.
++
+Plugins that implement the `MergeValidationListener` interface now get the
+caller (the user who initiated the merge) in the `onPreMerge` method.
++
+Existing plugins that implement this interface must be adapted to the new
+method signature.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3892[Issue 3892]:
+Allow plugins to suggest reviewers based on either change or project
+resources.
+
+Documentation
+~~~~~~~~~~~~~
+
+* Update documentation of `commentlink` to reflect changed search URL.
+
+* Add missing documentation of valid `database.type` values.
+
+Upgrades
+--------
+
+* Upgrade JGit to 4.1.2.201602141800-r.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt
new file mode 100644
index 0000000..500b015
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -0,0 +1,73 @@
+Release notes for Gerrit 2.12.2
+===============================
+
+Gerrit 2.12.2 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war]
+
+Schema Upgrade
+--------------
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.1.html[
+2.12.1] but a manual schema upgrade is necessary when upgrading from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, or from 2.12.1 having already
+done the migration, this manual step is not necessary and should be omitted.
+
+
+Bug Fixes
+---------
+
+* Upgrade Apache commons-collections to version 3.2.2.
++
+Includes a fix for a link:https://issues.apache.org/jira/browse/COLLECTIONS-580[
+remote code execution exploit].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
+Explicitly set parent project to 'All-Projects' when a project is created
+without giving the parent.
+
+* Don't add message twice on abandon or restore via ssh review command.
++
+When abandoning or reviewing a change via the ssh `review` command, and
+providing a message with the `--message` option, the message was added to
+the change twice.
+
+* Clear the input box after cancelling add reviewer action.
++
+When the action was cancelled, the content of the input box was still
+there when opening it again.
+
+* Fix internal server error when aborting ssh command.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3969[Issue 3969]:
+Fix internal server error when submitting a change with 'Rebase If Necessary'
+strategy.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.3.txt b/ReleaseNotes/ReleaseNotes-2.12.3.txt
new file mode 100644
index 0000000..dba10e9
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.3.txt
@@ -0,0 +1,117 @@
+Release notes for Gerrit 2.12.3
+===============================
+
+Gerrit 2.12.3 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war]
+
+Gerrit 2.12.3 includes the bug fixes done with
+link:ReleaseNotes-2.11.8.html[Gerrit 2.11.8] and
+link:ReleaseNotes-2.11.9.html[Gerrit 2.11.9]. These bug fixes are *not*
+listed in these release notes.
+
+Schema Upgrade
+--------------
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.2.html[
+2.12.2] but a manual schema upgrade is necessary when upgrading from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
+having already done the migration, this manual step is not necessary and
+should be omitted.
+
+
+Bug Fixes
+---------
+
+* Fix SSL security issue in the SMTP email relay.
++
+The hostname of the SSL socket was not verified. This made the read
+from the socket insecure since without verifying the hostname it may
+be link:https://www.cs.utexas.edu/~shmat/shmat_ccs12.pdf[vulnerable
+to a man-in-the-middle attack].
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3895[Issue 3895]:
+Fix failure to submit with 'Rebase if Necessary' after changes were reordered
+with interactive rebase.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4052[Issue 4052]:
+Fix failure to start server after upgrade from version 2.9.4.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3891[Issue 3891]:
+Fix query with `label:` operator and zero value.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4112[Issue 4112]:
+Fix failure to submit changes caused by empty user edit ref.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4087[Issue 4087]:
+Fix failure to submit change when a branch is created on the change ref.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4155[Issue 4155]:
+Fix tags REST API to correctly return all tags.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4154[Issue 4154]:
+Add support for `.team` and several more TLDs in email address validation.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4163[Issue 4163]:
+Prevent removal of non-voting reviewers on submit of change.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2647[Issue 2647]:
+Fix usage of `CTRL-C` on change screen.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4236[Issue 4236]:
+Fix internal error when pushing an amended commit with the `%edit` option.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3426[Issue 3426]:
+Fix pushing changes with `%base` option or `newChangeForAllNotInTarget` option.
+
+* Show 'Submitted Together' tab for changes with same topic.
+
+* Improve submit button tooltip messages shown when change is not submittable.
+
+* Fix firing of the `topic-changed` hook.
+
+* Remove `--dry-run` option from the `Reindex` site program.
++
+The implementation of the option was removed, but the option was mistakenly
+added back to the command and did not actually work.
+
+* Print proper task names in the output of the `show-queues` command.
+
+* Replication plugin: Double check if a ref is missing locally before deleting
+from remote.
+
+* Show an error message when trying to add a non-existent group to an ACL.
+
+Updates
+-------
+
+* Update commons-validator to 1.5.1.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.4.txt b/ReleaseNotes/ReleaseNotes-2.12.4.txt
new file mode 100644
index 0000000..64252c6
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.4.txt
@@ -0,0 +1,128 @@
+= Release notes for Gerrit 2.12.4
+
+Gerrit 2.12.4 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war]
+
+== Schema Upgrade
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.3.html[
+2.12.3] but a manual schema upgrade is necessary when upgrading from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
+having already done the migration, this manual step is not necessary and
+should be omitted.
+
+== Known Issues
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]:
+'value too long for type character varying(255)' in patch_sets table when
+migrating to schema version 108.
++
+This error may occur under some circumstances when running the schema
+migration from an earlier version of Gerrit.
++
+On sites where this occurs, it can be fixed with a manual schema update
+according to the comments in the issue.
+
+== Bug Fixes
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4400[Issue 4400]:
+Fix `AlreadyClosedException` in Lucene index.
++
+If a Lucene indexing thread was interrupted by an SSH connection being
+closed, this would also close file handles being used to read the index.
++
+Lucene queries are now executed on background threads to isolate them
+from SSH threads.
++
+This may also reduce latency for user dashboards on a multi-core system as
+each query for the different sections can now run on separate threads and
+return results when ready.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4249[Issue 4249]:
+Fix 'Duplicate stages not allowed' error during indexing.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4238[Issue 4238]:
+Fix 'not found' error when browsing tree in gitweb.
++
+The `refs/heads/` prefix was incorrectly being added to `HEAD`, causing a
+'404 Not Found' error.
+
+* Allow to read repositories that do not end with `.git`.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4262[Issue 4262]:
+Fix GPG push certificate for first patch set of new changes.
++
+The GPG certificate was not being set for the first patch set of new
+changes.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4296[Issue 4296]:
+Fix internal error when a query does not contain any token.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4241[Issue 4241]:
+Fix 'Cannot format velocity template' error when sending notification emails.
+
+* Fix `sshd.idleTimeout` setting being ignored.
++
+The `sshd.idleTimeout` setting was not being correctly set on the SSHD
+backend, causing idle sessions to not time out.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4324[Issue 4324]:
+Set the correct uploader on new patch sets created via the inline editor.
+
+* Log a warning instead of failing when invalid commentlinks are configured.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4136[Issue 4136]:
+Fix support for `HEAD` requests in the REST API.
++
+Sending a `HEAD` request failed with '404 Not Found'.
+
+* Return proper error response when trying to confirm an email that is already
+used by another user.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4318[Issue 4318]
+Fix 'Rebase if Necessary' merge strategy to prevent introducing a duplicate
+commit when submitting a merge commit.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4332[Issue 4332]:
+Allow `local` as a valid TLD for outgoing emails.
+
+* Bypass hostname verification when `sendemail.sslVerify` is disabled.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4398[Issue 4398]:
+Replication: Consider ref visibility when scheduling replication.
++
+It was possible for refs to be replicated to remotes despite not being
+visible to groups mentioned in the `authGroup` setting.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4036[Issue 4036]:
+Fix hanging query when using `is:watched` without authentication.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.5.txt b/ReleaseNotes/ReleaseNotes-2.12.5.txt
new file mode 100644
index 0000000..12d6870
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.5.txt
@@ -0,0 +1,101 @@
+= Release notes for Gerrit 2.12.5
+
+Gerrit 2.12.5 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.5.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.5.war]
+
+== Schema Upgrade
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.4.html[
+2.12.4] but a manual schema upgrade is necessary when upgrading from 2.12.
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
+  alter table patch_sets modify push_certficate clob;
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
+  alter table patch_sets alter column push_certficate type text;
+----
+
+For other database types, execute the appropriate equivalent command.
+
+Note that the misspelled `push_certficate` is the actual name of the
+column.
+
+When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
+having already done the migration, this manual step is not necessary and
+should be omitted.
+
+== Known Issues
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]:
+'value too long for type character varying(255)' in patch_sets table when
+migrating to schema version 108.
++
+This error may occur under some circumstances when running the schema
+migration from an earlier version of Gerrit.
++
+On sites where this occurs, it can be fixed with a manual schema update
+according to the comments in the issue.
+
+== New Features
+
+* New preference to enable line wrapping in diff screen and inline editor.
+
+== Bug Fixes
+
+* Fix the diff and edit preference dialogs for smaller screens.
++
+On smaller screens the options at the bottom of the dialogs would
+get cut off, making it difficult to change them.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4521[Issue 4521]:
+Fix internal server error during validation of email addresses.
++
+When creating a new account or adding a new email address to an existing
+account, the email validation crashed.
+
+* Lucene stability improvements.
++
+Each Lucene index is now written using a dedicated background thread. Lucene
+threads may not be cancelled, to prevent interruptions while writing.
+
+* Don't try to change username that is already set.
++
+Since Gerrit version 2.1.4 it is not allowed to change the username once
+it has been set, and attempting to do so results in an exception.
++
+If `ldap.accountSshUserName` is set in the `gerrit.config` using
+`${userPrincipalName.localPart}` to initialize the username from the user's
+email address, and then the email address is changed, the username gets
+resolved to something different and the account manager tried to change it.
+As a result, an exception was raised and the user could no longer log in.
++
+Instead of trying to change the username, a warning is logged.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4006[Issue 4006]:
+Prevent search limit parameter from exceeding maximum integer value.
+
+* Fix internal server error when generating task names.
+
+* Print proper names for query tasks in the output of the `show-queue` command.
+
+* Double-check change status when auto-abandoning changes.
++
+It was possible that changes could be updated in the time between the query
+results being returned and the change being abandoned.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
new file mode 100644
index 0000000..e8e8aec
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -0,0 +1,592 @@
+Release notes for Gerrit 2.12
+=============================
+
+
+Gerrit 2.12 is now available:
+
+link:https://www.gerritcodereview.com/download/gerrit-2.12.war[
+https://www.gerritcodereview.com/download/gerrit-2.12.war]
+
+Important Notes
+---------------
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* To use online reindexing when upgrading to 2.12.x, the server must
+first be upgraded to 2.8 (or 2.9) and then through 2.10 and 2.11 to 2.12.x. If
+reindexing will be done offline, you may ignore this warning and upgrade directly
+to 2.12.x.
+
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[issue 3084].
+
+*WARNING:* The Solr secondary index is no longer supported. With this release
+the only supported secondary index is Lucene.
+
+*WARNING:* The format of the `ref-updated` event has changed. Users of the
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[
+Jenkins Gerrit Trigger plugin] with jobs triggering on `ref-updated` should
+upgrade to at least
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[
+version 2.15.1]. If an upgrade of the plugin is not possible, a workaround is
+to change the branch configuration to type `Path` with a pattern like
+`refs/*/master` instead of `Plain` and `master`.
+
+
+Release Highlights
+------------------
+
+This release includes the following new features. See the sections below for
+further details.
+
+* New change submission workflows: 'Submit Whole Topic' and 'Submitted Together'.
+
+* Support for GPG Keys and signed pushes.
+
+
+New Features
+------------
+
+New Change Submission Workflows
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* New 'Submit Whole Topic' setting.
++
+When the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#change.submitWholeTopic[
+`change.submitWholeTopic`] setting is enabled, all changes belonging to the same
+topic will be submitted at the same time.
++
+This setting should be considered experimental, and is disabled by default.
+
+* Submission of changes may include ancestors.
++
+If a change is submitted that has submittable ancestor changes, those changes
+will also be submitted.
+
+* The merge queue is removed.
++
+Changes that cannot be submitted due to missing dependencies will no longer
+enter the 'Submitted, Merge Pending' state.
+
+
+GPG Keys and Signed Pushes
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* Signed push can be enabled by setting
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.enableSignedPush[
+`receive.enableSignedPush`] to true.
++
+When a client pushes with `git push --signed`, Gerrit ensures that the push
+certificate is valid and signed with a valid public key stored in the
+`refs/meta/gpg-keys` branch of the `All-Users` repository.
+
+* When signed push is enabled, and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#gerrit.editGpgKeys[
+`gerrit.editGpgKeys`] is set to true, users may upload their public GPG
+key via the REST API or UI.
++
+If this setting is not enabled, GPG keys may only be added by administrators
+with direct access to the `All-Users` repository.
+
+* Administrators may also configure
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSeed[
+`receive.certNonceSeed`] and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSlop[
+`receive.certNonceSlop`].
+
+
+Secondary Index
+~~~~~~~~~~~~~~~
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3333[Issue 3333]:
+Support searching for changes by author and committer.
++
+Changes are indexed by the git author and committer of the latest patch set,
+and can be searched with the `author:` and `committer:` operators.
++
+Changes are matched on either the exact whole email address, or on parts of the
+name or email address.
+
+* Add `from:` search operator to match by owner of change or author of comments.
+
+* Add `commentby:` search operator to search by author of comments.
+
+* Change the `topic:` search operator to search by the exact topic name.
+
+* Add `intopic:` search operator to search by topics containing the search term.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3291[Issue 3291]:
+Add `has:edit` search operator to match changes that have edit revisions on them.
+
+* Allow configuration of maximum query size.
++
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#index.maxTerms[
+`index.maxTerms`] can be set to limit the number of leaf index terms.
+
+* Expose Lucene index writers for plugins.
++
+Plugins can now reconfigure various Lucene performance related parameters
+at runtime.
+
+* Make Lucene index writers auto-commit writers.
++
+Plugins can now temporarily turn on auto-committing in situations where it makes
+sense to enforce all changes to be written to disk ASAP.
+
+
+UI
+~~
+
+General
+^^^^^^^
+
+* Edit and diff preferences can be modified from the user preferences screen.
++
+Previously it was only possible to edit these preferences from the actual
+diff and edit screens.
+
+* Add 'Edits' to the 'My' dashboard menu to list changes on which the user
+has an unpublished edit revision.
+
+* Support for URL aliases.
++
+Administrators may define
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#urlAlias[
+URL aliases] to map plugin screens into the Gerrit URL namespace.
++
+Plugins may use user-specific URL aliases to replace certain screens for certain
+users.
+
+
+Project Screen
+^^^^^^^^^^^^^^
+
+* New tab to list the project's tags, similar to the branch list.
+
+
+Inline Editor
+^^^^^^^^^^^^^
+
+* Store and load edit preferences in git.
++
+Edit preferences are stored and loaded to/from the `All-Users` repository.
+
+* Add 'auto close brackets' feature.
+
+* Add 'match brackets' feature.
+
+* Make the cursor blink rate customizable.
+
+* Add support for Emacs and Vim key maps.
+
+
+Change Screen
+^^^^^^^^^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3318[Issue 3318]:
+Highlight 'Reply' button if there are draft comments on any patch set.
++
+If any patch set of the change has a draft comment by the current user,
+the 'Reply' button is highlighted.
++
+The icons depicting draft comments are removed from the revisions drop-down
+list.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1100[Issue 1100]:
+Publish all draft comments when replying to a change.
++
+All draft comments, including those on older patch sets, are published when
+replying to a change.
+
+* Show file size increase/decrease for binary files.
+
+* Show uploader if different from change owner.
+
+* Show push certificate status.
+
+* Show change subject as tooltip on related changes list.
++
+This helps to identify changes when the subject is truncated in the list.
+
+
+Side-By-Side Diff
+^^^^^^^^^^^^^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3293[Issue 3293]:
+Add syntax highlighting for Puppet.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3447[Issue 3447]:
+Add syntax highlighting for VHDL.
+
+
+Group Screen
+^^^^^^^^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1479[Issue 1479]:
+The group screen now includes an 'Audit Log' panel showing member additions,
+removals, and the user who made the change.
+
+
+API
+~~~
+
+Several new APIs are added.
+
+Accounts
+^^^^^^^^
+
+* Suggest accounts.
+
+Tags
+^^^^
+
+* List tags.
+
+* Get tag.
+
+
+REST API
+~~~~~~~~
+
+New REST API endpoints and new options on existing endpoints.
+
+
+Accounts
+^^^^^^^^
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#set-username[
+Set Username]: Set the username of an account.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#get-detail[
+Get Account Details]: Get the details of an account.
++
+In addition to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#account-info[
+AccountInfo] fields returned by the existing
+ link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#get-account[
+Get Account] endpoint, the new REST endpoint returns the registration date of
+the account and the timestamp of when contact information was filed for this
+account.
+
+
+Changes
+^^^^^^^
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
+Set Review]: Add an option to omit duplicate comments.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#get-safe-content[
+Download Content]: Download the content of a file from a certain revision, in a
+safe format that poses no risk for inadvertent execution of untrusted code.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#submitted-together[
+Get Submitted Together]: Get the list of all changes that will be submitted at
+the same time as the change.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1100[Issue 1100]:
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
+Set Review]: Add an option to publish draft comments on all revisions.
+
+Config
+^^^^^^
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#get-info[
+Get Server Info]: Return information about the Gerrit server configuration.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#confirm-email[
+Confirm Email]: Confirm that the user owns an email address.
+
+
+Groups
+^^^^^^
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#list-group[
+List Groups]: Add option to suggest groups.
++
+This allows group auto-completion to be used in a plugin's UI.
+
+*  link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#get-audit-log[
+Get Audit Log]: Get the audit log of a Gerrit internal group, showing member
+additions, removals, and the user who made the change.
+
+
+Projects
+^^^^^^^^
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#run-gc[
+Run GC]: Add `aggressive` option to specify whether or not to run an aggressive
+garbage collection.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#list-tags[
+List Tags]: Support filtering by substring and regex, and pagination with
+`--start` and `--end`.
+
+
+SSH
+~~~
+
+* Add support for ZLib Compression.
++
+To enable compression use the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#sshd.enableCompression[
+`sshd.enableCompression` setting].
+
+* Add support for hmac-sha2-256 and hmac-sha2-512 as MACs.
+
+Plugins
+~~~~~~~
+
+General
+^^^^^^^
+
+* Gerrit client can now pass JavaScriptObjects to extension panels.
+
+* New UI extension point for header bar in change screen.
+
+* New UI extension point to password screen.
+
+* New UI extension points to project info screen.
+
+* New UI extension point for pop down buttons on change screen.
+
+* New UI extension point for buttons in header bar on change screen.
+
+* New UI extension point at bottom of the user preferences screen.
+
+* New UI extension point for the 'Included In' drop-down panel.
++
+By implementing the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/dev-plugins.html#included-in[
+Included In interface], plugins may add entries to the 'Included In' dropdown
+menu on the change screen.
+
+* Plugins can extend Gerrit screens with GWT controls.
+
+* Plugins can add custom settings screens.
+
+* Referencing groups in `project.config`.
++
+Plugins can refer to groups so that when they are renamed, the project
+config will also be updated in this section.
+
+* API
+
+** Allow to use `CurrentSchemaVersion`.
+
+** Allow to use `InternalChangeQuery.query()`.
+
+** Allow to use `JdbcUtil.port()`.
+
+** Allow to use GWTORM `Key` classes.
+
+
+Other
+~~~~~
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3401[Issue 3401]:
+Add option to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#sendemail.allowRegisterNewEmail[
+disable registration of new email addresses].
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2061[Issue 2061]
+Add Support for `git-upload-archive`.
++
+This allows use the standard `git archive` command to create an archive
+of the content of a repository.
+
+* Add a background job to automatically abandon inactive changes.
++
+The
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#changeCleanup[
+changeCleanup] configuration can be set to periodically check for inactive
+changes and automatically abandon them.
+
+* Add support for the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/database-setup.html#createdb_db2[
+DB2 database].
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3441[Issue 3441]:
+Add support for the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/database-setup.html#createdb_derby[
+Apache Derby database].
+
+* Download commands plugin: Use commit IDs for download commands when change refs are hidden.
++
+Git has a configuration option to hide refs from the initial advertisement
+(`uploadpack.hideRefs`). This option can be used to hide the change refs from
+the client. As consequence this prevented fetching changes by change ref from
+working.
++
+Setting `download.checkForHiddenChangeRefs` in the `gerrit.config` to true
+allows the download commands plugin to check for hidden change refs.
+
+* Add a new 'Maintain Server' global capability.
++
+Members of a group with the 'Maintain Server' capability may view caches, tasks,
+and queues, and invoke the index REST API on changes.
+
+
+Bug Fixes
+---------
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3499[Issue 3499]:
+Fix syntax highlighting of raw string literals in go.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3643[Issue 3643]:
+Fix syntax highlighting of ES6 string templating using backticks.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3653[Issue 3653]:
+Correct timezone in sshd log after DST change.
++
+When encountering a DST switch, the timezone wasn't updated until
+the server was reloaded.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3306[Issue 3306]:
+Allow admins to read, push and create on `refs/users/default`.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3212[Issue 3212]:
+Fix failure to run `init` when `--site-path` option is not explicitly given.
+
+* Make email validation case insensitive.
++
+While link:https://tools.ietf.org/html/rfc5321#section-2.3.11[
+RFC 5321 section 2.3.11] allows for the local-part (the part left of
+the '@') of an email address to be case sensitive, the domain portion is
+case insensitive according to
+link:https://tools.ietf.org/html/rfc1035#section-3.1[RFC 1035 section 3.1].
+And in practice, even the local-part is typically case insensitive also.
+
+* `commit-msg` hook: Don't add `Change-Id` line on temporary commits.
++
+Commits created with `git commit --fixup` or `git commit --squash` are not
+intended to be pushed to Gerrit, and don't need a `Change-Id` line.
++
+This also prevents changes from being accidentally uploaded, at least for
+projects that have the 'Require Change-Id' configuration enabled.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3444[Issue 3444]:
+download-commands plugin: Fix clone with commit-msg hook when project name
+contains '/'.
+
+* Use full ref name in `refName` attribute of `ref-updated` events.
++
+The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/json.html#refUpdate[
+refUpdate attribute] in `ref-updated` events did not include the full name
+of the ref in the `refName` attribute, i.e. `master` was used instead of
+`refs/heads/master`.
++
+Support for the new format is added in
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[
+version 2.15.1 of the Jenkins Gerrit Trigger plugin].
++
+Users who are unable to upgrade the plugin may instead change the
+trigger's branch configuration to type `Path` with a pattern like
+`refs/*/master` instead of `Plain` and `master`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3714[Issue 3714]:
+Improve visibility of comments on dark themes.
+
+* Fix highlighting of search results and trailing whitespaces in intraline
+diff chunks.
+
+* Fix server error when listing annotated/signed tag that has no tagger info.
+
+* Don't create new account when claimed OAuth identity is unknown.
++
+The Claimed Identity feature was enabled to support old Google OpenID accounts,
+that cannot be activated anymore. In some corner cases, when for example the URL
+is not from the production Gerrit site, for example on a staging instance, the
+OpenID identity may deviate from the original one. In case of mismatch, the lookup
+of the user for the claimed identity would fail, causing a new account to be
+created.
+
+* Suggest to upgrade installed plugins per default during site initialization
+to new Gerrit version.
++
+The default was 'No' which resulted in some sites not upgrading core
+plugins and running the wrong versions.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3698[Issue 3698]:
+Fix creation of the administrator user on databases with pre-allocated
+auto-increment column values.
++
+When using a database configuration where auto-increment column values are
+pre-allocated, it was possible that the 'Administrators' group was created
+with an ID other than `1`. In this case, the created admin user was not added
+to the correct group, and did not have the correct admin permissions.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3018[Issue 3018]:
+Fix query for changes using a label with a group operator.
++
+The `group` operator was being ignored when searching for changes with labels
+because the search index does not contain group information.
+
+* Fix online reindexing of changes that don't already exist in the index.
++
+Changes are now always reloaded from the database during online reindex.
+
+* Fix reading of plugin documentation.
++
+Under some circumstances it was possible to fail with an IO error.
+
+Documentation Updates
+---------------------
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]:
+Update documentation of `commentlink.match` regular expression to clarify
+that the expression is applied to the rendered HTML.
+
+* Remove warning about unstable change edit REST API endpoints.
++
+These endpoints should be considered stable since version 2.11.
+
+* Document that `ldap.groupBase` and `ldap.accountBase` are repeatable.
+
+Upgrades
+--------
+
+* Upgrade Asciidoctor to 1.5.2
+
+* Upgrade AutoValue to 1.1
+
+* Upgrade Bouncy Castle to 1.52
+
+* Upgrade CodeMirror to 5.7
+
+* Upgrade gson to 2.3.1
+
+* Upgrade guava to 19.0-RC2
+
+* Upgrade gwtorm to 1.14-20-gec13fdc
+
+* Upgrade H2 to 1.3.176
+
+* Upgrade httpcomponents to 4.4.1
+
+* Upgrade Jetty to 9.2.13.v20150730
+
+* Upgrade JGit to 4.1.1.201511131810-r
+
+* Upgrade joda-time to 2.8
+
+* Upgrade JRuby to 1.7.18
+
+* Upgrade jsch to 0.1.53
+
+* Upgrade JUnit to 4.11
+
+* Upgrade Lucene to 5.3.0
+
+* Upgrade Prolog Cafe 1.4.1
+
+* Upgrade servlet API to 8.0.24
+
+* Upgrade Truth to version 0.27
+
diff --git a/ReleaseNotes/ReleaseNotes-2.2.0.txt b/ReleaseNotes/ReleaseNotes-2.2.0.txt
index 58605fe..5938f66 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.0.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.0.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.2.0 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.0.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.0.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.2.0.war[https://www.gerritcodereview.com/download/gerrit-2.2.0.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.1.txt
index ff8040e..6a4829e 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.2.1 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.2.1.war[https://www.gerritcodereview.com/download/gerrit-2.2.1.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
index 4613787..aabe03a 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.2.2.1 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.2.2.1.war[https://www.gerritcodereview.com/download/gerrit-2.2.2.1.war]
 
 
 There are no schema changes from 2.2.2.  However, if upgrading from
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
index f7c299e..2747ab0 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.2.2.2 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.2.2.2.war[https://www.gerritcodereview.com/download/gerrit-2.2.2.2.war]
 
 There are no schema changes from 2.2.2, or 2.2.2.1.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index 2c14fa1..3889dcc 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.2.2 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.2.2.war[https://www.gerritcodereview.com/download/gerrit-2.2.2.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.3.1.txt b/ReleaseNotes/ReleaseNotes-2.3.1.txt
index c37fbe4..8914c69 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.3.1 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.3.1.war[https://www.gerritcodereview.com/download/gerrit-2.3.1.war]
 
 There are no schema changes from 2.3.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.3.txt b/ReleaseNotes/ReleaseNotes-2.3.txt
index cf371c8..9cdc886 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.3 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.3.war[https://www.gerritcodereview.com/download/gerrit-2.3.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.4.1.txt b/ReleaseNotes/ReleaseNotes-2.4.1.txt
index 15dc1d3..dbe6c4b 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.4.1 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.4.1.war[https://www.gerritcodereview.com/download/gerrit-2.4.1.war]
 
 
 There are no schema changes from 2.4.  However, if upgrading from
diff --git a/ReleaseNotes/ReleaseNotes-2.4.2.txt b/ReleaseNotes/ReleaseNotes-2.4.2.txt
index 9ae9fd7..5652d15 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.4.2 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.4.2.war[https://www.gerritcodereview.com/download/gerrit-2.4.2.war]
 
 There are no schema changes from 2.4, or 2.4.1.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.4.3.txt b/ReleaseNotes/ReleaseNotes-2.4.3.txt
index c9c2d2c..6745564 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.3.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.4.2.html[2.4.2].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.4.3.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.4.3.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.4.3.war[https://www.gerritcodereview.com/download/gerrit-2.4.3.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.4.4.txt b/ReleaseNotes/ReleaseNotes-2.4.4.txt
index de9e2cb..5570271 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.4.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.4.4.html[2.4.4].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.4.4.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.4.4.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.4.4.war[https://www.gerritcodereview.com/download/gerrit-2.4.4.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
index 6f10c0b..0e11550 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.4 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.4.war[https://www.gerritcodereview.com/download/gerrit-2.4.war]
 
 Schema Change
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.5.1.txt b/ReleaseNotes/ReleaseNotes-2.5.1.txt
index 6fc0dc5..6e6a481 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.1.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.5.1 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-full-2.5.1.war[https://www.gerritcodereview.com/download/gerrit-full-2.5.1.war]
 
 There are no schema changes from 2.5, or 2.5.1.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
index f87328e..cc436ac 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.5.2 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-full-2.5.2.war[https://www.gerritcodereview.com/download/gerrit-full-2.5.2.war]
 
 There are no schema changes from 2.5, or 2.5.1.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.5.3.txt b/ReleaseNotes/ReleaseNotes-2.5.3.txt
index 60efa7a..8e9db0c 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.3.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.5.3 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.5.3.war[https://www.gerritcodereview.com/download/gerrit-2.5.3.war]
 
 There are no schema changes from any of the 2.5.x versions.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.5.4.txt b/ReleaseNotes/ReleaseNotes-2.5.4.txt
index 1657d9b..4d51528 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.4.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.5.4 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.4.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.4.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.5.4.war[https://www.gerritcodereview.com/download/gerrit-2.5.4.war]
 
 There are no schema changes from any of the 2.5.x versions.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.5.5.txt b/ReleaseNotes/ReleaseNotes-2.5.5.txt
index 57b6d24..146fd40 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.5.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.5.4.html[2.5.4].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.5.5.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.5.5.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.5.5.war[https://www.gerritcodereview.com/download/gerrit-2.5.5.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.5.6.txt b/ReleaseNotes/ReleaseNotes-2.5.6.txt
index 2e9e888..b1e88f9 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.6.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.5.6.html[2.5.6].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.5.6.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.5.6.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.5.6.war[https://www.gerritcodereview.com/download/gerrit-2.5.6.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index 4abed47..8519ee9 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.5 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.war]
+link:https://www.gerritcodereview.com/download/gerrit-full-2.5.war[https://www.gerritcodereview.com/download/gerrit-full-2.5.war]
 
 Gerrit 2.5 includes the bug fixes done with
 link:ReleaseNotes-2.4.1.html[Gerrit 2.4.1] and
diff --git a/ReleaseNotes/ReleaseNotes-2.6.1.txt b/ReleaseNotes/ReleaseNotes-2.6.1.txt
index e163b43d..e43b077 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.1.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.6.html[2.6].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.6.1.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.6.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.6.1.war[https://www.gerritcodereview.com/download/gerrit-2.6.1.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index a000a62..dfd5d80 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -3,7 +3,7 @@
 
 Gerrit 2.6 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.6.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.6.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.6.war[https://www.gerritcodereview.com/download/gerrit-2.6.war]
 
 Gerrit 2.6 includes the bug fixes done with
 link:ReleaseNotes-2.5.1.html[Gerrit 2.5.1],
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
index 5133c04..9782b08 100644
--- a/ReleaseNotes/ReleaseNotes-2.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -4,8 +4,8 @@
 
 Gerrit 2.7 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.7.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.7.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.7.war[
+https://www.gerritcodereview.com/download/gerrit-2.7.war]
 
 Gerrit 2.7 includes the bug fixes done with link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1].
 These bug fixes are *not* listed in these release notes.
diff --git a/ReleaseNotes/ReleaseNotes-2.8.1.txt b/ReleaseNotes/ReleaseNotes-2.8.1.txt
index b155f80..26414a1 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.1.txt
@@ -3,7 +3,7 @@
 
 There are no schema changes from link:ReleaseNotes-2.8.html[2.8].
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.1.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.8.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.1.war[https://www.gerritcodereview.com/download/gerrit-2.8.1.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.8.2.txt b/ReleaseNotes/ReleaseNotes-2.8.2.txt
index e963ff4..926db02 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.2.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.8.1.html[2.8.1].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.2.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.2.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.2.war]
 
 
 Lucene Index
diff --git a/ReleaseNotes/ReleaseNotes-2.8.3.txt b/ReleaseNotes/ReleaseNotes-2.8.3.txt
index 6cf66c8..2bd4aa7 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.3.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.8.2.html[2.8.2].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.3.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.3.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.3.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.3.war]
 
 
 Bug Fixes
diff --git a/ReleaseNotes/ReleaseNotes-2.8.4.txt b/ReleaseNotes/ReleaseNotes-2.8.4.txt
index 1719294..b80ac17 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.4.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.8.3.html[2.8.3].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.4.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.4.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.4.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.4.war]
 
 
 Bug Fixes
diff --git a/ReleaseNotes/ReleaseNotes-2.8.5.txt b/ReleaseNotes/ReleaseNotes-2.8.5.txt
index 4785642..db18083 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.5.txt
@@ -2,8 +2,8 @@
 ==============================
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.5.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.5.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.5.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.5.war]
 
 Schema Changes and Upgrades
 ---------------------------
diff --git a/ReleaseNotes/ReleaseNotes-2.8.6.1.txt b/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
index 2783ec6..d1ed9e9 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.6.1.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.8.6.html[2.8.6].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.6.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.6.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.6.1.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.6.1.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.8.6.txt b/ReleaseNotes/ReleaseNotes-2.8.6.txt
index 3a132e4..ab79a20 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.6.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.8.5.html[2.8.5].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.6.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.6.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.6.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.6.war]
 
 *Warning*: Support for MySQL's MyISAM storage engine is discontinued.
 Only transactional storage engines are supported.
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index 92cdda2..2d1dc7a 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -4,8 +4,8 @@
 
 Gerrit 2.8 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.8.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.8.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.8.war[
+https://www.gerritcodereview.com/download/gerrit-2.8.war]
 
 
 Schema Change
diff --git a/ReleaseNotes/ReleaseNotes-2.9.1.txt b/ReleaseNotes/ReleaseNotes-2.9.1.txt
index 656b5b2..3377df4 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.1.txt
@@ -4,8 +4,8 @@
 There are no schema changes from link:ReleaseNotes-2.9.html[2.9].
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.9.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.9.1.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.9.1.war[
+https://www.gerritcodereview.com/download/gerrit-2.9.1.war]
 
 *WARNING:* When upgrading from version 2.8.4 or older with a site that uses
 Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
diff --git a/ReleaseNotes/ReleaseNotes-2.9.2.txt b/ReleaseNotes/ReleaseNotes-2.9.2.txt
index a586b45..4e5de01 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.2.txt
@@ -2,8 +2,8 @@
 ==============================
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.9.2.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.9.2.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.9.2.war[
+https://www.gerritcodereview.com/download/gerrit-2.9.2.war]
 
 Important Notes
 ---------------
diff --git a/ReleaseNotes/ReleaseNotes-2.9.3.txt b/ReleaseNotes/ReleaseNotes-2.9.3.txt
index f3fcf16..e6c8573 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.3.txt
@@ -2,8 +2,8 @@
 ==============================
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.9.3.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.9.3.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.9.3.war[
+https://www.gerritcodereview.com/download/gerrit-2.9.3.war]
 
 Important Notes
 ---------------
diff --git a/ReleaseNotes/ReleaseNotes-2.9.4.txt b/ReleaseNotes/ReleaseNotes-2.9.4.txt
index 0a7010d..5063489 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.4.txt
@@ -2,8 +2,8 @@
 ==============================
 
 Download:
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.9.4.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.9.4.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.9.4.war[
+https://www.gerritcodereview.com/download/gerrit-2.9.4.war]
 
 Important Notes
 ---------------
diff --git a/ReleaseNotes/ReleaseNotes-2.9.txt b/ReleaseNotes/ReleaseNotes-2.9.txt
index de5c665..3387f98 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.txt
@@ -4,8 +4,8 @@
 
 Gerrit 2.9 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.9.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.9.war]
+link:https://www.gerritcodereview.com/download/gerrit-2.9.war[
+https://www.gerritcodereview.com/download/gerrit-2.9.war]
 
 *WARNING:* Support for Java 1.6 has been discontinued.
 As of Gerrit 2.9, Java 1.7 is required.
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index c272a25..09e9b73 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,16 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_12]]
+Version 2.12.x
+--------------
+* link:ReleaseNotes-2.12.5.html[2.12.5]
+* link:ReleaseNotes-2.12.4.html[2.12.4]
+* link:ReleaseNotes-2.12.3.html[2.12.3]
+* link:ReleaseNotes-2.12.2.html[2.12.2]
+* link:ReleaseNotes-2.12.1.html[2.12.1]
+* link:ReleaseNotes-2.12.html[2.12]
+
 [[2_11]]
 Version 2.11.x
 --------------
@@ -154,4 +164,4 @@
 
 GERRIT
 ------
-Part of link:http://code.google.com/p/gerrit/[Gerrit Code Review]
+Part of link:https://www.gerritcodereview.com/[Gerrit Code Review]
diff --git a/VERSION b/VERSION
index a7d4603..db4fdc4 100644
--- a/VERSION
+++ b/VERSION
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = '2.11.11'
+GERRIT_VERSION = '2.12.9-SNAPSHOT'
diff --git a/bucklets/gerrit_plugin.bucklet b/bucklets/gerrit_plugin.bucklet
index ae7e1a2..367fe71 100644
--- a/bucklets/gerrit_plugin.bucklet
+++ b/bucklets/gerrit_plugin.bucklet
@@ -14,7 +14,8 @@
 # When compiling from standalone cookbook-plugin, bucklets directory points
 # to cloned bucklets library that includes real gerrit_plugin.bucklet code.
 
+GERRIT_GWT_API = ['//gerrit-plugin-gwtui:gwtui-api']
 GERRIT_PLUGIN_API = ['//gerrit-plugin-api:lib']
-GERRIT_GWT_API = ['//gerrit-plugin-gwtui/gerrit:gwtui-api']
+GERRIT_TESTS = ['//gerrit-acceptance-framework:lib']
 
 STANDALONE_MODE = False
diff --git a/bucklets/local_jar.bucklet b/bucklets/local_jar.bucklet
deleted file mode 120000
index 8904824..0000000
--- a/bucklets/local_jar.bucklet
+++ /dev/null
@@ -1 +0,0 @@
-../lib/local.defs
\ No newline at end of file
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
new file mode 100755
index 0000000..32edf84
--- /dev/null
+++ b/contrib/abandon_stale.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# The MIT License
+#
+# Copyright 2014 Sony Mobile Communications. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Script to abandon stale changes from the review server.
+
+Fetches a list of open changes that have not been updated since a
+given age in months or years (default 6 months), and then abandons them.
+
+Assumes that the user's credentials are in the .netrc file.  Supports
+either basic or digest authentication.
+
+Example to abandon changes that have not been updated for 3 months:
+
+  ./abandon_stale --gerrit-url http://review.example.com/ --age 3months
+
+Supports dry-run mode to only list the stale changes but not actually
+abandon them.
+
+Requires pygerrit (https://github.com/sonyxperiadev/pygerrit).
+
+"""
+
+import logging
+import optparse
+import re
+import sys
+
+from pygerrit.rest import GerritRestAPI
+from pygerrit.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
+
+
+def _main():
+    parser = optparse.OptionParser()
+    parser.add_option('-g', '--gerrit-url', dest='gerrit_url',
+                      metavar='URL',
+                      default=None,
+                      help='gerrit server URL')
+    parser.add_option('-b', '--basic-auth', dest='basic_auth',
+                      action='store_true',
+                      help='use HTTP basic authentication instead of digest')
+    parser.add_option('-n', '--dry-run', dest='dry_run',
+                      action='store_true',
+                      help='enable dry-run mode: show stale changes but do '
+                           'not abandon them')
+    parser.add_option('-a', '--age', dest='age',
+                      metavar='AGE',
+                      default="6months",
+                      help='age of change since last update '
+                           '(default: %default)')
+    parser.add_option('-m', '--message', dest='message',
+                      metavar='STRING', default=None,
+                      help='Custom message to append to abandon message')
+    parser.add_option('--exclude-branch', dest='exclude_branches',
+                      metavar='BRANCH_NAME',
+                      default=[],
+                      action='append',
+                      help='Do not abandon changes on given branch')
+    parser.add_option('--exclude-project', dest='exclude_projects',
+                      metavar='PROJECT_NAME',
+                      default=[],
+                      action='append',
+                      help='Do not abandon changes on given project')
+    parser.add_option('--owner', dest='owner',
+                      metavar='USERNAME',
+                      default=None,
+                      action='store',
+                      help='Only abandon changes owned by the given user')
+    parser.add_option('-v', '--verbose', dest='verbose',
+                      action='store_true',
+                      help='enable verbose (debug) logging')
+
+    (options, _args) = parser.parse_args()
+
+    level = logging.DEBUG if options.verbose else logging.INFO
+    logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
+                        level=level)
+
+    if not options.gerrit_url:
+        logging.error("Gerrit URL is required")
+        return 1
+
+    pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
+    match = pattern.match(options.age)
+    if not match:
+        logging.error("Invalid age: %s", options.age)
+        return 1
+    message = "Abandoning after %s %s or more of inactivity." % \
+        (match.group(1), match.group(2))
+
+    if options.basic_auth:
+        auth_type = HTTPBasicAuthFromNetrc
+    else:
+        auth_type = HTTPDigestAuthFromNetrc
+
+    try:
+        auth = auth_type(url=options.gerrit_url)
+        gerrit = GerritRestAPI(url=options.gerrit_url, auth=auth)
+    except Exception as e:
+        logging.error(e)
+        return 1
+
+    logging.info(message)
+    try:
+        stale_changes = []
+        offset = 0
+        step = 500
+        query_terms = ["status:new", "age:%s" % options.age] + \
+                      ["-branch:%s" % b for b in options.exclude_branches] + \
+                      ["-project:%s" % p for p in options.exclude_projects]
+        if options.owner:
+            query_terms += ["owner:%s" % options.owner]
+        query = "%20".join(query_terms)
+        while True:
+            q = query + "&n=%d&S=%d" % (step, offset)
+            logging.debug("Query: %s", q)
+            url = "/changes/?q=" + q
+            result = gerrit.get(url)
+            logging.debug("%d changes", len(result))
+            if not result:
+                break
+            stale_changes += result
+            last = result[-1]
+            if "_more_changes" in last:
+                logging.debug("More...")
+                offset += step
+            else:
+                break
+    except Exception as e:
+        logging.error(e)
+        return 1
+
+    abandoned = 0
+    errors = 0
+    abandon_message = message
+    if options.message:
+        abandon_message += "\n\n" + options.message
+    for change in stale_changes:
+        number = change["_number"]
+        try:
+            owner = change["owner"]["name"]
+        except:
+            owner = "Unknown"
+        subject = change["subject"]
+        if len(subject) > 70:
+            subject = subject[:65] + " [...]"
+        change_id = change["id"]
+        logging.info("%s (%s): %s", number, owner, subject)
+        if options.dry_run:
+            continue
+
+        try:
+            gerrit.post("/changes/" + change_id + "/abandon",
+                        data='{"message" : "%s"}' % abandon_message)
+            abandoned += 1
+        except Exception as e:
+            errors += 1
+            logging.error(e)
+    logging.info("Total %d stale open changes", len(stale_changes))
+    if not options.dry_run:
+        logging.info("Abandoned %d changes. %d errors.", abandoned, errors)
+
+if __name__ == "__main__":
+    sys.exit(_main())
diff --git a/contrib/convertkey/BUCK b/contrib/convertkey/BUCK
new file mode 100644
index 0000000..40ad9c4
--- /dev/null
+++ b/contrib/convertkey/BUCK
@@ -0,0 +1,50 @@
+include_defs('//lib/maven.defs')
+
+genrule(
+  name = 'bcprov__unsign',
+  cmd = ' && '.join([
+    'unzip -qd $TMP $(location //lib/bouncycastle:bcprov)',
+    'cd $TMP',
+    'zip -Drq $OUT . -x META-INF/\*.RSA META-INF/\*.DSA META-INF/\*.SF META-INF/\*.LIST',
+  ]),
+  out = 'bcprov-unsigned.jar',
+)
+
+prebuilt_jar(
+  name = 'bcprov',
+  binary_jar = ':bcprov__unsign',
+)
+
+genrule(
+  name = 'bcpkix__unsign',
+  cmd = ' && '.join([
+    'unzip -qd $TMP $(location //lib/bouncycastle:bcpkix)',
+    'cd $TMP',
+    'zip -Drq $OUT . -x META-INF/\*.RSA META-INF/\*.DSA META-INF/\*.SF META-INF/\*.LIST',
+  ]),
+  out = 'bcpkix-unsigned.jar',
+)
+
+prebuilt_jar(
+  name = 'bcpkix',
+  binary_jar = ':bcpkix__unsign',
+)
+
+java_library(
+  name = 'convertkey__lib',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    ':bcprov',
+    ':bcpkix',
+    '//lib:jsch',
+    '//lib/log:nop',
+    '//lib/mina:sshd',
+  ],
+)
+
+java_binary(
+  name = 'convertkey',
+  deps = [':convertkey__lib'],
+  main_class = 'com.googlesource.gerrit.convertkey.ConvertKey',
+)
+
diff --git a/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java b/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java
new file mode 100644
index 0000000..5c6ef58
--- /dev/null
+++ b/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2015 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.googlesource.gerrit.convertkey;
+
+import com.jcraft.jsch.HostKey;
+import com.jcraft.jsch.JSchException;
+
+import org.apache.sshd.common.util.Buffer;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.security.KeyPair;
+import java.security.GeneralSecurityException;
+
+public class ConvertKey {
+  public static void main(String[] args)
+      throws GeneralSecurityException, JSchException, IOException {
+    SimpleGeneratorHostKeyProvider p;
+
+    if (args.length != 1) {
+      System.err.println("Error: requires path to the SSH host key");
+      return;
+    } else {
+      File file = new File(args[0]);
+      if (!file.exists() || !file.isFile() || !file.canRead()) {
+        System.err.println("Error: ssh key should exist and be readable");
+        return;
+      }
+    }
+
+    p = new SimpleGeneratorHostKeyProvider();
+    // Gerrit's SSH "simple" keys are always RSA.
+    p.setPath(args[0]);
+    p.setAlgorithm("RSA");
+    Iterable<KeyPair> keys = p.loadKeys(); // forces the key to generate.
+    for (KeyPair k : keys) {
+      System.out.println("Public Key (" + k.getPublic().getAlgorithm() + "):");
+      // From Gerrit's SshDaemon class; use JSch to get the public
+      // key/type
+      final Buffer buf = new Buffer();
+      buf.putRawPublicKey(k.getPublic());
+      final byte[] keyBin = buf.getCompactData();
+      HostKey pub = new HostKey("localhost", keyBin);
+      System.out.println(pub.getType() + " " + pub.getKey());
+      System.out.println("Private Key:");
+      // Use Bouncy Castle to write the private key back in PEM format
+      // (PKCS#1)
+      // http://stackoverflow.com/questions/25129822/export-rsa-public-key-to-pem-string-using-java
+      StringWriter privout = new StringWriter();
+      JcaPEMWriter privWriter = new JcaPEMWriter(privout);
+      privWriter.writeObject(k.getPrivate());
+      privWriter.close();
+      System.out.println(privout);
+    }
+  }
+
+}
diff --git a/contrib/git-push-review b/contrib/git-push-review
index 898b023..e77785a 100755
--- a/contrib/git-push-review
+++ b/contrib/git-push-review
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2014 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
new file mode 100644
index 0000000..d8f0276
--- /dev/null
+++ b/gerrit-acceptance-framework/BUCK
@@ -0,0 +1,87 @@
+SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java'])
+
+DEPS = [
+  '//gerrit-gpg:gpg',
+  '//gerrit-pgm:daemon',
+  '//gerrit-pgm:util-nodep',
+  '//gerrit-server:testutil',
+  '//lib/auto:auto-value',
+  '//lib/httpcomponents:fluent-hc',
+  '//lib/httpcomponents:httpclient',
+  '//lib/httpcomponents:httpcore',
+  '//lib/jgit:junit',
+  '//lib/log:impl_log4j',
+  '//lib/log:log4j',
+]
+
+PROVIDED = [
+  '//gerrit-common:annotations',
+  '//gerrit-common:server',
+  '//gerrit-extension-api:api',
+  '//gerrit-httpd:httpd',
+  '//gerrit-lucene:lucene',
+  '//gerrit-pgm:init',
+  '//gerrit-reviewdb:server',
+  '//gerrit-server:server',
+  '//lib:gson',
+  '//lib/jgit:jgit',
+  '//lib:jsch',
+  '//lib/mina:sshd',
+  '//lib:servlet-api-3_1',
+]
+
+java_binary(
+  name = 'acceptance-framework',
+  deps = [':lib'],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'lib',
+  srcs = SRCS,
+  exported_deps = DEPS + [
+    '//lib:truth',
+  ],
+  provided_deps = PROVIDED + [
+    '//lib:gwtorm',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'src',
+  srcs = SRCS,
+  visibility = ['PUBLIC'],
+)
+
+# The above java_sources produces a .jar somewhere in the depths of
+# buck-out, but it does not bring it to
+# buck-out/gen/gerrit-acceptance-framework/gerrit-acceptance-framework-src.jar.
+# We fix that by the following java_binary.
+java_binary(
+  name = 'acceptance-framework-src',
+  deps = [ ':src' ],
+  visibility = ['PUBLIC'],
+)
+
+java_doc(
+  name = 'acceptance-framework-javadoc',
+  title = 'Gerrit Acceptance Test Framework Documentation',
+  pkgs = [' com.google.gerrit.acceptance'],
+  paths = ['src/test/java'],
+  srcs = SRCS,
+  deps = DEPS + PROVIDED + [
+    '//lib:guava',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice_library',
+    '//lib/guice:guice-servlet',
+    '//lib/guice:javax-inject',
+    '//lib:gwtorm_client',
+    '//lib:junit__jar',
+    '//lib:truth__jar',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
new file mode 100644
index 0000000..29f4998
--- /dev/null
+++ b/gerrit-acceptance-framework/pom.xml
@@ -0,0 +1,59 @@
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.google.gerrit</groupId>
+  <artifactId>gerrit-acceptance-framework</artifactId>
+  <version>2.12.9-SNAPSHOT</version>
+  <packaging>jar</packaging>
+  <name>Gerrit Code Review - Acceptance Test Framework</name>
+  <description>API for Gerrit Plugins</description>
+  <url>https://www.gerritcodereview.com/</url>
+
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <scm>
+    <url>https://gerrit.googlesource.com/gerrit</url>
+    <connection>https://gerrit.googlesource.com/gerrit</connection>
+  </scm>
+
+  <developers>
+    <developer>
+      <name>Dave Borowitz</name>
+    </developer>
+    <developer>
+      <name>David Pursehouse</name>
+    </developer>
+    <developer>
+      <name>Edwin Kempin</name>
+    </developer>
+    <developer>
+      <name>Martin Fick</name>
+    </developer>
+    <developer>
+      <name>Saša Živkov</name>
+    </developer>
+    <developer>
+      <name>Shawn Pearce</name>
+    </developer>
+  </developers>
+
+  <mailingLists>
+    <mailingList>
+      <name>Repo and Gerrit Discussion</name>
+      <post>repo-discuss@googlegroups.com</post>
+      <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe>
+      <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe>
+      <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive>
+    </mailingList>
+  </mailingLists>
+
+  <issueManagement>
+    <url>https://bugs.chromium.org/p/gerrit/issues/list</url>
+    <system>Gerrit Issue Tracker</system>
+  </issueManagement>
+</project>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
new file mode 100644
index 0000000..9194371
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -0,0 +1,665 @@
+// Copyright (C) 2013 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.initSsh;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.block;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Chars;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TempFileUtil;
+import com.google.gson.Gson;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.Transport;
+import org.junit.AfterClass;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+@RunWith(ConfigSuite.class)
+public abstract class AbstractDaemonTest {
+  private static GerritServer commonServer;
+
+  @ConfigSuite.Parameter
+  public Config baseConfig;
+
+  @ConfigSuite.Name
+  private String configName;
+
+  @Inject
+  protected AllProjectsName allProjects;
+
+  @Inject
+  protected AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  protected GerritApi gApi;
+
+  @Inject
+  protected AcceptanceTestRequestScope atrScope;
+
+  @Inject
+  protected AccountCache accountCache;
+
+  @Inject
+  private IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  @Inject
+  protected PushOneCommit.Factory pushFactory;
+
+  @Inject
+  protected MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  protected ProjectCache projectCache;
+
+  @Inject
+  protected GroupCache groupCache;
+
+  @Inject
+  protected GitRepositoryManager repoManager;
+
+  @Inject
+  protected ChangeIndexer indexer;
+
+  @Inject
+  protected Provider<InternalChangeQuery> queryProvider;
+
+  @Inject
+  @CanonicalWebUrl
+  protected Provider<String> canonicalWebUrl;
+
+  @Inject
+  @GerritServerConfig
+  protected Config cfg;
+
+  @Inject
+  private InProcessProtocol inProcessProtocol;
+
+  @Inject
+  private Provider<AnonymousUser> anonymousUser;
+
+  @Inject
+  @GerritPersonIdent
+  protected Provider<PersonIdent> serverIdent;
+
+  protected TestRepository<InMemoryRepository> testRepo;
+  protected GerritServer server;
+  protected TestAccount admin;
+  protected TestAccount user;
+  protected RestSession adminSession;
+  protected RestSession userSession;
+  protected SshSession sshSession;
+  protected ReviewDb db;
+  protected Project.NameKey project;
+
+  @Inject
+  protected NotesMigration notesMigration;
+
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
+  private String resourcePrefix;
+  private List<Repository> toClose;
+
+  @Rule
+  public TestRule testRunner = new TestRule() {
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+      return new Statement() {
+        @Override
+        public void evaluate() throws Throwable {
+          beforeTest(description);
+          try {
+            base.evaluate();
+          } finally {
+            afterTest();
+          }
+        }
+      };
+    }
+  };
+
+  @Rule
+  public TemporaryFolder tempSiteDir = new TemporaryFolder();
+
+  @AfterClass
+  public static void stopCommonServer() throws Exception {
+    if (commonServer != null) {
+      commonServer.stop();
+      commonServer = null;
+    }
+    TempFileUtil.cleanup();
+  }
+
+  protected static Config submitWholeTopicEnabledConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "submitWholeTopic", true);
+    return cfg;
+  }
+
+  protected static Config allowDraftsDisabledConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowDrafts", false);
+    return cfg;
+  }
+
+  protected boolean isAllowDrafts() {
+    return cfg.getBoolean("change", "allowDrafts", true);
+  }
+
+  protected boolean isSubmitWholeTopicEnabled() {
+    return cfg.getBoolean("change", null, "submitWholeTopic", false);
+  }
+
+  private static boolean isNoteDbTestEnabled() {
+    final String[] RUN_FLAGS = {"yes", "y", "true"};
+    String value = System.getenv("GERRIT_ENABLE_NOTEDB");
+    return value != null &&
+        Arrays.asList(RUN_FLAGS).contains(value.toLowerCase());
+  }
+
+  protected void beforeTest(Description description) throws Exception {
+    GerritServer.Description classDesc =
+      GerritServer.Description.forTestClass(description, configName);
+    GerritServer.Description methodDesc =
+      GerritServer.Description.forTestMethod(description, configName);
+
+    if (isNoteDbTestEnabled()) {
+      NotesMigration.setAllEnabledConfig(baseConfig);
+    }
+    baseConfig.setString("gerrit", null, "tempSiteDir",
+        tempSiteDir.getRoot().getPath());
+    if (classDesc.equals(methodDesc) && !classDesc.sandboxed() &&
+        !methodDesc.sandboxed()) {
+      if (commonServer == null) {
+        commonServer = GerritServer.start(classDesc, baseConfig);
+      }
+      server = commonServer;
+    } else {
+      server = GerritServer.start(methodDesc, baseConfig);
+    }
+
+    server.getTestInjector().injectMembers(this);
+    Transport.register(inProcessProtocol);
+    toClose = Collections.synchronizedList(new ArrayList<Repository>());
+    admin = accounts.admin();
+    user = accounts.user();
+
+    // Evict cached user state in case tests modify it.
+    accountCache.evict(admin.getId());
+    accountCache.evict(user.getId());
+
+    adminSession = new RestSession(server, admin);
+    userSession = new RestSession(server, user);
+    initSsh(admin);
+    db = reviewDbProvider.open();
+    Context ctx = newRequestContext(admin);
+    atrScope.set(ctx);
+    sshSession = ctx.getSession();
+    sshSession.open();
+    resourcePrefix = UNSAFE_PROJECT_NAME.matcher(
+        description.getClassName() + "_"
+        + description.getMethodName() + "_").replaceAll("");
+
+    project = createProject(projectInput(description));
+    testRepo = cloneProject(project, getCloneAsAccount(description));
+  }
+
+  private TestAccount getCloneAsAccount(Description description) {
+    TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
+    return accounts.get(ann != null ? ann.cloneAs() : "admin");
+  }
+
+  private ProjectInput projectInput(Description description) {
+    ProjectInput in = new ProjectInput();
+    TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
+    in.name = name("project");
+    if (ann != null) {
+      in.parent = Strings.emptyToNull(ann.parent());
+      in.description = Strings.emptyToNull(ann.description());
+      in.createEmptyCommit = ann.createEmptyCommit();
+      in.submitType = ann.submitType();
+      in.useContentMerge = ann.useContributorAgreements();
+      in.useSignedOffBy = ann.useSignedOffBy();
+      in.useContentMerge = ann.useContentMerge();
+    } else {
+      // Defaults should match TestProjectConfig, omitting nullable values.
+      in.createEmptyCommit = true;
+    }
+    updateProjectInput(in);
+    return in;
+  }
+
+  private static final Pattern UNSAFE_PROJECT_NAME =
+      Pattern.compile("[^a-zA-Z0-9._/-]+");
+
+  protected Git git() {
+    return testRepo.git();
+  }
+
+  protected InMemoryRepository repo() {
+    return testRepo.getRepository();
+  }
+
+  /**
+   * Return a resource name scoped to this test method.
+   * <p>
+   * Test methods in a single class by default share a running server. For any
+   * resource name you require to be unique to a test method, wrap it in a call
+   * to this method.
+   *
+   * @param name resource name (group, project, topic, etc.)
+   * @return name prefixed by a string unique to this test method.
+   */
+  protected String name(String name) {
+    return resourcePrefix + name;
+  }
+
+  protected Project.NameKey createProject(String nameSuffix)
+      throws RestApiException {
+    return createProject(nameSuffix, null);
+  }
+
+  protected Project.NameKey createProject(String nameSuffix,
+      Project.NameKey parent) throws RestApiException {
+    // Default for createEmptyCommit should match TestProjectConfig.
+    return createProject(nameSuffix, parent, true);
+  }
+
+  protected Project.NameKey createProject(String nameSuffix,
+      Project.NameKey parent, boolean createEmptyCommit)
+      throws RestApiException {
+    return createProject(
+        nameSuffix, parent, createEmptyCommit, SubmitType.MERGE_IF_NECESSARY);
+  }
+
+  protected Project.NameKey createProject(String nameSuffix,
+      Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
+      throws RestApiException {
+    ProjectInput in = new ProjectInput();
+    in.name = name(nameSuffix);
+    in.parent = parent != null ? parent.get() : null;
+    in.createEmptyCommit = createEmptyCommit;
+    in.submitType = submitType;
+    return createProject(in);
+  }
+
+  private Project.NameKey createProject(ProjectInput in)
+      throws RestApiException {
+    gApi.projects().create(in);
+    return new Project.NameKey(in.name);
+  }
+
+  /**
+   * Modify a project input before creating the initial test project.
+   *
+   * @param in input; may be modified in place.
+   */
+  protected void updateProjectInput(ProjectInput in) {
+    // Default implementation does nothing.
+  }
+
+  protected TestRepository<InMemoryRepository> cloneProject(Project.NameKey p)
+      throws Exception {
+    return cloneProject(p, admin);
+  }
+
+  protected TestRepository<InMemoryRepository> cloneProject(Project.NameKey p,
+      TestAccount testAccount) throws Exception {
+    InProcessProtocol.Context ctx = new InProcessProtocol.Context(
+        reviewDbProvider, identifiedUserFactory, testAccount.getId(), p);
+    Repository repo = repoManager.openRepository(p);
+    toClose.add(repo);
+    return GitUtil.cloneProject(
+        p, inProcessProtocol.register(ctx, repo).toString());
+  }
+
+  private void afterTest() throws Exception {
+    Transport.unregister(inProcessProtocol);
+    for (Repository repo : toClose) {
+      repo.close();
+    }
+    db.close();
+    sshSession.close();
+    if (server != commonServer) {
+      server.stop();
+    }
+  }
+
+  protected TestRepository<?>.CommitBuilder commitBuilder() throws Exception {
+    return testRepo.branch("HEAD").commit().insertChangeId();
+  }
+
+  protected TestRepository<?>.CommitBuilder amendBuilder() throws Exception {
+    ObjectId head = repo().exactRef("HEAD").getObjectId();
+    TestRepository<?>.CommitBuilder b = testRepo.amendRef("HEAD");
+    Optional<String> id = GitUtil.getChangeId(testRepo, head);
+    // TestRepository behaves like "git commit --amend -m foo", which does not
+    // preserve an existing Change-Id. Tests probably want this.
+    if (id.isPresent()) {
+      b.insertChangeId(id.get().substring(1));
+    } else {
+      b.insertChangeId();
+    }
+    return b;
+  }
+
+  protected PushOneCommit.Result createChange() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+    return result;
+  }
+
+  protected BranchApi createBranchWithRevision(Branch.NameKey branch,
+      String revision) throws Exception {
+    BranchInput in = new BranchInput();
+    in.revision = revision;
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get())
+        .create(in);
+  }
+
+  private static final List<Character> RANDOM =
+      Chars.asList(new char[]{'a','b','c','d','e','f','g','h'});
+  protected PushOneCommit.Result amendChange(String changeId)
+      throws Exception {
+    return amendChange(changeId, "refs/for/master");
+  }
+
+  protected PushOneCommit.Result amendChange(String changeId, String ref)
+      throws Exception {
+    Collections.shuffle(RANDOM);
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
+    return push.to(ref);
+  }
+
+  protected PushOneCommit.Result amendChangeAsDraft(String changeId)
+      throws Exception {
+    return amendChange(changeId, "refs/drafts/master");
+  }
+
+  protected ChangeInfo info(String id)
+      throws RestApiException {
+    return gApi.changes().id(id).info();
+  }
+
+  protected ChangeInfo get(String id)
+      throws RestApiException {
+    return gApi.changes().id(id).get();
+  }
+
+  protected EditInfo getEdit(String id)
+      throws RestApiException {
+    return gApi.changes().id(id).getEdit();
+  }
+
+  protected ChangeInfo get(String id, ListChangesOption... options)
+      throws RestApiException {
+    return gApi.changes().id(id).get(
+        Sets.newEnumSet(Arrays.asList(options), ListChangesOption.class));
+  }
+
+  protected List<ChangeInfo> query(String q) throws RestApiException {
+    return gApi.changes().query(q).get();
+  }
+
+  private Context newRequestContext(TestAccount account) {
+    return atrScope.newContext(reviewDbProvider, new SshSession(server, admin),
+        identifiedUserFactory.create(Providers.of(db), account.getId()));
+  }
+
+  protected Context setApiUser(TestAccount account) {
+    return atrScope.set(newRequestContext(account));
+  }
+
+  protected Context setApiUserAnonymous() {
+    return atrScope.newContext(reviewDbProvider, null, anonymousUser.get());
+  }
+
+  protected static Gson newGson() {
+    return OutputFormat.JSON_COMPACT.newGson();
+  }
+
+  protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
+    return gApi.changes()
+        .id(r.getChangeId())
+        .current();
+  }
+
+  protected void allow(String permission, AccountGroup.UUID id, String ref)
+      throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg, permission, id, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected void allowGlobalCapabilities(AccountGroup.UUID id,
+      String... capabilityNames) throws Exception {
+    allowGlobalCapabilities(id, Arrays.asList(capabilityNames));
+  }
+
+  protected void allowGlobalCapabilities(AccountGroup.UUID id,
+      Iterable<String> capabilityNames) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    for (String capabilityName : capabilityNames) {
+      Util.allow(cfg, capabilityName, id);
+    }
+    saveProjectConfig(allProjects, cfg);
+  }
+
+  protected void removeGlobalCapabilities(AccountGroup.UUID id,
+      String... capabilityNames) throws Exception {
+    removeGlobalCapabilities(id, Arrays.asList(capabilityNames));
+  }
+
+  protected void removeGlobalCapabilities(AccountGroup.UUID id,
+      Iterable<String> capabilityNames) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    for (String capabilityName : capabilityNames) {
+      Util.remove(cfg, capabilityName, id);
+    }
+    saveProjectConfig(allProjects, cfg);
+  }
+
+  protected void setUseContributorAgreements(InheritableBoolean value)
+      throws Exception {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    ProjectConfig config = ProjectConfig.read(md);
+    config.getProject().setUseContributorAgreements(value);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  protected void setUseSignedOffBy(InheritableBoolean value)
+      throws Exception {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    ProjectConfig config = ProjectConfig.read(md);
+    config.getProject().setUseSignedOffBy(value);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  protected void deny(String permission, AccountGroup.UUID id, String ref)
+      throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.deny(cfg, permission, id, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
+      throws Exception {
+    MetaDataUpdate md = metaDataUpdateFactory.create(p);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
+    projectCache.evict(cfg.getProject());
+  }
+
+  protected void grant(String permission, Project.NameKey project, String ref)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    grant(permission, project, ref, false);
+  }
+
+  protected void grant(String permission, Project.NameKey project, String ref,
+      boolean force) throws RepositoryNotFoundException, IOException,
+      ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    md.setMessage(String.format("Grant %s on %s", permission, ref));
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection(ref, true);
+    Permission p = s.getPermission(permission, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+    rule.setForce(force);
+    p.add(rule);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  protected void blockRead(Project.NameKey project, String ref) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    block(cfg, Permission.READ, REGISTERED_USERS, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected void blockForgeCommitter(Project.NameKey project, String ref)
+      throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected PushOneCommit.Result pushTo(String ref) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    return push.to(ref);
+  }
+
+  protected void approve(String id) throws Exception {
+    gApi.changes()
+      .id(id)
+      .revision("current")
+      .review(ReviewInput.approve());
+  }
+
+  protected Map<String, ActionInfo> getActions(String id) throws Exception {
+    return gApi.changes()
+      .id(id)
+      .revision(1)
+      .actions();
+  }
+
+  protected void assertSubmittedTogether(String chId, String... expected)
+      throws Exception {
+    List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
+    assertThat(actual).hasSize(expected.length);
+    assertThat(Iterables.transform(actual,
+        new Function<ChangeInfo, String>() {
+      @Override
+      public String apply(ChangeInfo input) {
+        return input.changeId;
+      }
+    })).containsExactly((Object[])expected).inOrder();
+  }
+
+  protected TestRepository<?> createProjectWithPush(String name,
+      @Nullable Project.NameKey parent,
+      SubmitType submitType) throws Exception {
+    Project.NameKey project = createProject(name, parent, true, submitType);
+    grant(Permission.PUSH, project, "refs/heads/*");
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
+    return cloneProject(project);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
similarity index 89%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 2a578c2..34379a1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
@@ -41,7 +42,7 @@
   private static final Key<RequestScopedReviewDbProvider> DB_KEY =
       Key.get(RequestScopedReviewDbProvider.class);
 
-  public class Context implements RequestContext {
+  public static class Context implements RequestContext {
     private final RequestCleanup cleanup = new RequestCleanup();
     private final Map<Key<?>, Object> map = Maps.newHashMap();
     private final SchemaFactory<ReviewDb> schemaFactory;
@@ -75,7 +76,7 @@
     }
 
     @Override
-    public CurrentUser getCurrentUser() {
+    public CurrentUser getUser() {
       if (user == null) {
         throw new IllegalStateException("user == null, forgot to set it?");
       }
@@ -152,7 +153,7 @@
   }
 
   private Context newContinuingContext(Context ctx) {
-    return new Context(ctx, ctx.getSession(), ctx.getCurrentUser());
+    return new Context(ctx, ctx.getSession(), ctx.getUser());
   }
 
   public Context set(Context ctx) {
@@ -162,6 +163,25 @@
     return old;
   }
 
+  public Context get() {
+    return current.get();
+  }
+
+  public Context disableDb() {
+    Context old = current.get();
+    SchemaFactory<ReviewDb> sf = new SchemaFactory<ReviewDb>() {
+      @Override
+      public ReviewDb open() {
+        return new DisabledReviewDb();
+      }
+    };
+    Context ctx = new Context(sf, old.session, old.user, old.created);
+
+    current.set(ctx);
+    local.setContext(ctx);
+    return old;
+  }
+
   /** Returns exactly one instance per command executed. */
   static final Scope REQUEST = new Scope() {
     @Override
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
similarity index 86%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index a376332..9eaf266 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -28,6 +30,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
@@ -36,8 +39,12 @@
 import java.io.ByteArrayOutputStream;
 import java.io.UnsupportedEncodingException;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
+@Singleton
 public class AccountCreator {
+  private final Map<String, TestAccount> accounts;
 
   private SchemaFactory<ReviewDb> reviewDbProvider;
   private GroupCache groupCache;
@@ -49,6 +56,7 @@
   AccountCreator(SchemaFactory<ReviewDb> schema, GroupCache groupCache,
       SshKeyCache sshKeyCache, AccountCache accountCache,
       AccountByEmailCache byEmailCache) {
+    accounts = new HashMap<>();
     reviewDbProvider = schema;
     this.groupCache = groupCache;
     this.sshKeyCache = sshKeyCache;
@@ -56,11 +64,14 @@
     this.byEmailCache = byEmailCache;
   }
 
-  public TestAccount create(String username, String email, String fullName,
-      String... groups)
+  public synchronized TestAccount create(String username, String email,
+      String fullName, String... groups)
       throws OrmException, UnsupportedEncodingException, JSchException {
-    ReviewDb db = reviewDbProvider.open();
-    try {
+    TestAccount account = accounts.get(username);
+    if (account != null) {
+      return account;
+    }
+    try (ReviewDb db = reviewDbProvider.open()) {
       Account.Id id = new Account.Id(db.nextAccountId());
       KeyPair sshKey = genSshKey();
       AccountSshKey key =
@@ -99,9 +110,10 @@
       accountCache.evictByUsername(username);
       byEmailCache.evict(email);
 
-      return new TestAccount(id, username, email, fullName, sshKey, httpPass);
-    } finally {
-      db.close();
+      account =
+          new TestAccount(id, username, email, fullName, sshKey, httpPass);
+      accounts.put(username, account);
+      return account;
     }
   }
 
@@ -137,6 +149,12 @@
     return create("user2", "user2@example.com", "User2");
   }
 
+  public TestAccount get(String username) {
+    return checkNotNull(
+        accounts.get(username),
+        "No TestAccount created for %s", username);
+  }
+
   private AccountExternalId.Key getEmailKey(String email) {
     return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
similarity index 75%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 07d0f50..b07ed30 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.acceptance;
 
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 class ConfigAnnotationParser {
   private static Splitter splitter = Splitter.on(".").trimResults();
@@ -45,9 +47,19 @@
   private static void parseAnnotation(Config cfg, GerritConfig c) {
     ArrayList<String> l = Lists.newArrayList(splitter.split(c.name()));
     if (l.size() == 2) {
-      cfg.setString(l.get(0), null, l.get(1), c.value());
+      if (!Strings.isNullOrEmpty(c.value())) {
+        cfg.setString(l.get(0), null, l.get(1), c.value());
+      } else {
+        String[] values = c.values();
+        cfg.setStringList(l.get(0), null, l.get(1), Arrays.asList(values));
+      }
     } else if (l.size() == 3) {
-      cfg.setString(l.get(0), l.get(1), l.get(2), c.value());
+      if (!Strings.isNullOrEmpty(c.value())) {
+        cfg.setString(l.get(0), l.get(1), l.get(2), c.value());
+      } else {
+        cfg.setStringList(l.get(0), l.get(1), l.get(2),
+            Arrays.asList(c.value()));
+      }
     } else {
       throw new IllegalArgumentException(
           "GerritConfig.name must be of the format"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GcAssert.java
similarity index 82%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GcAssert.java
index 4c7289b2..5f8a8ed 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GcAssert.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assert_;
 
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -39,23 +39,26 @@
   public void assertHasPackFile(Project.NameKey... projects)
       throws RepositoryNotFoundException, IOException {
     for (Project.NameKey p : projects) {
-      assertTrue("Project " + p.get() + " has no pack files.",
-          getPackFiles(p).length > 0);
+      assert_()
+        .withFailureMessage("Project " + p.get() + " has no pack files.")
+        .that(getPackFiles(p))
+        .isNotEmpty();
     }
   }
 
   public void assertHasNoPackFile(Project.NameKey... projects)
       throws RepositoryNotFoundException, IOException {
     for (Project.NameKey p : projects) {
-      assertTrue("Project " + p.get() + " has pack files.",
-          getPackFiles(p).length == 0);
+      assert_()
+        .withFailureMessage("Project " + p.get() + " has pack files.")
+        .that(getPackFiles(p))
+        .isEmpty();
     }
   }
 
   private String[] getPackFiles(Project.NameKey p)
       throws RepositoryNotFoundException, IOException {
-    Repository repo = repoManager.openRepository(p);
-    try {
+    try (Repository repo = repoManager.openRepository(p)) {
       File packDir = new File(repo.getDirectory(), "objects/pack");
       return packDir.list(new FilenameFilter() {
         @Override
@@ -63,8 +66,6 @@
           return name.endsWith(".pack");
         }
       });
-    } finally {
-      repo.close();
     }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
similarity index 93%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
index 5cb1229..4b956a2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
@@ -24,5 +24,6 @@
 @Retention(RUNTIME)
 public @interface GerritConfig {
   String name();
-  String value();
+  String value() default "";
+  String[] values() default "";
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
similarity index 62%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 3be8195..5b0d311 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -14,15 +14,21 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.Daemon;
 import com.google.gerrit.pgm.Init;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
+import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -35,6 +41,7 @@
 import org.eclipse.jgit.util.FS;
 
 import java.io.File;
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -47,10 +54,69 @@
 import java.util.concurrent.TimeUnit;
 
 public class GerritServer {
+  @AutoValue
+  abstract static class Description {
+    static Description forTestClass(org.junit.runner.Description testDesc,
+        String configName) {
+      return new AutoValue_GerritServer_Description(
+          configName,
+          true, // @UseLocalDisk is only valid on methods.
+          !has(NoHttpd.class, testDesc.getTestClass()),
+          has(Sandboxed.class, testDesc.getTestClass()),
+          null, // @GerritConfig is only valid on methods.
+          null); // @GerritConfigs is only valid on methods.
+
+    }
+
+    static Description forTestMethod(org.junit.runner.Description testDesc,
+        String configName) {
+      return new AutoValue_GerritServer_Description(
+          configName,
+          testDesc.getAnnotation(UseLocalDisk.class) == null,
+          testDesc.getAnnotation(NoHttpd.class) == null
+            && !has(NoHttpd.class, testDesc.getTestClass()),
+          testDesc.getAnnotation(Sandboxed.class) != null ||
+              has(Sandboxed.class, testDesc.getTestClass()),
+          testDesc.getAnnotation(GerritConfig.class),
+          testDesc.getAnnotation(GerritConfigs.class));
+    }
+
+    private static boolean has(
+        Class<? extends Annotation> annotation, Class<?> clazz) {
+      for (; clazz != null; clazz = clazz.getSuperclass()) {
+        if (clazz.getAnnotation(annotation) != null) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    @Nullable abstract String configName();
+    abstract boolean memory();
+    abstract boolean httpd();
+    abstract boolean sandboxed();
+    @Nullable abstract GerritConfig config();
+    @Nullable abstract GerritConfigs configs();
+
+    private Config buildConfig(Config baseConfig) {
+      if (configs() != null && config() != null) {
+        throw new IllegalStateException(
+            "Use either @GerritConfigs or @GerritConfig not both");
+      }
+      if (configs() != null) {
+        return ConfigAnnotationParser.parse(baseConfig, configs());
+      } else if (config() != null) {
+        return ConfigAnnotationParser.parse(baseConfig, config());
+      } else {
+        return baseConfig;
+      }
+    }
+  }
 
   /** Returns fully started Gerrit server */
-  static GerritServer start(Config cfg, boolean memory, boolean enableHttpd)
+  static GerritServer start(Description desc, Config baseConfig)
       throws Exception {
+    Config cfg = desc.buildConfig(baseConfig);
     Logger.getLogger("com.google.gerrit").setLevel(Level.DEBUG);
     final CyclicBarrier serverStarted = new CyclicBarrier(2);
     final Daemon daemon = new Daemon(new Runnable() {
@@ -58,17 +124,16 @@
       public void run() {
         try {
           serverStarted.await();
-        } catch (InterruptedException e) {
-          throw new RuntimeException(e);
-        } catch (BrokenBarrierException e) {
+        } catch (InterruptedException | BrokenBarrierException e) {
           throw new RuntimeException(e);
         }
       }
     });
+    daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
 
     final File site;
     ExecutorService daemonService = null;
-    if (memory) {
+    if (desc.memory()) {
       site = null;
       mergeTestConfig(cfg);
       // Set the log4j configuration to an invalid one to prevent system logs
@@ -78,10 +143,9 @@
       cfg.setBoolean("sshd", null, "requestLog", false);
       cfg.setBoolean("index", "lucene", "testInmemory", true);
       cfg.setString("gitweb", null, "cgi", "");
-      daemon.setEnableHttpd(enableHttpd);
+      daemon.setEnableHttpd(desc.httpd());
       daemon.setLuceneModule(new LuceneIndexModule(
-          ChangeSchemas.getLatest().getVersion(),
-          Runtime.getRuntime().availableProcessors(), null));
+          ChangeSchemas.getLatest().getVersion(), 0, null));
       daemon.setDatabaseForTesting(ImmutableList.<Module>of(
           new InMemoryTestingDatabaseModule(cfg)));
       daemon.start();
@@ -91,10 +155,11 @@
       daemonService.submit(new Callable<Void>() {
         @Override
         public Void call() throws Exception {
-          int rc = daemon.main(new String[] {"-d", site.getPath(), "--headless" });
+          int rc = daemon.main(new String[] {
+              "-d", site.getPath(),
+              "--headless", "--console-log", "--show-stack-trace"});
           if (rc != 0) {
-            System.out.println("Failed to start Gerrit daemon. Check "
-                + site.getPath() + "/logs/error_log");
+            System.err.println("Failed to start Gerrit daemon");
             serverStarted.reset();
           }
           return null;
@@ -105,7 +170,7 @@
     }
 
     Injector i = createTestInjector(daemon);
-    return new GerritServer(i, daemon, daemonService);
+    return new GerritServer(desc, i, daemon, daemonService);
   }
 
   private static File initSite(Config base) throws Exception {
@@ -136,11 +201,17 @@
     cfg.setString("httpd", null, "listenUrl", url);
     cfg.setString("sshd", null, "listenAddress", forceEphemeralPort);
     cfg.setBoolean("sshd", null, "testUseInsecureRandom", true);
-    cfg.setString("cache", null, "directory", null);
+    cfg.unset("cache", null, "directory");
     cfg.setString("gerrit", null, "basePath", "git");
-    cfg.setBoolean("sendemail", null, "enable", false);
+    cfg.setBoolean("sendemail", null, "enable", true);
+    cfg.setInt("sendemail", null, "threadPoolSize", 0);
     cfg.setInt("cache", "projects", "checkFrequency", 0);
     cfg.setInt("plugins", null, "checkFrequency", 0);
+
+    cfg.setInt("sshd", null, "threads", 1);
+    cfg.setInt("sshd", null, "commandStartThreads", 1);
+    cfg.setInt("receive", null, "threadPoolSize", 1);
+    cfg.setInt("index", null, "threads", 1);
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
@@ -150,6 +221,9 @@
       protected void configure() {
         bind(AccountCreator.class);
         factory(PushOneCommit.Factory.class);
+        install(InProcessProtocol.module());
+        install(new NoSshModule());
+        install(new AsyncReceiveCommits.Module());
       }
     };
     return sysInjector.createChildInjector(module);
@@ -167,6 +241,8 @@
     return InetAddress.getLoopbackAddress();
   }
 
+  private final Description desc;
+
   private Daemon daemon;
   private ExecutorService daemonService;
   private Injector testInjector;
@@ -174,8 +250,9 @@
   private InetSocketAddress sshdAddress;
   private InetSocketAddress httpAddress;
 
-  private GerritServer(Injector testInjector, Daemon daemon,
+  private GerritServer(Description desc, Injector testInjector, Daemon daemon,
       ExecutorService daemonService) {
+    this.desc = desc;
     this.testInjector = testInjector;
     this.daemon = daemon;
     this.daemonService = daemonService;
@@ -207,6 +284,10 @@
     return testInjector;
   }
 
+  Description getDescription() {
+    return desc;
+  }
+
   void stop() throws Exception {
     daemon.getLifecycleManager().stop();
     if (daemonService != null) {
@@ -216,4 +297,9 @@
     }
     RepositoryCache.clear();
   }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).addValue(desc).toString();
+  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
new file mode 100644
index 0000000..e8f8925
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -0,0 +1,165 @@
+// Copyright (C) 2013 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;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.reviewdb.client.Project;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+import org.eclipse.jgit.api.FetchCommand;
+import org.eclipse.jgit.api.PushCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.util.FS;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class GitUtil {
+  private static final AtomicInteger testRepoCount = new AtomicInteger();
+  private static final int TEST_REPO_WINDOW_DAYS = 2;
+
+  public static void initSsh(final TestAccount a) {
+    final Properties config = new Properties();
+    config.put("StrictHostKeyChecking", "no");
+    JSch.setConfig(config);
+
+    // register a JschConfigSessionFactory that adds the private key as identity
+    // to the JSch instance of JGit so that SSH communication via JGit can
+    // succeed
+    SshSessionFactory.setInstance(new JschConfigSessionFactory() {
+      @Override
+      protected void configure(Host hc, Session session) {
+        try {
+          final JSch jsch = getJSch(hc, FS.DETECTED);
+          jsch.addIdentity("KeyPair", a.privateKey(),
+              a.sshKey.getPublicKeyBlob(), null);
+        } catch (JSchException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    });
+  }
+
+  /**
+   * Create a new {@link TestRepository} with a distinct commit clock.
+   * <p>
+   * It is very easy for tests to create commits with identical subjects and
+   * trees; if such commits also have identical authors/committers, then the
+   * computed Change-Id is identical as well. Tests may generally assume that
+   * Change-Ids are unique, so to ensure this, we provision TestRepository
+   * instances with non-overlapping commit clock times.
+   * <p>
+   * Space test repos 1 day apart, which allows for about 86k ticks per repo
+   * before overlapping, and about 8k instances per process before hitting
+   * JGit's year 2038 limit.
+   *
+   * @param repo repository to wrap.
+   * @return wrapped test repository with distinct commit time space.
+   */
+  public static <R extends Repository> TestRepository<R> newTestRepository(
+      R repo) throws IOException {
+    TestRepository<R> tr = new TestRepository<>(repo);
+    tr.tick(Ints.checkedCast(TimeUnit.SECONDS.convert(
+        testRepoCount.getAndIncrement() * TEST_REPO_WINDOW_DAYS,
+        TimeUnit.DAYS)));
+    return tr;
+  }
+
+  public static TestRepository<InMemoryRepository> cloneProject(
+      Project.NameKey project, String uri) throws Exception {
+    DfsRepositoryDescription desc =
+        new DfsRepositoryDescription("clone of " + project.get());
+    InMemoryRepository dest = new InMemoryRepository.Builder()
+        .setRepositoryDescription(desc)
+        // SshTransport depends on a real FS to read ~/.ssh/config, but
+        // InMemoryRepository by default uses a null FS.
+        // TODO(dborowitz): Remove when we no longer depend on SSH.
+        .setFS(FS.detect())
+        .build();
+    Config cfg = dest.getConfig();
+    cfg.setString("remote", "origin", "url", uri);
+    cfg.setString("remote", "origin", "fetch",
+        "+refs/heads/*:refs/remotes/origin/*");
+    TestRepository<InMemoryRepository> testRepo = newTestRepository(dest);
+    FetchResult result = testRepo.git().fetch().setRemote("origin").call();
+    String originMaster = "refs/remotes/origin/master";
+    if (result.getTrackingRefUpdate(originMaster) != null) {
+      testRepo.reset(originMaster);
+    }
+    return testRepo;
+  }
+
+  public static TestRepository<InMemoryRepository> cloneProject(
+      Project.NameKey project, SshSession sshSession) throws Exception {
+    return cloneProject(project, sshSession.getUrl() + "/" + project.get());
+  }
+
+  public static void fetch(TestRepository<?> testRepo, String spec)
+      throws GitAPIException {
+    FetchCommand fetch = testRepo.git().fetch();
+    fetch.setRefSpecs(new RefSpec(spec));
+    fetch.call();
+  }
+
+  public static PushResult pushHead(TestRepository<?> testRepo, String ref,
+      boolean pushTags) throws GitAPIException {
+    return pushHead(testRepo, ref, pushTags, false);
+  }
+
+  public static PushResult pushHead(TestRepository<?> testRepo, String ref,
+      boolean pushTags, boolean force) throws GitAPIException {
+    PushCommand pushCmd = testRepo.git().push();
+    pushCmd.setForce(force);
+    pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
+    if (pushTags) {
+      pushCmd.setPushTags();
+    }
+    Iterable<PushResult> r = pushCmd.call();
+    return Iterables.getOnlyElement(r);
+  }
+
+  public static Optional<String> getChangeId(TestRepository<?> tr, ObjectId id)
+      throws IOException {
+    RevCommit c = tr.getRevWalk().parseCommit(id);
+    tr.getRevWalk().parseBody(c);
+    List<String> ids = c.getFooterLines(FooterConstants.CHANGE_ID);
+    if (ids.isEmpty()) {
+      return Optional.absent();
+    }
+    return Optional.of(ids.get(ids.size() - 1));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
similarity index 88%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
index 872c912..390cae3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -51,6 +51,16 @@
     return response.getStatusLine().getStatusCode();
   }
 
+  public String getContentType() {
+    return response.getFirstHeader("X-FYI-Content-Type").getValue();
+  }
+
+  public boolean hasContent() {
+    Preconditions.checkNotNull(response,
+        "Response is not initialized.");
+    return response.getEntity() != null;
+  }
+
   public String getEntityContent() throws IOException {
     Preconditions.checkNotNull(response,
         "Response is not initialized.");
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
new file mode 100644
index 0000000..1e0920e
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2014 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;
+
+import com.google.common.base.CharMatcher;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.fluent.Executor;
+import org.apache.http.client.fluent.Request;
+
+import java.io.IOException;
+import java.net.URI;
+
+public class HttpSession {
+
+  protected final String url;
+  private final Executor executor;
+
+  public HttpSession(GerritServer server, TestAccount account) {
+    this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
+    URI uri = URI.create(url);
+    this.executor = Executor
+        .newInstance()
+        .auth(new HttpHost(uri.getHost(), uri.getPort()),
+            account.username, account.httpPassword);
+  }
+
+  protected RestResponse execute(Request request) throws IOException {
+    return new RestResponse(executor.execute(request).returnResponse());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
similarity index 94%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 8548b5c..634db7c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -43,7 +43,8 @@
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 import org.eclipse.jgit.lib.Config;
 
-import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 class InMemoryTestingDatabaseModule extends LifecycleModule {
   private final Config cfg;
@@ -58,9 +59,10 @@
       .annotatedWith(GerritServerConfig.class)
       .toInstance(cfg);
 
-    bind(File.class)
+    // TODO(dborowitz): Use jimfs.
+    bind(Path.class)
       .annotatedWith(SitePath.class)
-      .toInstance(new File("UNIT_TEST_GERRIT_SITE"));
+      .toInstance(Paths.get(cfg.getString("gerrit", null, "tempSiteDir")));
 
     bind(GitRepositoryManager.class)
       .toInstance(new InMemoryRepositoryManager());
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
new file mode 100644
index 0000000..c16eed7
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -0,0 +1,351 @@
+// Copyright (C) 2015 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;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.InProcessProtocol.Context;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.RemotePeer;
+import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
+import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.ChangeCache;
+import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.validators.UploadValidators;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.Provides;
+import com.google.inject.Scope;
+import com.google.inject.servlet.RequestScoped;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PostReceiveHook;
+import org.eclipse.jgit.transport.PostReceiveHookChain;
+import org.eclipse.jgit.transport.PreUploadHook;
+import org.eclipse.jgit.transport.PreUploadHookChain;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.TestProtocol;
+import org.eclipse.jgit.transport.UploadPack;
+import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
+import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.transport.resolver.UploadPackFactory;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class InProcessProtocol extends TestProtocol<Context> {
+  static Module module() {
+    return new AbstractModule() {
+      @Override
+      public void configure() {
+        install(new GerritRequestModule());
+        bind(RequestScopePropagator.class).to(Propagator.class);
+        bindScope(RequestScoped.class, InProcessProtocol.REQUEST);
+      }
+
+      @Provides
+      @RemotePeer
+      SocketAddress getSocketAddress() {
+        // TODO(dborowitz): Could potentially fake this with thread ID or
+        // something.
+        throw new OutOfScopeException("No remote peer in acceptance tests");
+      }
+    };
+  }
+
+  private static final Scope REQUEST = new Scope() {
+    @Override
+    public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
+      return new Provider<T>() {
+        @Override
+        public T get() {
+          Context ctx = current.get();
+          if (ctx == null) {
+            throw new OutOfScopeException("Not in TestProtocol scope");
+          }
+          return ctx.get(key, creator);
+        }
+
+        @Override
+        public String toString() {
+          return String.format("%s[%s]", creator, REQUEST);
+        }
+      };
+    }
+
+    @Override
+    public String toString() {
+      return "InProcessProtocol.REQUEST";
+    }
+  };
+
+  private static class Propagator
+      extends ThreadLocalRequestScopePropagator<Context> {
+    @Inject
+    Propagator(ThreadLocalRequestContext local,
+        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
+      super(REQUEST, current, local, dbProviderProvider);
+    }
+
+    @Override
+    protected Context continuingContext(Context ctx) {
+      return ctx.newContinuingContext();
+    }
+  }
+
+  private static final ThreadLocal<Context> current = new ThreadLocal<>();
+
+  // TODO(dborowitz): Merge this with AcceptanceTestRequestScope.
+  /**
+   * Multi-purpose session/context object.
+   * <p>
+   * Confusingly, Gerrit has two ideas of what a "context" object is:
+   * one for Guice {@link RequestScoped}, and one for its own simplified
+   * version of request scoping using {@link ThreadLocalRequestContext}.
+   * This class provides both, in essence just delegating the {@code
+   * ThreadLocalRequestContext} scoping to the Guice scoping mechanism.
+   * <p>
+   * It is also used as the session type for {@code UploadPackFactory} and
+   * {@code ReceivePackFactory}, since, after all, it encapsulates all the
+   * information about a single request.
+   */
+  static class Context implements RequestContext {
+    private static final Key<RequestScopedReviewDbProvider> DB_KEY =
+        Key.get(RequestScopedReviewDbProvider.class);
+    private static final Key<RequestCleanup> RC_KEY =
+        Key.get(RequestCleanup.class);
+    private static final Key<CurrentUser> USER_KEY = Key.get(CurrentUser.class);
+
+    private final SchemaFactory<ReviewDb> schemaFactory;
+    private final IdentifiedUser.GenericFactory userFactory;
+    private final Account.Id accountId;
+    private final Project.NameKey project;
+    private final RequestCleanup cleanup;
+    private final Map<Key<?>, Object> map;
+
+    Context(SchemaFactory<ReviewDb> schemaFactory,
+        IdentifiedUser.GenericFactory userFactory,
+        Account.Id accountId,
+        Project.NameKey project) {
+      this.schemaFactory = schemaFactory;
+      this.userFactory = userFactory;
+      this.accountId = accountId;
+      this.project = project;
+      map = new HashMap<>();
+      cleanup = new RequestCleanup();
+      map.put(DB_KEY,
+          new RequestScopedReviewDbProvider(
+            schemaFactory, Providers.of(cleanup)));
+      map.put(RC_KEY, cleanup);
+
+      IdentifiedUser user = userFactory.create(accountId);
+      user.setAccessPath(AccessPath.GIT);
+      map.put(USER_KEY, user);
+    }
+
+    private Context newContinuingContext() {
+      return new Context(schemaFactory, userFactory, accountId, project);
+    }
+
+    @Override
+    public CurrentUser getUser() {
+      return get(USER_KEY, null);
+    }
+
+    @Override
+    public Provider<ReviewDb> getReviewDbProvider() {
+      return get(DB_KEY, null);
+    }
+
+    private synchronized <T> T get(Key<T> key, Provider<T> creator) {
+      @SuppressWarnings("unchecked")
+      T t = (T) map.get(key);
+      if (t == null) {
+        t = creator.get();
+        map.put(key, t);
+      }
+      return t;
+    }
+  }
+
+  private static class Upload implements UploadPackFactory<Context> {
+    private final Provider<ReviewDb> dbProvider;
+    private final Provider<CurrentUser> userProvider;
+    private final TagCache tagCache;
+    private final ChangeCache changeCache;
+    private final ProjectControl.GenericFactory projectControlFactory;
+    private final TransferConfig transferConfig;
+    private final DynamicSet<PreUploadHook> preUploadHooks;
+    private final UploadValidators.Factory uploadValidatorsFactory;
+    private final ThreadLocalRequestContext threadContext;
+
+    @Inject
+    Upload(
+        Provider<ReviewDb> dbProvider,
+        Provider<CurrentUser> userProvider,
+        TagCache tagCache,
+        ChangeCache changeCache,
+        ProjectControl.GenericFactory projectControlFactory,
+        TransferConfig transferConfig,
+        DynamicSet<PreUploadHook> preUploadHooks,
+        UploadValidators.Factory uploadValidatorsFactory,
+        ThreadLocalRequestContext threadContext) {
+      this.dbProvider = dbProvider;
+      this.userProvider = userProvider;
+      this.tagCache = tagCache;
+      this.changeCache = changeCache;
+      this.projectControlFactory = projectControlFactory;
+      this.transferConfig = transferConfig;
+      this.preUploadHooks = preUploadHooks;
+      this.uploadValidatorsFactory = uploadValidatorsFactory;
+      this.threadContext = threadContext;
+    }
+
+    @Override
+    public UploadPack create(Context req, final Repository repo)
+        throws ServiceNotAuthorizedException {
+      // Set the request context, but don't bother unsetting, since we don't
+      // have an easy way to run code when this instance is done being used.
+      // Each operation is run in its own thread, so we don't need to recover
+      // its original context anyway.
+      threadContext.setContext(req);
+      current.set(req);
+      try {
+        ProjectControl ctl = projectControlFactory.controlFor(
+            req.project, userProvider.get());
+        if (!ctl.canRunUploadPack()) {
+          throw new ServiceNotAuthorizedException();
+        }
+
+        UploadPack up = new UploadPack(repo);
+        up.setPackConfig(transferConfig.getPackConfig());
+        up.setTimeout(transferConfig.getTimeout());
+
+        if (!ctl.allRefsAreVisible()) {
+          up.setAdvertiseRefsHook(new VisibleRefFilter(
+              tagCache, changeCache, repo, ctl, dbProvider.get(), true));
+        }
+        List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
+        hooks.add(uploadValidatorsFactory.create(
+            ctl.getProject(), repo, "localhost-test"));
+        up.setPreUploadHook(PreUploadHookChain.newChain(hooks));
+        return up;
+      } catch (NoSuchProjectException | IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  private static class Receive implements ReceivePackFactory<Context> {
+    private final Provider<CurrentUser> userProvider;
+    private final ProjectControl.GenericFactory projectControlFactory;
+    private final AsyncReceiveCommits.Factory factory;
+    private final TransferConfig config;
+    private final DynamicSet<ReceivePackInitializer> receivePackInitializers;
+    private final DynamicSet<PostReceiveHook> postReceiveHooks;
+    private final ThreadLocalRequestContext threadContext;
+
+    @Inject
+    Receive(
+        Provider<CurrentUser> userProvider,
+        ProjectControl.GenericFactory projectControlFactory,
+        AsyncReceiveCommits.Factory factory,
+        TransferConfig config,
+        DynamicSet<ReceivePackInitializer> receivePackInitializers,
+        DynamicSet<PostReceiveHook> postReceiveHooks,
+        ThreadLocalRequestContext threadContext) {
+      this.userProvider = userProvider;
+      this.projectControlFactory = projectControlFactory;
+      this.factory = factory;
+      this.config = config;
+      this.receivePackInitializers = receivePackInitializers;
+      this.postReceiveHooks = postReceiveHooks;
+      this.threadContext = threadContext;
+    }
+
+    @Override
+    public ReceivePack create(final Context req, Repository db)
+        throws ServiceNotAuthorizedException {
+      // Set the request context, but don't bother unsetting, since we don't
+      // have an easy way to run code when this instance is done being used.
+      // Each operation is run in its own thread, so we don't need to recover
+      // its original context anyway.
+      threadContext.setContext(req);
+      current.set(req);
+      try {
+        ProjectControl ctl =
+            projectControlFactory.controlFor(req.project, userProvider.get());
+        if (!ctl.canRunReceivePack()) {
+          throw new ServiceNotAuthorizedException();
+        }
+
+        ReceiveCommits rc = factory.create(ctl, db).getReceiveCommits();
+        ReceivePack rp = rc.getReceivePack();
+
+        Capable r = rc.canUpload();
+        if (r != Capable.OK) {
+          throw new ServiceNotAuthorizedException();
+        }
+
+        rp.setRefLogIdent(ctl.getUser().asIdentifiedUser().newRefLogIdent());
+        rp.setTimeout(config.getTimeout());
+        rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
+
+        for (ReceivePackInitializer initializer : receivePackInitializers) {
+          initializer.init(ctl.getProject().getNameKey(), rp);
+        }
+
+        rp.setPostReceiveHook(PostReceiveHookChain.newChain(
+            Lists.newArrayList(postReceiveHooks)));
+        return rp;
+      } catch (NoSuchProjectException | IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  @Inject
+  InProcessProtocol(Upload uploadPackFactory,
+      Receive receivePackFactory) {
+    super(uploadPackFactory, receivePackFactory);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
new file mode 100644
index 0000000..f0b9f46
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -0,0 +1,220 @@
+// Copyright (C) 2015 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;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.config.SitePaths;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ProcessBuilder.Redirect;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class PluginDaemonTest extends AbstractDaemonTest {
+
+  private static final String BUCKLC = "buck";
+  private static final String BUCKOUT = "buck-out";
+
+  private Path gen;
+  private Path testSite;
+  private Path pluginRoot;
+  private Path pluginsSitePath;
+  private Path pluginSubPath;
+  private Path pluginSource;
+  private String pluginName;
+  private boolean standalone;
+
+  @Override
+  protected void beforeTest(Description description) throws Exception {
+    locatePaths();
+    retrievePluginName();
+    buildPluginJar();
+    createTestSiteDirs();
+    copyJarToTestSite();
+    super.beforeTest(description);
+  }
+
+  protected void setPluginConfigString(String name, String value)
+      throws IOException, ConfigInvalidException {
+    SitePaths sitePath = new SitePaths(testSite);
+    FileBasedConfig cfg = getGerritConfigFile(sitePath);
+    cfg.load();
+    cfg.setString("plugin", pluginName, name, value);
+    cfg.save();
+  }
+
+  private FileBasedConfig getGerritConfigFile(SitePaths sitePath)
+      throws IOException {
+    FileBasedConfig cfg =
+        new FileBasedConfig(sitePath.gerrit_config.toFile(), FS.DETECTED);
+    if (!cfg.getFile().exists()) {
+      Path etc_path = Files.createDirectories(sitePath.etc_dir);
+      Files.createFile(etc_path.resolve("gerrit.config"));
+    }
+    return cfg;
+  }
+
+  private void locatePaths() {
+    URL pluginClassesUrl =
+        getClass().getProtectionDomain().getCodeSource().getLocation();
+    Path basePath = Paths.get(pluginClassesUrl.getPath()).getParent();
+
+    int idx = 0;
+    int buckOutIdx = 0;
+    int pluginsIdx = 0;
+    for (Path subPath : basePath) {
+      if (subPath.endsWith("plugins")) {
+        pluginsIdx = idx;
+      }
+      if (subPath.endsWith(BUCKOUT)) {
+        buckOutIdx = idx;
+      }
+      idx++;
+    }
+    standalone = checkStandalone(basePath);
+    pluginRoot = basePath.getRoot().resolve(basePath.subpath(0, buckOutIdx));
+    gen = pluginRoot.resolve(BUCKOUT).resolve("gen");
+
+    if (standalone) {
+      pluginSource = pluginRoot;
+    } else {
+      pluginSubPath = basePath.subpath(pluginsIdx, pluginsIdx + 2);
+      pluginSource = pluginRoot.resolve(pluginSubPath);
+    }
+  }
+
+  private boolean checkStandalone(Path basePath) {
+    String pathCharStringOrNone = "[a-zA-Z0-9._-]*?";
+    Pattern pattern = Pattern.compile(pathCharStringOrNone + "gerrit" +
+        pathCharStringOrNone);
+    Path partialPath = basePath;
+    for (int i = basePath.getNameCount(); i > 0; i--) {
+      int count = partialPath.getNameCount();
+      if (count > 1) {
+        String gerritDirCandidate =
+            partialPath.subpath(count - 2, count - 1).toString();
+        if (pattern.matcher(gerritDirCandidate).matches()) {
+          if (partialPath.endsWith(gerritDirCandidate + "/" + BUCKOUT)) {
+            return false;
+          }
+        }
+      }
+      partialPath = partialPath.getParent();
+    }
+    return true;
+  }
+
+  private void retrievePluginName() throws IOException {
+    Path buckFile = pluginSource.resolve("BUCK");
+    byte[] bytes = Files.readAllBytes(buckFile);
+    String buckContent =
+        new String(bytes, UTF_8).replaceAll("\\s+", "");
+    Matcher matcher =
+        Pattern.compile("gerrit_plugin\\(name='(.*?)'").matcher(buckContent);
+    if (matcher.find()) {
+      pluginName = matcher.group(1);
+    }
+    if (Strings.isNullOrEmpty(pluginName)) {
+      if (standalone) {
+        pluginName = pluginRoot.getFileName().toString();
+      } else {
+        pluginName = pluginSubPath.getFileName().toString();
+      }
+    }
+  }
+
+  private void buildPluginJar() throws IOException, InterruptedException {
+    Properties properties = loadBuckProperties();
+    String buck =
+        MoreObjects.firstNonNull(properties.getProperty(BUCKLC), BUCKLC);
+    String target;
+    if (standalone) {
+      target = "//:" + pluginName;
+    } else {
+      target = pluginSubPath.toString();
+    }
+
+    ProcessBuilder processBuilder =
+        new ProcessBuilder(buck, "build", target).directory(pluginRoot.toFile())
+            .redirectErrorStream(true);
+    // otherwise plugin jar creation fails:
+    processBuilder.environment().put("NO_BUCKD", "1");
+
+    Path forceJar = pluginSource.resolve("src/main/java/ForceJarIfMissing.java");
+    // if exists after cancelled test:
+    Files.deleteIfExists(forceJar);
+
+    Files.createFile(forceJar);
+    testSite = tempSiteDir.getRoot().toPath();
+
+    // otherwise process often hangs:
+    Path log = testSite.resolve("log");
+    processBuilder.redirectErrorStream(true);
+    processBuilder.redirectOutput(Redirect.appendTo(log.toFile()));
+
+    try {
+      processBuilder.start().waitFor();
+    } finally {
+      Files.delete(forceJar);
+      // otherwise jar not made next time if missing again:
+      processBuilder.start().waitFor();
+    }
+  }
+
+  private Properties loadBuckProperties() throws IOException {
+    Properties properties = new Properties();
+    Path propertiesPath = gen.resolve(Paths.get("tools/buck/buck.properties"));
+    if (Files.exists(propertiesPath)) {
+      try (InputStream in = Files.newInputStream(propertiesPath)) {
+        properties.load(in);
+      }
+    }
+    return properties;
+  }
+
+  private void createTestSiteDirs() throws IOException {
+    SitePaths sitePath = new SitePaths(testSite);
+    pluginsSitePath = Files.createDirectories(sitePath.plugins_dir);
+    Files.createDirectories(sitePath.tmp_dir);
+    Files.createDirectories(sitePath.etc_dir);
+  }
+
+  private void copyJarToTestSite() throws IOException {
+    Path pluginOut;
+    if (standalone) {
+      pluginOut = gen;
+    } else {
+      pluginOut = gen.resolve(pluginSubPath);
+    }
+    Path jar = pluginOut.resolve(pluginName + ".jar");
+    Path dest = pluginsSitePath.resolve(jar.getFileName());
+    Files.copy(jar, dest, StandardCopyOption.REPLACE_EXISTING);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
similarity index 69%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 67b6f51..6a090fd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -15,17 +15,11 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.add;
-import static com.google.gerrit.acceptance.GitUtil.amendCommit;
-import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
-import com.google.common.base.Function;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -40,12 +34,8 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.TagCommand;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.api.errors.InvalidTagNameException;
-import org.eclipse.jgit.api.errors.NoHeadException;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -53,23 +43,40 @@
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Set;
+import java.util.List;
 
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
   public static final String FILE_NAME = "a.txt";
   public static final String FILE_CONTENT = "some content";
+  public static final String PATCH =
+      "From %s Mon Sep 17 00:00:00 2001\n" +
+      "From: Administrator <admin@example.com>\n" +
+      "Date: %s\n" +
+      "Subject: [PATCH] test commit\n" +
+      "\n" +
+      "Change-Id: %s\n" +
+      "---\n" +
+      "\n" +
+      "diff --git a/a.txt b/a.txt\n" +
+      "new file mode 100644\n" +
+      "index 0000000..f0eec86\n" +
+      "--- /dev/null\n" +
+      "+++ b/a.txt\n" +
+      "@@ -0,0 +1 @@\n" +
+      "+some content\n" +
+      "\\ No newline at end of file\n";
 
   public interface Factory {
     PushOneCommit create(
         ReviewDb db,
-        PersonIdent i);
+        PersonIdent i,
+        TestRepository<?> testRepo);
 
     PushOneCommit create(
         ReviewDb db,
         PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content);
@@ -77,6 +84,7 @@
     PushOneCommit create(
         ReviewDb db,
         PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content,
@@ -106,7 +114,7 @@
   private final ApprovalsUtil approvalsUtil;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ReviewDb db;
-  private final PersonIdent i;
+  private final TestRepository<?> testRepo;
 
   private final String subject;
   private final String fileName;
@@ -115,14 +123,17 @@
   private Tag tag;
   private boolean force;
 
+  private final TestRepository<?>.CommitBuilder commitBuilder;
+
   @AssistedInject
   PushOneCommit(ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
       @Assisted ReviewDb db,
-      @Assisted PersonIdent i) {
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo) throws Exception {
     this(notesFactory, approvalsUtil, queryProvider,
-        db, i, SUBJECT, FILE_NAME, FILE_CONTENT);
+        db, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT);
   }
 
   @AssistedInject
@@ -131,11 +142,12 @@
       Provider<InternalChangeQuery> queryProvider,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
-      @Assisted("content") String content) {
+      @Assisted("content") String content) throws Exception {
     this(notesFactory, approvalsUtil, queryProvider,
-        db, i, subject, fileName, content, null);
+        db, i, testRepo, subject, fileName, content, null);
   }
 
   @AssistedInject
@@ -144,42 +156,55 @@
       Provider<InternalChangeQuery> queryProvider,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
       @Assisted("content") String content,
-      @Nullable @Assisted("changeId") String changeId) {
+      @Nullable @Assisted("changeId") String changeId) throws Exception {
     this.db = db;
+    this.testRepo = testRepo;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.queryProvider = queryProvider;
-    this.i = i;
     this.subject = subject;
     this.fileName = fileName;
     this.content = content;
     this.changeId = changeId;
-  }
-
-  public Result to(Git git, String ref) throws GitAPIException, IOException {
-    add(git, fileName, content);
-    return execute(git, ref);
-  }
-
-  public Result rm(Git git, String ref) throws GitAPIException {
-    GitUtil.rm(git, fileName);
-    return execute(git, ref);
-  }
-
-  private Result execute(Git git, String ref) throws GitAPIException,
-      ConcurrentRefUpdateException, InvalidTagNameException, NoHeadException {
-    Commit c;
     if (changeId != null) {
-      c = amendCommit(git, i, subject, changeId);
+      commitBuilder = testRepo.amendRef("HEAD")
+          .insertChangeId(changeId.substring(1));
     } else {
-      c = createCommit(git, i, subject);
-      changeId = c.getChangeId();
+      commitBuilder = testRepo.branch("HEAD").commit().insertChangeId();
+    }
+    commitBuilder.message(subject)
+      .author(i)
+      .committer(new PersonIdent(i, testRepo.getDate()));
+  }
+
+  public void setParents(List<RevCommit> parents) throws Exception {
+    commitBuilder.noParents();
+    for (RevCommit p : parents) {
+      commitBuilder.parent(p);
+    }
+  }
+
+  public Result to(String ref) throws Exception {
+    commitBuilder.add(fileName, content);
+    return execute(ref);
+  }
+
+  public Result rm(String ref) throws Exception {
+    commitBuilder.rm(fileName);
+    return execute(ref);
+  }
+
+  public Result execute(String ref) throws Exception {
+    RevCommit c = commitBuilder.create();
+    if (changeId == null) {
+      changeId = GitUtil.getChangeId(testRepo, c).get();
     }
     if (tag != null) {
-      TagCommand tagCommand = git.tag().setName(tag.name);
+      TagCommand tagCommand = testRepo.git().tag().setName(tag.name);
       if (tag instanceof AnnotatedTag) {
         AnnotatedTag annotatedTag = (AnnotatedTag)tag;
         tagCommand.setAnnotated(true)
@@ -190,7 +215,8 @@
       }
       tagCommand.call();
     }
-    return new Result(ref, pushHead(git, ref, tag != null, force), c, subject);
+    return new Result(ref, pushHead(testRepo, ref, tag != null, force), c,
+        subject);
   }
 
   public void setTag(final Tag tag) {
@@ -204,10 +230,10 @@
   public class Result {
     private final String ref;
     private final PushResult result;
-    private final Commit commit;
+    private final RevCommit commit;
     private final String resSubj;
 
-    private Result(String ref, PushResult resSubj, Commit commit,
+    private Result(String ref, PushResult resSubj, RevCommit commit,
         String subject) {
       this.ref = ref;
       this.result = resSubj;
@@ -217,7 +243,7 @@
 
     public ChangeData getChange() throws OrmException {
       return Iterables.getOnlyElement(
-          queryProvider.get().byKeyPrefix(commit.getChangeId()));
+          queryProvider.get().byKeyPrefix(changeId));
     }
 
     public PatchSet getPatchSet() throws OrmException {
@@ -229,47 +255,33 @@
     }
 
     public String getChangeId() {
-      return commit.getChangeId();
+      return changeId;
     }
 
     public ObjectId getCommitId() {
-      return commit.getCommit().getId();
+      return commit;
     }
 
     public RevCommit getCommit() {
-      return commit.getCommit();
+      return commit;
     }
 
     public void assertChange(Change.Status expectedStatus,
         String expectedTopic, TestAccount... expectedReviewers)
         throws OrmException {
       Change c = getChange().change();
-      assertThat(resSubj).isEqualTo(c.getSubject());
-      assertThat(expectedStatus).isEqualTo(c.getStatus());
-      assertThat(expectedTopic).isEqualTo(Strings.emptyToNull(c.getTopic()));
+      assertThat(c.getSubject()).isEqualTo(resSubj);
+      assertThat(c.getStatus()).isEqualTo(expectedStatus);
+      assertThat(Strings.emptyToNull(c.getTopic())).isEqualTo(expectedTopic);
       assertReviewers(c, expectedReviewers);
     }
 
     private void assertReviewers(Change c, TestAccount... expectedReviewers)
         throws OrmException {
-      Set<Account.Id> expectedReviewerIds =
-          Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
-              new Function<TestAccount, Account.Id>() {
-                @Override
-                public Account.Id apply(TestAccount a) {
-                  return a.id;
-                }
-              }));
-
-      for (Account.Id accountId
-          : approvalsUtil.getReviewers(db, notesFactory.create(c)).values()) {
-        assertThat(expectedReviewerIds.remove(accountId))
-          .named("unexpected reviewer " + accountId)
-          .isTrue();
-      }
-      assertThat((Iterable<?>)expectedReviewerIds)
-        .named("missing reviewers: " + expectedReviewerIds)
-        .isEmpty();
+      Iterable<Account.Id> actualIds =
+          approvalsUtil.getReviewers(db, notesFactory.create(c)).values();
+      assertThat(actualIds).containsExactlyElementsIn(
+          Sets.newHashSet(TestAccount.ids(expectedReviewers)));
     }
 
     public void assertOkStatus() {
@@ -280,6 +292,13 @@
       assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage);
     }
 
+    public void assertErrorStatus() {
+      RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+      assertThat(refUpdate.getStatus())
+        .named(message(refUpdate))
+        .isEqualTo(Status.REJECTED_OTHER_REASON);
+    }
+
     private void assertStatus(Status expectedStatus, String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
       assertThat(refUpdate.getStatus())
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
similarity index 87%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
index 6c7dbfe..261b894 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
-import java.nio.charset.StandardCharsets;
 
 public class RestResponse extends HttpResponse {
 
@@ -30,9 +30,8 @@
   @Override
   public Reader getReader() throws IllegalStateException, IOException {
     if (reader == null && response.getEntity() != null) {
-      reader =
-          new InputStreamReader(response.getEntity().getContent(),
-              StandardCharsets.UTF_8);
+      reader = new InputStreamReader(
+          response.getEntity().getContent(), UTF_8);
       reader.skip(JSON_MAGIC.length);
     }
     return reader;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
similarity index 74%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
index bf6f928..4b22d0a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -14,17 +14,15 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.common.base.Charsets;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Preconditions;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.server.OutputFormat;
 
 import org.apache.http.Header;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.fluent.Request;
 import org.apache.http.entity.BufferedHttpEntity;
 import org.apache.http.entity.InputStreamEntity;
 import org.apache.http.entity.StringEntity;
@@ -33,7 +31,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
 
 public class RestSession extends HttpSession {
 
@@ -41,7 +38,6 @@
     super(server, account);
   }
 
-  @Override
   public RestResponse get(String endPoint) throws IOException {
     return getWithHeader(endPoint, null);
   }
@@ -53,11 +49,11 @@
 
   private RestResponse getWithHeader(String endPoint, Header header)
       throws IOException {
-    HttpGet get = new HttpGet(url + "/a" + endPoint);
+    Request get = Request.Get(url + "/a" + endPoint);
     if (header != null) {
       get.addHeader(header);
     }
-    return new RestResponse(getClient().execute(get));
+    return execute(get);
   }
 
   public RestResponse put(String endPoint) throws IOException {
@@ -65,25 +61,38 @@
   }
 
   public RestResponse put(String endPoint, Object content) throws IOException {
-    HttpPut put = new HttpPut(url + "/a" + endPoint);
+    return putWithHeader(endPoint, null, content);
+  }
+
+  public RestResponse putWithHeader(String endPoint, Header header)
+      throws IOException {
+    return putWithHeader(endPoint, header, null);
+  }
+
+  public RestResponse putWithHeader(String endPoint, Header header,
+      Object content) throws IOException {
+    Request put = Request.Put(url + "/a" + endPoint);
+    if (header != null) {
+      put.addHeader(header);
+    }
     if (content != null) {
       put.addHeader(new BasicHeader("Content-Type", "application/json"));
-      put.setEntity(new StringEntity(
+      put.body(new StringEntity(
           OutputFormat.JSON_COMPACT.newGson().toJson(content),
-          Charsets.UTF_8.name()));
+          UTF_8));
     }
-    return new RestResponse(getClient().execute(put));
+    return execute(put);
   }
 
   public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
     Preconditions.checkNotNull(stream);
-    HttpPut put = new HttpPut(url + "/a" + endPoint);
+    Request put = Request.Put(url + "/a" + endPoint);
     put.addHeader(new BasicHeader("Content-Type", stream.getContentType()));
-    put.setEntity(new BufferedHttpEntity(
+    put.body(new BufferedHttpEntity(
         new InputStreamEntity(
             stream.getInputStream(),
             stream.getContentLength())));
-    return new RestResponse(getClient().execute(put));
+    return execute(put);
   }
 
   public RestResponse post(String endPoint) throws IOException {
@@ -91,24 +100,26 @@
   }
 
   public RestResponse post(String endPoint, Object content) throws IOException {
-    HttpPost post = new HttpPost(url + "/a" + endPoint);
+    Request post = Request.Post(url + "/a" + endPoint);
     if (content != null) {
       post.addHeader(new BasicHeader("Content-Type", "application/json"));
-      post.setEntity(new StringEntity(
+      post.body(new StringEntity(
           OutputFormat.JSON_COMPACT.newGson().toJson(content),
-          Charsets.UTF_8.name()));
+          UTF_8));
     }
-    return new RestResponse(getClient().execute(post));
+    return execute(post);
   }
 
   public RestResponse delete(String endPoint) throws IOException {
-    HttpDelete delete = new HttpDelete(url + "/a" + endPoint);
-    return new RestResponse(getClient().execute(delete));
+    return execute(Request.Delete(url + "/a" + endPoint));
   }
 
+  public RestResponse head(String endPoint) throws IOException {
+    return execute(Request.Head(url + "/a" + endPoint));
+  }
 
   public static RawInput newRawInput(String content) {
-    return newRawInput(content.getBytes(StandardCharsets.UTF_8));
+    return newRawInput(content.getBytes(UTF_8));
   }
 
   public static RawInput newRawInput(final byte[] bytes) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/Sandboxed.java
similarity index 82%
copy from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
copy to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/Sandboxed.java
index 5cb1229..11446e0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/Sandboxed.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -15,14 +15,13 @@
 package com.google.gerrit.acceptance;
 
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
-@Target({METHOD})
+@Target({TYPE, METHOD})
 @Retention(RUNTIME)
-public @interface GerritConfig {
-  String name();
-  String value();
+public @interface Sandboxed {
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
similarity index 85%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
index 701b337..794f832 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -42,11 +42,12 @@
   }
 
   @SuppressWarnings("resource")
-  public String exec(String command) throws JSchException, IOException {
+  public String exec(String command, InputStream opt) throws JSchException,
+      IOException {
     ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
     try {
       channel.setCommand(command);
-      channel.setInputStream(null);
+      channel.setInputStream(opt);
       InputStream in = channel.getInputStream();
       channel.connect();
 
@@ -60,6 +61,20 @@
     }
   }
 
+  public InputStream exec2(String command, InputStream opt) throws JSchException,
+      IOException {
+    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
+    channel.setCommand(command);
+    channel.setInputStream(opt);
+    InputStream in = channel.getInputStream();
+    channel.connect();
+    return in;
+  }
+
+  public String exec(String command) throws JSchException, IOException {
+    return exec(command, null);
+  }
+
   public boolean hasError() {
     return error != null;
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
similarity index 64%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index bd5f19f..4a6d22d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.Account;
 
 import com.jcraft.jsch.KeyPair;
@@ -21,9 +23,38 @@
 import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.ByteArrayOutputStream;
-
+import java.util.Arrays;
 
 public class TestAccount {
+  public static FluentIterable<Account.Id> ids(
+      Iterable<TestAccount> accounts) {
+    return FluentIterable.from(accounts)
+        .transform(new Function<TestAccount, Account.Id>() {
+          @Override
+          public Account.Id apply(TestAccount in) {
+            return in.id;
+          }
+        });
+  }
+
+  public static FluentIterable<Account.Id> ids(TestAccount... accounts) {
+    return ids(Arrays.asList(accounts));
+  }
+
+  public static FluentIterable<String> names(Iterable<TestAccount> accounts) {
+    return FluentIterable.from(accounts)
+        .transform(new Function<TestAccount, String>() {
+          @Override
+          public String apply(TestAccount in) {
+            return in.fullName;
+          }
+        });
+  }
+
+  public static FluentIterable<String> names(TestAccount... accounts) {
+    return names(Arrays.asList(accounts));
+  }
+
   public final Account.Id id;
   public final String username;
   public final String email;
@@ -48,7 +79,7 @@
   }
 
   public PersonIdent getIdent() {
-    return new PersonIdent(username, email);
+    return new PersonIdent(fullName, email);
   }
 
   public String getHttpUrl(GerritServer server) {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java
new file mode 100644
index 0000000..4ad37e2
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 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;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface TestProjectInput {
+  // Fields from ProjectInput for creating the project.
+
+  String parent() default "";
+  boolean createEmptyCommit() default true;
+  String description() default "";
+
+  // These may be null in a ProjectInput, but annotations do not allow null
+  // default values. Thus these defaults should match ProjectConfig.
+  SubmitType submitType() default SubmitType.MERGE_IF_NECESSARY;
+  InheritableBoolean useContributorAgreements()
+      default InheritableBoolean.INHERIT;
+  InheritableBoolean useSignedOffBy() default InheritableBoolean.INHERIT;
+  InheritableBoolean useContentMerge() default InheritableBoolean.INHERIT;
+  InheritableBoolean requireChangeId() default InheritableBoolean.INHERIT;
+
+
+  // Fields specific to acceptance test behavior.
+
+  /** Username to use for initial clone, passed to {@link AccountCreator}. */
+  String cloneAs() default "admin";
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
rename to gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index c7bea4e..0a39ea7 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -2,9 +2,11 @@
   name = 'lib',
   srcs = glob(['src/test/java/com/google/gerrit/acceptance/*.java']),
   exported_deps = [
+    '//gerrit-acceptance-framework:lib',
     '//gerrit-common:annotations',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
+    '//gerrit-gpg:testutil',
     '//gerrit-launcher:launcher',
     '//gerrit-lucene:lucene',
     '//gerrit-httpd:httpd',
@@ -13,32 +15,28 @@
     '//gerrit-pgm:util',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
-    '//gerrit-server/src/main/prolog:common',
     '//gerrit-server:testutil',
+    '//gerrit-server/src/main/prolog:common',
     '//gerrit-sshd:sshd',
 
     '//lib:args4j',
     '//lib:gson',
-    '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
     '//lib:h2',
     '//lib:jsch',
-    '//lib:junit',
     '//lib:servlet-api-3_1',
-    '//lib:truth',
 
-    '//lib/httpcomponents:httpclient',
-    '//lib/httpcomponents:httpcore',
-    '//lib/log:impl_log4j',
-    '//lib/log:log4j',
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
-    '//lib/jgit:junit',
     '//lib/mina:sshd',
   ],
   visibility = [
+    '//gerrit-plugin-api/...',
     '//tools/eclipse:classpath',
     '//gerrit-acceptance-tests/...',
   ],
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
deleted file mode 100644
index 056b4ed..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ /dev/null
@@ -1,391 +0,0 @@
-// Copyright (C) 2013 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;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static com.google.gerrit.acceptance.GitUtil.initSsh;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
-
-import com.google.common.base.Joiner;
-import com.google.common.primitives.Chars;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.changes.RevisionApi;
-import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.EditInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.Util;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gerrit.testutil.TempFileUtil;
-import com.google.gson.Gson;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.util.Providers;
-
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.junit.Rule;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-
-@RunWith(ConfigSuite.class)
-public abstract class AbstractDaemonTest {
-  @ConfigSuite.Parameter
-  public Config baseConfig;
-
-  @Inject
-  protected AllProjectsName allProjects;
-
-  @Inject
-  protected AccountCreator accounts;
-
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  protected GerritApi gApi;
-
-  @Inject
-  private AcceptanceTestRequestScope atrScope;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
-  protected PushOneCommit.Factory pushFactory;
-
-  @Inject
-  protected MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  protected ProjectCache projectCache;
-
-  @Inject
-  protected GroupCache groupCache;
-
-  @Inject
-  protected GitRepositoryManager repoManager;
-
-  @Inject
-  protected ChangeIndexer indexer;
-
-  @Inject
-  protected Provider<InternalChangeQuery> queryProvider;
-
-  @Inject
-  protected @GerritServerConfig Config cfg;
-
-  protected Git git;
-  protected GerritServer server;
-  protected TestAccount admin;
-  protected TestAccount user;
-  protected RestSession adminSession;
-  protected RestSession userSession;
-  protected SshSession sshSession;
-  protected ReviewDb db;
-  protected Project.NameKey project;
-
-  @Rule
-  public TestRule testRunner = new TestRule() {
-    @Override
-    public Statement apply(final Statement base, final Description description) {
-      return new Statement() {
-        @Override
-        public void evaluate() throws Throwable {
-          boolean mem = description.getAnnotation(UseLocalDisk.class) == null;
-          boolean enableHttpd = description.getAnnotation(NoHttpd.class) == null
-              && description.getTestClass().getAnnotation(NoHttpd.class) == null;
-          beforeTest(config(description), mem, enableHttpd);
-          base.evaluate();
-          afterTest();
-        }
-      };
-    }
-  };
-
-  private Config config(Description description) {
-    GerritConfigs cfgs = description.getAnnotation(GerritConfigs.class);
-    GerritConfig cfg = description.getAnnotation(GerritConfig.class);
-    if (cfgs != null && cfg != null) {
-      throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig not both");
-    }
-    if (cfgs != null) {
-      return ConfigAnnotationParser.parse(baseConfig, cfgs);
-    } else if (cfg != null) {
-      return ConfigAnnotationParser.parse(baseConfig, cfg);
-    } else {
-      return baseConfig;
-    }
-  }
-
-  protected static Config submitWholeTopicEnabledConfig() {
-    Config cfg = new Config();
-    cfg.setBoolean("change", null, "submitWholeTopic", true);
-    return cfg;
-  }
-
-  protected static Config allowDraftsDisabledConfig() {
-    Config cfg = new Config();
-    cfg.setBoolean("change", null, "allowDrafts", false);
-    return cfg;
-  }
-
-  protected boolean isAllowDrafts() {
-    return cfg.getBoolean("change", "allowDrafts", true);
-  }
-
-  protected boolean isSubmitWholeTopicEnabled() {
-    return cfg.getBoolean("change", null, "submitWholeTopic", false);
-  }
-
-  private void beforeTest(Config cfg, boolean memory, boolean enableHttpd) throws Exception {
-    server = startServer(cfg, memory, enableHttpd);
-    server.getTestInjector().injectMembers(this);
-    admin = accounts.admin();
-    user = accounts.user();
-    adminSession = new RestSession(server, admin);
-    userSession = new RestSession(server, user);
-    initSsh(admin);
-    db = reviewDbProvider.open();
-    Context ctx = newRequestContext(admin);
-    atrScope.set(ctx);
-    sshSession = ctx.getSession();
-    project = new Project.NameKey("p");
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-  }
-
-  protected GerritServer startServer(Config cfg, boolean memory,
-      boolean enableHttpd) throws Exception {
-    return GerritServer.start(cfg, memory, enableHttpd);
-  }
-
-  private void afterTest() throws Exception {
-    db.close();
-    sshSession.close();
-    server.stop();
-    TempFileUtil.cleanup();
-  }
-
-  protected PushOneCommit.Result createChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master");
-  }
-
-  private static final List<Character> RANDOM =
-      Chars.asList(new char[]{'a','b','c','d','e','f','g','h'});
-  protected PushOneCommit.Result amendChange(String changeId)
-      throws GitAPIException, IOException {
-    return amendChange(changeId, "refs/for/master");
-  }
-
-  protected PushOneCommit.Result amendChange(String changeId, String ref)
-      throws GitAPIException, IOException {
-    Collections.shuffle(RANDOM);
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
-            PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
-    return push.to(git, ref);
-  }
-
-  protected PushOneCommit.Result amendChangeAsDraft(String changeId)
-      throws GitAPIException, IOException {
-    return amendChange(changeId, "refs/drafts/master");
-  }
-
-  protected ChangeInfo getChange(String changeId, ListChangesOption... options)
-      throws IOException {
-    return getChange(adminSession, changeId, options);
-  }
-
-  protected ChangeInfo getChange(RestSession session, String changeId,
-      ListChangesOption... options) throws IOException {
-    String q = options.length > 0 ? "?o=" + Joiner.on("&o=").join(options) : "";
-    RestResponse r = session.get("/changes/" + changeId + q);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(), ChangeInfo.class);
-  }
-
-  protected ChangeInfo info(String id)
-      throws RestApiException {
-    return gApi.changes().id(id).info();
-  }
-
-  protected ChangeInfo get(String id)
-      throws RestApiException {
-    return gApi.changes().id(id).get();
-  }
-
-  protected EditInfo getEdit(String id)
-      throws RestApiException {
-    return gApi.changes().id(id).getEdit();
-  }
-
-  protected ChangeInfo get(String id, ListChangesOption... options)
-      throws RestApiException {
-    EnumSet<ListChangesOption> s = EnumSet.noneOf(ListChangesOption.class);
-    s.addAll(Arrays.asList(options));
-    return gApi.changes().id(id).get(s);
-  }
-
-  protected List<ChangeInfo> query(String q) throws RestApiException {
-    return gApi.changes().query(q).get();
-  }
-
-  private Context newRequestContext(TestAccount account) {
-    return atrScope.newContext(reviewDbProvider, new SshSession(server, admin),
-        identifiedUserFactory.create(Providers.of(db), account.getId()));
-  }
-
-  protected Context setApiUser(TestAccount account) {
-    return atrScope.set(newRequestContext(account));
-  }
-
-  protected static Gson newGson() {
-    return OutputFormat.JSON_COMPACT.newGson();
-  }
-
-  protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
-    return gApi.changes()
-        .id(r.getChangeId())
-        .current();
-  }
-
-  protected void allow(String permission, AccountGroup.UUID id, String ref)
-      throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    Util.allow(cfg, permission, id, ref);
-    saveProjectConfig(project, cfg);
-  }
-
-  protected void allowGlobalCapability(String capabilityName,
-      AccountGroup.UUID id) throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    Util.allow(cfg, capabilityName, id);
-    saveProjectConfig(allProjects, cfg);
-  }
-
-  protected void setUseContributorAgreements(InheritableBoolean value)
-      throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    ProjectConfig config = ProjectConfig.read(md);
-    config.getProject().setUseContributorAgreements(value);
-    config.commit(md);
-    projectCache.evict(config.getProject());
-  }
-
-  protected void setUseSignedOffBy(InheritableBoolean value)
-      throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    ProjectConfig config = ProjectConfig.read(md);
-    config.getProject().setUseSignedOffBy(value);
-    config.commit(md);
-    projectCache.evict(config.getProject());
-  }
-
-  protected void deny(String permission, AccountGroup.UUID id, String ref)
-      throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    Util.deny(cfg, permission, id, ref);
-    saveProjectConfig(project, cfg);
-  }
-
-  protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
-      throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(p);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
-    projectCache.evict(cfg.getProject());
-  }
-
-  protected void grant(String permission, Project.NameKey project, String ref)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    grant(permission, project, ref, false);
-  }
-
-  protected void grant(String permission, Project.NameKey project, String ref,
-      boolean force) throws RepositoryNotFoundException, IOException,
-      ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant %s on %s", permission, ref));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(ref, true);
-    Permission p = s.getPermission(permission, true);
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
-    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
-    rule.setForce(force);
-    p.add(rule);
-    config.commit(md);
-    projectCache.evict(config.getProject());
-  }
-
-  protected void blockRead(Project.NameKey project, String ref) throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.READ, REGISTERED_USERS, ref);
-    saveProjectConfig(project, cfg);
-  }
-
-  protected void blockForgeCommitter(Project.NameKey project, String ref)
-      throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
-    saveProjectConfig(project, cfg);
-  }
-
-  protected PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
deleted file mode 100644
index dee36ef..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ /dev/null
@@ -1,231 +0,0 @@
-// Copyright (C) 2013 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;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.collect.Iterables;
-import com.google.gerrit.common.FooterConstants;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.testutil.TempFileUtil;
-
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.Session;
-
-import org.eclipse.jgit.api.AddCommand;
-import org.eclipse.jgit.api.CheckoutCommand;
-import org.eclipse.jgit.api.CloneCommand;
-import org.eclipse.jgit.api.CommitCommand;
-import org.eclipse.jgit.api.FetchCommand;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.PushCommand;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig.Host;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.util.ChangeIdUtil;
-import org.eclipse.jgit.util.FS;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Properties;
-
-public class GitUtil {
-
-  public static void initSsh(final TestAccount a) {
-    final Properties config = new Properties();
-    config.put("StrictHostKeyChecking", "no");
-    JSch.setConfig(config);
-
-    // register a JschConfigSessionFactory that adds the private key as identity
-    // to the JSch instance of JGit so that SSH communication via JGit can
-    // succeed
-    SshSessionFactory.setInstance(new JschConfigSessionFactory() {
-      @Override
-      protected void configure(Host hc, Session session) {
-        try {
-          final JSch jsch = getJSch(hc, FS.DETECTED);
-          jsch.addIdentity("KeyPair", a.privateKey(),
-              a.sshKey.getPublicKeyBlob(), null);
-        } catch (JSchException e) {
-          throw new RuntimeException(e);
-        }
-      }
-    });
-  }
-
-  public static void createProject(SshSession s, String name)
-      throws JSchException, IOException {
-    createProject(s, name, null);
-  }
-
-  public static void createProject(SshSession s, String name, Project.NameKey parent)
-      throws JSchException, IOException {
-    createProject(s, name, parent, true);
-  }
-
-  public static void createProject(SshSession s, String name,
-      Project.NameKey parent, boolean emptyCommit)
-      throws JSchException, IOException {
-    StringBuilder b = new StringBuilder();
-    b.append("gerrit create-project");
-    if (emptyCommit) {
-      b.append(" --empty-commit");
-    }
-    b.append(" --name \"");
-    b.append(name);
-    b.append("\"");
-    if (parent != null) {
-      b.append(" --parent \"");
-      b.append(parent.get());
-      b.append("\"");
-    }
-    s.exec(b.toString());
-    if (s.hasError()) {
-      throw new IllegalStateException(
-          "gerrit create-project returned error: " + s.getError());
-    }
-  }
-
-  public static Git cloneProject(String url) throws GitAPIException, IOException {
-    return cloneProject(url, true);
-  }
-
-  public static Git cloneProject(String url, boolean checkout) throws GitAPIException, IOException {
-    final File gitDir = TempFileUtil.createTempDirectory();
-    final CloneCommand cloneCmd = Git.cloneRepository();
-    cloneCmd.setURI(url);
-    cloneCmd.setDirectory(gitDir);
-    cloneCmd.setNoCheckout(!checkout);
-    return cloneCmd.call();
-  }
-
-  public static void add(Git git, String path, String content)
-      throws GitAPIException, IOException {
-    File f = new File(git.getRepository().getDirectory().getParentFile(), path);
-    File p = f.getParentFile();
-    if (!p.exists() && !p.mkdirs()) {
-      throw new IOException("failed to create dir: " + p.getAbsolutePath());
-    }
-    FileOutputStream s = new FileOutputStream(f);
-    BufferedWriter out = new BufferedWriter(
-        new OutputStreamWriter(s, StandardCharsets.UTF_8));
-    try {
-      out.write(content);
-    } finally {
-      out.close();
-    }
-
-    final AddCommand addCmd = git.add();
-    addCmd.addFilepattern(path);
-    addCmd.call();
-  }
-
-  public static void rm(Git gApi, String path)
-      throws GitAPIException {
-    gApi.rm()
-        .addFilepattern(path)
-        .call();
-  }
-
-  public static Commit createCommit(Git git, PersonIdent i, String msg)
-      throws GitAPIException {
-    return createCommit(git, i, msg, null);
-  }
-
-  public static Commit amendCommit(Git git, PersonIdent i, String msg, String changeId)
-      throws GitAPIException {
-    msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
-    return createCommit(git, i, msg, changeId);
-  }
-
-  private static Commit createCommit(Git git, PersonIdent i, String msg,
-      String changeId) throws GitAPIException {
-
-    final CommitCommand commitCmd = git.commit();
-    commitCmd.setAmend(changeId != null);
-    commitCmd.setAuthor(i);
-    commitCmd.setCommitter(i);
-    commitCmd.setMessage(msg);
-    commitCmd.setInsertChangeId(changeId == null);
-
-    RevCommit c = commitCmd.call();
-
-    List<String> ids = c.getFooterLines(FooterConstants.CHANGE_ID);
-    checkState(ids.size() >= 1,
-        "No Change-Id found in new commit:\n%s", c.getFullMessage());
-    changeId = ids.get(ids.size() - 1);
-
-    return new Commit(c, changeId);
-  }
-
-  public static void fetch(Git git, String spec) throws GitAPIException {
-    FetchCommand fetch = git.fetch();
-    fetch.setRefSpecs(new RefSpec(spec));
-    fetch.call();
-  }
-
-  public static void checkout(Git git, String name) throws GitAPIException {
-    CheckoutCommand checkout = git.checkout();
-    checkout.setName(name);
-    checkout.call();
-  }
-
-  public static PushResult pushHead(Git git, String ref, boolean pushTags)
-      throws GitAPIException {
-    return pushHead(git, ref, pushTags, false);
-  }
-
-  public static PushResult pushHead(Git git, String ref, boolean pushTags,
-      boolean force) throws GitAPIException {
-    PushCommand pushCmd = git.push();
-    pushCmd.setForce(force);
-    pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
-    if (pushTags) {
-      pushCmd.setPushTags();
-    }
-    Iterable<PushResult> r = pushCmd.call();
-    return Iterables.getOnlyElement(r);
-  }
-
-  public static class Commit {
-    private final RevCommit commit;
-    private final String changeId;
-
-    Commit(RevCommit commit, String changeId) {
-      this.commit = commit;
-      this.changeId = changeId;
-    }
-
-    public RevCommit getCommit() {
-      return commit;
-    }
-
-    public String getChangeId() {
-      return changeId;
-    }
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
deleted file mode 100644
index f765e7a..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2014 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;
-
-import com.google.common.base.CharMatcher;
-
-import org.apache.http.auth.AuthScope;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.BasicCredentialsProvider;
-import org.apache.http.impl.client.HttpClientBuilder;
-
-import java.io.IOException;
-import java.net.URI;
-
-public class HttpSession {
-
-  protected final String url;
-  private final TestAccount account;
-  private HttpClient client;
-
-  public HttpSession(GerritServer server, TestAccount account) {
-    this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
-    this.account = account;
-  }
-
-  public HttpResponse get(String path) throws IOException {
-    HttpGet get = new HttpGet(url + path);
-    return new HttpResponse(getClient().execute(get));
-  }
-
-  protected HttpClient getClient() {
-    if (client == null) {
-      URI uri = URI.create(url);
-      BasicCredentialsProvider creds = new BasicCredentialsProvider();
-      creds.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
-          new UsernamePasswordCredentials(account.username,
-              account.httpPassword));
-      client = HttpClientBuilder
-          .create()
-          .setDefaultCredentialsProvider(creds)
-          .setMaxConnPerRoute(512)
-          .setMaxConnTotal(1024)
-          .build();
-    }
-    return client;
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
new file mode 100644
index 0000000..6ead346
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.account.PutUsername;
+
+import org.apache.http.HttpStatus;
+import org.junit.After;
+import org.junit.Test;
+
+@Sandboxed
+public class SandboxTest extends AbstractDaemonTest {
+  @After
+  public void addUser() throws Exception {
+    PutUsername.Input in = new PutUsername.Input();
+    in.username = "sandboxuser";
+    RestResponse r =
+        adminSession.put("/accounts/sandboxuser", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+  }
+
+  private void testUserNotPresent() throws Exception {
+    RestResponse r = adminSession.get("/accounts/sandboxuser");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+
+  @Test
+  public void testUserNotPresent1() throws Exception {
+    testUserNotPresent();
+  }
+
+  @Test
+  public void testUserNotPresent2() throws Exception {
+    testUserNotPresent();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 8945a22d..b6a54b6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -14,15 +14,120 @@
 
 package com.google.gerrit.acceptance.api.accounts;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.allValidKeys;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.gpg.Fingerprint;
+import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.gpg.server.GpgKeys;
+import com.google.gerrit.gpg.testutil.TestKey;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
 public class AccountIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config enableSignedPushConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("receive", null, "enableSignedPush", true);
+    return cfg;
+  }
+
+  @Inject
+  private Provider<PublicKeyStore> publicKeyStoreProvider;
+
+  @Inject
+  private AllUsersName allUsers;
+
+  private List<AccountExternalId> savedExternalIds;
+
+  @Before
+  public void saveExternalIds() throws Exception {
+    savedExternalIds = new ArrayList<>();
+    savedExternalIds.addAll(getExternalIds(admin));
+    savedExternalIds.addAll(getExternalIds(user));
+  }
+
+  @After
+  public void restoreExternalIds() throws Exception {
+    db.accountExternalIds().delete(getExternalIds(admin));
+    db.accountExternalIds().delete(getExternalIds(user));
+    db.accountExternalIds().insert(savedExternalIds);
+  }
+
+  @After
+  public void clearPublicKeyStore() throws Exception {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      Ref ref = repo.exactRef(REFS_GPG_KEYS);
+      if (ref != null) {
+        RefUpdate ru = repo.updateRef(REFS_GPG_KEYS);
+        ru.setForceUpdate(true);
+        assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      }
+    }
+  }
+
+  private List<AccountExternalId> getExternalIds(TestAccount account)
+      throws Exception {
+    return db.accountExternalIds().byAccount(account.getId()).toList();
+  }
+
+  @After
+  public void deleteGpgKeys() throws Exception {
+    String ref = REFS_GPG_KEYS;
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      if (repo.getRefDatabase().exactRef(ref) != null) {
+        RefUpdate ru = repo.updateRef(ref);
+        ru.setForceUpdate(true);
+        assert_().withFailureMessage("Failed to delete " + ref)
+            .that(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      }
+    }
+  }
 
   @Test
   public void get() throws Exception {
@@ -49,14 +154,284 @@
   @Test
   public void starUnstarChange() throws Exception {
     PushOneCommit.Result r = createChange();
-    String triplet = "p~master~" + r.getChangeId();
+    String triplet = project.get() + "~master~" + r.getChangeId();
     gApi.accounts()
         .self()
         .starChange(triplet);
-    assertThat(getChange(triplet).starred).isTrue();
+    assertThat(info(triplet).starred).isTrue();
     gApi.accounts()
         .self()
         .unstarChange(triplet);
-    assertThat(getChange(triplet).starred).isNull();
+    assertThat(info(triplet).starred).isNull();
+  }
+
+  @Test
+  public void suggestAccounts() throws Exception {
+    String adminUsername = "admin";
+    List<AccountInfo> result = gApi.accounts()
+        .suggestAccounts().withQuery(adminUsername).get();
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).username).isEqualTo(adminUsername);
+
+    List<AccountInfo> resultShortcutApi = gApi.accounts()
+        .suggestAccounts(adminUsername).get();
+    assertThat(resultShortcutApi).hasSize(result.size());
+
+    List<AccountInfo> emptyResult = gApi.accounts()
+        .suggestAccounts("unknown").get();
+    assertThat(emptyResult).isEmpty();
+  }
+
+  @Test
+  public void addEmail() throws Exception {
+    List<String> emails = ImmutableList.of(
+        "new.email@example.com", "new.email@example.systems");
+    for (String email : emails) {
+      EmailInput input = new EmailInput();
+      input.email = email;
+      input.noConfirmation = true;
+      gApi.accounts().self().addEmail(input);
+    }
+  }
+
+  @Test
+  public void addInvalidEmail() throws Exception {
+    EmailInput input  = new EmailInput();
+    input.email = "invalid@";
+    input.noConfirmation = true;
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("invalid email address");
+    gApi.accounts().self().addEmail(input);
+  }
+
+  @Test
+  public void addGpgKey() throws Exception {
+    TestKey key = validKeyWithoutExpiration();
+    String id = key.getKeyIdString();
+    addExternalIdEmail(admin, "test1@example.com");
+
+    assertKeyMapContains(key, addGpgKey(key.getPublicKeyArmored()));
+    assertKeys(key);
+
+    setApiUser(user);
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage(id);
+    gApi.accounts().self().gpgKey(id).get();
+  }
+
+  @Test
+  public void reAddExistingGpgKey() throws Exception {
+    addExternalIdEmail(admin, "test5@example.com");
+    TestKey key = validKeyWithSecondUserId();
+    String id = key.getKeyIdString();
+    PGPPublicKey pk = key.getPublicKey();
+
+    GpgKeyInfo info = addGpgKey(armor(pk)).get(id);
+    assertThat(info.userIds).hasSize(2);
+    assertIteratorSize(2, getOnlyKeyFromStore(key).getUserIDs());
+
+    pk = PGPPublicKey.removeCertification(pk, "foo:myId");
+    info = addGpgKey(armor(pk)).get(id);
+    assertThat(info.userIds).hasSize(1);
+    assertIteratorSize(1, getOnlyKeyFromStore(key).getUserIDs());
+  }
+
+  @Test
+  public void addOtherUsersGpgKey_Conflict() throws Exception {
+    // Both users have a matching external ID for this key.
+    addExternalIdEmail(admin, "test5@example.com");
+    AccountExternalId extId = new AccountExternalId(
+        user.getId(), new AccountExternalId.Key("foo:myId"));
+
+    db.accountExternalIds().insert(Collections.singleton(extId));
+
+    TestKey key = validKeyWithSecondUserId();
+    addGpgKey(key.getPublicKeyArmored());
+    setApiUser(user);
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("GPG key already associated with another account");
+    addGpgKey(key.getPublicKeyArmored());
+  }
+
+  @Test
+  public void listGpgKeys() throws Exception {
+    List<TestKey> keys = allValidKeys();
+    List<String> toAdd = new ArrayList<>(keys.size());
+    for (TestKey key : keys) {
+      addExternalIdEmail(admin,
+          PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
+      toAdd.add(key.getPublicKeyArmored());
+    }
+    gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.<String> of());
+    assertKeys(keys);
+  }
+
+  @Test
+  public void deleteGpgKey() throws Exception {
+    TestKey key = validKeyWithoutExpiration();
+    String id = key.getKeyIdString();
+    addExternalIdEmail(admin, "test1@example.com");
+    addGpgKey(key.getPublicKeyArmored());
+    assertKeys(key);
+
+    gApi.accounts().self().gpgKey(id).delete();
+    assertKeys();
+
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage(id);
+    gApi.accounts().self().gpgKey(id).get();
+  }
+
+  @Test
+  public void addAndRemoveGpgKeys() throws Exception {
+    for (TestKey key : allValidKeys()) {
+      addExternalIdEmail(admin,
+          PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
+    }
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
+    TestKey key5 = validKeyWithSecondUserId();
+
+    Map<String, GpgKeyInfo> infos = gApi.accounts().self().putGpgKeys(
+        ImmutableList.of(
+          key1.getPublicKeyArmored(),
+          key2.getPublicKeyArmored()),
+        ImmutableList.of(key5.getKeyIdString()));
+    assertThat(infos.keySet())
+        .containsExactly(key1.getKeyIdString(), key2.getKeyIdString());
+    assertKeys(key1, key2);
+
+    infos = gApi.accounts().self().putGpgKeys(
+        ImmutableList.of(key5.getPublicKeyArmored()),
+        ImmutableList.of(key1.getKeyIdString()));
+    assertThat(infos.keySet())
+        .containsExactly(key1.getKeyIdString(), key5.getKeyIdString());
+    assertKeyMapContains(key5, infos);
+    assertThat(infos.get(key1.getKeyIdString()).key).isNull();
+    assertKeys(key2, key5);
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("Cannot both add and delete key: "
+        + keyToString(key2.getPublicKey()));
+    infos = gApi.accounts().self().putGpgKeys(
+        ImmutableList.of(key2.getPublicKeyArmored()),
+        ImmutableList.of(key2.getKeyIdString()));
+  }
+
+  private PGPPublicKey getOnlyKeyFromStore(TestKey key) throws Exception {
+    try (PublicKeyStore store = publicKeyStoreProvider.get()) {
+      Iterable<PGPPublicKeyRing> keys = store.get(key.getKeyId());
+      assertThat(keys).hasSize(1);
+      return keys.iterator().next().getPublicKey();
+    }
+  }
+
+  private static String armor(PGPPublicKey key) throws Exception {
+    ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+    try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
+      key.encode(aout);
+    }
+    return new String(out.toByteArray(), UTF_8);
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private static void assertIteratorSize(int size, Iterator it) {
+    assertThat(ImmutableList.copyOf(it)).hasSize(size);
+  }
+
+  private static void assertKeyMapContains(TestKey expected,
+      Map<String, GpgKeyInfo> actualMap) {
+    GpgKeyInfo actual = actualMap.get(expected.getKeyIdString());
+    assertThat(actual).isNotNull();
+    assertThat(actual.id).isNull();
+    actual.id = expected.getKeyIdString();
+    assertKeyEquals(expected, actual);
+  }
+
+  private void assertKeys(TestKey... expectedKeys) throws Exception {
+    assertKeys(Arrays.asList(expectedKeys));
+  }
+
+  private void assertKeys(Iterable<TestKey> expectedKeys) throws Exception {
+    // Check via API.
+    FluentIterable<TestKey> expected = FluentIterable.from(expectedKeys);
+    Map<String, GpgKeyInfo> keyMap = gApi.accounts().self().listGpgKeys();
+    assertThat(keyMap.keySet())
+        .named("keys returned by listGpgKeys()")
+        .containsExactlyElementsIn(
+          expected.transform(new Function<TestKey, String>() {
+            @Override
+            public String apply(TestKey in) {
+              return in.getKeyIdString();
+            }
+          }));
+
+    for (TestKey key : expected) {
+      assertKeyEquals(key, gApi.accounts().self().gpgKey(
+          key.getKeyIdString()).get());
+      assertKeyEquals(key, gApi.accounts().self().gpgKey(
+          Fingerprint.toString(key.getPublicKey().getFingerprint())).get());
+      assertKeyMapContains(key, keyMap);
+    }
+
+    // Check raw external IDs.
+    Account.Id currAccountId = atrScope.get().getUser().getAccountId();
+    assertThat(
+        GpgKeys.getGpgExtIds(db, currAccountId)
+          .transform(new Function<AccountExternalId, String>() {
+            @Override
+            public String apply(AccountExternalId in) {
+              return in.getSchemeRest();
+            }
+          }))
+        .named("external IDs in database")
+        .containsExactlyElementsIn(
+            expected.transform(new Function<TestKey, String>() {
+              @Override
+              public String apply(TestKey in) {
+                return BaseEncoding.base16().encode(
+                    in.getPublicKey().getFingerprint());
+              }
+            }));
+
+    // Check raw stored keys.
+    for (TestKey key : expected) {
+      getOnlyKeyFromStore(key);
+    }
+  }
+
+  private static void assertKeyEquals(TestKey expected, GpgKeyInfo actual) {
+    String id = expected.getKeyIdString();
+    assertThat(actual.id).named(id).isEqualTo(id);
+    assertThat(actual.fingerprint).named(id).isEqualTo(
+        Fingerprint.toString(expected.getPublicKey().getFingerprint()));
+    @SuppressWarnings("unchecked")
+    List<String> userIds =
+        ImmutableList.copyOf(expected.getPublicKey().getUserIDs());
+    assertThat(actual.userIds).named(id).containsExactlyElementsIn(userIds);
+    assertThat(actual.key).named(id)
+        .startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
+    assertThat(actual.status).isEqualTo(GpgKeyInfo.Status.TRUSTED);
+    assertThat(actual.problems).isEmpty();
+  }
+
+  private void addExternalIdEmail(TestAccount account, String email)
+      throws Exception {
+    checkNotNull(email);
+    AccountExternalId extId = new AccountExternalId(
+        account.getId(), new AccountExternalId.Key(name("test"), email));
+    extId.setEmailAddress(email);
+    db.accountExternalIds().insert(Collections.singleton(extId));
+    // Clear saved AccountState and AccountExternalIds.
+    accountCache.evict(account.getId());
+    setApiUser(account);
+  }
+
+  private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
+    return gApi.accounts().self().putGpgKeys(
+        ImmutableList.of(armored),
+        ImmutableList.<String> of());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
index 1152d88..814dcf4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-account',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
index 1152d88..5db2054 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-change',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 5a2e05c..d92a887 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -15,6 +15,14 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.blockLabel;
+import static com.google.gerrit.server.project.Util.category;
+import static com.google.gerrit.server.project.Util.value;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -22,22 +30,35 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
 
 import org.eclipse.jgit.lib.Constants;
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -48,19 +69,19 @@
   @Test
   public void get() throws Exception {
     PushOneCommit.Result r = createChange();
-    String triplet = "p~master~" + r.getChangeId();
+    String triplet = project.get() + "~master~" + r.getChangeId();
     ChangeInfo c = info(triplet);
     assertThat(c.id).isEqualTo(triplet);
-    assertThat(c.project).isEqualTo("p");
+    assertThat(c.project).isEqualTo(project.get());
     assertThat(c.branch).isEqualTo("master");
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
     assertThat(c.subject).isEqualTo("test commit");
     assertThat(c.mergeable).isTrue();
     assertThat(c.changeId).isEqualTo(r.getChangeId());
     assertThat(c.created).isEqualTo(c.updated);
-    assertThat(c._number).is(1);
+    assertThat(c._number).isEqualTo(r.getChange().getId().get());
 
-    assertThat(c.owner._accountId).is(admin.getId().get());
+    assertThat(c.owner._accountId).isEqualTo(admin.getId().get());
     assertThat(c.owner.name).isNull();
     assertThat(c.owner.email).isNull();
     assertThat(c.owner.username).isNull();
@@ -70,20 +91,32 @@
   @Test
   public void abandon() throws Exception {
     PushOneCommit.Result r = createChange();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes()
         .id(r.getChangeId())
         .abandon();
+    ChangeInfo info = get(r.getChangeId());
+    assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("abandoned");
   }
 
   @Test
   public void restore() throws Exception {
     PushOneCommit.Result r = createChange();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
     gApi.changes()
         .id(r.getChangeId())
         .abandon();
+    assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.ABANDONED);
+
     gApi.changes()
         .id(r.getChangeId())
         .restore();
+    ChangeInfo info = get(r.getChangeId());
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("restored");
   }
 
   @Test
@@ -97,15 +130,112 @@
         .id(r.getChangeId())
         .revision(r.getCommit().name())
         .submit();
+    ChangeInfo revertChange =
+        gApi.changes()
+            .id(r.getChangeId())
+            .revert().get();
+
+    // expected messages on source change:
+    // 1. Uploaded patch set 1.
+    // 2. Patch Set 1: Code-Review+2
+    // 3. Change has been successfully merged by Administrator
+    // 4. Patch Set 1: Reverted
+    List<ChangeMessageInfo> sourceMessages = new ArrayList<>(
+        gApi.changes().id(r.getChangeId()).get().messages);
+    assertThat(sourceMessages).hasSize(4);
+    String expectedMessage = String.format(
+        "Patch Set 1: Reverted\n\n" +
+        "This patchset was reverted in change: %s",
+        revertChange.changeId);
+    assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
+
+    assertThat(revertChange.messages).hasSize(1);
+    assertThat(revertChange.messages.iterator().next().message)
+        .isEqualTo("Uploaded patch set 1.");
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void revertInitialCommit() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Cannot revert initial commit");
     gApi.changes()
         .id(r.getChangeId())
         .revert();
   }
 
-  // Change is already up to date
-  @Test(expected = ResourceConflictException.class)
+  @Test
   public void rebase() throws Exception {
+    // Create two changes both with the same parent
     PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Approve and submit the first change
+    RevisionApi revision = gApi.changes()
+        .id(r.getChangeId())
+        .current();
+    revision.review(ReviewInput.approve());
+    revision.submit();
+
+    // Rebase the second change
+    gApi.changes()
+        .id(r2.getChangeId())
+        .current()
+        .rebase();
+
+    // Second change should have 2 patch sets
+    assertThat(r2.getPatchSetId().get()).isEqualTo(2);
+
+    // ...and the committer should be correct
+    ChangeInfo info = gApi.changes()
+        .id(r2.getChangeId()).get(EnumSet.of(
+            ListChangesOption.CURRENT_REVISION,
+            ListChangesOption.CURRENT_COMMIT));
+    GitPerson committer = info.revisions.get(
+        info.currentRevision).commit.committer;
+    assertThat(committer.name).isEqualTo(admin.fullName);
+    assertThat(committer.email).isEqualTo(admin.email);
+  }
+
+  @Test(expected = ResourceConflictException.class)
+  public void rebaseUpToDateChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .rebase();
+  }
+
+  @Test
+  public void rebaseConflict() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, "other content",
+        "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    exception.expect(ResourceConflictException.class);
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
@@ -126,7 +256,7 @@
         .revision(r3.getCommit().name())
         .rebase(ri);
     PatchSet ps3 = r3.getPatchSet();
-    assertThat(ps3.getId().get()).is(2);
+    assertThat(ps3.getId().get()).isEqualTo(2);
 
     // rebase r2 onto r3 (referenced by ref)
     ri.base = ps3.getId().toRefName();
@@ -135,7 +265,7 @@
         .revision(r2.getCommit().name())
         .rebase(ri);
     PatchSet ps2 = r2.getPatchSet();
-    assertThat(ps2.getId().get()).is(2);
+    assertThat(ps2.getId().get()).isEqualTo(2);
 
     // rebase r1 onto r2 (referenced by commit)
     ri.base = ps2.getRevision().get();
@@ -144,7 +274,7 @@
         .revision(r1.getCommit().name())
         .rebase(ri);
     PatchSet ps1 = r1.getPatchSet();
-    assertThat(ps1.getId().get()).is(2);
+    assertThat(ps1.getId().get()).isEqualTo(2);
 
     // rebase r1 onto r3 (referenced by change number)
     ri.base = String.valueOf(r3.getChange().getId().get());
@@ -152,7 +282,7 @@
         .id(r1.getChangeId())
         .revision(ps1.getRevision().get())
         .rebase(ri);
-    assertThat(r1.getPatchSetId().get()).is(3);
+    assertThat(r1.getPatchSetId().get()).isEqualTo(3);
   }
 
   @Test(expected = ResourceConflictException.class)
@@ -188,7 +318,7 @@
         .id(r.getChangeId())
         .addReviewer(in);
 
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(user.id));
   }
 
@@ -204,7 +334,7 @@
         .revision(r.getCommit().name())
         .submit();
 
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
       .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
 
     AddReviewerInput in = new AddReviewerInput();
@@ -212,11 +342,76 @@
     gApi.changes()
         .id(r.getChangeId())
         .addReviewer(in);
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
   }
 
   @Test
+  public void nonVotingReviewerStaysAfterSubmit() throws Exception {
+    LabelType verified = category("Verified",
+        value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().put(verified.getName(), verified);
+    String heads = "refs/heads/*";
+    AccountGroup.UUID owners =
+        SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
+    AccountGroup.UUID registered =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    Util.allow(cfg,
+        Permission.forLabel(verified.getName()), -1, 1, owners, heads);
+    Util.allow(cfg,
+        Permission.forLabel("Code-Review"), -2, +2, registered, heads);
+    saveProjectConfig(project, cfg);
+
+    // Set Code-Review+2 and Verified+1 as admin (change owner)
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String commit = r.getCommit().name();
+    ReviewInput input = ReviewInput.approve();
+    input.label(verified.getName(), 1);
+    gApi.changes()
+        .id(changeId)
+        .revision(commit)
+        .review(input);
+
+    // Reviewers should only be "admin"
+    assertThat(getReviewers(changeId))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+
+    // Add the user as reviewer
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes()
+        .id(changeId)
+        .addReviewer(in);
+    assertThat(getReviewers(changeId))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+
+    // Approve the change as user, then remove the approval
+    // (only to confirm that the user does have Code-Review+2 permission)
+    setApiUser(user);
+    gApi.changes()
+        .id(changeId)
+        .revision(commit)
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(changeId)
+        .revision(commit)
+        .review(ReviewInput.noScore());
+
+    // Submit the change
+    setApiUser(admin);
+    gApi.changes()
+        .id(changeId)
+        .revision(commit)
+        .submit();
+
+    // User should still be on the change
+    assertThat(getReviewers(changeId))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
+  }
+
+  @Test
   public void createEmptyChange() throws Exception {
     ChangeInfo in = new ChangeInfo();
     in.branch = Constants.MASTER;
@@ -235,38 +430,35 @@
 
   @Test
   public void queryChangesNoQuery() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = createChange();
+    PushOneCommit.Result r = createChange();
     List<ChangeInfo> results = gApi.changes().query().get();
-    assertThat(results).hasSize(2);
-    assertThat(results.get(0).changeId).isEqualTo(r2.getChangeId());
-    assertThat(results.get(1).changeId).isEqualTo(r1.getChangeId());
+    assertThat(results.size()).isAtLeast(1);
+    List<Integer> ids = new ArrayList<>(results.size());
+    for (int i = 0; i < results.size(); i++) {
+      ChangeInfo info = results.get(i);
+      if (i == 0) {
+        assertThat(info._number).isEqualTo(r.getChange().getId().get());
+      }
+      assertThat(Change.Status.forChangeStatus(info.status).isOpen()).isTrue();
+      ids.add(info._number);
+    }
+    assertThat(ids).contains(r.getChange().getId().get());
   }
 
   @Test
   public void queryChangesNoResults() throws Exception {
     createChange();
-    List<ChangeInfo> results = query("status:open");
-    assertThat(results).hasSize(1);
-    results = query("status:closed");
-    assertThat(results).isEmpty();
+    assertThat(query("message:test")).isNotEmpty();
+    assertThat(query("message:{" + getClass().getName() + "fhqwhgads}"))
+        .isEmpty();
   }
 
   @Test
-  public void queryChangesOneTerm() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 = createChange();
-    List<ChangeInfo> results = query("status:open");
-    assertThat(results).hasSize(2);
-    assertThat(results.get(0).changeId).isEqualTo(r2.getChangeId());
-    assertThat(results.get(1).changeId).isEqualTo(r1.getChangeId());
-  }
-
-  @Test
-  public void queryChangesMultipleTerms() throws Exception {
+  public void queryChanges() throws Exception {
     PushOneCommit.Result r1 = createChange();
     createChange();
-    List<ChangeInfo> results = query("status:open " + r1.getChangeId());
+    List<ChangeInfo> results =
+        query("project:{" + project.get() + "} " + r1.getChangeId());
     assertThat(Iterables.getOnlyElement(results).changeId)
         .isEqualTo(r1.getChangeId());
   }
@@ -285,7 +477,8 @@
   public void queryChangesStart() throws Exception {
     PushOneCommit.Result r1 = createChange();
     createChange();
-    List<ChangeInfo> results = gApi.changes().query().withStart(1).get();
+    List<ChangeInfo> results = gApi.changes()
+        .query("project:{" + project.get() + "}").withStart(1).get();
     assertThat(Iterables.getOnlyElement(results).changeId)
         .isEqualTo(r1.getChangeId());
   }
@@ -295,7 +488,7 @@
     PushOneCommit.Result r = createChange();
     ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId()));
     assertThat(result.labels).isNull();
-    assertThat((Iterable<?>)result.messages).isNull();
+    assertThat(result.messages).isNull();
     assertThat(result.revisions).isNull();
     assertThat(result.actions).isNull();
   }
@@ -309,13 +502,13 @@
         .get());
     assertThat(Iterables.getOnlyElement(result.labels.keySet()))
         .isEqualTo("Code-Review");
-    assertThat((Iterable<?>)result.messages).hasSize(1);
+    assertThat(result.messages).hasSize(1);
     assertThat(result.actions).isNotEmpty();
 
     RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
     assertThat(rev._number).isEqualTo(r.getPatchSetId().get());
     assertThat(rev.created).isNotNull();
-    assertThat(rev.uploader._accountId).is(admin.getId().get());
+    assertThat(rev.uploader._accountId).isEqualTo(admin.getId().get());
     assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName());
     assertThat(rev.actions).isNotEmpty();
   }
@@ -323,7 +516,8 @@
   @Test
   public void queryChangesOwnerWithDifferentUsers() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(Iterables.getOnlyElement(query("owner:self")).changeId)
+    assertThat(Iterables.getOnlyElement(
+            query("project:{" + project.get() + "} owner:self")).changeId)
         .isEqualTo(r.getChangeId());
     setApiUser(user);
     assertThat(query("owner:self")).isEmpty();
@@ -377,4 +571,143 @@
         .get(EnumSet.of(ListChangesOption.CHECK))
         .problems).isEmpty();
   }
+
+  @Test
+  public void commitFooters() throws Exception {
+    LabelType verified = category("Verified",
+        value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+    LabelType custom1 = category("Custom1",
+        value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+    LabelType custom2 = category("Custom2",
+        value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().put(verified.getName(), verified);
+    cfg.getLabelSections().put(custom1.getName(), custom1);
+    cfg.getLabelSections().put(custom2.getName(), custom2);
+    String heads = "refs/heads/*";
+    AccountGroup.UUID anon =
+        SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel("Verified"), -1, 1, anon, heads);
+    Util.allow(cfg, Permission.forLabel("Custom1"), -1, 1, anon, heads);
+    Util.allow(cfg, Permission.forLabel("Custom2"), -1, 1, anon, heads);
+    saveProjectConfig(project, cfg);
+
+    PushOneCommit.Result r1 = createChange();
+    r1.assertOkStatus();
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content",
+          r1.getChangeId())
+        .to("refs/for/master");
+    r2.assertOkStatus();
+
+    ReviewInput in = new ReviewInput();
+    in.label("Code-Review", 1);
+    in.label("Verified", 1);
+    in.label("Custom1", -1);
+    in.label("Custom2", 1);
+    gApi.changes().id(r2.getChangeId()).current().review(in);
+
+    EnumSet<ListChangesOption> options = EnumSet.of(
+        ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS);
+    ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(options);
+    assertThat(actual.revisions).hasSize(2);
+
+    // No footers except on latest patch set.
+    assertThat(actual.revisions.get(r1.getCommit().getName()).commitWithFooters)
+        .isNull();
+
+    List<String> footers =
+        new ArrayList<>(Arrays.asList(
+            actual.revisions.get(r2.getCommit().getName())
+            .commitWithFooters.split("\\n")));
+    // remove subject + blank line
+    footers.remove(0);
+    footers.remove(0);
+
+    List<String> expectedFooters = Arrays.asList(
+        "Change-Id: " + r2.getChangeId(),
+        "Reviewed-on: "
+            + canonicalWebUrl.get() + r2.getChange().getId(),
+        "Reviewed-by: Administrator <admin@example.com>",
+        "Custom2: Administrator <admin@example.com>",
+        "Tested-by: Administrator <admin@example.com>");
+
+    assertThat(footers).containsExactlyElementsIn(expectedFooters);
+  }
+
+  @Test
+  public void defaultSearchDoesNotTouchDatabase() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .submit();
+
+    createChange();
+
+    setApiUserAnonymous(); // Identified user may async get stars from DB.
+    atrScope.disableDb();
+    assertThat(gApi.changes().query()
+          .withQuery(
+            "project:{" + project.get() + "} (status:open OR status:closed)")
+          // Options should match defaults in AccountDashboardScreen.
+          .withOption(ListChangesOption.LABELS)
+          .withOption(ListChangesOption.DETAILED_ACCOUNTS)
+          .withOption(ListChangesOption.REVIEWED)
+          .get())
+        .hasSize(2);
+  }
+
+  @Test
+  public void votable() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    gApi.changes().id(triplet).addReviewer(user.username);
+    ChangeInfo c = gApi.changes().id(triplet).get(EnumSet.of(
+        ListChangesOption.DETAILED_LABELS));
+    LabelInfo codeReview = c.labels.get("Code-Review");
+    assertThat(codeReview.all).hasSize(1);
+    ApprovalInfo approval = codeReview.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval.value).isEqualTo(0);
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*");
+    saveProjectConfig(project, cfg);
+    c = gApi.changes().id(triplet).get(EnumSet.of(
+        ListChangesOption.DETAILED_LABELS));
+    codeReview = c.labels.get("Code-Review");
+    assertThat(codeReview.all).hasSize(1);
+    approval = codeReview.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval.value).isNull();
+  }
+
+  @Test
+  public void pushCertificates() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = amendChange(r1.getChangeId());
+
+    ChangeInfo info = gApi.changes()
+        .id(r1.getChangeId())
+        .get(EnumSet.of(
+            ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.PUSH_CERTIFICATES));
+
+    RevisionInfo rev1 = info.revisions.get(r1.getCommit().name());
+    assertThat(rev1).isNotNull();
+    assertThat(rev1.pushCertificate).isNotNull();
+    assertThat(rev1.pushCertificate.certificate).isNull();
+    assertThat(rev1.pushCertificate.key).isNull();
+
+    RevisionInfo rev2 = info.revisions.get(r2.getCommit().name());
+    assertThat(rev2).isNotNull();
+    assertThat(rev2.pushCertificate).isNotNull();
+    assertThat(rev2.pushCertificate.certificate).isNull();
+    assertThat(rev2.pushCertificate.key).isNull();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
new file mode 100644
index 0000000..4918a95
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
@@ -0,0 +1,7 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  group = 'api-config',
+  srcs = glob(['*IT.java']),
+  labels = ['api'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/ServerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/ServerIT.java
new file mode 100644
index 0000000..8646aff
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/ServerIT.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 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.api.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.Version;
+
+import org.junit.Test;
+
+@NoHttpd
+public class ServerIT extends AbstractDaemonTest {
+  @Test
+  public void getVersion() throws Exception {
+    assertThat(gApi.config().server().getVersion())
+        .isEqualTo(Version.getVersion());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
new file mode 100644
index 0000000..06be8ee
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
@@ -0,0 +1,23 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  group = 'api-group',
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
+  ],
+  labels = ['api']
+)
+
+java_library(
+  name = 'util',
+  srcs = ['GroupAssert.java'],
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//lib:gwtorm',
+    '//lib:truth',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
similarity index 94%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
index 97503f4..c3c2224 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest.group;
+package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 
 import java.util.Set;
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
new file mode 100644
index 0000000..8d1d89f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -0,0 +1,549 @@
+// Copyright (C) 2015 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.api.group;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
+import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo.GroupMemberAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo.Type;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.SystemGroupBackend;
+
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@NoHttpd
+public class GroupsIT extends AbstractDaemonTest {
+  @Test
+  public void addToNonExistingGroup_NotFound() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.groups().id("non-existing").addMembers("admin");
+  }
+
+  @Test
+  public void removeFromNonExistingGroup_NotFound() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.groups().id("non-existing").removeMembers("admin");
+  }
+
+  @Test
+  public void addRemoveMember() throws Exception {
+    String g = createGroup("users");
+    gApi.groups().id(g).addMembers("user");
+    assertMembers(g, user);
+
+    gApi.groups().id(g).removeMembers("user");
+    assertNoMembers(g);
+  }
+
+  @Test
+  public void addExistingMember_OK() throws Exception {
+    String g = "Administrators";
+    assertMembers(g, admin);
+    gApi.groups().id("Administrators").addMembers("admin");
+    assertMembers(g, admin);
+  }
+
+  @Test
+  public void addNonExistingMember_UnprocessableEntity() throws Exception {
+    exception.expect(UnprocessableEntityException.class);
+    gApi.groups().id("Administrators").addMembers("non-existing");
+  }
+
+  @Test
+  public void addMultipleMembers() throws Exception {
+    String g = createGroup("users");
+    TestAccount u1 = accounts.create("u1", "u1@example.com", "Full Name 1");
+    TestAccount u2 = accounts.create("u2", "u2@example.com", "Full Name 2");
+    gApi.groups().id(g).addMembers(u1.username, u2.username);
+    assertMembers(g, u1, u2);
+  }
+
+  @Test
+  public void includeRemoveGroup() throws Exception {
+    String p = createGroup("parent");
+    String g = createGroup("newGroup");
+    gApi.groups().id(p).addGroups(g);
+    assertIncludes(p, g);
+
+    gApi.groups().id(p).removeGroups(g);
+    assertNoIncludes(p);
+  }
+
+  @Test
+  public void includeExistingGroup_OK() throws Exception {
+    String p = createGroup("parent");
+    String g = createGroup("newGroup");
+    gApi.groups().id(p).addGroups(g);
+    assertIncludes(p, g);
+    gApi.groups().id(p).addGroups(g);
+    assertIncludes(p, g);
+  }
+
+  @Test
+  public void addMultipleIncludes() throws Exception {
+    String p = createGroup("parent");
+    String g1 = createGroup("newGroup1");
+    String g2 = createGroup("newGroup2");
+    List<String> groups = Lists.newLinkedList();
+    groups.add(g1);
+    groups.add(g2);
+    gApi.groups().id(p).addGroups(g1, g2);
+    assertIncludes(p, g1, g2);
+  }
+
+  @Test
+  public void testCreateGroup() throws Exception {
+    String newGroupName = name("newGroup");
+    GroupInfo g = gApi.groups().create(newGroupName).get();
+    assertGroupInfo(getFromCache(newGroupName), g);
+  }
+
+  @Test
+  public void testCreateGroupWithProperties() throws Exception {
+    GroupInput in = new GroupInput();
+    in.name = name("newGroup");
+    in.description = "Test description";
+    in.visibleToAll = true;
+    in.ownerId = getFromCache("Administrators").getGroupUUID().get();
+    GroupInfo g = gApi.groups().create(in).detail();
+    assertThat(g.description).isEqualTo(in.description);
+    assertThat(g.options.visibleToAll).isEqualTo(in.visibleToAll);
+    assertThat(g.ownerId).isEqualTo(in.ownerId);
+  }
+
+  @Test
+  public void testCreateGroupWithoutCapability_Forbidden() throws Exception {
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    gApi.groups().create(name("newGroup"));
+  }
+
+  @Test
+  public void testCreateGroupWhenGroupAlreadyExists_Conflict()
+      throws Exception {
+    exception.expect(ResourceConflictException.class);
+    gApi.groups().create("Administrators");
+  }
+
+  @Test
+  public void testGetGroup() throws Exception {
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    testGetGroup(adminGroup.getGroupUUID().get(), adminGroup);
+    testGetGroup(adminGroup.getName(), adminGroup);
+    testGetGroup(adminGroup.getId().get(), adminGroup);
+  }
+
+  private void testGetGroup(Object id, AccountGroup expectedGroup)
+      throws Exception {
+    GroupInfo group = gApi.groups().id(id.toString()).get();
+    assertGroupInfo(expectedGroup, group);
+  }
+
+  @Test
+  public void testGroupName() throws Exception {
+    String name = name("group");
+    gApi.groups().create(name);
+
+    // get name
+    assertThat(gApi.groups().id(name).name()).isEqualTo(name);
+
+    // set name to same name
+    gApi.groups().id(name).name(name);
+    assertThat(gApi.groups().id(name).name()).isEqualTo(name);
+
+    // set name with name conflict
+    String other = name("other");
+    gApi.groups().create(other);
+    exception.expect(ResourceConflictException.class);
+    gApi.groups().id(name).name(other);
+  }
+
+  @Test
+  public void testGroupRename() throws Exception {
+    String name = name("group");
+    gApi.groups().create(name);
+
+    String newName = name("newName");
+    gApi.groups().id(name).name(newName);
+    assertThat(getFromCache(newName)).isNotNull();
+    assertThat(gApi.groups().id(newName).name()).isEqualTo(newName);
+
+    assertThat(getFromCache(name)).isNull();
+    exception.expect(ResourceNotFoundException.class);
+    gApi.groups().id(name).get();
+  }
+
+  @Test
+  public void testGroupDescription() throws Exception {
+    String name = name("group");
+    gApi.groups().create(name);
+
+    // get description
+    assertThat(gApi.groups().id(name).description()).isEmpty();
+
+    // set description
+    String desc = "New description for the group.";
+    gApi.groups().id(name).description(desc);
+    assertThat(gApi.groups().id(name).description()).isEqualTo(desc);
+
+    // set description to null
+    gApi.groups().id(name).description(null);
+    assertThat(gApi.groups().id(name).description()).isEmpty();
+
+    // set description to empty string
+    gApi.groups().id(name).description("");
+    assertThat(gApi.groups().id(name).description()).isEmpty();
+  }
+
+  @Test
+  public void testGroupOptions() throws Exception {
+    String name = name("group");
+    gApi.groups().create(name);
+
+    // get options
+    assertThat(gApi.groups().id(name).options().visibleToAll).isNull();
+
+    // set options
+    GroupOptionsInfo options = new GroupOptionsInfo();
+    options.visibleToAll = true;
+    gApi.groups().id(name).options(options);
+    assertThat(gApi.groups().id(name).options().visibleToAll).isTrue();
+  }
+
+  @Test
+  public void testGroupOwner() throws Exception {
+    String name = name("group");
+    GroupInfo info = gApi.groups().create(name).get();
+    String adminUUID = getFromCache("Administrators").getGroupUUID().get();
+    String registeredUUID = SystemGroupBackend.REGISTERED_USERS.get();
+
+    // get owner
+    assertThat(Url.decode(gApi.groups().id(name).owner().id))
+        .isEqualTo(info.id);
+
+    // set owner by name
+    gApi.groups().id(name).owner("Registered Users");
+    assertThat(Url.decode(gApi.groups().id(name).owner().id))
+        .isEqualTo(registeredUUID);
+
+    // set owner by UUID
+    gApi.groups().id(name).owner(adminUUID);
+    assertThat(Url.decode(gApi.groups().id(name).owner().id))
+        .isEqualTo(adminUUID);
+
+    // set non existing owner
+    exception.expect(UnprocessableEntityException.class);
+    gApi.groups().id(name).owner("Non-Existing Group");
+  }
+
+  @Test
+  public void listNonExistingGroupIncludes_NotFound() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.groups().id("non-existing").includedGroups();
+  }
+
+  @Test
+  public void listEmptyGroupIncludes() throws Exception {
+    String gx = createGroup("gx");
+    assertThat(gApi.groups().id(gx).includedGroups()).isEmpty();
+  }
+
+  @Test
+  public void includeNonExistingGroup() throws Exception {
+    String gx = createGroup("gx");
+    exception.expect(UnprocessableEntityException.class);
+    gApi.groups().id(gx).addGroups("non-existing");
+  }
+
+  @Test
+  public void listNonEmptyGroupIncludes() throws Exception {
+    String gx = createGroup("gx");
+    String gy = createGroup("gy");
+    String gz = createGroup("gz");
+    gApi.groups().id(gx).addGroups(gy);
+    gApi.groups().id(gx).addGroups(gz);
+    assertIncludes(gApi.groups().id(gx).includedGroups(), gy, gz);
+  }
+
+  @Test
+  public void listOneIncludeMember() throws Exception {
+    String gx = createGroup("gx");
+    String gy = createGroup("gy");
+    gApi.groups().id(gx).addGroups(gy);
+    assertIncludes(gApi.groups().id(gx).includedGroups(), gy);
+  }
+
+  @Test
+  public void listNonExistingGroupMembers_NotFound() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.groups().id("non-existing").members();
+  }
+
+  @Test
+  public void listEmptyGroupMembers() throws Exception {
+    String group = createGroup("empty");
+    assertThat(gApi.groups().id(group).members()).isEmpty();
+  }
+
+  @Test
+  public void listNonEmptyGroupMembers() throws Exception {
+    String group = createGroup("group");
+    String user1 = createAccount("user1", group);
+    String user2 = createAccount("user2", group);
+    assertMembers(gApi.groups().id(group).members(), user1, user2);
+  }
+
+  @Test
+  public void listOneGroupMember() throws Exception {
+    String group = createGroup("group");
+    String user = createAccount("user1", group);
+    assertMembers(gApi.groups().id(group).members(), user);
+  }
+
+  @Test
+  public void listGroupMembersRecursively() throws Exception {
+    String gx = createGroup("gx");
+    String ux = createAccount("ux", gx);
+
+    String gy = createGroup("gy");
+    String uy = createAccount("uy", gy);
+
+    String gz = createGroup("gz");
+    String uz = createAccount("uz", gz);
+
+    gApi.groups().id(gx).addGroups(gy);
+    gApi.groups().id(gy).addGroups(gz);
+    assertMembers(gApi.groups().id(gx).members(), ux);
+    assertMembers(gApi.groups().id(gx).members(true), ux, uy, uz);
+  }
+
+  @Test
+  public void defaultGroupsCreated() throws Exception {
+    Iterable<String> names = gApi.groups().list().getAsMap().keySet();
+    assertThat(names).containsAllOf("Administrators", "Non-Interactive Users")
+        .inOrder();
+  }
+
+  @Test
+  public void testListAllGroups() throws Exception {
+    List<String> expectedGroups = FluentIterable
+          .from(groupCache.all())
+          .transform(new Function<AccountGroup, String>() {
+            @Override
+            public String apply(AccountGroup group) {
+              return group.getName();
+            }
+          }).toSortedList(Ordering.natural());
+    assertThat(expectedGroups.size()).isAtLeast(2);
+    assertThat(gApi.groups().list().getAsMap().keySet())
+        .containsExactlyElementsIn(expectedGroups).inOrder();
+  }
+
+  @Test
+  public void testOnlyVisibleGroupsReturned() throws Exception {
+    String newGroupName = name("newGroup");
+    GroupInput in = new GroupInput();
+    in.name = newGroupName;
+    in.description = "a hidden group";
+    in.visibleToAll = false;
+    in.ownerId = getFromCache("Administrators").getGroupUUID().get();
+    gApi.groups().create(in);
+
+    setApiUser(user);
+    assertThat(gApi.groups().list().getAsMap())
+        .doesNotContainKey(newGroupName);
+
+    setApiUser(admin);
+    gApi.groups().id(newGroupName).addMembers(user.username);
+
+    setApiUser(user);
+    assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
+  }
+
+  @Test
+  public void testSuggestGroup() throws Exception {
+    Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
+    assertThat(groups).containsKey("Administrators");
+    assertThat(groups).hasSize(1);
+  }
+
+  @Test
+  public void testAllGroupInfoFieldsSetCorrectly() throws Exception {
+    AccountGroup adminGroup = getFromCache("Administrators");
+    Map<String, GroupInfo> groups =
+        gApi.groups().list().addGroup(adminGroup.getName()).getAsMap();
+    assertThat(groups).hasSize(1);
+    assertThat(groups).containsKey("Administrators");
+    assertGroupInfo(adminGroup, Iterables.getOnlyElement(groups.values()));
+  }
+
+  @Test
+  public void getAuditLog() throws Exception {
+    GroupApi g = gApi.groups().create(name("group"));
+    List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
+    assertThat(auditEvents).hasSize(1);
+    assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
+
+    g.addMembers(user.username);
+    auditEvents = g.auditLog();
+    assertThat(auditEvents).hasSize(2);
+    assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+
+    g.removeMembers(user.username);
+    auditEvents = g.auditLog();
+    assertThat(auditEvents).hasSize(3);
+    assertAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
+
+    String otherGroup = name("otherGroup");
+    gApi.groups().create(otherGroup);
+    g.addGroups(otherGroup);
+    auditEvents = g.auditLog();
+    assertThat(auditEvents).hasSize(4);
+    assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+
+    g.removeGroups(otherGroup);
+    auditEvents = g.auditLog();
+    assertThat(auditEvents).hasSize(5);
+    assertAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
+
+    Timestamp lastDate = null;
+    for (GroupAuditEventInfo auditEvent : auditEvents) {
+      if (lastDate != null) {
+        assertThat(lastDate).isGreaterThan(auditEvent.date);
+      }
+      lastDate = auditEvent.date;
+    }
+  }
+
+  private void assertAuditEvent(GroupAuditEventInfo info, Type expectedType,
+      Account.Id expectedUser, Account.Id expectedMember) {
+    assertThat(info.user._accountId).isEqualTo(expectedUser.get());
+    assertThat(info.type).isEqualTo(expectedType);
+    assertThat(info).isInstanceOf(UserMemberAuditEventInfo.class);
+    assertThat(((UserMemberAuditEventInfo) info).member._accountId).isEqualTo(
+        expectedMember.get());
+  }
+
+  private void assertAuditEvent(GroupAuditEventInfo info, Type expectedType,
+      Account.Id expectedUser, String expectedMemberGroupName) {
+    assertThat(info.user._accountId).isEqualTo(expectedUser.get());
+    assertThat(info.type).isEqualTo(expectedType);
+    assertThat(info).isInstanceOf(GroupMemberAuditEventInfo.class);
+    assertThat(((GroupMemberAuditEventInfo) info).member.name).isEqualTo(
+        expectedMemberGroupName);
+  }
+
+  private void assertMembers(String group, TestAccount... expectedMembers)
+      throws Exception {
+    assertMembers(
+        gApi.groups().id(group).members(),
+        TestAccount.names(expectedMembers).toArray(String.class));
+    assertAccountInfos(
+        Arrays.asList(expectedMembers),
+        gApi.groups().id(group).members());
+  }
+
+  private void assertMembers(Iterable<AccountInfo> members,
+      String... expectedNames) {
+    Iterable<String> memberNames = Iterables.transform(members,
+        new Function<AccountInfo, String>() {
+          @Override
+          public String apply(@Nullable AccountInfo info) {
+            return info.name;
+          }
+        });
+    assertThat(memberNames)
+        .containsExactlyElementsIn(Arrays.asList(expectedNames)).inOrder();
+  }
+
+  private void assertNoMembers(String group) throws Exception {
+    assertThat(gApi.groups().id(group).members()).isEmpty();
+  }
+
+  private void assertIncludes(String group, String... expectedNames)
+      throws Exception {
+    assertIncludes(gApi.groups().id(group).includedGroups(), expectedNames);
+  }
+
+  private static void assertIncludes(
+      Iterable<GroupInfo> includes, String... expectedNames) {
+    Iterable<String> includeNames = Iterables.transform(
+        includes,
+        new Function<GroupInfo, String>() {
+          @Override
+          public String apply(@Nullable GroupInfo info) {
+            return info.name;
+          }
+        });
+    assertThat(includeNames)
+        .containsExactlyElementsIn(Arrays.asList(expectedNames)).inOrder();
+  }
+
+  private void assertNoIncludes(String group) throws Exception {
+    assertThat(gApi.groups().id(group).includedGroups()).isEmpty();
+  }
+
+  private AccountGroup getFromCache(String name) throws Exception {
+    return groupCache.get(new AccountGroup.NameKey(name));
+  }
+
+  private String createGroup(String name) throws Exception {
+    return createGroup(name, "Administrators");
+  }
+
+  private String createGroup(String name, String owner) throws Exception {
+    name = name(name);
+    GroupInput in = new GroupInput();
+    in.name = name;
+    in.ownerId = owner;
+    gApi.groups().create(in);
+    return name;
+  }
+
+  private String createAccount(String name, String group) throws Exception {
+    name = name(name);
+    accounts.create(name, group);
+    return name;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
index 1152d88..9dab9f8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-project',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 7de4712..d837565 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -22,57 +22,55 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
-import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
 
 import org.junit.Test;
 
-import java.util.List;
-
 @NoHttpd
 public class ProjectIT extends AbstractDaemonTest  {
 
   @Test
   public void createProjectFoo() throws Exception {
-    String name = "foo";
+    String name = name("foo");
     assertThat(name).isEqualTo(
         gApi.projects()
-            .name(name)
-            .create()
+            .create(name)
             .get()
             .name);
   }
 
   @Test
   public void createProjectFooWithGitSuffix() throws Exception {
-    String name = "foo";
+    String name = name("foo");
     assertThat(name).isEqualTo(
         gApi.projects()
-            .name(name + ".git")
-            .create()
+            .create(name + ".git")
             .get()
             .name);
   }
 
-  @Test(expected = RestApiException.class)
-  public void createProjectFooBar() throws Exception {
+  @Test
+  public void createProjectWithMismatchedInput() throws Exception {
     ProjectInput in = new ProjectInput();
-    in.name = "foo";
+    in.name = name("foo");
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("name must match input.name");
     gApi.projects()
         .name("bar")
         .create(in);
   }
 
-  @Test(expected = ResourceConflictException.class)
+  @Test
   public void createProjectDuplicate() throws Exception {
     ProjectInput in = new ProjectInput();
-    in.name = "baz";
+    in.name = name("baz");
     gApi.projects()
-        .name("baz")
         .create(in);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Project already exists");
     gApi.projects()
-        .name("baz")
         .create(in);
   }
 
@@ -86,43 +84,19 @@
   }
 
   @Test
-  public void listProjects() throws Exception {
-    List<ProjectInfo> initialProjects = gApi.projects().list().get();
-
-    gApi.projects().name("foo").create();
-    gApi.projects().name("bar").create();
-
-    List<ProjectInfo> allProjects = gApi.projects().list().get();
-    assertThat(allProjects).hasSize(initialProjects.size() + 2);
-
-    List<ProjectInfo> projectsWithDescription = gApi.projects().list()
-        .withDescription(true)
-        .get();
-    assertThat(projectsWithDescription.get(0).description).isNotNull();
-
-    List<ProjectInfo> projectsWithoutDescription = gApi.projects().list()
-        .withDescription(false)
-        .get();
-    assertThat(projectsWithoutDescription.get(0).description).isNull();
-
-    List<ProjectInfo> noMatchingProjects = gApi.projects().list()
-        .withPrefix("fox")
-        .get();
-    assertThat(noMatchingProjects).isEmpty();
-
-    List<ProjectInfo> matchingProject = gApi.projects().list()
-        .withPrefix("fo")
-        .get();
-    assertThat(matchingProject).hasSize(1);
-
-    List<ProjectInfo> limitOneProject = gApi.projects().list()
-        .withLimit(1)
-        .get();
-    assertThat(limitOneProject).hasSize(1);
-
-    List<ProjectInfo> startAtOneProjects = gApi.projects().list()
-        .withStart(1)
-        .get();
-    assertThat(startAtOneProjects).hasSize(allProjects.size() - 1);
+  public void description() throws Exception {
+    assertThat(gApi.projects()
+            .name(project.get())
+            .description())
+        .isEmpty();
+    PutDescriptionInput in = new PutDescriptionInput();
+    in.description = "new project description";
+    gApi.projects()
+        .name(project.get())
+        .description(in);
+    assertThat(gApi.projects()
+            .name(project.get())
+            .description())
+        .isEqualTo(in.description);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
index 1152d88..c916755 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'api-revision',
   srcs = glob(['*IT.java']),
   labels = ['api'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 9cc8f9c..74bfa4a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -17,14 +17,16 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_OK;
 import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.junit.Assert.fail;
 
 import com.google.common.base.Predicate;
 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.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -41,27 +43,27 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Patch;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
-@NoHttpd
 public class RevisionIT extends AbstractDaemonTest {
 
   private TestAccount admin2;
@@ -75,7 +77,7 @@
   public void reviewTriplet() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(project.get() + "~master~" + r.getChangeId())
         .revision(r.getCommit().name())
         .review(ReviewInput.approve());
   }
@@ -108,11 +110,11 @@
   public void submit() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(project.get() + "~master~" + r.getChangeId())
         .current()
         .review(ReviewInput.approve());
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(project.get() + "~master~" + r.getChangeId())
         .current()
         .submit();
   }
@@ -121,14 +123,13 @@
   public void submitOnBehalfOf() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(project.get() + "~master~" + r.getChangeId())
         .current()
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = admin2.email;
-    in.waitForMerge = true;
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(project.get() + "~master~" + r.getChangeId())
         .current()
         .submit(in);
   }
@@ -153,12 +154,12 @@
         .branch(in.destination)
         .create(new BranchInput());
     ChangeApi orig = gApi.changes()
-        .id("p~master~" + r.getChangeId());
+        .id(project.get() + "~master~" + r.getChangeId());
 
-    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    assertThat(orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertThat((Iterable<?>)orig.get().messages).hasSize(2);
+    assertThat(orig.get().messages).hasSize(2);
 
     String cherryPickedRevision = cherry.get().currentRevision;
     String expectedMessage = String.format(
@@ -170,13 +171,33 @@
     origIt.next();
     assertThat(origIt.next().message).isEqualTo(expectedMessage);
 
-    assertThat((Iterable<?>)cherry.get().messages).hasSize(1);
+    assertThat(cherry.get().messages).hasSize(1);
     Iterator<ChangeMessageInfo> cherryIt = cherry.get().messages.iterator();
     expectedMessage = "Patch Set 1: Cherry Picked from branch master.";
     assertThat(cherryIt.next().message).isEqualTo(expectedMessage);
 
     assertThat(cherry.get().subject).contains(in.message);
-    assertThat(cherry.get().topic).isEqualTo("someTopic");
+    assertThat(cherry.get().topic).isEqualTo("someTopic-foo");
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+  }
+
+  @Test
+  public void cherryPickwithNoTopic() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "foo";
+    in.message = "it goes to stable branch";
+    gApi.projects()
+        .name(project.get())
+        .branch(in.destination)
+        .create(new BranchInput());
+    ChangeApi orig = gApi.changes()
+        .id(project.get() + "~master~" + r.getChangeId());
+
+    ChangeApi cherry = orig.revision(r.getCommit().name())
+        .cherryPick(in);
+    assertThat(cherry.get().topic).isNull();
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
   }
@@ -188,11 +209,11 @@
     in.destination = "master";
     in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
     ChangeInfo cherryInfo = gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(project.get() + "~master~" + r.getChangeId())
         .revision(r.getCommit().name())
         .cherryPick(in)
         .get();
-    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    assertThat(cherryInfo.messages).hasSize(2);
     Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
@@ -202,8 +223,8 @@
   public void cherryPickToSameBranchWithRebase() throws Exception {
     // Push a new change, then merge it
     PushOneCommit.Result baseChange = createChange();
-    RevisionApi baseRevision =
-        gApi.changes().id("p~master~" + baseChange.getChangeId()).current();
+    String triplet = project.get() + "~master~" + baseChange.getChangeId();
+    RevisionApi baseRevision = gApi.changes().id(triplet).current();
     baseRevision.review(ReviewInput.approve());
     baseRevision.submit();
 
@@ -214,22 +235,23 @@
     String subject = "Test change\n\n" +
         "Change-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), subject,
+        pushFactory.create(db, admin.getIdent(), testRepo, subject,
             "another_file.txt", "another content");
-    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    PushOneCommit.Result r2 = push.to("refs/for/master");
 
     // Change 2's parent should be change 1
     assertThat(r2.getCommit().getParents()[0].name())
       .isEqualTo(r1.getCommit().name());
 
     // Cherry pick change 2 onto the same branch
-    ChangeApi orig = gApi.changes().id("p~master~" + r2.getChangeId());
+    triplet = project.get() + "~master~" + r2.getChangeId();
+    ChangeApi orig = gApi.changes().id(triplet);
     CherryPickInput in = new CherryPickInput();
     in.destination = "master";
     in.message = subject;
     ChangeApi cherry = orig.revision(r2.getCommit().name()).cherryPick(in);
     ChangeInfo cherryInfo = cherry.get();
-    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    assertThat(cherryInfo.messages).hasSize(2);
     Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
@@ -252,23 +274,20 @@
         .branch(in.destination)
         .create(new BranchInput());
     ChangeApi orig = gApi.changes()
-        .id("p~master~" + r.getChangeId());
+        .id(project.get() + "~master~" + r.getChangeId());
 
-    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    assertThat(orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertThat((Iterable<?>)orig.get().messages).hasSize(2);
+    assertThat(orig.get().messages).hasSize(2);
 
     assertThat(cherry.get().subject).contains(in.message);
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
 
-    try {
-      orig.revision(r.getCommit().name()).cherryPick(in);
-      fail("Cherry-pick identical tree error expected");
-    } catch (RestApiException e) {
-      assertThat(e.getMessage()).isEqualTo("Cherry pick failed: identical tree");
-    }
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Cherry pick failed: identical tree");
+    orig.revision(r.getCommit().name()).cherryPick(in);
   }
 
   @Test
@@ -283,29 +302,27 @@
         .create(new BranchInput());
 
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME, "another content");
-    push.to(git, "refs/heads/foo");
+    push.to("refs/heads/foo");
 
-    ChangeApi orig = gApi.changes().id("p~master~" + r.getChangeId());
-    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    String triplet = project.get() + "~master~" + r.getChangeId();
+    ChangeApi orig = gApi.changes().id(triplet);
+    assertThat(orig.get().messages).hasSize(1);
 
-    try {
-      orig.revision(r.getCommit().name()).cherryPick(in);
-      fail("Cherry-pick merge conflict error expected");
-    } catch (RestApiException e) {
-      assertThat(e.getMessage()).isEqualTo("Cherry pick failed: merge conflict");
-    }
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Cherry pick failed: merge conflict");
+    orig.revision(r.getCommit().name()).cherryPick(in);
   }
 
   @Test
   public void canRebase() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    PushOneCommit.Result r1 = push.to(git, "refs/for/master");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, admin.getIdent());
-    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r2 = push.to("refs/for/master");
     boolean canRebase = gApi.changes()
         .id(r2.getChangeId())
         .revision(r2.getCommit().name())
@@ -313,9 +330,9 @@
     assertThat(canRebase).isFalse();
     merge(r2);
 
-    git.checkout().setName(r1.getCommit().name()).call();
-    push = pushFactory.create(db, admin.getIdent());
-    PushOneCommit.Result r3 = push.to(git, "refs/for/master");
+    testRepo.reset(r1.getCommit());
+    push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r3 = push.to("refs/for/master");
 
     canRebase = gApi.changes()
         .id(r3.getChangeId())
@@ -326,8 +343,8 @@
 
   @Test
   public void setUnsetReviewedFlag() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
 
     gApi.changes()
         .id(r.getChangeId())
@@ -345,31 +362,31 @@
         .current()
         .setReviewed(PushOneCommit.FILE_NAME, false);
 
-    assertThat((Iterable<?>)gApi.changes().id(r.getChangeId()).current().reviewed())
+    assertThat(gApi.changes().id(r.getChangeId()).current().reviewed())
         .isEmpty();
   }
 
   @Test
   public void mergeable() throws Exception {
-    ObjectId initial = git.getRepository().getRef(HEAD).getLeaf().getObjectId();
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
 
     PushOneCommit push1 =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME, "push 1 content");
 
-    PushOneCommit.Result r1 = push1.to(git, "refs/for/master");
+    PushOneCommit.Result r1 = push1.to("refs/for/master");
     assertMergeable(r1.getChangeId(), true);
     merge(r1);
 
     // Reset HEAD to initial so the new change is a merge conflict.
-    RefUpdate ru = git.getRepository().updateRef(HEAD);
+    RefUpdate ru = repo().updateRef(HEAD);
     ru.setNewObjectId(initial);
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME, "push 2 content");
-    PushOneCommit.Result r2 = push2.to(git, "refs/for/master");
+    PushOneCommit.Result r2 = push2.to("refs/for/master");
     assertMergeable(r2.getChangeId(), false);
     // TODO(dborowitz): Test for other-branches.
   }
@@ -412,10 +429,24 @@
         .content();
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
-    String res = new String(os.toByteArray(), StandardCharsets.UTF_8);
+    String res = new String(os.toByteArray(), UTF_8);
     assertThat(res).isEqualTo(FILE_CONTENT);
   }
 
+  @Test
+  public void contentType() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    String endPoint = "/changes/" + r.getChangeId()
+      + "/revisions/" + r.getCommit().name()
+      + "/files/" + FILE_NAME
+      + "/content";
+    RestResponse response = adminSession.head(endPoint);
+    assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+    assertThat(response.getContentType()).startsWith("text/plain");
+    assertThat(response.hasContent()).isFalse();
+  }
+
   private void assertMergeable(String id, boolean expected) throws Exception {
     MergeableInfo m = gApi.changes().id(id).current().mergeable();
     assertThat(m.mergeable).isEqualTo(expected);
@@ -505,6 +536,19 @@
     CommentInfo comment = Iterables.getOnlyElement(out.get(FILE_NAME));
     assertThat(comment.message).isEqualTo(in.message);
     assertThat(comment.author.email).isEqualTo(admin.email);
+    assertThat(comment.path).isNull();
+
+    List<CommentInfo> list = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .commentsAsList();
+    assertThat(list).hasSize(1);
+
+    CommentInfo comment2 = list.get(0);
+    assertThat(comment2.path).isEqualTo(FILE_NAME);
+    assertThat(comment2.line).isEqualTo(comment.line);
+    assertThat(comment2.message).isEqualTo(comment.message);
+    assertThat(comment2.author.email).isEqualTo(comment.author.email);
 
     assertThat(gApi.changes()
         .id(r.getChangeId())
@@ -515,21 +559,41 @@
       .isEqualTo(in.message);
   }
 
+  @Test
+  public void patch() throws Exception {
+    PushOneCommit.Result r = createChange();
+    ChangeApi changeApi = gApi.changes()
+        .id(r.getChangeId());
+    BinaryResult bin = changeApi
+        .revision(r.getCommit().name())
+        .patch();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String res = new String(os.toByteArray(), UTF_8);
+    ChangeInfo change = changeApi.get();
+    RevisionInfo rev = change.revisions.get(change.currentRevision);
+    DateFormat df = new SimpleDateFormat(
+        "EEE, dd MMM yyyy HH:mm:ss Z",
+        Locale.US);
+    String date = df.format(rev.commit.author.date);
+    assertThat(res).isEqualTo(
+        String.format(PATCH, r.getCommitId().name(), date, r.getChangeId()));
+  }
+
   private void merge(PushOneCommit.Result r) throws Exception {
     revision(r).review(ReviewInput.approve());
     revision(r).submit();
   }
 
   private PushOneCommit.Result updateChange(PushOneCommit.Result r,
-      String content) throws GitAPIException, IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+      String content) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         "test commit", "a.txt", content, r.getChangeId());
-    return push.to(git, "refs/for/master");
+    return push.to("refs/for/master");
   }
 
-  private PushOneCommit.Result createDraft() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/drafts/master");
+  private PushOneCommit.Result createDraft() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    return push.to("refs/drafts/master");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
index be6fcdc..c3274db 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
@@ -1,10 +1,11 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'edit',
   srcs = ['ChangeEditIT.java'],
-  labels = ['edit'],
   deps = [
     '//lib/commons:codec',
     '//lib/joda:joda-time',
   ],
+  labels = ['edit'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 4299896..815ea3f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -16,30 +16,30 @@
 
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.http.HttpStatus.SC_FORBIDDEN;
 import static org.apache.http.HttpStatus.SC_CONFLICT;
+import static org.apache.http.HttpStatus.SC_FORBIDDEN;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
 import static org.apache.http.HttpStatus.SC_NO_CONTENT;
 import static org.apache.http.HttpStatus.SC_OK;
-import static org.junit.Assert.fail;
 
 import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -56,28 +56,28 @@
 import com.google.gerrit.server.edit.UnchangedCommitMessageException;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.gson.stream.JsonReader;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import org.apache.commons.codec.binary.StringUtils;
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.Iterator;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicLong;
 
 public class ChangeEditIT extends AbstractDaemonTest {
 
@@ -108,33 +108,33 @@
   private PatchSet ps;
   private PatchSet ps2;
 
+  @BeforeClass
+  public static void setTimeForTesting() {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+  }
+
+  @AfterClass
+  public static void restoreTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
   @Before
   public void setUp() throws Exception {
     db = reviewDbProvider.open();
-    changeId = newChange(git, admin.getIdent());
+    changeId = newChange(admin.getIdent());
     ps = getCurrentPatchSet(changeId);
-    amendChange(git, admin.getIdent(), changeId);
+    amendChange(admin.getIdent(), changeId);
     change = getChange(changeId);
     assertThat(ps).isNotNull();
-    changeId2 = newChange2(git, admin.getIdent());
+    changeId2 = newChange2(admin.getIdent());
     change2 = getChange(changeId2);
     assertThat(change2).isNotNull();
     ps2 = getCurrentPatchSet(changeId2);
     assertThat(ps2).isNotNull();
-    final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
-    final AtomicLong clockMs = new AtomicLong(
-        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
-      @Override
-      public long getMillis() {
-        return clockMs.getAndAdd(clockStepMs);
-      }
-    });
   }
 
   @After
   public void cleanup() {
-    DateTimeUtils.setCurrentMillisSystem();
     db.close();
   }
 
@@ -156,7 +156,12 @@
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
             RestSession.newRawInput(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED);
     editUtil.publish(editUtil.byChange(change).get());
-    assertThat(editUtil.byChange(change).isPresent()).isFalse();
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(edit.isPresent()).isFalse();
+    assertChangeMessages(change,
+        ImmutableList.of("Uploaded patch set 1.",
+            "Uploaded patch set 2.",
+            "Patch Set 3: Published edit on patch set 2."));
   }
 
   @Test
@@ -174,6 +179,10 @@
     assertThat(edit.isPresent()).isFalse();
     PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
     assertThat(newCurrentPatchSet.getId()).isNotEqualTo(oldCurrentPatchSet.getId());
+    assertChangeMessages(change,
+        ImmutableList.of("Uploaded patch set 1.",
+            "Uploaded patch set 2.",
+            "Patch Set 3: Published edit on patch set 2."));
   }
 
   @Test
@@ -263,9 +272,9 @@
     assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
         current.getPatchSetId());
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT, FILE_NAME,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, FILE_NAME,
             new String(CONTENT_NEW2), changeId2);
-    push.to(git, "refs/for/master").assertOkStatus();
+    push.to("refs/for/master").assertOkStatus();
     RestResponse r = adminSession.post(urlRebase());
     assertThat(r.getStatusCode()).isEqualTo(SC_CONFLICT);
   }
@@ -285,10 +294,11 @@
   }
 
   @Test
+  @TestProjectInput(createEmptyCommit = false)
   public void updateRootCommitMessage() throws Exception {
-    createProject(sshSession, "root-msg-test", null, false);
-    git = cloneProject(sshSession.getUrl() + "/root-msg-test");
-    changeId = newChange(git, admin.getIdent());
+    // Re-clone empty repo; TestRepository doesn't let us reset to unborn head.
+    testRepo = cloneProject(project);
+    changeId = newChange(admin.getIdent());
     change = getChange(changeId);
 
     assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
@@ -305,14 +315,26 @@
   }
 
   @Test
-  public void updateMessage() throws Exception {
+  public void updateMessageNoChange() throws Exception {
     assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
         .isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
 
+    exception.expect(UnchangedCommitMessageException.class);
+    exception.expectMessage(
+        "New commit message cannot be same as existing commit message");
+    modifier.modifyMessage(
+        edit.get(),
+        edit.get().getEditCommit().getFullMessage());
+  }
+
+  @Test
+  public void updateMessage() throws Exception {
+    assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
+        .isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage());
     assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage() + "\n\n");
-
     String msg = String.format("New commit message\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(modifier.modifyMessage(edit.get(), msg)).isEqualTo(
@@ -327,6 +349,11 @@
         ListChangesOption.CURRENT_REVISION);
     assertThat(info.revisions.get(info.currentRevision).commit.message)
         .isEqualTo(msg);
+
+    assertChangeMessages(change,
+        ImmutableList.of("Uploaded patch set 1.",
+            "Uploaded patch set 2.",
+            "Patch Set 3: Commit message was updated."));
   }
 
   @Test
@@ -352,6 +379,11 @@
     edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
+    editUtil.publish(edit.get());
+    assertChangeMessages(change,
+        ImmutableList.of("Uploaded patch set 1.",
+            "Uploaded patch set 2.",
+            "Patch Set 3: Commit message was updated."));
   }
 
   @Test
@@ -395,12 +427,9 @@
     assertThat(modifier.deleteFile(edit.get(), FILE_NAME)).isEqualTo(
         RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
-    try {
-      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
-          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
-      fail("ResourceNotFoundException expected");
-    } catch (ResourceNotFoundException rnfe) {
-    }
+    exception.expect(ResourceNotFoundException.class);
+    fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
   }
 
   @Test
@@ -412,12 +441,9 @@
     edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD);
-    try {
-      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
-          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
-      fail("ResourceNotFoundException expected");
-    } catch (ResourceNotFoundException rnfe) {
-    }
+    exception.expect(ResourceNotFoundException.class);
+    fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
   }
 
   @Test
@@ -425,12 +451,9 @@
     RestResponse r = adminSession.delete(urlEditFile());
     assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    try {
-      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
-          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
-      fail("ResourceNotFoundException expected");
-    } catch (ResourceNotFoundException rnfe) {
-    }
+    exception.expect(ResourceNotFoundException.class);
+    fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
   }
 
   @Test
@@ -445,12 +468,9 @@
     assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
         SC_NO_CONTENT);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    try {
-      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
-          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
-      fail("ResourceNotFoundException expected");
-    } catch (ResourceNotFoundException rnfe) {
-    }
+    exception.expect(ResourceNotFoundException.class);
+    fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
   }
 
   @Test
@@ -466,6 +486,30 @@
   }
 
   @Test
+  public void revertChanges() throws Exception {
+    assertThat(modifier.createEdit(change2, ps2)).isEqualTo(
+        RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change2);
+    assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
+    assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+    editUtil.delete(edit.get());
+  }
+
+  @Test
   public void renameFileRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Post.Input in = new Post.Input();
@@ -476,12 +520,9 @@
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
         ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD);
-    try {
-      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
-          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
-      fail("ResourceNotFoundException expected");
-    } catch (ResourceNotFoundException rnfe) {
-    }
+    exception.expect(ResourceNotFoundException.class);
+    fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
   }
 
   @Test
@@ -581,14 +622,11 @@
     assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
         SC_NO_CONTENT);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-    try {
-      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
-          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
-      fail("ResourceNotFoundException expected");
-    } catch (ResourceNotFoundException rnfe) {
-    }
     RestResponse r = adminSession.get(urlEditFile());
     assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    exception.expect(ResourceNotFoundException.class);
+    fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
   }
 
   @Test
@@ -621,24 +659,22 @@
   @Test
   public void writeNoChanges() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
-    try {
-      modifier.modifyFile(
-          editUtil.byChange(change).get(),
-          FILE_NAME,
-          RestSession.newRawInput(CONTENT_OLD));
-      fail();
-    } catch (InvalidChangeOperationException e) {
-      assertThat(e.getMessage()).isEqualTo("no changes were made");
-    }
+    exception.expect(InvalidChangeOperationException.class);
+    exception.expectMessage("no changes were made");
+    modifier.modifyFile(
+        editUtil.byChange(change).get(),
+        FILE_NAME,
+        RestSession.newRawInput(CONTENT_OLD));
   }
 
   @Test
   public void editCommitMessageCopiesLabelScores() throws Exception {
     String cr = "Code-Review";
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    cfg.getLabelSections().get(cr)
-        .setCopyAllScoresIfNoCodeChange(true);
-    saveProjectConfig(allProjects, cfg);
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType codeReview = Util.codeReview();
+    codeReview.setCopyAllScoresIfNoCodeChange(true);
+    cfg.getLabelSections().put(cr, codeReview);
+    saveProjectConfig(project, cfg);
 
     String changeId = change.getKey().get();
     ReviewInput r = new ReviewInput();
@@ -667,36 +703,64 @@
 
   private void assertUnchangedMessage(Optional<ChangeEdit> edit, String message)
       throws Exception {
-    try {
-      modifier.modifyMessage(
-          edit.get(),
-          message);
-      fail("UnchangedCommitMessageException expected");
-    } catch (UnchangedCommitMessageException ex) {
-      assertThat(ex.getMessage()).isEqualTo(
-          "New commit message cannot be same as existing commit message");
-    }
+    exception.expect(UnchangedCommitMessageException.class);
+    exception.expectMessage(
+        "New commit message cannot be same as existing commit message");
+    modifier.modifyMessage(edit.get(), message);
   }
 
-  private String newChange(Git git, PersonIdent ident) throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME,
-            new String(CONTENT_OLD, StandardCharsets.UTF_8));
-    return push.to(git, "refs/for/master").getChangeId();
+  @Test
+  public void testHasEditPredicate() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(queryEdits()).hasSize(1);
+
+    PatchSet current = getCurrentPatchSet(changeId2);
+    assertThat(modifier.createEdit(change2, current)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    assertThat(queryEdits()).hasSize(2);
+
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    editUtil.delete(editUtil.byChange(change).get());
+    assertThat(queryEdits()).hasSize(1);
+
+    editUtil.publish(editUtil.byChange(change2).get());
+    assertThat(queryEdits()).hasSize(0);
+
+    setApiUser(user);
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(queryEdits()).hasSize(1);
+
+    setApiUser(admin);
+    assertThat(queryEdits()).hasSize(0);
   }
 
-  private String amendChange(Git git, PersonIdent ident, String changeId) throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME2,
-            new String(CONTENT_NEW2, StandardCharsets.UTF_8), changeId);
-    return push.to(git, "refs/for/master").getChangeId();
+  private List<ChangeInfo> queryEdits() throws Exception {
+    return query("project:{" + project.get() + "} has:edit");
   }
 
-  private String newChange2(Git git, PersonIdent ident) throws Exception {
+  private String newChange(PersonIdent ident) throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME,
-            new String(CONTENT_OLD, StandardCharsets.UTF_8));
-    return push.rm(git, "refs/for/master").getChangeId();
+        pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME,
+            new String(CONTENT_OLD, UTF_8));
+    return push.to("refs/for/master").getChangeId();
+  }
+
+  private String amendChange(PersonIdent ident, String changeId) throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME2,
+            new String(CONTENT_NEW2, UTF_8), changeId);
+    return push.to("refs/for/master").getChangeId();
+  }
+
+  private String newChange2(PersonIdent ident) throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME,
+            new String(CONTENT_OLD, UTF_8));
+    return push.rm("refs/for/master").getChangeId();
   }
 
   private Change getChange(String changeId) throws Exception {
@@ -767,4 +831,19 @@
     jsonReader.setLenient(true);
     return newGson().fromJson(jsonReader, String.class);
   }
+
+  private void assertChangeMessages(Change c, List<String> expectedMessages)
+      throws Exception {
+    ChangeInfo ci = get(c.getId().toString());
+    assertThat(ci.messages).isNotNull();
+    assertThat(ci.messages).hasSize(expectedMessages.size());
+    List<String> actualMessages = new ArrayList<>();
+    Iterator<ChangeMessageInfo> it = ci.messages.iterator();
+    while (it.hasNext()) {
+      actualMessages.add(it.next().message);
+    }
+    assertThat(actualMessages)
+      .containsExactlyElementsIn(expectedMessages)
+      .inOrder();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 9496818..87e4656 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -16,17 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.Permission;
@@ -35,41 +31,24 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
+import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.gerrit.server.git.ProjectConfig;
 
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicLong;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
-  @Inject
-  private NotesMigration notesMigration;
-
   protected enum Protocol {
+    // TODO(dborowitz): TEST.
     SSH, HTTP
   }
 
@@ -77,20 +56,12 @@
 
   @BeforeClass
   public static void setTimeForTesting() {
-    final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
-    final AtomicLong clockMs = new AtomicLong(
-        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
-      @Override
-      public long getMillis() {
-        return clockMs.getAndAdd(clockStepMs);
-      }
-    });
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
   }
 
   @AfterClass
   public static void restoreTime() {
-    DateTimeUtils.setCurrentMillisSystem();
+    TestTimeUtil.useSystemTime();
   }
 
   @Before
@@ -98,7 +69,7 @@
     sshUrl = sshSession.getUrl();
   }
 
-  protected void selectProtocol(Protocol p) throws GitAPIException, IOException {
+  protected void selectProtocol(Protocol p) throws Exception {
     String url;
     switch (p) {
       case SSH:
@@ -110,20 +81,51 @@
       default:
         throw new IllegalArgumentException("unexpected protocol: " + p);
     }
-    git = cloneProject(url + "/" + project.get());
+    testRepo = GitUtil.cloneProject(project, url + "/" + project.get());
   }
 
   @Test
-  public void testPushForMaster() throws GitAPIException, OrmException,
-      IOException {
+  public void testPushForMaster() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     r.assertChange(Change.Status.NEW, null);
   }
 
   @Test
-  public void testPushForMasterWithTopic() throws GitAPIException,
-      OrmException, IOException {
+  public void testOutput() throws Exception {
+    String url = canonicalWebUrl.get();
+    ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
+    PushOneCommit.Result r1 = pushTo("refs/for/master");
+    Change.Id id1 = r1.getChange().getId();
+    r1.assertOkStatus();
+    r1.assertChange(Change.Status.NEW, null);
+    r1.assertMessage(
+        "New changes:\n"
+        + "  " + url + id1 + " " + r1.getCommit().getShortMessage() + "\n");
+
+    testRepo.reset(initialHead);
+    String newMsg = r1.getCommit().getShortMessage() + " v2";
+    testRepo.branch("HEAD").commit()
+        .message(newMsg)
+        .insertChangeId(r1.getChangeId().substring(1))
+        .create();
+    PushOneCommit.Result r2 = pushFactory.create(
+            db, admin.getIdent(), testRepo, "another commit", "b.txt", "bbb")
+        .to("refs/for/master");
+    Change.Id id2 = r2.getChange().getId();
+    r2.assertOkStatus();
+    r2.assertChange(Change.Status.NEW, null);
+    r2.assertMessage(
+        "New changes:\n"
+        + "  " + url + id2 + " another commit\n"
+        + "\n"
+        + "\n"
+        + "Updated changes:\n"
+        + "  " + url + id1 + " " + newMsg + "\n");
+  }
+
+  @Test
+  public void testPushForMasterWithTopic() throws Exception {
     // specify topic in ref
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
@@ -137,8 +139,7 @@
   }
 
   @Test
-  public void testPushForMasterWithCc() throws GitAPIException, OrmException,
-      IOException, JSchException {
+  public void testPushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
@@ -161,8 +162,7 @@
   }
 
   @Test
-  public void testPushForMasterWithReviewer() throws GitAPIException,
-      OrmException, IOException, JSchException {
+  public void testPushForMasterWithReviewer() throws Exception {
     // add one reviewer
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
@@ -186,8 +186,7 @@
   }
 
   @Test
-  public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
-      IOException {
+  public void testPushForMasterAsDraft() throws Exception {
     // create draft by pushing to 'refs/drafts/'
     PushOneCommit.Result r = pushTo("refs/drafts/master");
     r.assertOkStatus();
@@ -200,8 +199,7 @@
   }
 
   @Test
-  public void testPushForMasterAsEdit() throws GitAPIException,
-      IOException, RestApiException {
+  public void testPushForMasterAsEdit() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     EditInfo edit = getEdit(r.getChangeId());
@@ -212,25 +210,28 @@
     r.assertOkStatus();
     edit = getEdit(r.getChangeId());
     assertThat(edit).isNotNull();
+    r.assertMessage("Updated Changes:\n  "
+        + canonicalWebUrl.get()
+        + r.getChange().getId()
+        + " " + edit.commit.subject + " [EDIT]\n");
   }
 
   @Test
-  public void testPushForMasterWithApprovals() throws GitAPIException,
-      IOException, RestApiException {
+  public void testPushForMasterWithApprovals() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
     r.assertOkStatus();
     ChangeInfo ci = get(r.getChangeId());
     LabelInfo cr = ci.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
-    assertThat(cr.all.get(0).value.intValue()).is(1);
+    assertThat(cr.all.get(0).value).isEqualTo(1);
     assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
         "Uploaded patch set 1: Code-Review+1.");
 
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent", r.getChangeId());
-    r = push.to(git, "refs/for/master/%l=Code-Review+2");
+    r = push.to("refs/for/master/%l=Code-Review+2");
 
     ci = get(r.getChangeId());
     cr = ci.labels.get("Code-Review");
@@ -239,12 +240,12 @@
 
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
-    assertThat(cr.all.get(0).value.intValue()).is(2);
+    assertThat(cr.all.get(0).value).isEqualTo(2);
 
     push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "c.txt", "moreContent", r.getChangeId());
-    r = push.to(git, "refs/for/master/%l=Code-Review+2");
+    r = push.to("refs/for/master/%l=Code-Review+2");
     ci = get(r.getChangeId());
     assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
         "Uploaded patch set 3.");
@@ -262,12 +263,17 @@
    */
   @Test
   public void testPushForMasterWithApprovalsForgeCommitterButNoForgeVote()
-      throws GitAPIException, RestApiException {
+      throws Exception {
     // Create a commit with "User" as author and committer
-    Commit c = createCommit(git, user.getIdent(), PushOneCommit.SUBJECT);
+    RevCommit c = commitBuilder()
+        .author(user.getIdent())
+        .committer(user.getIdent())
+        .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
+        .message(PushOneCommit.SUBJECT)
+        .create();
 
     // Push this commit as "Administrator" (requires Forge Committer Identity)
-    pushHead(git, "refs/for/master/%l=Code-Review+1", false);
+    pushHead(testRepo, "refs/for/master/%l=Code-Review+1", false);
 
     // Expected Code-Review votes:
     // 1. 0 from User (committer):
@@ -276,56 +282,51 @@
     // 2. +1 from Administrator (uploader):
     //    On push Code-Review+1 was specified, hence we expect a +1 vote from
     //    the uploader.
-    ChangeInfo ci = get(c.getChangeId());
+    ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get());
     LabelInfo cr = ci.labels.get("Code-Review");
     assertThat(cr.all).hasSize(2);
     int indexAdmin = admin.fullName.equals(cr.all.get(0).name) ? 0 : 1;
     int indexUser = indexAdmin == 0 ? 1 : 0;
     assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName);
-    assertThat(cr.all.get(indexAdmin).value.intValue()).is(1);
+    assertThat(cr.all.get(indexAdmin).value.intValue()).isEqualTo(1);
     assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName);
-    assertThat(cr.all.get(indexUser).value.intValue()).is(0);
+    assertThat(cr.all.get(indexUser).value.intValue()).isEqualTo(0);
     assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
         "Uploaded patch set 1: Code-Review+1.");
   }
 
   @Test
-  public void testPushNewPatchsetToRefsChanges() throws GitAPIException,
-    IOException, OrmException {
+  public void testPushNewPatchsetToRefsChanges() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent", r.getChangeId());
-    r = push.to(git, "refs/changes/" + r.getChange().change().getId().get());
+    r = push.to("refs/changes/" + r.getChange().change().getId().get());
     r.assertOkStatus();
   }
 
   @Test
-  public void testPushForMasterWithApprovals_MissingLabel() throws GitAPIException,
-      IOException {
+  public void testPushForMasterWithApprovals_MissingLabel() throws Exception {
       PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify");
       r.assertErrorStatus("label \"Verify\" is not a configured label");
   }
 
   @Test
-  public void testPushForMasterWithApprovals_ValueOutOfRange() throws GitAPIException,
-      IOException {
+  public void testPushForMasterWithApprovals_ValueOutOfRange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3");
     r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
   }
 
   @Test
-  public void testPushForNonExistingBranch() throws GitAPIException,
-      IOException {
+  public void testPushForNonExistingBranch() throws Exception {
     String branchName = "non-existing";
     PushOneCommit.Result r = pushTo("refs/for/" + branchName);
     r.assertErrorStatus("branch " + branchName + " not found");
   }
 
   @Test
-  public void testPushForMasterWithHashtags() throws GitAPIException,
-      OrmException, IOException, RestApiException {
+  public void testPushForMasterWithHashtags() throws Exception {
 
     // Hashtags currently only work when noteDB is enabled
     assume().that(notesMigration.enabled()).isTrue();
@@ -338,23 +339,22 @@
     r.assertChange(Change.Status.NEW, null);
 
     Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
 
     // specify a single hashtag as option in new patch set
     String hashtag2 = "tag2";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent", r.getChangeId());
-    r = push.to(git, "refs/for/master/%hashtag=" + hashtag2);
+    r = push.to("refs/for/master/%hashtag=" + hashtag2);
     r.assertOkStatus();
     expected = ImmutableSet.of(hashtag1, hashtag2);
     hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
   }
 
   @Test
-  public void testPushForMasterWithMultipleHashtags() throws GitAPIException,
-      OrmException, IOException, RestApiException {
+  public void testPushForMasterWithMultipleHashtags() throws Exception {
 
     // Hashtags currently only work when noteDB is enabled
     assume().that(notesMigration.enabled()).isTrue();
@@ -369,25 +369,23 @@
     r.assertChange(Change.Status.NEW, null);
 
     Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
 
     // specify multiple hashtags as options in new patch set
     String hashtag3 = "tag3";
     String hashtag4 = "tag4";
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent", r.getChangeId());
-    r = push.to(git,
-        "refs/for/master%hashtag=" + hashtag3 + ",hashtag=" + hashtag4);
+    r = push.to("refs/for/master%hashtag=" + hashtag3 + ",hashtag=" + hashtag4);
     r.assertOkStatus();
     expected = ImmutableSet.of(hashtag1, hashtag2, hashtag3, hashtag4);
     hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
   }
 
   @Test
-  public void testPushForMasterWithHashtagsNoteDbDisabled() throws GitAPIException,
-      IOException {
+  public void testPushForMasterWithHashtagsNoteDbDisabled() throws Exception {
     // push with hashtags should fail when noteDb is disabled
     assume().that(notesMigration.enabled()).isFalse();
     PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
@@ -395,6 +393,59 @@
   }
 
   @Test
+  public void testPushCommitUsingSignedOffBy() throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    setUseSignedOffBy(InheritableBoolean.TRUE);
+    blockForgeCommitter(project, "refs/heads/master");
+
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT + String.format(
+            "\n\nSigned-off-by: %s <%s>", admin.fullName, admin.email),
+        "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertErrorStatus(
+        "not Signed-off-by author/committer/uploader in commit message footer");
+  }
+
+  @Test
+  public void testCreateNewChangeForAllNotInTarget() throws Exception {
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "a.txt", "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    gApi.projects()
+        .name(project.get())
+        .branch("otherBranch")
+        .create(new BranchInput());
+
+    PushOneCommit.Result r2 = push.to("refs/for/otherBranch");
+    r2.assertOkStatus();
+    assertTwoChangesWithSameRevision(r);
+  }
+
+  @Test
   public void testPushSameCommitTwiceUsingMagicBranchBaseOption()
       throws Exception {
     grant(Permission.PUSH, project, "refs/heads/master");
@@ -407,17 +458,22 @@
         .create(new BranchInput());
 
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent");
 
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
 
     PushResult pr = GitUtil.pushHead(
-        git, "refs/for/foo%base=" + rBase.getCommitId().name(), false, false);
+        testRepo, "refs/for/foo%base=" + rBase.getCommitId().name(), false, false);
     assertThat(pr.getMessages()).containsMatch("changes: .*new: 1.*done");
 
-    List<ChangeInfo> changes = query(r.getCommitId().name());
+    assertTwoChangesWithSameRevision(r);
+  }
+
+  private void assertTwoChangesWithSameRevision(PushOneCommit.Result result)
+      throws Exception {
+    List<ChangeInfo> changes = query(result.getCommitId().name());
     assertThat(changes).hasSize(2);
     ChangeInfo c1 = get(changes.get(0).id);
     ChangeInfo c2 = get(changes.get(1).id);
@@ -426,29 +482,4 @@
     assertThat(c1.changeId).isEqualTo(c2.changeId);
     assertThat(c1.currentRevision).isEqualTo(c2.currentRevision);
   }
-
-  @Test
-  public void testPushCommitUsingSignedOffBy() throws Exception {
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
-            "b.txt", "anotherContent");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
-    r.assertOkStatus();
-
-    setUseSignedOffBy(InheritableBoolean.TRUE);
-    blockForgeCommitter(project, "refs/heads/master");
-
-    push = pushFactory.create(db, admin.getIdent(),
-        PushOneCommit.SUBJECT + String.format(
-            "\n\nSigned-off-by: %s <%s>", admin.fullName, admin.email),
-        "b.txt", "anotherContent");
-    r = push.to(git, "refs/for/master");
-    r.assertOkStatus();
-
-    push = pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
-        "b.txt", "anotherContent");
-    r = push.to(git, "refs/for/master");
-    r.assertErrorStatus(
-        "not Signed-off-by author/committer/uploader in commit message footer");
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
new file mode 100644
index 0000000..53412cb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2015 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
+  protected TestRepository<?> createProjectWithPush(String name)
+      throws Exception {
+    Project.NameKey project = createProject(name);
+    grant(Permission.PUSH, project, "refs/heads/*");
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
+    return cloneProject(project);
+  }
+
+  private static AtomicInteger contentCounter = new AtomicInteger(0);
+
+  protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
+      String message, String topic) throws Exception {
+    ObjectId ret = repo.branch("HEAD").commit().insertChangeId()
+      .message(message)
+      .add("a.txt", "a contents: " + contentCounter.incrementAndGet())
+      .create();
+    String refspec = "HEAD:" + ref;
+    if (!topic.isEmpty()) {
+      refspec += "/" + topic;
+    }
+    repo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec(refspec)).call();
+    return ret;
+  }
+
+  protected ObjectId pushChangeTo(TestRepository<?> repo, String branch)
+      throws Exception {
+    return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
+  }
+
+  protected void createSubmoduleSubscription(TestRepository<?> repo, String branch,
+      String subscribeToRepo, String subscribeToBranch) throws Exception {
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToBranch);
+    pushSubmoduleConfig(repo, branch, config);
+  }
+
+  protected void prepareSubmoduleConfigEntry(Config config,
+      String subscribeToRepo, String subscribeToBranch) {
+    subscribeToRepo = name(subscribeToRepo);
+    // The submodule subscription module checks for gerrit.canonicalWebUrl to
+    // detect if it's configured for automatic updates. It doesn't matter if
+    // it serves from that URL.
+    String url = cfg.getString("gerrit", null, "canonicalWebUrl") + "/"
+        + subscribeToRepo;
+    config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
+    config.setString("submodule", subscribeToRepo, "url", url);
+    if (subscribeToBranch != null) {
+      config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+    }
+  }
+
+  protected void pushSubmoduleConfig(TestRepository<?> repo,
+      String branch, Config config) throws Exception {
+
+    repo.branch("HEAD").commit().insertChangeId()
+      .message("subject: adding new subscription")
+      .add(".gitmodules", config.toText().toString())
+      .create();
+
+    repo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/" + branch)).call();
+  }
+
+  protected void expectToHaveSubmoduleState(TestRepository<?> repo,
+      String branch, String submodule, ObjectId expectedId) throws Exception {
+
+    submodule = name(submodule);
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    try (RevWalk rw = repo.getRevWalk()) {
+      RevCommit c = rw.parseCommit(commitId);
+      rw.parseBody(c.getTree());
+
+      RevTree tree = c.getTree();
+      RevObject actualId = repo.get(tree, submodule);
+
+      assertThat(actualId).isEqualTo(expectedId);
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
index 73183d7..f6796a5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -1,21 +1,15 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  srcs = [
-    'DraftChangeBlockedIT.java',
-    'ForcePushIT.java',
-    'SubmitOnPushIT.java',
-    'VisibleRefFilterIT.java',
+  group = 'git',
+  srcs = glob(['*IT.java']),
+  deps = [
+    ':submodule_util',
+    ':push_for_review',
   ],
   labels = ['git'],
 )
 
-acceptance_tests(
-  srcs = ['HttpPushForReviewIT.java', 'SshPushForReviewIT.java'],
-  deps = [':push_for_review'],
-  labels = ['git'],
-)
-
 java_library(
   name = 'push_for_review',
   srcs = ['AbstractPushForReview.java'],
@@ -24,3 +18,9 @@
     '//lib/joda:joda-time',
   ],
 )
+
+java_library(
+  name = 'submodule_util',
+  srcs = ['AbstractSubmoduleSubscription.java',],
+  deps = ['//gerrit-acceptance-tests:lib',]
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
index 20a698d..27b8b0a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -21,23 +21,19 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 @NoHttpd
 public class DraftChangeBlockedIT extends AbstractDaemonTest {
 
   @Before
   public void setUp() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    saveProjectConfig(cfg);
-    projectCache.evict(cfg.getProject());
+    saveProjectConfig(project, cfg);
   }
 
   @Test
@@ -53,13 +49,4 @@
     PushOneCommit.Result r = pushTo("refs/for/master%draft");
     r.assertErrorStatus("cannot upload drafts");
   }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java
index 5da524e..38f28df 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -29,42 +29,42 @@
 
   @Test
   public void forcePushNotAllowed() throws Exception {
-    ObjectId initial = git.getRepository().getRef(HEAD).getLeaf().getObjectId();
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
     PushOneCommit push1 =
-        pushFactory.create(db, admin.getIdent(), "change1", "a.txt", "content");
-    PushOneCommit.Result r1 = push1.to(git, "refs/heads/master");
+        pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+    PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
 
     // Reset HEAD to initial so the new change is a non-fast forward
-    RefUpdate ru = git.getRepository().updateRef(HEAD);
+    RefUpdate ru = repo().updateRef(HEAD);
     ru.setNewObjectId(initial);
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(db, admin.getIdent(), "change2", "b.txt", "content");
+        pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
     push2.setForce(true);
-    PushOneCommit.Result r2 = push2.to(git, "refs/heads/master");
+    PushOneCommit.Result r2 = push2.to("refs/heads/master");
     r2.assertErrorStatus("non-fast forward");
   }
 
   @Test
   public void forcePushAllowed() throws Exception {
-    ObjectId initial = git.getRepository().getRef(HEAD).getLeaf().getObjectId();
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
     grant(Permission.PUSH, project, "refs/*", true);
     PushOneCommit push1 =
-        pushFactory.create(db, admin.getIdent(), "change1", "a.txt", "content");
-    PushOneCommit.Result r1 = push1.to(git, "refs/heads/master");
+        pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content");
+    PushOneCommit.Result r1 = push1.to("refs/heads/master");
     r1.assertOkStatus();
 
     // Reset HEAD to initial so the new change is a non-fast forward
-    RefUpdate ru = git.getRepository().updateRef(HEAD);
+    RefUpdate ru = repo().updateRef(HEAD);
     ru.setNewObjectId(initial);
     assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
 
     PushOneCommit push2 =
-        pushFactory.create(db, admin.getIdent(), "change2", "b.txt", "content");
+        pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
     push2.setForce(true);
-    PushOneCommit.Result r2 = push2.to(git, "refs/heads/master");
+    PushOneCommit.Result r2 = push2.to("refs/heads/master");
     r2.assertOkStatus();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index cf0945e..cbfa8c7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -26,15 +26,12 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -54,9 +51,6 @@
   @Inject
   private ChangeNotes.Factory changeNotesFactory;
 
-  @Inject
-  private @GerritPersonIdent PersonIdent serverIdent;
-
   @Test
   public void submitOnPush() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
@@ -73,9 +67,9 @@
     grant(Permission.CREATE, project, "refs/tags/*");
     grant(Permission.PUSH, project, "refs/tags/*");
     PushOneCommit.Tag tag = new PushOneCommit.Tag("v1.0");
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     push.setTag(tag);
-    PushOneCommit.Result r = push.to(git, "refs/for/master%submit");
+    PushOneCommit.Result r = push.to("refs/for/master%submit");
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
     assertSubmitApproval(r.getPatchSetId());
@@ -86,13 +80,11 @@
   @Test
   public void submitOnPushWithAnnotatedTag() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
-    grant(Permission.CREATE, project, "refs/tags/*");
-    grant(Permission.PUSH, project, "refs/tags/*");
     PushOneCommit.AnnotatedTag tag =
         new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent());
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     push.setTag(tag);
-    PushOneCommit.Result r = push.to(git, "refs/for/master%submit");
+    PushOneCommit.Result r = push.to("refs/for/master%submit");
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
     assertSubmitApproval(r.getPatchSetId());
@@ -104,9 +96,8 @@
   public void submitOnPushToRefsMetaConfig() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/meta/config");
 
-    git.fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
-    ObjectId objectId = git.getRepository().getRef("refs/meta/config").getObjectId();
-    git.checkout().setName(objectId.getName()).call();
+    git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
+    testRepo.reset("refs/meta/config");
 
     PushOneCommit.Result r = pushTo("refs/for/refs/meta/config%submit");
     r.assertOkStatus();
@@ -117,25 +108,24 @@
 
   @Test
   public void submitOnPushMergeConflict() throws Exception {
-    String master = "refs/heads/master";
-    ObjectId objectId = git.getRepository().getRef(master).getObjectId();
-    push(master, "one change", "a.txt", "some content");
-    git.checkout().setName(objectId.getName()).call();
+    ObjectId objectId = repo().exactRef("HEAD").getObjectId();
+    push("refs/heads/master", "one change", "a.txt", "some content");
+    testRepo.reset(objectId);
 
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     PushOneCommit.Result r =
         push("refs/for/master%submit", "other change", "a.txt", "other content");
-    r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, null, admin);
+    r.assertErrorStatus();
+    r.assertChange(Change.Status.NEW, null);
     r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
   }
 
   @Test
   public void submitOnPushSuccessfulMerge() throws Exception {
     String master = "refs/heads/master";
-    ObjectId objectId = git.getRepository().getRef(master).getObjectId();
+    ObjectId objectId = repo().exactRef("HEAD").getObjectId();
     push(master, "one change", "a.txt", "some content");
-    git.checkout().setName(objectId.getName()).call();
+    testRepo.reset(objectId);
 
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     PushOneCommit.Result r =
@@ -198,7 +188,7 @@
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
     r.assertOkStatus();
 
-    git.push()
+    git().push()
         .setRefSpecs(new RefSpec(r.getCommitId().name() + ":refs/heads/master"))
         .call();
     assertCommit(project, "refs/heads/master");
@@ -214,10 +204,10 @@
     r.assertOkStatus();
 
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             "b.txt", "anotherContent", r.getChangeId());
 
-    r = push.to(git, "refs/heads/master");
+    r = push.to("refs/heads/master");
     r.assertOkStatus();
 
     assertCommit(project, "refs/heads/master");
@@ -243,7 +233,7 @@
   private void assertCommit(Project.NameKey project, String branch) throws IOException {
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
-      RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+      RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
       assertThat(c.getShortMessage()).isEqualTo(PushOneCommit.SUBJECT);
       assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
       assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
@@ -254,19 +244,19 @@
   private void assertMergeCommit(String branch, String subject) throws IOException {
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
-      RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
-      assertThat(c.getParentCount()).is(2);
+      RevCommit c = rw.parseCommit(r.exactRef(branch).getObjectId());
+      assertThat(c.getParentCount()).isEqualTo(2);
       assertThat(c.getShortMessage()).isEqualTo("Merge \"" + subject + "\"");
       assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
       assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
-          serverIdent.getEmailAddress());
+          serverIdent.get().getEmailAddress());
     }
   }
 
   private void assertTag(Project.NameKey project, String branch,
       PushOneCommit.Tag tag) throws IOException {
     try (Repository repo = repoManager.openRepository(project)) {
-      Ref tagRef = repo.getRef(tag.name);
+      Ref tagRef = repo.findRef(tag.name);
       assertThat(tagRef).isNotNull();
       ObjectId taggedCommit = null;
       if (tag instanceof PushOneCommit.AnnotatedTag) {
@@ -283,24 +273,23 @@
       } else {
         taggedCommit = tagRef.getObjectId();
       }
-      ObjectId headCommit = repo.getRef(branch).getObjectId();
+      ObjectId headCommit = repo.exactRef(branch).getObjectId();
       assertThat(taggedCommit).isNotNull();
       assertThat(taggedCommit).isEqualTo(headCommit);
     }
   }
 
   private PushOneCommit.Result push(String ref, String subject,
-      String fileName, String content) throws GitAPIException, IOException {
+      String fileName, String content) throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), subject, fileName, content);
-    return push.to(git, ref);
+        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    return push.to(ref);
   }
 
   private PushOneCommit.Result push(String ref, String subject,
-      String fileName, String content, String changeId) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), subject,
+      String fileName, String content, String changeId) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, subject,
         fileName, content, changeId);
-    return push.to(git, ref);
+    return push.to(ref);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
new file mode 100644
index 0000000..1216b00
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -0,0 +1,255 @@
+// Copyright (C) 2015 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Test;
+
+@NoHttpd
+public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
+  @Test
+  public void testSubscriptionToEmptyRepo() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
+  public void testSubscriptionToExistingRepo() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
+  @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
+  public void testSubmoduleShortCommitMessage() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+
+    // The first update doesn't include any commit messages
+    ObjectId subRepoId = pushChangeTo(subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subRepoId);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Updated git submodules\n\n");
+
+    // Any following update also has a short message
+    subRepoId = pushChangeTo(subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subRepoId);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Updated git submodules\n\n");
+  }
+
+  @Test
+
+  public void testSubmoduleCommitMessage() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+
+    // The first update doesn't include the rev log
+    RevWalk rw = subRepo.getRevWalk();
+    RevCommit subCommitMsg = rw.parseCommit(subHEAD);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Updated git submodules\n\n" +
+        "Project: " + name("subscribed-to-project")
+            + " master " + subHEAD.name() + "\n\n");
+
+    // The next commit should generate only its commit message,
+    // omitting previous commit logs
+    subHEAD = pushChangeTo(subRepo, "master");
+    subCommitMsg = rw.parseCommit(subHEAD);
+    expectToHaveCommitMessage(superRepo, "master",
+        "Updated git submodules\n\n" +
+        "Project: " + name("subscribed-to-project")
+            + " master " + subHEAD.name() + "\n\n" +
+        subCommitMsg.getFullMessage() + "\n\n");
+  }
+
+  @Test
+  public void testSubscriptionUnsubscribe() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+
+    pushChangeTo(subRepo, "master");
+    ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
+
+    deleteAllSubscriptions(superRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEADbeforeUnsubscribing);
+
+    pushChangeTo(superRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
+    pushChangeTo(subRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEADbeforeUnsubscribing);
+  }
+
+  @Test
+  public void testSubscriptionUnsubscribeByDeletingGitModules() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+
+    pushChangeTo(subRepo, "master");
+    ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
+
+    deleteGitModulesFile(superRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEADbeforeUnsubscribing);
+
+    pushChangeTo(superRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
+    pushChangeTo(subRepo, "refs/heads/master",
+        "commit after unsubscribe", "");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEADbeforeUnsubscribing);
+  }
+
+  @Test
+  public void testSubscriptionToDifferentBranches() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "foo");
+    ObjectId subFoo = pushChangeTo(subRepo, "foo");
+    pushChangeTo(subRepo, "master");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subFoo);
+  }
+
+  @Test
+  public void testCircularSubscriptionIsDetected() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(subRepo, "master", "super-project", "master");
+
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+    pushChangeTo(superRepo, "master");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD);
+
+    assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
+  }
+
+  private void deleteAllSubscriptions(TestRepository<?> repo, String branch)
+      throws Exception {
+    repo.git().fetch().setRemote("origin").call();
+    repo.reset("refs/remotes/origin/" + branch);
+
+    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
+      .message("delete contents in .gitmodules")
+      .add(".gitmodules", "") // Just remove the contents of the file!
+      .create();
+    repo.git().push().setRemote("origin").setRefSpecs(
+      new RefSpec("HEAD:refs/heads/" + branch)).call();
+
+    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
+      .getAdvertisedRef("refs/heads/master").getObjectId();
+    assertThat(actualId).isEqualTo(expectedId);
+  }
+
+  private void deleteGitModulesFile(TestRepository<?> repo, String branch)
+      throws Exception {
+    repo.git().fetch().setRemote("origin").call();
+    repo.reset("refs/remotes/origin/" + branch);
+
+    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
+      .message("delete .gitmodules")
+      .rm(".gitmodules")
+      .create();
+    repo.git().push().setRemote("origin").setRefSpecs(
+      new RefSpec("HEAD:refs/heads/" + branch)).call();
+
+    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
+      .getAdvertisedRef("refs/heads/master").getObjectId();
+    assertThat(actualId).isEqualTo(expectedId);
+  }
+
+  private boolean hasSubmodule(TestRepository<?> repo, String branch,
+      String submodule) throws Exception {
+
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    rw.parseBody(c.getTree());
+
+    RevTree tree = c.getTree();
+    try {
+      repo.get(tree, submodule);
+      return true;
+    } catch (AssertionError e) {
+      return false;
+    }
+  }
+
+  private void expectToHaveCommitMessage(TestRepository<?> repo,
+      String branch, String expectedMessage) throws Exception {
+
+    ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+    RevWalk rw = repo.getRevWalk();
+    RevCommit c = rw.parseCommit(commitId);
+    assertThat(c.getFullMessage()).isEqualTo(expectedMessage);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
new file mode 100644
index 0000000..e4a054a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2015 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 static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.GitUtil.getChangeId;
+
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Test;
+
+@NoHttpd
+public class SubmoduleSubscriptionsWholeTopicMergeIT
+  extends AbstractSubmoduleSubscription {
+
+  @ConfigSuite.Default
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
+  @Test
+  public void testSubscriptionUpdateOfManyChanges() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+
+    ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
+        .message("some change")
+        .add("a.txt", "a contents ")
+        .create();
+    subRepo.git().push().setRemote("origin").setRefSpecs(
+          new RefSpec("HEAD:refs/heads/master")).call();
+
+    RevCommit c = subRepo.getRevWalk().parseCommit(subHEAD);
+
+    RevCommit c1 = subRepo.branch("HEAD").commit().insertChangeId()
+      .message("first change")
+      .add("asdf", "asdf\n")
+      .create();
+    subRepo.git().push().setRemote("origin")
+      .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+      .call();
+
+    subRepo.reset(c.getId());
+    RevCommit c2 = subRepo.branch("HEAD").commit().insertChangeId()
+      .message("qwerty")
+      .add("qwerty", "qwerty")
+      .create();
+
+    RevCommit c3 = subRepo.branch("HEAD").commit().insertChangeId()
+      .message("qwerty followup")
+      .add("qwerty", "qwerty\nqwerty\n")
+      .create();
+    subRepo.git().push().setRemote("origin")
+      .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
+      .call();
+
+    String id1 = getChangeId(subRepo, c1).get();
+    String id2 = getChangeId(subRepo, c2).get();
+    String id3 = getChangeId(subRepo, c3).get();
+    gApi.changes().id(id1).current().review(ReviewInput.approve());
+    gApi.changes().id(id2).current().review(ReviewInput.approve());
+    gApi.changes().id(id3).current().review(ReviewInput.approve());
+
+    gApi.changes().id(id1).current().submit();
+    ObjectId subRepoId = subRepo.git().fetch().setRemote("origin").call()
+        .getAdvertisedRef("refs/heads/master").getObjectId();
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subRepoId);
+  }
+
+  @Test
+  public void testUpdateManySubmodules() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> sub1 = createProjectWithPush("sub1");
+    TestRepository<?> sub2 = createProjectWithPush("sub2");
+    TestRepository<?> sub3 = createProjectWithPush("sub3");
+
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, "sub1", "master");
+    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    prepareSubmoduleConfigEntry(config, "sub3", "master");
+    pushSubmoduleConfig(superRepo, "master", config);
+
+    ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master",
+        "some message", "same-topic");
+    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master",
+        "some message", "same-topic");
+    ObjectId sub3Id = pushChangeTo(sub3, "refs/for/master",
+        "some message", "same-topic");
+
+    approve(getChangeId(sub1, sub1Id).get());
+    approve(getChangeId(sub2, sub2Id).get());
+    approve(getChangeId(sub3, sub3Id).get());
+
+    gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
+
+    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1Id);
+    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2Id);
+    expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3Id);
+
+    superRepo.git().fetch().setRemote("origin").call()
+      .getAdvertisedRef("refs/heads/master").getObjectId();
+
+    assertWithMessage("submodule subscription update "
+        + "should have made one commit")
+        .that(superRepo.getRepository().resolve("origin/master^"))
+        .isEqualTo(superPreviousId);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
index 4a66a4169..1ded088 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -34,56 +34,54 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.Util;
-import com.google.gerrit.testutil.ConfigSuite;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.List;
 
-@RunWith(ConfigSuite.class)
 @NoHttpd
 public class VisibleRefFilterIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbWriteEnabled() {
-    Config cfg = new Config();
-    cfg.setBoolean("notedb", "changes", "write", true);
-    return cfg;
-  }
-
-  @Inject
-  private NotesMigration notesMigration;
-
   @Inject
   private ChangeEditModifier editModifier;
 
   private AccountGroup.UUID admins;
 
+  private Change.Id c1;
+  private Change.Id c2;
+  private String r1;
+  private String r2;
+
   @Before
   public void setUp() throws Exception {
     admins = groupCache.get(new AccountGroup.NameKey("Administrators"))
         .getGroupUUID();
-    setUpChanges();
     setUpPermissions();
+    setUpChanges();
   }
 
   private void setUpPermissions() throws Exception {
+    // Remove read permissions for all users besides admin. This method is
+    // idempotent, so is safe to call on every test setup.
     ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
     for (AccessSection sec : pc.getAccessSections()) {
       sec.removePermission(Permission.READ);
     }
+    Util.allow(pc, Permission.READ, admins, "refs/*");
     saveProjectConfig(allProjects, pc);
   }
 
+  private static String changeRefPrefix(Change.Id id) {
+    String ps = new PatchSet.Id(id, 1).toRefName();
+    return ps.substring(0, ps.length() - 1);
+  }
+
   private void setUpChanges() throws Exception {
     gApi.projects()
         .name(project.get())
@@ -91,28 +89,29 @@
         .create(new BranchInput());
 
     allow(Permission.SUBMIT, admins, "refs/for/refs/heads/*");
-    PushOneCommit.Result mr = pushFactory.create(db, admin.getIdent())
-        .to(git, "refs/for/master%submit");
+    PushOneCommit.Result mr = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/master%submit");
     mr.assertOkStatus();
-    PushOneCommit.Result br = pushFactory.create(db, admin.getIdent())
-        .to(git, "refs/for/branch%submit");
+    c1 = mr.getChange().getId();
+    r1 = changeRefPrefix(c1);
+    PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/branch%submit");
     br.assertOkStatus();
+    c2 = br.getChange().getId();
+    r2 = changeRefPrefix(c2);
 
-    Repository repo = repoManager.openRepository(project);
-    try {
+    try (Repository repo = repoManager.openRepository(project)) {
       // master-tag -> master
       RefUpdate mtu = repo.updateRef("refs/tags/master-tag");
       mtu.setExpectedOldObjectId(ObjectId.zeroId());
-      mtu.setNewObjectId(repo.getRef("refs/heads/master").getObjectId());
+      mtu.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId());
       assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW);
 
       // branch-tag -> branch
       RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
-      btu.setNewObjectId(repo.getRef("refs/heads/branch").getObjectId());
+      btu.setNewObjectId(repo.exactRef("refs/heads/branch").getObjectId());
       assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
-    } finally {
-      repo.close();
     }
   }
 
@@ -126,10 +125,10 @@
 
     assertRefs(
         "HEAD",
-        "refs/changes/01/1/1",
-        "refs/changes/01/1/meta",
-        "refs/changes/02/2/1",
-        "refs/changes/02/2/meta",
+        r1 + "1",
+        r1 + "meta",
+        r2 + "1",
+        r2 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         "refs/tags/branch-tag",
@@ -143,10 +142,10 @@
 
     assertRefs(
         "HEAD",
-        "refs/changes/01/1/1",
-        "refs/changes/01/1/meta",
-        "refs/changes/02/2/1",
-        "refs/changes/02/2/meta",
+        r1 + "1",
+        r1 + "meta",
+        r2 + "1",
+        r2 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         "refs/meta/config",
@@ -161,8 +160,8 @@
 
     assertRefs(
         "HEAD",
-        "refs/changes/01/1/1",
-        "refs/changes/01/1/meta",
+        r1 + "1",
+        r1 + "meta",
         "refs/heads/master",
         "refs/tags/master-tag");
   }
@@ -173,8 +172,8 @@
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
     assertRefs(
-        "refs/changes/02/2/1",
-        "refs/changes/02/2/meta",
+        r2 + "1",
+        r2 + "meta",
         "refs/heads/branch",
         "refs/tags/branch-tag",
         // master branch is not visible but master-tag is reachable from branch
@@ -187,53 +186,57 @@
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-    Change c1 = db.changes().get(new Change.Id(1));
-    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1.getId(), 1));
+    Change change1 = db.changes().get(c1);
+    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1, 1));
 
     // Admin's edit is not visible.
     setApiUser(admin);
-    editModifier.createEdit(c1, ps1);
+    editModifier.createEdit(change1, ps1);
 
     // User's edit is visible.
     setApiUser(user);
-    editModifier.createEdit(c1, ps1);
+    editModifier.createEdit(change1, ps1);
 
     assertRefs(
         "HEAD",
-        "refs/changes/01/1/1",
-        "refs/changes/01/1/meta",
+        r1 + "1",
+        r1 + "meta",
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/users/01/1000001/edit-1/1");
+        "refs/users/01/1000001/edit-" + c1.get() + "/1");
   }
 
   @Test
   public void subsetOfRefsVisibleWithAccessDatabase() throws Exception {
-    deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
-    allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
-    allowGlobalCapability(GlobalCapability.ACCESS_DATABASE, REGISTERED_USERS);
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    try {
+      deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+      allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-    Change c1 = db.changes().get(new Change.Id(1));
-    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1.getId(), 1));
-    setApiUser(admin);
-    editModifier.createEdit(c1, ps1);
-    setApiUser(user);
-    editModifier.createEdit(c1, ps1);
+      Change change1 = db.changes().get(c1);
+      PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1, 1));
+      setApiUser(admin);
+      editModifier.createEdit(change1, ps1);
+      setApiUser(user);
+      editModifier.createEdit(change1, ps1);
 
-    assertRefs(
-        // Change 1 is visible due to accessDatabase capability, even though
-        // refs/heads/master is not.
-        "refs/changes/01/1/1",
-        "refs/changes/01/1/meta",
-        "refs/changes/02/2/1",
-        "refs/changes/02/2/meta",
-        "refs/heads/branch",
-        "refs/tags/branch-tag",
-        // See comment in subsetOfBranchesVisibleNotIncludingHead.
-        "refs/tags/master-tag",
-        // All edits are visible due to accessDatabase capability.
-        "refs/users/00/1000000/edit-1/1",
-        "refs/users/01/1000001/edit-1/1");
+      assertRefs(
+          // Change 1 is visible due to accessDatabase capability, even though
+          // refs/heads/master is not.
+          r1 + "1",
+          r1 + "meta",
+          r2 + "1",
+          r2 + "meta",
+          "refs/heads/branch",
+          "refs/tags/branch-tag",
+          // See comment in subsetOfBranchesVisibleNotIncludingHead.
+          "refs/tags/master-tag",
+          // All edits are visible due to accessDatabase capability.
+          "refs/users/00/1000000/edit-" + c1.get() + "/1",
+          "refs/users/01/1000001/edit-" + c1.get() + "/1");
+    } finally {
+      removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    }
   }
 
   /**
@@ -258,8 +261,8 @@
       }
     }
 
-    Splitter s = Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings();
-    assertThat(filtered).containsSequence(
-        Ordering.natural().sortedCopy(s.split(out)));
+    Splitter s = Splitter.on(CharMatcher.whitespace()).omitEmptyStrings();
+    assertThat(filtered).containsExactlyElementsIn(
+        Ordering.natural().sortedCopy(s.split(out))).inOrder();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
index 00b53f9..ff167ac 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
@@ -1,7 +1,8 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'pgm',
   srcs = glob(['*IT.java']),
-  labels = ['pgm'],
   source_under_test = ['//gerrit-pgm:pgm'],
+  labels = ['pgm'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
index c538b85..5d0e7df 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.pgm;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.Files;
 import com.google.gerrit.launcher.GerritLauncher;
@@ -26,7 +27,6 @@
 import org.junit.Test;
 
 import java.io.File;
-import java.nio.charset.StandardCharsets;
 
 public class RebuildNotedbIT {
   private File sitePath;
@@ -48,7 +48,7 @@
     initSite();
     Files.append(NotesMigration.allEnabledConfig().toText(),
         new File(sitePath.toString(), "etc/gerrit.config"),
-        StandardCharsets.UTF_8);
+        UTF_8);
     runGerrit("RebuildNotedb", "-d", sitePath.toString(),
         "--show-stack-trace");
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index c90924d..91ee332 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -16,8 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
+
+import java.util.List;
 
 public class AccountAssert {
 
@@ -26,4 +31,21 @@
     assertThat(a.fullName).isEqualTo(ai.name);
     assertThat(a.email).isEqualTo(ai.email);
   }
+
+  public static void assertAccountInfos(List<TestAccount> expected,
+      List<AccountInfo> actual) {
+    Iterable<Account.Id> expectedIds = TestAccount.ids(expected);
+    Iterable<Account.Id> actualIds = Iterables.transform(
+        actual,
+        new Function<AccountInfo, Account.Id>() {
+          @Override
+          public Account.Id apply(AccountInfo in) {
+            return new Account.Id(in._accountId);
+          }
+        });
+    assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder();
+    for (int i = 0; i < expected.size(); i++) {
+      AccountAssert.assertAccountInfo(expected.get(i), actual.get(i));
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
index f081ada..b7c1819 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-account',
   srcs = glob(['*IT.java']),
   deps = [':util'],
   labels = ['rest']
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index 6ce96ee..3e7c2bf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -24,51 +24,56 @@
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_AS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class CapabilitiesIT extends AbstractDaemonTest {
 
   @Test
   public void testCapabilitiesUser() throws Exception {
-    grantAllCapabilities();
-    RestResponse r =
-        userSession.get("/accounts/self/capabilities");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
-        new TypeToken<CapabilityInfo>() {}.getType());
-    for (String c : GlobalCapability.getAllNames()) {
-      if (ADMINISTRATE_SERVER.equals(c)) {
-        assertThat(info.administrateServer).isFalse();
-      } else if (BATCH_CHANGES_LIMIT.equals(c)) {
-        assertThat(info.batchChangesLimit.min).isEqualTo((short) 0);
-        assertThat(info.batchChangesLimit.max).isEqualTo((short) DEFAULT_MAX_BATCH_CHANGES_LIMIT);
-      } else if (PRIORITY.equals(c)) {
-        assertThat(info.priority).isFalse();
-      } else if (QUERY_LIMIT.equals(c)) {
-        assertThat(info.queryLimit.min).isEqualTo((short) 0);
-        assertThat(info.queryLimit.max).isEqualTo((short) DEFAULT_MAX_QUERY_LIMIT);
-      } else {
-        assert_().withFailureMessage(String.format("capability %s was not granted", c))
-          .that((Boolean) CapabilityInfo.class.getField(c).get(info)).isTrue();
+    Iterable<String> all = Iterables.filter(GlobalCapability.getAllNames(),
+        new Predicate<String>() {
+          @Override
+          public boolean apply(String in) {
+            return !ADMINISTRATE_SERVER.equals(in) && !PRIORITY.equals(in);
+          }
+        });
+
+    allowGlobalCapabilities(REGISTERED_USERS, all);
+    try {
+      RestResponse r =
+          userSession.get("/accounts/self/capabilities");
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+      CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
+          new TypeToken<CapabilityInfo>() {}.getType());
+      for (String c : GlobalCapability.getAllNames()) {
+        if (ADMINISTRATE_SERVER.equals(c)) {
+          assertThat(info.administrateServer).isFalse();
+        } else if (BATCH_CHANGES_LIMIT.equals(c)) {
+          assertThat(info.batchChangesLimit.min).isEqualTo((short) 0);
+          assertThat(info.batchChangesLimit.max).isEqualTo((short) DEFAULT_MAX_BATCH_CHANGES_LIMIT);
+        } else if (PRIORITY.equals(c)) {
+          assertThat(info.priority).isFalse();
+        } else if (QUERY_LIMIT.equals(c)) {
+          assertThat(info.queryLimit.min).isEqualTo((short) 0);
+          assertThat(info.queryLimit.max).isEqualTo((short) DEFAULT_MAX_QUERY_LIMIT);
+        } else {
+          assert_().withFailureMessage(String.format("capability %s was not granted", c))
+            .that((Boolean) CapabilityInfo.class.getField(c).get(info)).isTrue();
+        }
       }
+    } finally {
+      removeGlobalCapabilities(REGISTERED_USERS, all);
     }
   }
 
@@ -101,35 +106,4 @@
       }
     }
   }
-
-  /**
-   * Grant all global capabilities except ADMINISTRATE_SERVER and PRIORITY.
-   * Set the default ranges for range permissions.
-   */
-  private void grantAllCapabilities() throws IOException,
-      ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
-    md.setMessage("Make super user");
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(
-        AccessSection.GLOBAL_CAPABILITIES);
-    for (String c : GlobalCapability.getAllNames()) {
-      if (ADMINISTRATE_SERVER.equals(c) || PRIORITY.equals(c)) {
-        continue;
-      }
-      Permission p = s.getPermission(c, true);
-      PermissionRule rule = new PermissionRule(
-          config.resolve(SystemGroupBackend.getGroup(
-              SystemGroupBackend.REGISTERED_USERS)));
-      if (GlobalCapability.hasRange(c)) {
-        PermissionRange.WithDefaults range = GlobalCapability.getRange(c);
-        if (range != null) {
-          rule.setRange(range.getDefaultMin(), range.getDefaultMax());
-        }
-      }
-      p.add(rule);
-    }
-    config.commit(md);
-    projectCache.evict(config.getProject());
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
index e537373..fac594a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
@@ -24,6 +24,7 @@
   public boolean emailReviewers;
   public boolean flushCaches;
   public boolean killTask;
+  public boolean maintainServer;
   public boolean modifyAccount;
   public boolean priority;
   public QueryLimit queryLimit;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
new file mode 100644
index 0000000..9dbb1ff
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2015 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.client.Theme;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+public class DiffPreferencesIT extends AbstractDaemonTest {
+  @Test
+  public void getDiffPreferencesOfNonExistingAccount_NotFound()
+      throws Exception {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        adminSession.get("/accounts/non-existing/preferences.diff")
+        .getStatusCode());
+  }
+
+  @Test
+  public void getDiffPreferences() throws Exception {
+    RestResponse r = adminSession.get("/accounts/" + admin.email
+        + "/preferences.diff");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
+    DiffPreferencesInfo o =
+        newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
+
+    assertThat(o.context).isEqualTo(d.context);
+    assertThat(o.tabSize).isEqualTo(d.tabSize);
+    assertThat(o.lineLength).isEqualTo(d.lineLength);
+    assertThat(o.cursorBlinkRate).isEqualTo(d.cursorBlinkRate);
+    assertThat(o.expandAllComments).isNull();
+    assertThat(o.intralineDifference).isEqualTo(d.intralineDifference);
+    assertThat(o.manualReview).isNull();
+    assertThat(o.retainHeader).isNull();
+    assertThat(o.showLineEndings).isEqualTo(d.showLineEndings);
+    assertThat(o.showTabs).isEqualTo(d.showTabs);
+    assertThat(o.showWhitespaceErrors).isEqualTo(d.showWhitespaceErrors);
+    assertThat(o.skipDeleted).isNull();
+    assertThat(o.skipUncommented).isNull();
+    assertThat(o.syntaxHighlighting).isEqualTo(d.syntaxHighlighting);
+    assertThat(o.hideTopMenu).isNull();
+    assertThat(o.autoHideDiffTableHeader).isEqualTo(d.autoHideDiffTableHeader);
+    assertThat(o.hideLineNumbers).isNull();
+    assertThat(o.renderEntireFile).isNull();
+    assertThat(o.hideEmptyPane).isNull();
+    assertThat(o.matchBrackets).isNull();
+    assertThat(o.lineWrapping).isNull();
+    assertThat(o.ignoreWhitespace).isEqualTo(d.ignoreWhitespace);
+    assertThat(o.theme).isEqualTo(d.theme);
+  }
+
+  @Test
+  public void setDiffPreferences() throws Exception {
+    DiffPreferencesInfo i = DiffPreferencesInfo.defaults();
+
+    // change all default values
+    i.context *= -1;
+    i.tabSize *= -1;
+    i.lineLength *= -1;
+    i.cursorBlinkRate = 500;
+    i.theme = Theme.MIDNIGHT;
+    i.ignoreWhitespace = Whitespace.IGNORE_ALL;
+    i.expandAllComments ^= true;
+    i.intralineDifference ^= true;
+    i.manualReview ^= true;
+    i.retainHeader ^= true;
+    i.showLineEndings ^= true;
+    i.showTabs ^= true;
+    i.showWhitespaceErrors ^= true;
+    i.skipDeleted ^= true;
+    i.skipUncommented ^= true;
+    i.syntaxHighlighting ^= true;
+    i.hideTopMenu ^= true;
+    i.autoHideDiffTableHeader ^= true;
+    i.hideLineNumbers ^= true;
+    i.renderEntireFile ^= true;
+    i.hideEmptyPane ^= true;
+    i.matchBrackets ^= true;
+    i.lineWrapping ^= true;
+
+    RestResponse r = adminSession.put("/accounts/" + admin.email
+        + "/preferences.diff", i);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    DiffPreferencesInfo o = newGson().fromJson(r.getReader(),
+        DiffPreferencesInfo.class);
+
+    assertThat(o.context).isEqualTo(i.context);
+    assertThat(o.tabSize).isEqualTo(i.tabSize);
+    assertThat(o.lineLength).isEqualTo(i.lineLength);
+    assertThat(o.cursorBlinkRate).isEqualTo(i.cursorBlinkRate);
+    assertThat(o.expandAllComments).isEqualTo(i.expandAllComments);
+    assertThat(o.intralineDifference).isNull();
+    assertThat(o.manualReview).isEqualTo(i.manualReview);
+    assertThat(o.retainHeader).isEqualTo(i.retainHeader);
+    assertThat(o.showLineEndings).isNull();
+    assertThat(o.showTabs).isNull();
+    assertThat(o.showWhitespaceErrors).isNull();
+    assertThat(o.skipDeleted).isEqualTo(i.skipDeleted);
+    assertThat(o.skipUncommented).isEqualTo(i.skipUncommented);
+    assertThat(o.syntaxHighlighting).isNull();
+    assertThat(o.hideTopMenu).isEqualTo(i.hideTopMenu);
+    assertThat(o.autoHideDiffTableHeader).isNull();
+    assertThat(o.hideLineNumbers).isEqualTo(i.hideLineNumbers);
+    assertThat(o.renderEntireFile).isEqualTo(i.renderEntireFile);
+    assertThat(o.hideEmptyPane).isEqualTo(i.hideEmptyPane);
+    assertThat(o.matchBrackets).isEqualTo(i.matchBrackets);
+    assertThat(o.lineWrapping).isEqualTo(i.lineWrapping);
+    assertThat(o.ignoreWhitespace).isEqualTo(i.ignoreWhitespace);
+    assertThat(o.theme).isEqualTo(i.theme);
+
+    // Partially fill input record
+    i = new DiffPreferencesInfo();
+    i.tabSize = 42;
+    r = adminSession.put("/accounts/" + admin.email
+        + "/preferences.diff", i);
+    DiffPreferencesInfo a = newGson().fromJson(r.getReader(),
+        DiffPreferencesInfo.class);
+
+    assertThat(a.context).isEqualTo(o.context);
+    assertThat(a.tabSize).isEqualTo(42);
+    assertThat(a.lineLength).isEqualTo(o.lineLength);
+    assertThat(a.expandAllComments).isEqualTo(o.expandAllComments);
+    assertThat(a.intralineDifference).isNull();
+    assertThat(a.manualReview).isEqualTo(o.manualReview);
+    assertThat(a.retainHeader).isEqualTo(o.retainHeader);
+    assertThat(a.showLineEndings).isNull();
+    assertThat(a.showTabs).isNull();
+    assertThat(a.showWhitespaceErrors).isNull();
+    assertThat(a.skipDeleted).isEqualTo(o.skipDeleted);
+    assertThat(a.skipUncommented).isEqualTo(o.skipUncommented);
+    assertThat(a.syntaxHighlighting).isNull();
+    assertThat(a.hideTopMenu).isEqualTo(o.hideTopMenu);
+    assertThat(a.autoHideDiffTableHeader).isNull();
+    assertThat(a.hideLineNumbers).isEqualTo(o.hideLineNumbers);
+    assertThat(a.renderEntireFile).isEqualTo(o.renderEntireFile);
+    assertThat(a.hideEmptyPane).isEqualTo(o.hideEmptyPane);
+    assertThat(a.ignoreWhitespace).isEqualTo(o.ignoreWhitespace);
+    assertThat(a.theme).isEqualTo(o.theme);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
new file mode 100644
index 0000000..b97219c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2014 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class EditPreferencesIT extends AbstractDaemonTest {
+  @Test
+  public void getSetEditPreferences() throws Exception {
+    String endPoint = "/accounts/" + admin.email + "/preferences.edit";
+    RestResponse r = adminSession.get(endPoint);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    EditPreferencesInfo out = getEditPrefInfo(r);
+
+    assertThat(out.lineLength).isEqualTo(100);
+    assertThat(out.tabSize).isEqualTo(8);
+    assertThat(out.cursorBlinkRate).isEqualTo(0);
+    assertThat(out.hideTopMenu).isNull();
+    assertThat(out.showTabs).isTrue();
+    assertThat(out.showWhitespaceErrors).isNull();
+    assertThat(out.syntaxHighlighting).isTrue();
+    assertThat(out.hideLineNumbers).isNull();
+    assertThat(out.matchBrackets).isTrue();
+    assertThat(out.lineWrapping).isNull();
+    assertThat(out.autoCloseBrackets).isNull();
+    assertThat(out.theme).isEqualTo(Theme.DEFAULT);
+    assertThat(out.keyMapType).isEqualTo(KeyMapType.DEFAULT);
+
+    // change some default values
+    out.lineLength = 80;
+    out.tabSize = 4;
+    out.cursorBlinkRate = 500;
+    out.hideTopMenu = true;
+    out.showTabs = false;
+    out.showWhitespaceErrors = true;
+    out.syntaxHighlighting = false;
+    out.hideLineNumbers = true;
+    out.matchBrackets = false;
+    out.lineWrapping = true;
+    out.autoCloseBrackets = true;
+    out.theme = Theme.TWILIGHT;
+    out.keyMapType = KeyMapType.EMACS;
+
+    r = adminSession.put(endPoint, out);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+
+    r = adminSession.get(endPoint);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    EditPreferencesInfo info = getEditPrefInfo(r);
+    assertEditPreferences(info, out);
+
+    // Partially filled input record
+    EditPreferencesInfo in = new EditPreferencesInfo();
+    in.tabSize = 42;
+    r = adminSession.put(endPoint, in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+
+    r = adminSession.get(endPoint);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    info = getEditPrefInfo(r);
+    out.tabSize = in.tabSize;
+    assertEditPreferences(info, out);
+  }
+
+  private EditPreferencesInfo getEditPrefInfo(RestResponse r)
+      throws IOException {
+    return newGson().fromJson(r.getReader(),
+        EditPreferencesInfo.class);
+  }
+
+  private void assertEditPreferences(EditPreferencesInfo out,
+      EditPreferencesInfo in) {
+    assertThat(out.lineLength).isEqualTo(in.lineLength);
+    assertThat(out.tabSize).isEqualTo(in.tabSize);
+    assertThat(out.cursorBlinkRate).isEqualTo(in.cursorBlinkRate);
+    assertThat(out.hideTopMenu).isEqualTo(in.hideTopMenu);
+    assertThat(out.showTabs).isNull();
+    assertThat(out.showWhitespaceErrors).isEqualTo(in.showWhitespaceErrors);
+    assertThat(out.syntaxHighlighting).isNull();
+    assertThat(out.hideLineNumbers).isEqualTo(in.hideLineNumbers);
+    assertThat(out.matchBrackets).isNull();
+    assertThat(out.lineWrapping).isEqualTo(in.lineWrapping);
+    assertThat(out.autoCloseBrackets).isEqualTo(in.autoCloseBrackets);
+    assertThat(out.theme).isEqualTo(in.theme);
+    assertThat(out.keyMapType).isEqualTo(in.keyMapType);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
new file mode 100644
index 0000000..da1d3ec
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.GetDetail.AccountDetailInfo;
+
+import org.junit.Test;
+
+public class GetAccountDetailIT extends AbstractDaemonTest {
+  @Test
+  public void getDetail() throws Exception {
+    RestResponse r = adminSession.get("/accounts/" + admin.username + "/detail/");
+    AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
+    assertAccountInfo(admin, info);
+    Account account = accountCache.get(admin.getId()).getAccount();
+    assertThat(info.registeredOn).isEqualTo(account.getRegisteredOn());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 63c6493..2944a57 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -14,54 +14,45 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
+@NoHttpd
 public class GetAccountIT extends AbstractDaemonTest {
-  @Test
+  @Test(expected = ResourceNotFoundException.class)
   public void getNonExistingAccount_NotFound() throws Exception {
-    assertThat(adminSession.get("/accounts/non-existing").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    gApi.accounts().id("non-existing").get();
   }
 
   @Test
   public void getAccount() throws Exception {
     // by formatted string
-    testGetAccount("/accounts/"
-        + Url.encode(admin.fullName + " <" + admin.email + ">"), admin);
+    testGetAccount(admin.fullName + " <" + admin.email + ">", admin);
 
     // by email
-    testGetAccount("/accounts/" + admin.email, admin);
+    testGetAccount(admin.email, admin);
 
     // by full name
-    testGetAccount("/accounts/" + admin.fullName, admin);
+    testGetAccount(admin.fullName, admin);
 
     // by account ID
-    testGetAccount("/accounts/" + admin.id.get(), admin);
+    testGetAccount(Integer.toString(admin.id.get()), admin);
 
     // by user name
-    testGetAccount("/accounts/" + admin.username, admin);
+    testGetAccount(admin.username, admin);
 
     // by 'self'
-    testGetAccount("/accounts/self", admin);
+    testGetAccount("self", admin);
   }
 
-  private void testGetAccount(String url, TestAccount expectedAccount)
-      throws IOException {
-    RestResponse r = adminSession.get(url);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertAccountInfo(expectedAccount, newGson()
-        .fromJson(r.getReader(), AccountInfo.class));
+  private void testGetAccount(String id, TestAccount expectedAccount)
+      throws Exception {
+    assertAccountInfo(expectedAccount, gApi.accounts().id(id).get());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
deleted file mode 100644
index 02a94f8..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2013 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.account;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-public class GetDiffPreferencesIT extends AbstractDaemonTest {
-  @Test
-  public void getDiffPreferencesOfNonExistingAccount_NotFound()
-      throws Exception {
-    assertThat(adminSession.get("/accounts/non-existing/preferences.diff").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void getDiffPreferences() throws Exception {
-    RestResponse r = adminSession.get("/accounts/" + admin.email + "/preferences.diff");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    DiffPreferencesInfo diffPreferences =
-        newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
-    assertDiffPreferences(new AccountDiffPreference(admin.id), diffPreferences);
-  }
-
-  private static void assertDiffPreferences(AccountDiffPreference expected, DiffPreferencesInfo actual) {
-    assertThat(actual.context).isEqualTo(expected.getContext());
-    assertThat(toBoolean(actual.expandAllComments)).isEqualTo(expected.isExpandAllComments());
-    assertThat(actual.ignoreWhitespace).isEqualTo(expected.getIgnoreWhitespace());
-    assertThat(toBoolean(actual.intralineDifference)).isEqualTo(expected.isIntralineDifference());
-    assertThat(actual.lineLength).isEqualTo(expected.getLineLength());
-    assertThat(toBoolean(actual.manualReview)).isEqualTo(expected.isManualReview());
-    assertThat(toBoolean(actual.retainHeader)).isEqualTo(expected.isRetainHeader());
-    assertThat(toBoolean(actual.showLineEndings)).isEqualTo(expected.isShowLineEndings());
-    assertThat(toBoolean(actual.showTabs)).isEqualTo(expected.isShowTabs());
-    assertThat(toBoolean(actual.showWhitespaceErrors)).isEqualTo(expected.isShowWhitespaceErrors());
-    assertThat(toBoolean(actual.skipDeleted)).isEqualTo(expected.isSkipDeleted());
-    assertThat(toBoolean(actual.skipUncommented)).isEqualTo(expected.isSkipUncommented());
-    assertThat(toBoolean(actual.syntaxHighlighting)).isEqualTo(expected.isSyntaxHighlighting());
-    assertThat(actual.tabSize).isEqualTo(expected.getTabSize());
-  }
-
-  private static boolean toBoolean(Boolean b) {
-    if (b == null) {
-      return false;
-    }
-    return b.booleanValue();
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
new file mode 100644
index 0000000..2543095
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2015 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.PutUsername;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.util.Collections;
+
+public class PutUsernameIT extends AbstractDaemonTest {
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Test
+  public void set() throws Exception {
+    PutUsername.Input in = new PutUsername.Input();
+    in.username = "myUsername";
+    RestResponse r =
+        adminSession.put("/accounts/" + createUser().get() + "/username", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(
+        in.username);
+  }
+
+  @Test
+  public void setExisting_Conflict() throws Exception {
+    PutUsername.Input in = new PutUsername.Input();
+    in.username = admin.username;
+    RestResponse r =
+        adminSession.put("/accounts/" + createUser().get() + "/username", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+  }
+
+  @Test
+  public void setNew_MethodNotAllowed() throws Exception {
+    PutUsername.Input in = new PutUsername.Input();
+    in.username = "newUsername";
+    RestResponse r =
+        adminSession.put("/accounts/" + admin.username + "/username", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+  }
+
+  @Test
+  public void delete_MethodNotAllowed() throws Exception {
+    RestResponse r =
+        adminSession.put("/accounts/" + admin.username + "/username");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+  }
+
+  private Account.Id createUser() throws OrmException {
+    try (ReviewDb db = reviewDbProvider.open()) {
+      Account.Id id = new Account.Id(db.nextAccountId());
+      Account a = new Account(id, TimeUtil.nowTs());
+      db.accounts().insert(Collections.singleton(a));
+      return id;
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
deleted file mode 100644
index face299a..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2013 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.account;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.Change;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class StarredChangesIT extends AbstractDaemonTest {
-
-  @Test
-  public void starredChangeState() throws Exception {
-    Result c1 = createChange();
-    Result c2 = createChange();
-    assertThat(getChange(c1.getChangeId()).starred).isNull();
-    assertThat(getChange(c2.getChangeId()).starred).isNull();
-    starChange(true, c1.getPatchSetId().getParentKey());
-    starChange(true, c2.getPatchSetId().getParentKey());
-    assertThat(getChange(c1.getChangeId()).starred).isTrue();
-    assertThat(getChange(c2.getChangeId()).starred).isTrue();
-    starChange(false, c1.getPatchSetId().getParentKey());
-    starChange(false, c2.getPatchSetId().getParentKey());
-    assertThat(getChange(c1.getChangeId()).starred).isNull();
-    assertThat(getChange(c2.getChangeId()).starred).isNull();
-  }
-
-  private void starChange(boolean on, Change.Id id) throws IOException {
-    String url = "/accounts/self/starred.changes/" + id.get();
-    if (on) {
-      RestResponse r = adminSession.put(url);
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    } else {
-      RestResponse r = adminSession.delete(url);
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    }
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index ac3066c..e7950a4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -14,55 +14,65 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.EventSource;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
-import com.google.gerrit.server.project.PutConfig;
+import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import com.jcraft.jsch.JSchException;
-
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -73,13 +83,19 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collections;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+@Sandboxed
 public abstract class AbstractSubmit extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
   private Map<String, String> mergeResults;
+  protected Multimap<String, RefUpdateAttribute> refUpdatedEvents;
 
   @Inject
   private ChangeNotes.Factory notesFactory;
@@ -96,6 +112,7 @@
   @Before
   public void setUp() throws Exception {
     mergeResults = Maps.newHashMap();
+    refUpdatedEvents = HashMultimap.create();
     CurrentUser listenerUser = factory.create(user.id);
     source.addEventListener(new EventListener() {
 
@@ -105,11 +122,14 @@
           ChangeMergedEvent changeMergedEvent = (ChangeMergedEvent) event;
           mergeResults.put(changeMergedEvent.change.number,
               changeMergedEvent.newRev);
+        } else if (event instanceof RefUpdatedEvent) {
+          RefUpdatedEvent e = (RefUpdatedEvent) event;
+          RefUpdateAttribute r = e.refUpdate;
+          refUpdatedEvents.put(r.project + "-" + r.refName, r);
         }
       }
 
     }, listenerUser);
-    project = new Project.NameKey("p2");
   }
 
   @After
@@ -120,125 +140,313 @@
   protected abstract SubmitType getSubmitType();
 
   @Test
-  public void submitToEmptyRepo() throws JSchException, IOException,
-      GitAPIException {
-    Git git = createProject(false);
-    PushOneCommit.Result change = createChange(git);
+  @TestProjectInput(createEmptyCommit = false)
+  public void submitToEmptyRepo() throws Exception {
+    PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     assertThat(getRemoteHead().getId()).isEqualTo(change.getCommitId());
   }
 
   @Test
-  public void submitWholeTopic() throws Exception {
+  public void submitWholeTopicMultipleProjects() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
-    Git git = createProject();
+    String topic = "test-topic";
+
+    // Create test projects
+    TestRepository<?> repoA = createProjectWithPush(
+        "project-a", null, getSubmitType());
+    TestRepository<?> repoB = createProjectWithPush(
+        "project-b", null, getSubmitType());
+
+    // Create changes on project-a
     PushOneCommit.Result change1 =
-        createChange(git, "Change 1", "a.txt", "content", "test-topic");
+        createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "b.txt", "content", "test-topic");
+        createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
+
+    // Create changes on project-b
+    PushOneCommit.Result change3 =
+        createChange(repoB, "master", "Change 3", "a.txt", "content", topic);
+    PushOneCommit.Result change4 =
+        createChange(repoB, "master", "Change 4", "b.txt", "content", topic);
+
     approve(change1.getChangeId());
     approve(change2.getChangeId());
-    submit(change2.getChangeId());
-    change1.assertChange(Change.Status.MERGED, "test-topic", admin);
-    change2.assertChange(Change.Status.MERGED, "test-topic", admin);
+    approve(change3.getChangeId());
+    approve(change4.getChangeId());
+    submit(change4.getChangeId());
+
+    String expectedTopic = name(topic);
+    change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change4.assertChange(Change.Status.MERGED, expectedTopic, admin);
   }
 
-  protected Git createProject() throws JSchException, IOException,
-      GitAPIException {
-    return createProject(true);
+  @Test
+  public void submitWholeTopicMultipleBranchesOnSameProject() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    String topic = "test-topic";
+
+    // Create test project
+    String projectName = "project-a";
+    TestRepository<?> repoA = createProjectWithPush(
+        projectName, null, getSubmitType());
+
+    RevCommit initialHead =
+        getRemoteHead(new Project.NameKey(name(projectName)), "master");
+
+    // Create the dev branch on the test project
+    BranchInput in = new BranchInput();
+    in.revision = initialHead.name();
+    gApi.projects().name(name(projectName)).branch("dev").create(in);
+
+    // Create changes on master
+    PushOneCommit.Result change1 =
+        createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
+    PushOneCommit.Result change2 =
+        createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
+
+    // Create  changes on dev
+    repoA.reset(initialHead);
+    PushOneCommit.Result change3 =
+        createChange(repoA, "dev", "Change 3", "a.txt", "content", topic);
+    PushOneCommit.Result change4 =
+        createChange(repoA, "dev", "Change 4", "b.txt", "content", topic);
+
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
+    approve(change4.getChangeId());
+    submit(change4.getChangeId());
+
+    String expectedTopic = name(topic);
+    change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
+    change4.assertChange(Change.Status.MERGED, expectedTopic, admin);
   }
 
-  private Git createProject(boolean emptyCommit)
-      throws JSchException, IOException, GitAPIException {
-    SshSession sshSession = new SshSession(server, admin);
-    try {
-      GitUtil.createProject(sshSession, project.get(), null, emptyCommit);
-      setSubmitType(getSubmitType());
-      return cloneProject(sshSession.getUrl() + "/" + project.get());
-    } finally {
-      sshSession.close();
+  @Test
+  public void submitWholeTopic() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    String topic = "test-topic";
+    PushOneCommit.Result change1 =
+        createChange("Change 1", "a.txt", "content", topic);
+    PushOneCommit.Result change2 =
+        createChange("Change 2", "b.txt", "content", topic);
+    PushOneCommit.Result change3 =
+        createChange("Change 3", "c.txt", "content", topic);
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
+    submit(change3.getChangeId());
+
+    change1.assertChange(Change.Status.MERGED, topic, admin);
+    change2.assertChange(Change.Status.MERGED, topic, admin);
+    change3.assertChange(Change.Status.MERGED, topic, admin);
+    // Check for the exact change to have the correct submitter.
+    assertSubmitter(change3);
+    // Also check submitters for changes submitted via the topic relationship.
+    assertSubmitter(change1);
+    assertSubmitter(change2);
+
+    // Check that the repo has the expected commits
+    List<RevCommit> log = getRemoteLog();
+    List<String> commitsInRepo = Lists.transform(log,
+        new Function<RevCommit, String>() {
+          @Override
+          public String apply(RevCommit input) {
+            return input.getShortMessage();
+          }
+        });
+    int expectedCommitCount = getSubmitType() == SubmitType.MERGE_ALWAYS
+        ? 5 // initial commit + 3 commits + merge commit
+        : 4; // initial commit + 3 commits
+    assertThat(log).hasSize(expectedCommitCount);
+
+    assertThat(commitsInRepo).containsAllOf(
+        "Initial empty repository", "Change 1", "Change 2", "Change 3");
+    if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
+      assertThat(commitsInRepo).contains(
+          "Merge changes from topic '" + topic + "'");
     }
   }
 
-  private void setSubmitType(SubmitType submitType) throws IOException {
-    PutConfig.Input in = new PutConfig.Input();
-    in.submitType = submitType;
-    in.useContentMerge = InheritableBoolean.FALSE;
-    RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/config", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
+  @Test
+  public void submitChangeWhenParentOfOtherBranchTip() throws Exception {
+    // Chain of two commits
+    // Push both to topic-branch
+    // Push the first commit for review and submit
+    //
+    // C2 -- tip of topic branch
+    //  |
+    // C1 -- pushed for review
+    //  |
+    // C0 -- Master
+    //
+    ProjectConfig config = projectCache.checkedGet(project).getConfig();
+    config.getProject().setCreateNewChangeForAllNotInTarget(
+        InheritableBoolean.TRUE);
+    saveProjectConfig(project, config);
+
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, "a.txt", "content");
+    PushOneCommit.Result c1 = push1.to("refs/heads/topic");
+    c1.assertOkStatus();
+    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, "b.txt", "anotherContent");
+    PushOneCommit.Result c2 = push2.to("refs/heads/topic");
+    c2.assertOkStatus();
+
+    PushOneCommit.Result change1 = push1.to("refs/for/master");
+    change1.assertOkStatus();
+
+    approve(change1.getChangeId());
+    submit(change1.getChangeId());
   }
 
-  protected void setUseContentMerge() throws IOException {
-    PutConfig.Input in = new PutConfig.Input();
-    in.useContentMerge = InheritableBoolean.TRUE;
-    RestResponse r =
-        adminSession.put("/projects/" + project.get() + "/config", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
+  @Test
+  public void submitMergeOfNonChangeBranchTip() throws Exception {
+    // Merge a branch with commits that have not been submitted as
+    // changes.
+    //
+    // M  -- mergeCommit (pushed for review and submitted)
+    // | \
+    // |  S -- stable (pushed directly to refs/heads/stable)
+    // | /
+    // I   -- master
+    //
+    RevCommit master = getRemoteHead(project, "master");
+    PushOneCommit stableTip = pushFactory.create(db, admin.getIdent(), testRepo,
+        "Tip of branch stable", "stable.txt", "");
+    PushOneCommit.Result stable = stableTip.to("refs/heads/stable");
+    PushOneCommit mergeCommit = pushFactory.create(db, admin.getIdent(),
+        testRepo, "The merge commit", "merge.txt", "");
+    mergeCommit.setParents(ImmutableList.of(master, stable.getCommit()));
+    PushOneCommit.Result mergeReview = mergeCommit.to("refs/for/master");
+    approve(mergeReview.getChangeId());
+    submit(mergeReview.getChangeId());
+
+    List<RevCommit> log = getRemoteLog();
+    assertThat(log).contains(stable.getCommit());
+    assertThat(log).contains(mergeReview.getCommit());
   }
 
-  protected PushOneCommit.Result createChange(Git git) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master");
+  @Test
+  public void submitMergeOfNonChangeBranchNonTip() throws Exception {
+    // Merge a branch with commits that have not been submitted as
+    // changes.
+    //
+    // MC  -- merge commit (pushed for review and submitted)
+    // |\   S2 -- new stable tip (pushed directly to refs/heads/stable)
+    // M \ /
+    // |  S1 -- stable (pushed directly to refs/heads/stable)
+    // | /
+    // I -- master
+    //
+    RevCommit initial = getRemoteHead(project, "master");
+    // push directly to stable to S1
+    PushOneCommit.Result s1 = pushFactory.create(
+      db, admin.getIdent(), testRepo, "new commit into stable", "stable1.txt", "")
+      .to("refs/heads/stable");
+    // move the stable tip ahead to S2
+    pushFactory.create(
+      db, admin.getIdent(), testRepo, "Tip of branch stable", "stable2.txt", "")
+      .to("refs/heads/stable");
+
+    testRepo.reset(initial);
+
+    // move the master ahead
+    PushOneCommit.Result m = pushFactory.create(
+      db, admin.getIdent(), testRepo, "Move master ahead", "master.txt", "")
+      .to("refs/heads/master");
+
+    // create merge change
+    PushOneCommit mc =
+      pushFactory.create(db, admin.getIdent(), testRepo, "The merge commit", "merge.txt", "");
+    mc.setParents(ImmutableList.of(m.getCommit(), s1.getCommit()));
+    PushOneCommit.Result mergeReview = mc.to("refs/for/master");
+    approve(mergeReview.getChangeId());
+    submit(mergeReview.getChangeId());
+
+    List<RevCommit> log = getRemoteLog();
+    assertThat(log).contains(s1.getCommit());
+    assertThat(log).contains(mergeReview.getCommit());
   }
 
-  protected PushOneCommit.Result createChange(Git git, String subject,
-      String fileName, String content) throws GitAPIException, IOException {
+  private void assertSubmitter(PushOneCommit.Result change) throws Exception {
+    ChangeInfo info = get(change.getChangeId(), ListChangesOption.MESSAGES);
+    assertThat(info.messages).isNotNull();
+    assertThat(info.messages).hasSize(3);
+    if (getSubmitType() == SubmitType.CHERRY_PICK) {
+      assertThat(Iterables.getLast(info.messages).message).startsWith(
+          "Change has been successfully cherry-picked as ");
+    } else {
+      assertThat(Iterables.getLast(info.messages).message).isEqualTo(
+          "Change has been successfully merged by Administrator");
+    }
+  }
+
+  @Override
+  protected void updateProjectInput(ProjectInput in) {
+    in.submitType = getSubmitType();
+    if (in.useContentMerge == InheritableBoolean.INHERIT) {
+      in.useContentMerge = InheritableBoolean.FALSE;
+    }
+  }
+
+  protected PushOneCommit.Result createChange(String subject,
+      String fileName, String content) throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), subject, fileName, content);
-    return push.to(git, "refs/for/master");
+        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    return push.to("refs/for/master");
   }
 
-  protected PushOneCommit.Result createChange(Git git, String subject,
+  protected PushOneCommit.Result createChange(String subject,
       String fileName, String content, String topic)
-          throws GitAPIException, IOException {
+          throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), subject, fileName, content);
-    return push.to(git, "refs/for/master/" + topic);
+        pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+    return push.to("refs/for/master/" + topic);
   }
 
-  protected void submit(String changeId) throws IOException {
-    submit(changeId, HttpStatus.SC_OK);
+  protected PushOneCommit.Result createChange(TestRepository<?> repo,
+      String branch, String subject, String fileName, String content,
+      String topic) throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), repo, subject, fileName, content);
+    return push.to("refs/for/" + branch + "/" + name(topic));
   }
 
-  protected void submitWithConflict(String changeId) throws IOException {
-    submit(changeId, HttpStatus.SC_CONFLICT);
+  protected void submit(String changeId) throws Exception {
+    submit(changeId, HttpStatus.SC_OK, null);
   }
 
-  protected void submitStatusOnly(String changeId)
-      throws IOException, OrmException {
-    approve(changeId);
-    Change c = queryProvider.get().byKeyPrefix(changeId).get(0).change();
-    c.setStatus(Change.Status.SUBMITTED);
-    db.changes().update(Collections.singleton(c));
-    db.patchSetApprovals().insert(Collections.singleton(
-        new PatchSetApproval(
-            new PatchSetApproval.Key(
-                c.currentPatchSetId(),
-                admin.id,
-                LabelId.SUBMIT),
-            (short) 1,
-            new Timestamp(System.currentTimeMillis()))));
-    indexer.index(db, c);
+  protected void submitWithConflict(String changeId,
+      String expectedError) throws Exception {
+    submit(changeId, HttpStatus.SC_CONFLICT, expectedError);
   }
 
-  private void submit(String changeId, int expectedStatus) throws IOException {
+  private void submit(String changeId, int expectedStatus, String msg)
+      throws Exception {
     approve(changeId);
     SubmitInput subm = new SubmitInput();
-    subm.waitForMerge = true;
     RestResponse r =
         adminSession.post("/changes/" + changeId + "/submit", subm);
     assertThat(r.getStatusCode()).isEqualTo(expectedStatus);
     if (expectedStatus == HttpStatus.SC_OK) {
+      checkArgument(msg == null, "msg must be null for successful submits");
       ChangeInfo change =
           newGson().fromJson(r.getReader(),
               new TypeToken<ChangeInfo>() {}.getType());
       assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
 
       checkMergeResult(change);
+    } else {
+      checkArgument(!Strings.isNullOrEmpty(msg), "msg must be a valid string " +
+          "containing an error message for unsuccessful submits");
+      assertThat(r.getEntityContent()).isEqualTo(msg);
     }
     r.consume();
   }
@@ -247,51 +455,64 @@
     // Get the revision of the branch after the submit to compare with the
     // newRev of the ChangeMergedEvent.
     RestResponse b =
-        adminSession.get("/projects/" + project.get() + "/branches/"
+        adminSession.get("/projects/" + change.project + "/branches/"
             + change.branch);
     if (b.getStatusCode() == HttpStatus.SC_OK) {
       BranchInfo branch =
           newGson().fromJson(b.getReader(),
               new TypeToken<BranchInfo>() {}.getType());
-      assertThat(branch.revision).isEqualTo(
-          mergeResults.get(Integer.toString(change._number)));
+      assertThat(mergeResults).isNotEmpty();
+      String newRev = mergeResults.get(Integer.toString(change._number));
+      assertThat(newRev).isNotNull();
+      assertThat(branch.revision).isEqualTo(newRev);
     }
     b.consume();
   }
 
-  private void approve(String changeId) throws IOException {
-    RestResponse r = adminSession.post(
-        "/changes/" + changeId + "/revisions/current/review",
-        new ReviewInput().label("Code-Review", 2));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
-  }
-
   protected void assertCurrentRevision(String changeId, int expectedNum,
-      ObjectId expectedId) throws IOException {
-    ChangeInfo c = getChange(changeId, CURRENT_REVISION);
+      ObjectId expectedId) throws Exception {
+    ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(expectedId.name());
     assertThat(c.revisions.get(expectedId.name())._number).isEqualTo(expectedNum);
-    Repository repo =
-        repoManager.openRepository(new Project.NameKey(c.project));
-    try {
-      Ref ref = repo.getRef(
-          new PatchSet.Id(new Change.Id(c._number), expectedNum).toRefName());
-      assertThat(ref).isNotNull();
+    try (Repository repo =
+        repoManager.openRepository(new Project.NameKey(c.project))) {
+      String refName = new PatchSet.Id(new Change.Id(c._number), expectedNum)
+          .toRefName();
+      Ref ref = repo.exactRef(refName);
+      assertThat(ref).named(refName).isNotNull();
       assertThat(ref.getObjectId()).isEqualTo(expectedId);
-    } finally {
-      repo.close();
     }
   }
 
-  protected void assertApproved(String changeId) throws IOException {
-    ChangeInfo c = getChange(changeId, DETAILED_LABELS);
+  protected void assertNew(String changeId) throws Exception {
+    assertThat(get(changeId).status).isEqualTo(ChangeStatus.NEW);
+  }
+
+  protected void assertApproved(String changeId) throws Exception {
+    ChangeInfo c = get(changeId, DETAILED_LABELS);
     LabelInfo cr = c.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
-    assertThat(cr.all.get(0).value.intValue()).isEqualTo(2);
+    assertThat(cr.all.get(0).value).isEqualTo(2);
     assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(admin.getId());
   }
 
+  protected void assertMerged(PushOneCommit.Result change)
+      throws RestApiException {
+    String changeId = change.getChangeId();
+    ChangeStatus status = gApi.changes().id(changeId).info().status;
+    assertThat(status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  protected void assertPersonEquals(PersonIdent expected,
+      PersonIdent actual) {
+    assertThat(actual.getEmailAddress())
+        .isEqualTo(expected.getEmailAddress());
+    assertThat(actual.getName())
+        .isEqualTo(expected.getName());
+    assertThat(actual.getTimeZone())
+        .isEqualTo(expected.getTimeZone());
+  }
+
   protected void assertSubmitter(String changeId, int psId)
       throws OrmException {
     ChangeNotes cn = notesFactory.create(
@@ -302,17 +523,26 @@
     assertThat(submitter.getAccountId()).isEqualTo(admin.getId());
   }
 
-  protected void assertCherryPick(Git localGit, boolean contentMerge)
-      throws IOException {
-    assertRebase(localGit, contentMerge);
+  protected void assertNoSubmitter(String changeId, int psId)
+      throws OrmException {
+    ChangeNotes cn = notesFactory.create(
+        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change());
+    PatchSetApproval submitter = approvalsUtil.getSubmitter(
+        db, cn, new PatchSet.Id(cn.getChangeId(), psId));
+    assertThat(submitter).isNull();
+  }
+
+  protected void assertCherryPick(TestRepository<?> testRepo,
+      boolean contentMerge) throws IOException {
+    assertRebase(testRepo, contentMerge);
     RevCommit remoteHead = getRemoteHead();
     assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty();
     assertThat(remoteHead.getFooterLines("Reviewed-By")).isNotEmpty();
   }
 
-  protected void assertRebase(Git localGit, boolean contentMerge)
+  protected void assertRebase(TestRepository<?> testRepo, boolean contentMerge)
       throws IOException {
-    Repository repo = localGit.getRepository();
+    Repository repo = testRepo.getRepository();
     RevCommit localHead = getHead(repo);
     RevCommit remoteHead = getRemoteHead();
     assert_().withFailureMessage(
@@ -329,24 +559,42 @@
     return getHead(repo, "HEAD");
   }
 
-  protected RevCommit getRemoteHead() throws IOException {
+  protected RevCommit getRemoteHead(Project.NameKey project, String branch)
+      throws IOException {
     try (Repository repo = repoManager.openRepository(project)) {
-      return getHead(repo, "refs/heads/master");
+      return getHead(repo, "refs/heads/" + branch);
     }
   }
 
-  protected List<RevCommit> getRemoteLog() throws IOException {
+  protected RevCommit getRemoteHead()
+      throws IOException {
+    return getRemoteHead(project, "master");
+  }
+
+
+  protected List<RevCommit> getRemoteLog(Project.NameKey project, String branch)
+      throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.markStart(rw.parseCommit(
-          repo.getRef("refs/heads/master").getObjectId()));
+          repo.exactRef("refs/heads/" + branch).getObjectId()));
       return Lists.newArrayList(rw);
     }
   }
 
+  protected List<RevCommit> getRemoteLog() throws IOException {
+    return getRemoteLog(project, "master");
+  }
+
+  protected RefUpdateAttribute getOneRefUpdate(String key) {
+    Collection<RefUpdateAttribute> refUpdates = refUpdatedEvents.get(key);
+    assertThat(refUpdates).hasSize(1);
+    return refUpdates.iterator().next();
+  }
+
   private RevCommit getHead(Repository repo, String name) throws IOException {
     try (RevWalk rw = new RevWalk(repo)) {
-      return rw.parseCommit(repo.getRef(name).getObjectId());
+      return rw.parseCommit(repo.exactRef(name).getObjectId());
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 1d60607..2b3d035 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -15,34 +15,27 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
 
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public abstract class AbstractSubmitByMerge extends AbstractSubmit {
 
   @Test
-  public void submitWithMerge() throws JSchException, IOException,
-      GitAPIException {
-    Git git = createProject();
+  public void submitWithMerge() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "b.txt", "other content");
+        createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
@@ -51,21 +44,19 @@
   }
 
   @Test
-  public void submitWithContentMerge() throws JSchException, IOException,
-      GitAPIException {
-    Git git = createProject();
-    setUseContentMerge();
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
+  public void submitWithContentMerge() throws Exception {
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+        createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+        createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, change.getCommitId().getName());
+    testRepo.reset(change.getCommitId());
     PushOneCommit.Result change3 =
-        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+        createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
@@ -74,20 +65,22 @@
   }
 
   @Test
-  public void submitWithContentMerge_Conflict() throws JSchException,
-      IOException, GitAPIException {
-    Git git = createProject();
-    setUseContentMerge();
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
+  public void submitWithContentMerge_Conflict() throws Exception {
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "a.txt", "other content");
-    submitWithConflict(change2.getChangeId());
+        createChange("Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId(),
+        "Cannot merge " + change2.getCommit().name() + "\n" +
+        "Change could not be merged due to a path conflict.\n\n" +
+        "Please rebase the change locally " +
+        "and upload the rebased commit for review.");
     assertThat(getRemoteHead()).isEqualTo(oldHead);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index acd096d..ba98963 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -18,22 +18,26 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ActionInfo;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.testutil.ConfigSuite;
 
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Map;
 
 public class ActionsIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
   @Test
   public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
-    String changeId = createChangeWithTopic("foo1").getChangeId();
+    String changeId = createChangeWithTopic().getChangeId();
     Map<String, ActionInfo> actions = getActions(changeId);
     assertThat(actions).containsKey("cherrypick");
     assertThat(actions).containsKey("rebase");
@@ -42,27 +46,20 @@
 
   @Test
   public void revisionActionsOneChangePerTopic() throws Exception {
-    String changeId = createChangeWithTopic("foo1").getChangeId();
+    String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
-    if (isSubmitWholeTopicEnabled()) {
-      ActionInfo info = actions.get("submit");
-      assertThat(info.enabled).isTrue();
-      assertThat(info.label).isEqualTo("Submit whole topic");
-      assertThat(info.method).isEqualTo("POST");
-      assertThat(info.title).isEqualTo("Submit all 1 changes of the same topic");
-    } else {
-      noSubmitWholeTopicAssertions(actions);
-    }
+    // We want to treat a single change in a topic not as a whole topic,
+    // so regardless of how submitWholeTopic is configured:
+    noSubmitWholeTopicAssertions(actions, 1);
   }
 
   @Test
-  public void revisionActionsTwoChangeChangesInTopic() throws Exception {
-    String changeId = createChangeWithTopic("foo2").getChangeId();
+  public void revisionActionsTwoChangesInTopic() throws Exception {
+    String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
-    // create another change with the same topic
-    createChangeWithTopic("foo2").getChangeId();
+    String changeId2 = createChangeWithTopic().getChangeId();
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
     if (isSubmitWholeTopicEnabled()) {
@@ -70,47 +67,105 @@
       assertThat(info.enabled).isNull();
       assertThat(info.label).isEqualTo("Submit whole topic");
       assertThat(info.method).isEqualTo("POST");
-      assertThat(info.title).isEqualTo("Other changes in this topic are not ready");
+      assertThat(info.title).isEqualTo("This change depends on other " +
+          "changes which are not ready");
     } else {
-      noSubmitWholeTopicAssertions(actions);
+      noSubmitWholeTopicAssertions(actions, 1);
+
+      assertThat(getActions(changeId2).get("submit")).isNull();
+      approve(changeId2);
+      noSubmitWholeTopicAssertions(getActions(changeId2), 2);
     }
   }
 
   @Test
-  public void revisionActionsTwoChangeChangesInTopicReady() throws Exception {
-    String changeId = createChangeWithTopic("foo2").getChangeId();
+  public void revisionActionsTwoChangesInTopic_conflicting() throws Exception {
+    String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
+
     // create another change with the same topic
-    String changeId2 = createChangeWithTopic("foo2").getChangeId();
+    String changeId2 = createChangeWithTopic(testRepo, "foo2", "touching b",
+        "b.txt", "real content").getChangeId();
     approve(changeId2);
+
+    // collide with the other change in the same topic
+    testRepo.reset("HEAD~2");
+    String collidingChange = createChangeWithTopic(testRepo, "off_topic",
+        "rewriting file b", "b.txt", "garbage\ngarbage\ngarbage").getChangeId();
+    gApi.changes().id(collidingChange).current().review(ReviewInput.approve());
+    gApi.changes().id(collidingChange).current().submit();
+
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
     if (isSubmitWholeTopicEnabled()) {
       ActionInfo info = actions.get("submit");
+      assertThat(info.enabled).isNull();
+      assertThat(info.label).isEqualTo("Submit whole topic");
+      assertThat(info.method).isEqualTo("POST");
+      assertThat(info.title).isEqualTo("Problems with change(s): 2");
+    } else {
+      noSubmitWholeTopicAssertions(actions, 1);
+    }
+  }
+
+  @Test
+  public void revisionActionsTwoChangesInTopicWithAncestorReady()
+      throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+    approve(changeId);
+    String changeId1 = createChangeWithTopic().getChangeId();
+    approve(changeId1);
+    // create another change with the same topic
+    String changeId2 = createChangeWithTopic().getChangeId();
+    approve(changeId2);
+    Map<String, ActionInfo> actions = getActions(changeId1);
+    commonActionsAssertions(actions);
+    if (isSubmitWholeTopicEnabled()) {
+      ActionInfo info = actions.get("submit");
       assertThat(info.enabled).isTrue();
       assertThat(info.label).isEqualTo("Submit whole topic");
       assertThat(info.method).isEqualTo("POST");
-      assertThat(info.title).isEqualTo("Submit all 2 changes of the same topic");
+      assertThat(info.title).isEqualTo("Submit all 2 changes of the same " +
+          "topic (3 changes including ancestors " +
+          "and other changes related by topic)");
     } else {
-      noSubmitWholeTopicAssertions(actions);
+      noSubmitWholeTopicAssertions(actions, 2);
     }
   }
 
-  private Map<String, ActionInfo> getActions(String changeId)
-      throws IOException {
-    return newGson().fromJson(
-        adminSession.get("/changes/"
-            + changeId
-            + "/revisions/1/actions").getReader(),
-        new TypeToken<Map<String, ActionInfo>>() {}.getType());
+  @Test
+  public void revisionActionsReadyWithAncestors() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+    approve(changeId);
+    String changeId1 = createChange().getChangeId();
+    approve(changeId1);
+    String changeId2 = createChangeWithTopic().getChangeId();
+    approve(changeId2);
+    Map<String, ActionInfo> actions = getActions(changeId2);
+    commonActionsAssertions(actions);
+    // The topic contains only one change, so standard text applies
+    noSubmitWholeTopicAssertions(actions, 3);
   }
 
-  private void noSubmitWholeTopicAssertions(Map<String, ActionInfo> actions) {
+  private void noSubmitWholeTopicAssertions(Map<String, ActionInfo> actions,
+      int nrChanges) {
     ActionInfo info = actions.get("submit");
     assertThat(info.enabled).isTrue();
-    assertThat(info.label).isEqualTo("Submit");
+    if (nrChanges == 1) {
+      assertThat(info.label).isEqualTo("Submit");
+    } else {
+      assertThat(info.label).isEqualTo("Submit including parents");
+    }
     assertThat(info.method).isEqualTo("POST");
-    assertThat(info.title).isEqualTo("Submit patch set 1 into master");
+    if (nrChanges == 1) {
+      assertThat(info.title).isEqualTo("Submit patch set 1 into master");
+    } else {
+      assertThat(info.title).isEqualTo(String.format(
+          "Submit patch set 1 and ancestors (%d changes " +
+          "altogether) into master", nrChanges));
+    }
   }
 
   private void commonActionsAssertions(Map<String, ActionInfo> actions) {
@@ -120,18 +175,18 @@
     assertThat(actions).containsKey("rebase");
   }
 
-  private PushOneCommit.Result createChangeWithTopic(String topic) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+  private PushOneCommit.Result createChangeWithTopic(
+      TestRepository<InMemoryRepository> repo, String topic,
+      String commitMsg, String fileName, String content) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+        repo, commitMsg, fileName, content);
     assertThat(topic).isNotEmpty();
-    return push.to(git, "refs/for/master/" + topic);
+    return push.to("refs/for/master/" + name(topic));
   }
 
-  private void approve(String changeId) throws IOException {
-    RestResponse r = adminSession.post(
-        "/changes/" + changeId + "/revisions/current/review",
-        new ReviewInput().label("Code-Review", 2));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
+  private PushOneCommit.Result createChangeWithTopic()
+      throws Exception {
+    return createChangeWithTopic(testRepo, "foo2",
+        "a message", "a.txt", "content\n");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
index f42a134..1a8e151 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -5,10 +5,11 @@
   'AbstractSubmitByMerge.java',
 ]
 
-SUBMIT_TESTS = glob(['Submit*IT.java'], excludes = SUBMIT_UTIL_SRCS)
-OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS + SUBMIT_UTIL_SRCS)
+SUBMIT_TESTS = glob(['Submit*IT.java'])
+OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS)
 
 acceptance_tests(
+  group = 'rest-change-other',
   srcs = OTHER_TESTS,
   deps = [
     ':submit_util',
@@ -16,8 +17,9 @@
   ],
   labels = ['rest'],
 )
-
+ 
 acceptance_tests(
+  group = 'rest-change-submit',
   srcs = SUBMIT_TESTS,
   deps = [
     ':submit_util',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 3c76355..91f2962 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -15,56 +15,35 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TestTimeUtil;
 
-import org.eclipse.jgit.lib.Config;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.Iterator;
-import java.util.concurrent.atomic.AtomicLong;
 
 @RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
   private String systemTimeZone;
-  private volatile long clockStepMs;
-
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
 
   @Before
   public void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    clockStepMs = MILLISECONDS.convert(1, SECONDS);
-    final AtomicLong clockMs = new AtomicLong(
-        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
-    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
-      @Override
-      public long getMillis() {
-        return clockMs.getAndAdd(clockStepMs);
-      }
-    });
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
   }
 
   @After
   public void resetTime() {
-    DateTimeUtils.setCurrentMillisSystem();
+    TestTimeUtil.useSystemTime();
     System.setProperty("user.timezone", systemTimeZone);
   }
 
@@ -73,15 +52,15 @@
     String changeId = createChange().getChangeId();
     postMessage(changeId, "Some nits need to be fixed.");
     ChangeInfo c = info(changeId);
-    assertThat((Iterable<?>)c.messages).isNull();
+    assertThat(c.messages).isNull();
   }
 
   @Test
   public void defaultMessage() throws Exception {
     String changeId = createChange().getChangeId();
     ChangeInfo c = get(changeId);
-    assertThat((Iterable<?>)c.messages).isNotNull();
-    assertThat((Iterable<?>)c.messages).hasSize(1);
+    assertThat(c.messages).isNotNull();
+    assertThat(c.messages).hasSize(1);
     assertThat(c.messages.iterator().next().message)
       .isEqualTo("Uploaded patch set 1.");
   }
@@ -94,8 +73,8 @@
     String secondMessage = "I like this feature.";
     postMessage(changeId, secondMessage);
     ChangeInfo c = get(changeId);
-    assertThat((Iterable<?>)c.messages).isNotNull();
-    assertThat((Iterable<?>)c.messages).hasSize(3);
+    assertThat(c.messages).isNotNull();
+    assertThat(c.messages).hasSize(3);
     Iterator<ChangeMessageInfo> it = c.messages.iterator();
     assertThat(it.next().message).isEqualTo("Uploaded patch set 1.");
     assertMessage(firstMessage, it.next().message);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 8e7daec..2229577 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -14,27 +14,22 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.common.data.Permission.LABEL;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Before;
 import org.junit.Test;
@@ -45,45 +40,47 @@
 
   private TestAccount user2;
 
-  private RestSession sessionOwner;
-  private RestSession sessionDev;
-
   @Before
   public void setUp() throws Exception {
-    sessionOwner = new RestSession(server, user);
-    SshSession sshSession = new SshSession(server, user);
-    initSsh(user);
-    sshSession.open();
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
+    setApiUser(user);
     user2 = accounts.user2();
-    sessionDev = new RestSession(server, user2);
   }
 
   @Test
+  @TestProjectInput(cloneAs = "user")
   public void testChangeOwner_OwnerACLNotGranted() throws Exception {
-    approve(sessionOwner, createMyChange(), HttpStatus.SC_FORBIDDEN);
+    assertApproveFails(user, createMyChange());
   }
 
   @Test
+  @TestProjectInput(cloneAs = "user")
   public void testChangeOwner_OwnerACLGranted() throws Exception {
     grantApproveToChangeOwner();
-    approve(sessionOwner, createMyChange(), HttpStatus.SC_OK);
+    approve(user, createMyChange());
   }
 
   @Test
+  @TestProjectInput(cloneAs = "user")
   public void testChangeOwner_NotOwnerACLGranted() throws Exception {
     grantApproveToChangeOwner();
-    approve(sessionDev, createMyChange(), HttpStatus.SC_FORBIDDEN);
+    assertApproveFails(user2, createMyChange());
   }
 
-  private void approve(RestSession s, String changeId, int expected)
-      throws IOException {
-    RestResponse r =
-        s.post("/changes/" + changeId + "/revisions/current/review",
-            new ReviewInput().label("Code-Review", 2));
-    assertThat(r.getStatusCode()).isEqualTo(expected);
-    r.consume();
+  private void approve(TestAccount a, String changeId) throws Exception {
+    Context old = setApiUser(a);
+    try {
+      gApi.changes().id(changeId).current().review(ReviewInput.approve());
+    } finally {
+      atrScope.set(old);
+    }
+  }
+
+  private void assertApproveFails(TestAccount a, String changeId) throws Exception {
+    try {
+      approve(a, changeId);
+    } catch (AuthException expected) {
+      // Expected.
+    }
   }
 
   private void grantApproveToChangeOwner() throws IOException,
@@ -102,9 +99,8 @@
     projectCache.evict(config.getProject());
   }
 
-  private String createMyChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, user.getIdent());
-    return push.to(git, "refs/for/master").getChangeId();
+  private String createMyChange() throws Exception {
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    return push.to("refs/for/master").getChangeId();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
new file mode 100644
index 0000000..c78231b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.Util;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConfigChangeIT extends AbstractDaemonTest {
+  @Before
+  public void setUp() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
+    Util.allow(
+        cfg, Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
+    Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/meta/config");
+    saveProjectConfig(project, cfg);
+
+    setApiUser(user);
+    fetchRefsMetaConfig();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void updateProjectConfig() throws Exception {
+    Config cfg = readProjectConfig();
+    assertThat(cfg.getString("project", null, "description")).isNull();
+    String desc = "new project description";
+    cfg.setString("project", null, "description", desc);
+
+    PushOneCommit.Result r = createConfigChange(cfg);
+    String id = r.getChangeId();
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    gApi.changes().id(id).current().submit();
+
+    assertThat(gApi.changes().id(id).info().status)
+        .isEqualTo(ChangeStatus.MERGED);
+    assertThat(gApi.projects().name(project.get()).get().description)
+        .isEqualTo(desc);
+    fetchRefsMetaConfig();
+    assertThat(readProjectConfig().getString("project", null, "description"))
+        .isEqualTo(desc);
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void onlyAdminMayUpdateProjectParent() throws Exception {
+    setApiUser(admin);
+    ProjectInput parent = new ProjectInput();
+    parent.name = name("parent");
+    parent.permissionsOnly = true;
+    gApi.projects().create(parent);
+
+    setApiUser(user);
+    Config cfg = readProjectConfig();
+    assertThat(cfg.getString("access", null, "inheritFrom"))
+        .isAnyOf(null, allProjects.get());
+    cfg.setString("access", null, "inheritFrom", parent.name);
+
+    PushOneCommit.Result r = createConfigChange(cfg);
+    String id = r.getChangeId();
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    try {
+      gApi.changes().id(id).current().submit();
+      fail("expected submit to fail");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(
+          "Cannot merge " + r.getCommit().name() + "\n"
+          + "Change contains a project configuration that changes the parent"
+          + " project.\n"
+          + "The change must be submitted by a Gerrit administrator.");
+    }
+
+    assertThat(gApi.projects().name(project.get()).get().parent)
+        .isEqualTo(allProjects.get());
+    fetchRefsMetaConfig();
+    assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
+        .isAnyOf(null, allProjects.get());
+
+    setApiUser(admin);
+    gApi.changes().id(id).current().submit();
+    assertThat(gApi.changes().id(id).info().status)
+        .isEqualTo(ChangeStatus.MERGED);
+    assertThat(gApi.projects().name(project.get()).get().parent)
+        .isEqualTo(parent.name);
+    fetchRefsMetaConfig();
+    assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
+        .isEqualTo(parent.name);
+  }
+
+  private void fetchRefsMetaConfig() throws Exception {
+    git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config"))
+        .call();
+    testRepo.reset("refs/meta/config");
+  }
+
+  private Config readProjectConfig() throws Exception {
+    RevWalk rw = testRepo.getRevWalk();
+    RevTree tree = rw.parseTree(testRepo.getRepository().resolve("HEAD"));
+    RevObject obj = rw.parseAny(testRepo.get(tree, "project.config"));
+    ObjectLoader loader = rw.getObjectReader().open(obj);
+    String text = new String(loader.getCachedBytes(), UTF_8);
+    Config cfg = new Config();
+    cfg.fromText(text);
+    return cfg;
+  }
+
+  private PushOneCommit.Result createConfigChange(Config cfg) throws Exception {
+    PushOneCommit.Result r = pushFactory.create(
+            db, user.getIdent(), testRepo,
+            "Update project config",
+            "project.config",
+            cfg.toText())
+        .to("refs/for/refs/meta/config");
+    r.assertOkStatus();
+    return r;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
deleted file mode 100644
index 6a1c0a6..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2013 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.change;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
-
-import com.google.common.base.Function;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gson.reflect.TypeToken;
-
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Set;
-
-public class ConflictsOperatorIT extends AbstractDaemonTest {
-
-  private int count;
-
-  @Test
-  public void noConflictingChanges() throws Exception {
-    PushOneCommit.Result change = createChange(git, true);
-    createChange(git, false);
-
-    Set<String> changes = queryConflictingChanges(change);
-    assertThat((Iterable<?>)changes).isEmpty();
-  }
-
-  @Test
-  public void conflictingChanges() throws Exception {
-    PushOneCommit.Result change = createChange(git, true);
-    PushOneCommit.Result conflictingChange1 = createChange(git, true);
-    PushOneCommit.Result conflictingChange2 = createChange(git, true);
-    createChange(git, false);
-
-    Set<String> changes = queryConflictingChanges(change);
-    assertChanges(changes, conflictingChange1, conflictingChange2);
-  }
-
-  private PushOneCommit.Result createChange(Git git, boolean conflicting)
-      throws GitAPIException, IOException {
-    checkout(git, "origin/master");
-    String file = conflicting ? "test.txt" : "test-" + count + ".txt";
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), "Change " + count, file,
-            "content " + count);
-    count++;
-    return push.to(git, "refs/for/master");
-  }
-
-  private Set<String> queryConflictingChanges(PushOneCommit.Result change)
-      throws IOException {
-    RestResponse r =
-        adminSession.get("/changes/?q=conflicts:" + change.getChangeId());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Set<ChangeInfo> changes =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<Set<ChangeInfo>>() {}.getType());
-    r.consume();
-    return ImmutableSet.copyOf(Iterables.transform(changes,
-        new Function<ChangeInfo, String>() {
-          @Override
-          public String apply(ChangeInfo input) {
-            return input.id;
-          }
-        }));
-  }
-
-  private void assertChanges(Set<String> actualChanges,
-      PushOneCommit.Result... expectedChanges) {
-    assertThat((Iterable<?>)actualChanges).hasSize(expectedChanges.length);
-    for (PushOneCommit.Result c : expectedChanges) {
-      assertThat(actualChanges.contains(id(c))).isTrue();
-    }
-  }
-
-  private String id(PushOneCommit.Result change) {
-    return project.get() + "~master~" + change.getChangeId();
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 311161a..9edf9b2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -16,31 +16,48 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TestTimeUtil;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+@NoHttpd
 public class CreateChangeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config allowDraftsDisabled() {
     return allowDraftsDisabledConfig();
   }
 
+  @BeforeClass
+  public static void setTimeForTesting() {
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
+  }
+
+  @AfterClass
+  public static void restoreTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
+
   @Test
   public void createEmptyChange_MissingBranch() throws Exception {
     ChangeInfo ci = new ChangeInfo();
     ci.project = project.get();
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
-    assertThat(r.getEntityContent()).contains("branch must be non-empty");
+    assertCreateFails(ci, BadRequestException.class,
+        "branch must be non-empty");
   }
 
   @Test
@@ -48,37 +65,34 @@
     ChangeInfo ci = new ChangeInfo();
     ci.project = project.get();
     ci.branch = "master";
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
-    assertThat(r.getEntityContent()).contains("commit message must be non-empty");
+    assertCreateFails(ci, BadRequestException.class,
+        "commit message must be non-empty");
   }
 
   @Test
   public void createEmptyChange_InvalidStatus() throws Exception {
-    ChangeInfo ci = newChangeInfo(ChangeStatus.SUBMITTED);
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
-    assertThat(r.getEntityContent()).contains("unsupported change status");
+    ChangeInfo ci = newChangeInfo(ChangeStatus.MERGED);
+    assertCreateFails(ci, BadRequestException.class,
+        "unsupported change status");
   }
 
   @Test
   public void createNewChange() throws Exception {
-    assertChange(newChangeInfo(ChangeStatus.NEW));
+    assertCreateSucceeds(newChangeInfo(ChangeStatus.NEW));
   }
 
   @Test
   public void createDraftChange() throws Exception {
     assume().that(isAllowDrafts()).isTrue();
-    assertChange(newChangeInfo(ChangeStatus.DRAFT));
+    assertCreateSucceeds(newChangeInfo(ChangeStatus.DRAFT));
   }
 
   @Test
   public void createDraftChangeNotAllowed() throws Exception {
     assume().that(isAllowDrafts()).isFalse();
     ChangeInfo ci = newChangeInfo(ChangeStatus.DRAFT);
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
-    assertThat(r.getEntityContent()).contains("draft workflow is disabled");
+    assertCreateFails(ci, MethodNotAllowedException.class,
+        "draft workflow is disabled");
   }
 
   private ChangeInfo newChangeInfo(ChangeStatus status) {
@@ -91,13 +105,8 @@
     return in;
   }
 
-  private void assertChange(ChangeInfo in) throws Exception {
-    RestResponse r = adminSession.post("/changes/", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-
-    ChangeInfo info = newGson().fromJson(r.getReader(), ChangeInfo.class);
-    ChangeInfo out = get(info.changeId);
-
+  private void assertCreateSucceeds(ChangeInfo in) throws Exception {
+    ChangeInfo out = gApi.changes().create(in).get();
     assertThat(out.branch).isEqualTo(in.branch);
     assertThat(out.subject).isEqualTo(in.subject);
     assertThat(out.topic).isEqualTo(in.topic);
@@ -107,6 +116,14 @@
     assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
   }
 
+  private void assertCreateFails(ChangeInfo in,
+      Class<? extends RestApiException> errType, String errSubstring)
+      throws Exception {
+    exception.expect(errType);
+    exception.expectMessage(errSubstring);
+    gApi.changes().create(in);
+  }
+
   private ChangeStatus booleanToDraftStatus(Boolean draft) {
     if (draft == null) {
       return ChangeStatus.NEW;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 871b1cc..47d071f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -29,7 +29,6 @@
 import com.google.gwtorm.server.OrmException;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -40,7 +39,7 @@
   public void deletePatchSet() throws Exception {
     String changeId = createChange().getChangeId();
     PatchSet ps = getCurrentPatchSet(changeId);
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
@@ -53,7 +52,7 @@
   public void deleteDraftPatchSetNoACL() throws Exception {
     String changeId = createDraftChangeWith2PS();
     PatchSet ps = getCurrentPatchSet(changeId);
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
@@ -66,26 +65,25 @@
   public void deleteDraftPatchSetAndChange() throws Exception {
     String changeId = createDraftChangeWith2PS();
     PatchSet ps = getCurrentPatchSet(changeId);
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
     RestResponse r = deletePatchSet(changeId, ps, adminSession);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    assertThat(getChange(changeId).patches().size()).isEqualTo(1);
+    assertThat(getChange(changeId).patchSets()).hasSize(1);
     ps = getCurrentPatchSet(changeId);
     r = deletePatchSet(changeId, ps, adminSession);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty();
   }
 
-  private String createDraftChangeWith2PS() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    Result result = push.to(git, "refs/drafts/master");
-    push = pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+  private String createDraftChangeWith2PS() throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    Result result = push.to("refs/drafts/master");
+    push = pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
         "b.txt", "4711", result.getChangeId());
-    return push.to(git, "refs/drafts/master").getChangeId();
+    return push.to("refs/drafts/master").getChangeId();
   }
 
   private PatchSet getCurrentPatchSet(String changeId) throws OrmException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index a3809a9..2707507 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtorm.server.OrmException;
@@ -45,7 +46,7 @@
     PushOneCommit.Result result = createChange();
     result.assertOkStatus();
     String changeId = result.getChangeId();
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
@@ -60,12 +61,15 @@
     PushOneCommit.Result result = createDraftChange();
     result.assertOkStatus();
     String changeId = result.getChangeId();
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
     RestResponse response = deleteChange(changeId, adminSession);
     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+
+    exception.expect(ResourceNotFoundException.class);
+    get(triplet);
   }
 
   @Test
@@ -74,14 +78,16 @@
     PushOneCommit.Result result = createDraftChange();
     result.assertOkStatus();
     String changeId = result.getChangeId();
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
+    assertThat(c.revisions.get(c.currentRevision).draft).isTrue();
     RestResponse response = publishChange(changeId);
     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     c = get(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(c.revisions.get(c.currentRevision).draft).isNull();
   }
 
   @Test
@@ -90,7 +96,7 @@
     PushOneCommit.Result result = createDraftChange();
     result.assertOkStatus();
     String changeId = result.getChangeId();
-    String triplet = "p~master~" + changeId;
+    String triplet = project.get() + "~master~" + changeId;
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 26d6a1e..aa7305a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -15,219 +15,206 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.truth.IterableSubject;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-
+@NoHttpd
 public class HashtagsIT extends AbstractDaemonTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return NotesMigration.allEnabledConfig();
-  }
-
-  private void assertResult(RestResponse r, List<String> expected)
-      throws IOException {
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    List<String> result = toHashtagList(r);
-    assertThat(result).containsExactlyElementsIn(expected);
+  @Before
+  public void before() {
+    assume().that(notesMigration.enabled()).isTrue();
   }
 
   @Test
   public void testGetNoHashtags() throws Exception {
-    // GET hashtags on a change with no hashtags returns an empty list
-    String changeId = createChange().getChangeId();
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Get on a change with no hashtags returns an empty list.
+    PushOneCommit.Result r = createChange();
+    assertThatGet(r).isEmpty();
   }
 
   @Test
   public void testAddSingleHashtag() throws Exception {
-    String changeId = createChange().getChangeId();
+    PushOneCommit.Result r = createChange();
 
-    // POST adding a single hashtag returns a single hashtag
-    List<String> expected = Arrays.asList("tag2");
-    assertResult(POST(changeId, "tag2", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding a single hashtag returns a single hashtag.
+    addHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag2");
 
-    // POST adding another single hashtag to change that already has one
-    // hashtag returns a sorted list of hashtags with existing and new
-    expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding another single hashtag to change that already has one hashtag
+    // returns a sorted list of hashtags with existing and new.
+    addHashtags(r, "tag1");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
   }
 
   @Test
   public void testAddMultipleHashtags() throws Exception {
-    String changeId = createChange().getChangeId();
+    PushOneCommit.Result r = createChange();
 
-    // POST adding multiple hashtags returns a sorted list of hashtags
-    List<String> expected = Arrays.asList("tag1", "tag3");
-    assertResult(POST(changeId, "tag3, tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding multiple hashtags returns a sorted list of hashtags.
+    addHashtags(r, "tag3", "tag1");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
 
-    // POST adding multiple hashtags to change that already has hashtags
-    // returns a sorted list of hashtags with existing and new
-    expected = Arrays.asList("tag1", "tag2", "tag3", "tag4");
-    assertResult(POST(changeId, "tag2, tag4", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding multiple hashtags to change that already has hashtags returns a
+    // sorted list of hashtags with existing and new.
+    addHashtags(r, "tag2", "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3", "tag4").inOrder();
   }
 
   @Test
   public void testAddAlreadyExistingHashtag() throws Exception {
-    // POST adding a hashtag that already exists on the change returns a
-    // sorted list of hashtags without duplicates
-    String changeId = createChange().getChangeId();
-    List<String> expected = Arrays.asList("tag2");
-    assertResult(POST(changeId, "tag2", null), expected);
-    assertResult(GET(changeId), expected);
-    assertResult(POST(changeId, "tag2", null), expected);
-    assertResult(GET(changeId), expected);
-    expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "tag2, tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding a hashtag that already exists on the change returns a sorted list
+    // of hashtags without duplicates.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag2");
+    addHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag2");
+    addHashtags(r, "tag1", "tag2");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
   }
 
   @Test
   public void testHashtagsWithPrefix() throws Exception {
-    String changeId = createChange().getChangeId();
+    PushOneCommit.Result r = createChange();
 
-    // Leading # is stripped from added tag
-    List<String> expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "#tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from added tag.
+    addHashtags(r, "#tag1");
+    assertThatGet(r).containsExactly("tag1");
 
-    // Leading # is stripped from multiple added tags
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, "#tag2, #tag3", null), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from multiple added tags.
+    addHashtags(r, "#tag2", "#tag3");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
 
-    // Leading # is stripped from removed tag
-    expected = Arrays.asList("tag1", "tag3");
-    assertResult(POST(changeId, null, "#tag2"), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from removed tag.
+    removeHashtags(r, "#tag2");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
 
-    // Leading # is stripped from multiple removed tags
-    expected = Collections.emptyList();
-    assertResult(POST(changeId, null, "#tag1, #tag3"), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from multiple removed tags.
+    removeHashtags(r, "#tag1", "#tag3");
+    assertThatGet(r).isEmpty();
 
-    // Leading # and space are stripped from added tag
-    expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "# tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # and space are stripped from added tag.
+    addHashtags(r, "# tag1");
+    assertThatGet(r).containsExactly("tag1");
 
-    // Multiple leading # are stripped from added tag
-    expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "##tag2", null), expected);
-    assertResult(GET(changeId), expected);
+    // Multiple leading # are stripped from added tag.
+    addHashtags(r, "##tag2");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
 
-    // Multiple leading spaces and # are stripped from added tag
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, " # # tag3", null), expected);
-    assertResult(GET(changeId), expected);
+    // Multiple leading spaces and # are stripped from added tag.
+    addHashtags(r, "# # tag3");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
   }
 
   @Test
   public void testRemoveSingleHashtag() throws Exception {
-    // POST removing a single tag from a change that only has that tag
-    // returns an empty list
-    String changeId = createChange().getChangeId();
-    List<String> expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "tag1", null), expected);
-    assertResult(POST(changeId, null, "tag1"), ImmutableList.<String>of());
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Removing a single tag from a change that only has that tag returns an
+    // empty list.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag1");
+    assertThatGet(r).containsExactly("tag1");
+    removeHashtags(r, "tag1");
+    assertThatGet(r).isEmpty();
 
-    // POST removing a single tag from a change that has multiple tags
-    // returns a sorted list of remaining tags
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, "tag1, tag2, tag3", null), expected);
-    expected = Arrays.asList("tag1", "tag3");
-    assertResult(POST(changeId, null, "tag2"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing a single tag from a change that has multiple tags returns a
+    // sorted list of remaining tags.
+    addHashtags(r, "tag1", "tag2", "tag3");
+    removeHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
   }
 
   @Test
   public void testRemoveMultipleHashtags() throws Exception {
-    // POST removing multiple tags from a change that only has those tags
-    // returns an empty list
-    String changeId = createChange().getChangeId();
-    List<String> expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "tag1, tag2", null), expected);
-    assertResult(POST(changeId, null, "tag1, tag2"), ImmutableList.<String>of());
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Removing multiple tags from a change that only has those tags returns an
+    // empty list.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag1", "tag2");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
+    removeHashtags(r, "tag1", "tag2");
+    assertThatGet(r).isEmpty();
 
-    // POST removing multiple tags from a change that has multiple changes
-    // returns a sorted list of remaining changes
-    expected = Arrays.asList("tag1", "tag2", "tag3", "tag4");
-    assertResult(POST(changeId, "tag1, tag2, tag3, tag4", null), expected);
-    expected = Arrays.asList("tag2", "tag4");
-    assertResult(POST(changeId, null, "tag1, tag3"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing multiple tags from a change that has multiple tags returns a
+    // sorted list of remaining tags.
+    addHashtags(r, "tag1", "tag2", "tag3", "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3", "tag4").inOrder();
+    removeHashtags(r, "tag2", "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
   }
 
   @Test
   public void testRemoveNotExistingHashtag() throws Exception {
-    // POST removing a single hashtag from change that has no hashtags
-    // returns an empty list
-    String changeId = createChange().getChangeId();
-    assertResult(POST(changeId, null, "tag1"), ImmutableList.<String>of());
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Removing a single hashtag from change that has no hashtags returns an
+    // empty list.
+    PushOneCommit.Result r = createChange();
+    removeHashtags(r, "tag1");
+    assertThatGet(r).isEmpty();
 
-    // POST removing a single non-existing tag from a change that only
-    // has one other tag returns a list of only one tag
-    List<String> expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "tag1", null), expected);
-    assertResult(POST(changeId, null, "tag4"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing a single non-existing tag from a change that only has one other
+    // tag returns a list of only one tag.
+    addHashtags(r, "tag1");
+    removeHashtags(r, "tag4");
+    assertThatGet(r).containsExactly("tag1");
 
-    // POST removing a single non-existing tag from a change that has multiple
-    // tags returns a sorted list of tags without any deleted
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, "tag1, tag2, tag3", null), expected);
-    assertResult(POST(changeId, null, "tag4"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing a single non-existing tag from a change that has multiple tags
+    // returns a sorted list of tags without any deleted.
+    addHashtags(r, "tag1", "tag2", "tag3");
+    removeHashtags(r, "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
   }
 
-  private RestResponse GET(String changeId) throws IOException {
-    return adminSession.get("/changes/" + changeId + "/hashtags/");
-  }
-
-  private RestResponse POST(String changeId, String toAdd, String toRemove)
-      throws IOException {
+  @Test
+  public void testAddAndRemove() throws Exception {
+    // Adding and remove hashtags in a single request performs correctly.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag1", "tag2");
     HashtagsInput input = new HashtagsInput();
-    if (toAdd != null) {
-      input.add = new HashSet<>(
-          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toAdd)));
-    }
-    if (toRemove != null) {
-      input.remove = new HashSet<>(
-          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toRemove)));
-    }
-    return adminSession.post("/changes/" + changeId + "/hashtags/", input);
+    input.add = Sets.newHashSet("tag3", "tag4");
+    input.remove = Sets.newHashSet("tag1");
+    gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+    assertThatGet(r).containsExactly("tag2", "tag3", "tag4");
+
+    // Adding and removing the same hashtag actually removes it.
+    addHashtags(r, "tag1", "tag2");
+    input = new HashtagsInput();
+    input.add = Sets.newHashSet("tag3", "tag4");
+    input.remove = Sets.newHashSet("tag3");
+    gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag4");
   }
 
-  private static List<String> toHashtagList(RestResponse r)
-      throws IOException {
-    List<String> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<String>>() {}.getType());
-    return result;
+  private IterableSubject<
+        ? extends IterableSubject<?, String, Iterable<String>>,
+        String, Iterable<String>>
+      assertThatGet(PushOneCommit.Result r) throws Exception {
+    return assertThat(gApi.changes()
+        .id(r.getChange().getId().get())
+        .getHashtags());
+  }
+
+  private void addHashtags(PushOneCommit.Result r, String... toAdd)
+      throws Exception {
+    HashtagsInput input = new HashtagsInput();
+    input.add = Sets.newHashSet(toAdd);
+    gApi.changes()
+        .id(r.getChange().getId().get())
+        .setHashtags(input);
+  }
+
+  private void removeHashtags(PushOneCommit.Result r, String... toRemove)
+      throws Exception {
+    HashtagsInput input = new HashtagsInput();
+    input.remove = Sets.newHashSet(toRemove);
+    gApi.changes()
+        .id(r.getChange().getId().get())
+        .setHashtags(input);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 1b00c39..405f8e5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -26,7 +26,7 @@
   @Test
   public void indexChange() throws Exception {
     String changeId = createChange().getChangeId();
-    RestResponse r = userSession.post("/changes/" + changeId + "/index/");
+    RestResponse r = adminSession.post("/changes/" + changeId + "/index/");
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index e649415..3728a51 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -51,8 +51,8 @@
     String subject = "Change subject";
     String fileName = "a.txt";
     PushOneCommit push = pushFactory.create(
-        db, admin.getIdent(), subject, fileName, content, baseChangeId);
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+        db, admin.getIdent(), testRepo, subject, fileName, content, baseChangeId);
+    PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     return r;
   }
@@ -68,7 +68,7 @@
   public void currentRevision() throws Exception {
     ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+    assertThat(c.revisions.keySet()).containsAllIn(
         ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
@@ -78,7 +78,7 @@
     ChangeInfo c = get(changeId, CURRENT_REVISION, MESSAGES);
     assertThat(c.revisions).hasSize(1);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+    assertThat(c.revisions.keySet()).containsAllIn(
         ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
@@ -87,7 +87,7 @@
   public void allRevisions() throws Exception {
     ChangeInfo c = get(changeId, ALL_REVISIONS);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+    assertThat(c.revisions.keySet()).containsAllIn(
         ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
     assertThat(c.revisions.get(commitId(0))._number).isEqualTo(1);
     assertThat(c.revisions.get(commitId(1))._number).isEqualTo(2);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 4863c3e..efaa1df 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -15,16 +15,16 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
@@ -39,53 +39,52 @@
 
   @Test
   public void submitWithCherryPickIfFastForwardPossible() throws Exception {
-    Git git = createProject();
-    PushOneCommit.Result change = createChange(git);
+    PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
-    assertCherryPick(git, false);
+    assertCherryPick(testRepo, false);
     assertThat(getRemoteHead().getParent(0))
       .isEqualTo(change.getCommit().getParent(0));
   }
 
   @Test
   public void submitWithCherryPick() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "b.txt", "other content");
+        createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
-    assertCherryPick(git, false);
+    assertCherryPick(testRepo, false);
     RevCommit newHead = getRemoteHead();
     assertThat(newHead.getParentCount()).isEqualTo(1);
     assertThat(newHead.getParent(0)).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
+    assertPersonEquals(admin.getIdent(), newHead.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), newHead.getCommitterIdent());
   }
 
   @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Exception {
-    Git git = createProject();
-    setUseContentMerge();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+        createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+        createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, change.getCommitId().getName());
+    testRepo.reset(change.getCommitId());
     PushOneCommit.Result change3 =
-        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+        createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
-    assertCherryPick(git, true);
+    assertCherryPick(testRepo, true);
     RevCommit newHead = getRemoteHead();
     assertThat(newHead.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
@@ -95,39 +94,42 @@
   }
 
   @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge_Conflict() throws Exception {
-    Git git = createProject();
-    setUseContentMerge();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "a.txt", "other content");
-    submitWithConflict(change2.getChangeId());
+        createChange("Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId(),
+        "Cannot merge " + change2.getCommitId().name() + "\n" +
+        "Change could not be merged due to a path conflict.\n\n" +
+        "Please rebase the change locally and " +
+        "upload the rebased commit for review.");
+
     assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
-    assertSubmitter(change2.getChangeId(), 1);
+    assertNoSubmitter(change2.getChangeId(), 1);
   }
 
   @Test
   public void submitOutOfOrder() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
-    createChange(git, "Change 2", "b.txt", "other content");
+    testRepo.reset(initialHead);
+    createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 =
-        createChange(git, "Change 3", "c.txt", "different content");
+        createChange("Change 3", "c.txt", "different content");
     submit(change3.getChangeId());
-    assertCherryPick(git, false);
+    assertCherryPick(testRepo, false);
     RevCommit newHead = getRemoteHead();
     assertThat(newHead.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
@@ -138,65 +140,60 @@
 
   @Test
   public void submitOutOfOrder_Conflict() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
-    createChange(git, "Change 2", "b.txt", "other content");
+    testRepo.reset(initialHead);
+    createChange("Change 2", "b.txt", "other content");
     PushOneCommit.Result change3 =
-        createChange(git, "Change 3", "b.txt", "different content");
-    submitWithConflict(change3.getChangeId());
+        createChange("Change 3", "b.txt", "different content");
+    submitWithConflict(change3.getChangeId(),
+        "Cannot merge " + change3.getCommitId().name() + "\n" +
+        "Change could not be merged due to a path conflict.\n\n" +
+        "Please rebase the change locally and " +
+        "upload the rebased commit for review.");
+
     assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertCurrentRevision(change3.getChangeId(), 1, change3.getCommitId());
-    assertSubmitter(change3.getChangeId(), 1);
+    assertNoSubmitter(change3.getChangeId(), 1);
   }
 
   @Test
   public void submitMultipleChanges() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change4 = createChange(git, "Change 4", "d", "d");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
 
-    submitStatusOnly(change2.getChangeId());
-    submitStatusOnly(change3.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
     submit(change4.getChangeId());
 
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-    assertThat(log.get(0).getParent(0)).isEqualTo(log.get(1));
+    assertThat(log.get(1).getId()).isEqualTo(initialHead.getId());
 
-    assertThat(log.get(1).getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
-    assertThat(log.get(1).getParent(0)).isEqualTo(log.get(2));
-
-    assertThat(log.get(2).getShortMessage()).isEqualTo(
-        change2.getCommit().getShortMessage());
-    assertThat(log.get(2).getParent(0)).isEqualTo(log.get(3));
-
-    assertThat(log.get(3).getId()).isEqualTo(initialHead.getId());
+    assertNew(change2.getChangeId());
+    assertNew(change3.getChangeId());
   }
 
   @Test
   public void submitDependentNonConflictingChangesOutOfOrder() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b");
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
+    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
     assertThat(change3.getCommit().getParent(0)).isEqualTo(change2.getCommit());
 
     // Submit succeeds; change3 is successfully cherry-picked onto head.
@@ -220,17 +217,20 @@
 
   @Test
   public void submitDependentConflictingChangesOutOfOrder() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b1");
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "b", "b2");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b1");
+    PushOneCommit.Result change3 = createChange("Change 3", "b", "b2");
     assertThat(change3.getCommit().getParent(0)).isEqualTo(change2.getCommit());
 
     // Submit fails; change3 contains the delta "b1" -> "b2", which cannot be
     // applied against tip.
-    submitWithConflict(change3.getChangeId());
+    submitWithConflict(change3.getChangeId(),
+        "Cannot merge " + change3.getCommitId().name() + "\n" +
+        "Change could not be merged due to a path conflict.\n\n" +
+        "Please rebase the change locally and " +
+        "upload the rebased commit for review.");
 
     ChangeInfo info3 = get(change3.getChangeId(), ListChangesOption.MESSAGES);
     assertThat(info3.status).isEqualTo(ChangeStatus.NEW);
@@ -240,64 +240,24 @@
     // Tip has not changed.
     List<RevCommit> log = getRemoteLog();
     assertThat(log.get(0)).isEqualTo(initialHead.getId());
+    assertNoSubmitter(change3.getChangeId(), 1);
   }
 
   @Test
   public void submitSubsetOfDependentChanges() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
-    checkout(git, initialHead.getId().getName());
-    createChange(git, "Change 2", "b", "b");
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
-    createChange(git, "Change 4", "d", "d");
-    PushOneCommit.Result change5 = createChange(git, "Change 5", "e", "e");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
+    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
+    PushOneCommit.Result change4 = createChange("Change 5", "e", "e");
 
-    // Out of the above, only submit 3 and 5.
-    submitStatusOnly(change3.getChangeId());
-    submit(change5.getChangeId());
+    // Out of the above, only submit 4. 2,3 are not related to 4
+    // by topic or ancestor (due to cherrypicking!)
+    approve(change3.getChangeId());
+    submit(change4.getChangeId());
 
-    ChangeInfo info3 = get(change3.getChangeId());
-    assertThat(info3.status).isEqualTo(ChangeStatus.MERGED);
-
-    List<RevCommit> log = getRemoteLog();
-    assertThat(log.get(0).getShortMessage())
-        .isEqualTo(change5.getCommit().getShortMessage());
-    assertThat(log.get(1).getShortMessage())
-        .isEqualTo(change3.getCommit().getShortMessage());
-    assertThat(log.get(2).getShortMessage())
-        .isEqualTo(initialHead.getShortMessage());
-  }
-
-  @Test
-  public void submitChangeAfterParentFailsDueToConflict() throws Exception {
-    Git git = createProject();
-    RevCommit initialHead = getRemoteHead();
-
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b1");
-    submit(change2.getChangeId());
-
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "b", "b2");
-    assertThat(change3.getCommit().getParent(0)).isEqualTo(initialHead);
-    PushOneCommit.Result change4 = createChange(git, "Change 3", "c", "c3");
-
-    submitStatusOnly(change3.getChangeId());
-    submitStatusOnly(change4.getChangeId());
-
-    // Merge fails; change3 contains the delta "b1" -> "b2", which cannot be
-    // applied against tip.
-    submitWithConflict(change3.getChangeId());
-
-    // change4 is a clean merge, so should succeed in the same run where change3
-    // failed.
-    ChangeInfo info4 = get(change4.getChangeId());
-    assertThat(info4.status).isEqualTo(ChangeStatus.MERGED);
-    List<RevCommit> log = getRemoteLog();
-    assertThat(log.get(0).getShortMessage())
-        .isEqualTo(change4.getCommit().getShortMessage());
-    assertThat(log.get(1).getShortMessage())
-        .isEqualTo(change2.getCommit().getShortMessage());
+    assertNew(change2.getChangeId());
+    assertNew(change3.getChangeId());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index d4e7e849..374fb13 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -15,15 +15,18 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.data.RefUpdateAttribute;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
+import java.util.Map;
+
 public class SubmitByFastForwardIT extends AbstractSubmit {
 
   @Override
@@ -33,9 +36,8 @@
 
   @Test
   public void submitWithFastForward() throws Exception {
-    Git git = createProject();
     RevCommit oldHead = getRemoteHead();
-    PushOneCommit.Result change = createChange(git);
+    PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getId()).isEqualTo(change.getCommitId());
@@ -44,18 +46,73 @@
   }
 
   @Test
+  public void submitTwoChangesWithFastForward() throws Exception {
+    RevCommit originalHead = getRemoteHead();
+
+    PushOneCommit.Result change = createChange();
+    PushOneCommit.Result change2 = createChange();
+
+    String id1 = change.getChangeId();
+    String id2 = change2.getChangeId();
+    approve(id1);
+    submit(id2);
+
+    RevCommit updatedHead = getRemoteHead();
+    assertThat(updatedHead.getId()).isEqualTo(change2.getCommit());
+    assertThat(updatedHead.getParent(0).getId()).isEqualTo(change.getCommit());
+    assertSubmitter(change.getChangeId(), 1);
+    assertSubmitter(change2.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), updatedHead.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), updatedHead.getCommitterIdent());
+    assertSubmittedTogether(id1, id2, id1);
+    assertSubmittedTogether(id2, id2, id1);
+
+    RefUpdateAttribute refUpdate = getOneRefUpdate(
+        project.get() + "-refs/heads/master");
+    assertThat(refUpdate).isNotNull();
+    assertThat(refUpdate.oldRev).isEqualTo(originalHead.name());
+    assertThat(refUpdate.newRev).isEqualTo(updatedHead.name());
+  }
+
+  @Test
+  public void submitTwoChangesWithFastForward_missingDependency() throws Exception {
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change1 = createChange();
+    PushOneCommit.Result change2 = createChange();
+
+    Change.Id id1 = change1.getPatchSetId().getParentKey();
+    submitWithConflict(change2.getChangeId(),
+        "The change could not be submitted because it depends on change(s) [" +
+        id1 + "], which could not be submitted because:\n" +
+        id1 + ": needs Code-Review;");
+
+    RevCommit head = getRemoteHead();
+    assertThat(head.getId()).isEqualTo(oldHead.getId());
+  }
+
+  @Test
   public void submitFastForwardNotPossible_Conflict() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "b.txt", "other content");
-    submitWithConflict(change2.getChangeId());
+        createChange("Change 2", "b.txt", "other content");
+
+    approve(change2.getChangeId());
+    Map<String, ActionInfo> actions = getActions(change2.getChangeId());
+
+    assertThat(actions).containsKey("submit");
+    ActionInfo info = actions.get("submit");
+    assertThat(info.enabled).isNull();
+
+    submitWithConflict(change2.getChangeId(),
+        "Cannot merge " + change2.getCommitId().name() + "\n" +
+        "Project policy requires all submissions to be a fast-forward.\n\n" +
+        "Please rebase the change locally and upload again for review.");
     assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index fa913d9..eb1d16b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -15,12 +15,10 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.client.SubmitType;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
@@ -35,48 +33,47 @@
 
   @Test
   public void submitWithMergeIfFastForwardPossible() throws Exception {
-    Git git = createProject();
     RevCommit oldHead = getRemoteHead();
-    PushOneCommit.Result change = createChange(git);
+    PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getParentCount()).isEqualTo(2);
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertThat(head.getParent(1)).isEqualTo(change.getCommitId());
     assertSubmitter(change.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), head.getCommitterIdent());
   }
 
   @Test
   public void submitMultipleChanges() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change4 = createChange(git, "Change 4", "d", "d");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
 
-    submitStatusOnly(change2.getChangeId());
-    submitStatusOnly(change3.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
     submit(change4.getChangeId());
 
     List<RevCommit> log = getRemoteLog();
     RevCommit tip = log.get(0);
     assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-
-    tip = tip.getParent(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
-        change3.getCommit().getShortMessage());
-
-    tip = tip.getParent(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
-        change2.getCommit().getShortMessage());
-
+    assertThat(tip.getParent(0).getShortMessage()).isEqualTo(
+        initialHead.getShortMessage());
     assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
+
+    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), tip.getCommitterIdent());
+
+    assertNew(change2.getChangeId());
+    assertNew(change3.getChangeId());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index c1ece45..04baacd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -1,12 +1,13 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
 
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project;
 
-import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
@@ -21,47 +22,290 @@
 
   @Test
   public void submitWithFastForward() throws Exception {
-    Git git = createProject();
     RevCommit oldHead = getRemoteHead();
-    PushOneCommit.Result change = createChange(git);
+    PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getId()).isEqualTo(change.getCommitId());
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
   }
 
   @Test
   public void submitMultipleChanges() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b");
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change4 = createChange(git, "Change 4", "d", "d");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change4 = createChange("Change 4", "d", "d");
 
-    submitStatusOnly(change2.getChangeId());
-    submitStatusOnly(change3.getChangeId());
+    // Change 2 stays untouched.
+    approve(change2.getChangeId());
+    // Change 3 is a fast-forward, no need to merge.
+    submit(change3.getChangeId());
+
+    RevCommit tip = getRemoteLog().get(0);
+    assertThat(tip.getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
+    assertThat(tip.getParent(0).getId()).isEqualTo(
+        initialHead.getId());
+    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), tip.getCommitterIdent());
+
+    // We need to merge change 4.
     submit(change4.getChangeId());
 
-    List<RevCommit> log = getRemoteLog();
-    RevCommit tip = log.get(0);
+    tip = getRemoteLog().get(0);
     assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
         change4.getCommit().getShortMessage());
-
-    tip = tip.getParent(0);
-    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+    assertThat(tip.getParent(0).getShortMessage()).isEqualTo(
         change3.getCommit().getShortMessage());
 
-    tip = tip.getParent(0);
-    assertThat(tip.getShortMessage()).isEqualTo(
+    assertPersonEquals(admin.getIdent(), tip.getAuthorIdent());
+    assertPersonEquals(serverIdent.get(), tip.getCommitterIdent());
+
+    assertNew(change2.getChangeId());
+  }
+
+  @Test
+  public void submitChangesAcrossRepos() throws Exception {
+    Project.NameKey p1 = createProject("project-where-we-submit");
+    Project.NameKey p2 = createProject("project-impacted-via-topic");
+    Project.NameKey p3 = createProject("project-impacted-indirectly-via-topic");
+
+    RevCommit initialHead2 = getRemoteHead(p2, "master");
+    RevCommit initialHead3 = getRemoteHead(p3, "master");
+
+    TestRepository<?> repo1 = cloneProject(p1);
+    TestRepository<?> repo2 = cloneProject(p2);
+    TestRepository<?> repo3 = cloneProject(p3);
+
+    PushOneCommit.Result change1a = createChange(repo1, "master",
+        "An ancestor of the change we want to submit",
+        "a.txt", "1", "dependent-topic");
+    PushOneCommit.Result change1b = createChange(repo1, "master",
+        "We're interested in submitting this change",
+        "a.txt", "2", "topic-to-submit");
+
+    PushOneCommit.Result change2a = createChange(repo2, "master",
+        "indirection level 1",
+        "a.txt", "1", "topic-indirect");
+    PushOneCommit.Result change2b = createChange(repo2, "master",
+        "should go in with first change",
+        "a.txt", "2", "dependent-topic");
+
+    PushOneCommit.Result change3 = createChange(repo3, "master",
+        "indirection level 2",
+        "a.txt", "1", "topic-indirect");
+
+    approve(change1a.getChangeId());
+    approve(change2a.getChangeId());
+    approve(change2b.getChangeId());
+    approve(change3.getChangeId());
+    submit(change1b.getChangeId());
+
+    RevCommit tip1  = getRemoteLog(p1, "master").get(0);
+    RevCommit tip2  = getRemoteLog(p2, "master").get(0);
+    RevCommit tip3  = getRemoteLog(p3, "master").get(0);
+
+    assertThat(tip1.getShortMessage()).isEqualTo(
+        change1b.getCommit().getShortMessage());
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(tip2.getShortMessage()).isEqualTo(
+          change2b.getCommit().getShortMessage());
+      assertThat(tip3.getShortMessage()).isEqualTo(
+          change3.getCommit().getShortMessage());
+    } else {
+      assertThat(tip2.getShortMessage()).isEqualTo(
+          initialHead2.getShortMessage());
+      assertThat(tip3.getShortMessage()).isEqualTo(
+          initialHead3.getShortMessage());
+    }
+  }
+
+  @Test
+  public void submitChangesAcrossReposBlocked() throws Exception {
+    Project.NameKey p1 = createProject("project-where-we-submit");
+    Project.NameKey p2 = createProject("project-impacted-via-topic");
+    Project.NameKey p3 = createProject("project-impacted-indirectly-via-topic");
+
+    TestRepository<?> repo1 = cloneProject(p1);
+    TestRepository<?> repo2 = cloneProject(p2);
+    TestRepository<?> repo3 = cloneProject(p3);
+
+    RevCommit initialHead1 = getRemoteHead(p1, "master");
+    RevCommit initialHead2 = getRemoteHead(p2, "master");
+    RevCommit initialHead3 = getRemoteHead(p3, "master");
+
+    PushOneCommit.Result change1a = createChange(repo1, "master",
+        "An ancestor of the change we want to submit",
+        "a.txt", "1", "dependent-topic");
+    PushOneCommit.Result change1b = createChange(repo1, "master",
+        "we're interested to submit this change",
+        "a.txt", "2", "topic-to-submit");
+
+    PushOneCommit.Result change2a = createChange(repo2, "master",
+        "indirection level 2a",
+        "a.txt", "1", "topic-indirect");
+    PushOneCommit.Result change2b = createChange(repo2, "master",
+        "should go in with first change",
+        "a.txt", "2", "dependent-topic");
+
+    PushOneCommit.Result change3 = createChange(repo3, "master",
+        "indirection level 2b",
+        "a.txt", "1", "topic-indirect");
+
+    // Create a merge conflict for change3 which is only indirectly related
+    // via topics.
+    repo3.reset(initialHead3);
+    PushOneCommit.Result change3Conflict = createChange(repo3, "master",
+        "conflicting change",
+        "a.txt", "2\n2", "conflicting-topic");
+    submit(change3Conflict.getChangeId());
+    RevCommit tipConflict = getRemoteLog(p3, "master").get(0);
+    assertThat(tipConflict.getShortMessage()).isEqualTo(
+        change3Conflict.getCommit().getShortMessage());
+
+    approve(change1a.getChangeId());
+    approve(change2a.getChangeId());
+    approve(change2b.getChangeId());
+    approve(change3.getChangeId());
+
+    if (isSubmitWholeTopicEnabled()) {
+      submitWithConflict(change1b.getChangeId(),
+          "Cannot merge " + change3.getCommit().name() + "\n" +
+          "Change could not be merged due to a path conflict.\n\n" +
+          "Please rebase the change locally " +
+          "and upload the rebased commit for review.");
+    } else {
+      submit(change1b.getChangeId());
+    }
+
+    RevCommit tip1  = getRemoteLog(p1, "master").get(0);
+    RevCommit tip2  = getRemoteLog(p2, "master").get(0);
+    RevCommit tip3  = getRemoteLog(p3, "master").get(0);
+    if (isSubmitWholeTopicEnabled()) {
+      assertThat(tip1.getShortMessage()).isEqualTo(
+          initialHead1.getShortMessage());
+      assertThat(tip2.getShortMessage()).isEqualTo(
+          initialHead2.getShortMessage());
+      assertThat(tip3.getShortMessage()).isEqualTo(
+          change3Conflict.getCommit().getShortMessage());
+      assertNoSubmitter(change1a.getChangeId(), 1);
+      assertNoSubmitter(change2a.getChangeId(), 1);
+      assertNoSubmitter(change2b.getChangeId(), 1);
+      assertNoSubmitter(change3.getChangeId(), 1);
+    } else {
+      assertThat(tip1.getShortMessage()).isEqualTo(
+          change1b.getCommit().getShortMessage());
+      assertThat(tip2.getShortMessage()).isEqualTo(
+          initialHead2.getShortMessage());
+      assertThat(tip3.getShortMessage()).isEqualTo(
+          change3Conflict.getCommit().getShortMessage());
+      assertNoSubmitter(change2a.getChangeId(), 1);
+      assertNoSubmitter(change2b.getChangeId(), 1);
+      assertNoSubmitter(change3.getChangeId(), 1);
+    }
+  }
+
+  @Test
+  public void submitWithMergedAncestorsOnOtherBranch() throws Exception {
+    PushOneCommit.Result change1 = createChange(testRepo,  "master",
+        "base commit",
+        "a.txt", "1", "");
+    submit(change1.getChangeId());
+
+    gApi.projects()
+        .name(project.get())
+        .branch("branch")
+        .create(new BranchInput());
+
+    PushOneCommit.Result change2 = createChange(testRepo,  "master",
+        "We want to commit this to master first",
+        "a.txt", "2", "");
+
+    submit(change2.getChangeId());
+
+    RevCommit tip1 = getRemoteLog(project, "master").get(0);
+    assertThat(tip1.getShortMessage()).isEqualTo(
         change2.getCommit().getShortMessage());
 
-    assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
+    RevCommit tip2 = getRemoteLog(project, "branch").get(0);
+    assertThat(tip2.getShortMessage()).isEqualTo(
+        change1.getCommit().getShortMessage());
+
+    PushOneCommit.Result change3 = createChange(testRepo,  "branch",
+        "This commit is based on master, which includes change2, "
+        + "but is targeted at branch, which doesn't include it.",
+        "a.txt", "3", "");
+
+    submit(change3.getChangeId());
+
+    List<RevCommit> log3 = getRemoteLog(project, "branch");
+    assertThat(log3.get(0).getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
+    assertThat(log3.get(1).getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
+  }
+
+  @Test
+  public void submitWithOpenAncestorsOnOtherBranch() throws Exception {
+    PushOneCommit.Result change1 = createChange(testRepo,  "master",
+        "base commit",
+        "a.txt", "1", "");
+    submit(change1.getChangeId());
+
+    gApi.projects()
+        .name(project.get())
+        .branch("branch")
+        .create(new BranchInput());
+
+    PushOneCommit.Result change2 = createChange(testRepo,  "master",
+        "We want to commit this to master first",
+        "a.txt", "2", "");
+
+    approve(change2.getChangeId());
+
+    RevCommit tip1 = getRemoteLog(project, "master").get(0);
+    assertThat(tip1.getShortMessage()).isEqualTo(
+        change1.getCommit().getShortMessage());
+
+    RevCommit tip2 = getRemoteLog(project, "branch").get(0);
+    assertThat(tip2.getShortMessage()).isEqualTo(
+        change1.getCommit().getShortMessage());
+
+    PushOneCommit.Result change3a = createChange(testRepo,  "branch",
+        "This commit is based on change2 pending for master, "
+        + "but is targeted itself at branch, which doesn't include it.",
+        "a.txt", "3", "a-topic-here");
+
+    Project.NameKey p3 = createProject("project-related-to-change3");
+    TestRepository<?> repo3 = cloneProject(p3);
+    RevCommit initialHead = getRemoteHead(p3, "master");
+    PushOneCommit.Result change3b = createChange(repo3, "master",
+        "some accompanying changes for change3a in another repo "
+        + "tied together via topic",
+        "a.txt", "1", "a-topic-here");
+    approve(change3b.getChangeId());
+
+    submitWithConflict(change3a.getChangeId(), "Cannot merge " +
+        change3a.getCommit().name() +
+        "\nMissing dependency");
+
+    RevCommit tipbranch = getRemoteLog(project, "branch").get(0);
+    assertThat(tipbranch.getShortMessage()).isEqualTo(
+        change1.getCommit().getShortMessage());
+
+    RevCommit tipmaster = getRemoteLog(p3, "master").get(0);
+    assertThat(tipmaster.getShortMessage()).isEqualTo(
+        initialHead.getShortMessage());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
index d3e8cb5..222816e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -15,13 +15,21 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
+import static com.google.gerrit.acceptance.GitUtil.getChangeId;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 
-import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 
 public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
@@ -32,10 +40,10 @@
   }
 
   @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithFastForward() throws Exception {
-    Git git = createProject();
     RevCommit oldHead = getRemoteHead();
-    PushOneCommit.Result change = createChange(git);
+    PushOneCommit.Result change = createChange();
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head.getId()).isEqualTo(change.getCommitId());
@@ -43,47 +51,110 @@
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
     assertSubmitter(change.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
   }
 
   @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithRebase() throws Exception {
-    Git git = createProject();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "b.txt", "other content");
+        createChange("Change 2", "b.txt", "other content");
     submit(change2.getChangeId());
-    assertRebase(git, false);
+    assertRebase(testRepo, false);
     RevCommit head = getRemoteHead();
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change2.getChangeId());
     assertCurrentRevision(change2.getChangeId(), 2, head);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
   }
 
   @Test
+  public void submitWithRebaseMergeCommit() throws Exception {
+    /*
+        *  (HEAD, origin/master, origin/HEAD) Merge changes X,Y
+        |\
+        | *   Merge branch 'master' into origin/master
+        | |\
+        | | * SHA Added a
+        | |/
+        * | Before
+        |/
+        * Initial empty repository
+     */
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
+
+    PushOneCommit change2Push = pushFactory.create(db, admin.getIdent(), testRepo,
+        "Merge to master", "m.txt", "");
+    change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit()));
+    PushOneCommit.Result change2 = change2Push.to("refs/for/master");
+
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change3 = createChange("Before", "b.txt", "");
+
+    approve(change3.getChangeId());
+    submit(change3.getChangeId());
+
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    submit(change2.getChangeId());
+
+    RevCommit newHead = getRemoteHead();
+    assertThat(newHead.getParentCount()).isEqualTo(2);
+
+    RevCommit headParent1 = parse(newHead.getParent(0).getId());
+    RevCommit headParent2 = parse(newHead.getParent(1).getId());
+
+    assertThat(headParent1.getId()).isEqualTo(change3.getCommit().getId());
+    assertThat(headParent1.getParentCount()).isEqualTo(1);
+    assertThat(headParent1.getParent(0)).isEqualTo(initialHead);
+
+    assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId());
+    assertThat(headParent2.getParentCount()).isEqualTo(2);
+
+    RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId());
+    RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId());
+
+    assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId());
+    assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId());
+  }
+
+  private RevCommit parse(ObjectId id) throws Exception {
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit c = rw.parseCommit(id);
+      rw.parseBody(c);
+      return c;
+    }
+  }
+
+  @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge() throws Exception {
-    Git git = createProject();
-    setUseContentMerge();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+        createChange("Change 1", "a.txt", "aaa\nbbb\nccc\n");
     submit(change.getChangeId());
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+        createChange("Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
     submit(change2.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, change.getCommitId().getName());
+    testRepo.reset(change.getCommitId());
     PushOneCommit.Result change3 =
-        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+        createChange("Change 3", "a.txt", "bbb\nccc\n");
     submit(change3.getChangeId());
-    assertRebase(git, true);
+    assertRebase(testRepo, true);
     RevCommit head = getRemoteHead();
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
@@ -93,22 +164,65 @@
   }
 
   @Test
+  @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
   public void submitWithContentMerge_Conflict() throws Exception {
-    Git git = createProject();
-    setUseContentMerge();
     RevCommit initialHead = getRemoteHead();
     PushOneCommit.Result change =
-        createChange(git, "Change 1", "a.txt", "content");
+        createChange("Change 1", "a.txt", "content");
     submit(change.getChangeId());
 
     RevCommit oldHead = getRemoteHead();
-    checkout(git, initialHead.getId().getName());
+    testRepo.reset(initialHead);
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "a.txt", "other content");
-    submitWithConflict(change2.getChangeId());
+        createChange("Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId(), "Cannot rebase " +
+        change2.getCommit().name() +
+        ": The change could not be rebased due to a conflict during merge.");
     RevCommit head = getRemoteHead();
     assertThat(head).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
-    assertSubmitter(change2.getChangeId(), 1);
+    assertNoSubmitter(change2.getChangeId(), 1);
+  }
+
+  @Test
+  public void submitAfterReorderOfCommits() throws Exception {
+    // Create two commits and push.
+    RevCommit c1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+
+    String id1 = getChangeId(testRepo, c1).get();
+    String id2 = getChangeId(testRepo, c2).get();
+
+    // Swap the order of commits and push again.
+    testRepo.reset("HEAD~2");
+    testRepo.cherryPick(c2);
+    testRepo.cherryPick(c1);
+    pushHead(testRepo, "refs/for/master", false);
+
+    approve(id1);
+    approve(id2);
+    submit(id1);
+  }
+
+  @Test
+  public void submitChangesAfterBranchOnSecond() throws Exception {
+    PushOneCommit.Result change = createChange();
+    approve(change.getChangeId());
+
+    PushOneCommit.Result change2nd = createChange();
+    approve(change2nd.getChangeId());
+    Project.NameKey project = change2nd.getChange().change().getProject();
+    Branch.NameKey branch = new Branch.NameKey(project, "branch");
+    createBranchWithRevision(branch, change2nd.getCommit().getName());
+    gApi.changes().id(change2nd.getChangeId()).current().submit();
+    assertMerged(change2nd);
+    assertMerged(change);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
new file mode 100644
index 0000000..0fb5ce1
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -0,0 +1,365 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.git.ChangeSet;
+import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@NoHttpd
+public class SubmitResolvingMergeCommitIT extends AbstractDaemonTest {
+  @Inject
+  private MergeSuperSet mergeSuperSet;
+
+  @Inject
+  private Submit submit;
+
+  @ConfigSuite.Default
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
+  @Test
+  public void resolvingMergeCommitAtEndOfChain() throws Exception {
+    /*
+      A <- B <- C <------- D
+      ^                    ^
+      |                    |
+      E <- F <- G <- H <-- M*
+
+      G has a conflict with C and is resolved in M which is a merge
+      commit of H and D.
+    */
+
+    PushOneCommit.Result a = createChange("A");
+    PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result c = createChange("C", ImmutableList.of(b.getCommit()));
+    PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit()));
+
+    PushOneCommit.Result e = createChange("E", ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result f = createChange("F", ImmutableList.of(e.getCommit()));
+    PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line",
+        ImmutableList.of(f.getCommit()));
+    PushOneCommit.Result h = createChange("H", ImmutableList.of(g.getCommit()));
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    approve(c.getChangeId());
+    approve(d.getChangeId());
+    submit(d.getChangeId());
+
+    approve(e.getChangeId());
+    approve(f.getChangeId());
+    approve(g.getChangeId());
+    approve(h.getChangeId());
+
+    assertMergeable(e.getChange(), true);
+    assertMergeable(f.getChange(), true);
+    assertMergeable(g.getChange(), false);
+    assertMergeable(h.getChange(), false);
+
+    PushOneCommit.Result m = createChange("M", "new.txt", "Resolved conflict",
+        ImmutableList.of(d.getCommit(), h.getCommit()));
+    approve(m.getChangeId());
+
+    assertChangeSetMergeable(m.getChange(), true);
+
+    assertMergeable(m.getChange(), true);
+    submit(m.getChangeId());
+
+    assertMerged(e.getChangeId());
+    assertMerged(f.getChangeId());
+    assertMerged(g.getChangeId());
+    assertMerged(h.getChangeId());
+    assertMerged(m.getChangeId());
+  }
+
+  @Test
+  public void resolvingMergeCommitComingBeforeConflict() throws Exception {
+    /*
+      A <- B <- C <- D
+      ^    ^
+      |    |
+      E <- F* <- G
+
+      F is a merge commit of E and B and resolves any conflict.
+      However G is conflicting with C.
+    */
+
+    PushOneCommit.Result a = createChange("A");
+    PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result c = createChange("C", "new.txt", "No conflict line #2",
+        ImmutableList.of(b.getCommit()));
+    PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit()));
+    PushOneCommit.Result e = createChange("E", "new.txt", "Conflicting line",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result f = createChange("F", "new.txt", "Resolved conflict",
+        ImmutableList.of(b.getCommit(), e.getCommit()));
+    PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line #2",
+        ImmutableList.of(f.getCommit()));
+
+    assertMergeable(e.getChange(), true);
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    submit(b.getChangeId());
+
+    assertMergeable(e.getChange(), false);
+    assertMergeable(f.getChange(), true);
+    assertMergeable(g.getChange(), true);
+
+    approve(c.getChangeId());
+    approve(d.getChangeId());
+    submit(d.getChangeId());
+
+    approve(e.getChangeId());
+    approve(f.getChangeId());
+    approve(g.getChangeId());
+
+    assertMergeable(g.getChange(), false);
+    assertChangeSetMergeable(g.getChange(), false);
+  }
+
+  @Test
+  public void resolvingMergeCommitWithTopics() throws Exception {
+    /*
+      Project1:
+        A <- B <-- C <---
+        ^    ^          |
+        |    |          |
+        E <- F* <- G <- L*
+
+      G clashes with C, and F resolves the clashes between E and B.
+      Later, L resolves the clashes between C and G.
+
+      Project2:
+        H <- I
+        ^    ^
+        |    |
+        J <- K*
+
+      J clashes with I, and K resolves all problems.
+      G, K and L are in the same topic.
+    */
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+    String project1Name = name("Project1");
+    String project2Name = name("Project2");
+    gApi.projects().create(project1Name);
+    gApi.projects().create(project2Name);
+    TestRepository<InMemoryRepository> project1 =
+        cloneProject(new Project.NameKey(project1Name));
+    TestRepository<InMemoryRepository> project2 =
+        cloneProject(new Project.NameKey(project2Name));
+
+    PushOneCommit.Result a = createChange(project1, "A");
+    PushOneCommit.Result b = createChange(project1, "B", "new.txt",
+        "No conflict line", ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result c = createChange(project1, "C", "new.txt",
+        "No conflict line #2", ImmutableList.of(b.getCommit()));
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    approve(c.getChangeId());
+    submit(c.getChangeId());
+
+    PushOneCommit.Result e = createChange(project1, "E", "new.txt",
+        "Conflicting line", ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result f = createChange(project1, "F", "new.txt",
+        "Resolved conflict", ImmutableList.of(b.getCommit(), e.getCommit()));
+    PushOneCommit.Result g = createChange(project1, "G", "new.txt",
+        "Conflicting line #2", ImmutableList.of(f.getCommit()),
+        "refs/for/master/" + name("topic1"));
+
+    PushOneCommit.Result h = createChange(project2, "H");
+    PushOneCommit.Result i = createChange(project2, "I", "new.txt",
+        "No conflict line", ImmutableList.of(h.getCommit()));
+    PushOneCommit.Result j = createChange(project2, "J", "new.txt",
+        "Conflicting line", ImmutableList.of(h.getCommit()));
+    PushOneCommit.Result k =
+        createChange(project2, "K", "new.txt", "Sadly conflicting topic-wise",
+            ImmutableList.of(i.getCommit(), j.getCommit()),
+            "refs/for/master/" + name("topic1"));
+
+    approve(h.getChangeId());
+    approve(i.getChangeId());
+    submit(i.getChangeId());
+
+    approve(e.getChangeId());
+    approve(f.getChangeId());
+    approve(g.getChangeId());
+    approve(j.getChangeId());
+    approve(k.getChangeId());
+
+    assertChangeSetMergeable(g.getChange(), false);
+    assertChangeSetMergeable(k.getChange(), false);
+
+    PushOneCommit.Result l =
+        createChange(project1, "L", "new.txt", "Resolving conflicts again",
+            ImmutableList.of(c.getCommit(), g.getCommit()),
+            "refs/for/master/" + name("topic1"));
+
+    approve(l.getChangeId());
+    assertChangeSetMergeable(l.getChange(), true);
+
+    submit(l.getChangeId());
+    assertMerged(c.getChangeId());
+    assertMerged(g.getChangeId());
+    assertMerged(k.getChangeId());
+  }
+
+  @Test
+  public void resolvingMergeCommitAtEndOfChainAndNotUpToDate() throws Exception {
+    /*
+        A <-- B
+         \
+          C  <- D
+           \   /
+             E
+
+        B is the target branch, and D should be merged with B, but one
+        of C conflicts with B
+    */
+
+    PushOneCommit.Result a = createChange("A");
+    PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line",
+        ImmutableList.of(a.getCommit()));
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    submit(b.getChangeId());
+
+    PushOneCommit.Result c = createChange("C", "new.txt", "Create conflicts",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result e = createChange("E", ImmutableList.of(c.getCommit()));
+    PushOneCommit.Result d = createChange("D", "new.txt", "Resolves conflicts",
+        ImmutableList.of(c.getCommit(), e.getCommit()));
+
+    approve(c.getChangeId());
+    approve(e.getChangeId());
+    approve(d.getChangeId());
+    assertMergeable(d.getChange(), false);
+    assertChangeSetMergeable(d.getChange(), false);
+  }
+
+  private void submit(String changeId) throws Exception {
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit();
+  }
+
+  private void assertChangeSetMergeable(ChangeData change,
+      boolean expected) throws MissingObjectException,
+          IncorrectObjectTypeException, IOException, OrmException {
+    ChangeSet cs = mergeSuperSet.completeChangeSet(db, change.change());
+    if (expected) {
+      assertThat(submit.unmergeableChanges(cs)).isEmpty();
+    } else {
+      assertThat(submit.unmergeableChanges(cs)).isNotEmpty();
+    }
+  }
+
+  private void assertMergeable(ChangeData change, boolean expected)
+      throws Exception {
+    change.setMergeable(null);
+    assertThat(change.isMergeable()).isEqualTo(expected);
+  }
+
+  private void assertMerged(String changeId) throws Exception {
+    assertThat(gApi
+        .changes()
+        .id(changeId)
+        .get()
+        .status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  private PushOneCommit.Result createChange(TestRepository<?> repo,
+      String subject, String fileName, String content, List<RevCommit> parents,
+      String ref) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo,
+        subject, fileName, content);
+
+    if (!parents.isEmpty()) {
+      push.setParents(parents);
+    }
+
+    PushOneCommit.Result result;
+    if (fileName.isEmpty()) {
+      result = push.execute(ref);
+    } else {
+      result = push.to(ref);
+    }
+    result.assertOkStatus();
+    return result;
+  }
+
+  private PushOneCommit.Result createChange(TestRepository<?> repo,
+      String subject) throws Exception {
+    return createChange(repo, subject, "x", "x", new ArrayList<RevCommit>(),
+        "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(TestRepository<?> repo,
+      String subject, String fileName, String content, List<RevCommit> parents)
+          throws Exception {
+    return createChange(repo, subject, fileName, content, parents,
+        "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(String subject) throws Exception {
+    return createChange(testRepo, subject, "", "",
+        Collections.<RevCommit> emptyList(), "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(String subject,
+      List<RevCommit> parents) throws Exception {
+    return createChange(testRepo, subject, "", "", parents, "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(String subject, String fileName,
+      String content, List<RevCommit> parents) throws Exception {
+    return createChange(testRepo, subject, fileName, content, parents,
+        "refs/for/master");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 34d6f26..7d2930a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -16,61 +16,65 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GerritConfigs;
-import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.CreateGroupArgs;
-import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.server.group.CreateGroup;
+import com.google.gerrit.server.group.GroupsCollection;
 import com.google.inject.Inject;
 
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Collections;
+import java.util.Arrays;
 import java.util.List;
 
 public class SuggestReviewersIT extends AbstractDaemonTest {
   @Inject
-  private PerformCreateGroup.Factory createGroupFactory;
+  private CreateGroup.Factory createGroupFactory;
+
+  @Inject
+  private GroupsCollection groups;
 
   private AccountGroup group1;
+  private AccountGroup group2;
+  private AccountGroup group3;
+
   private TestAccount user1;
   private TestAccount user2;
   private TestAccount user3;
+  private TestAccount user4;
 
   @Before
   public void setUp() throws Exception {
     group1 = group("users1");
-    group("users2");
-    group("users3");
+    group2 = group("users2");
+    group3 = group("users3");
 
-    user1 = accounts.create("user1", "user1@example.com", "First1 Last1",
-        "users1");
-    user2 = accounts.create("user2", "user2@example.com", "First2 Last2",
-        "users2");
-    user3 = accounts.create("user3", "USER3@example.com", "First3 Last3",
-        "users1", "users2");
+    user1 = user("user1", "First1 Last1", group1);
+    user2 = user("user2", "First2 Last2", group2);
+    user3 = user("user3", "First3 Last3", group1, group2);
+    user4 = user("jdoe", "John Doe", "JDOE");
   }
 
   @Test
   @GerritConfig(name = "suggest.accounts", value = "false")
   public void suggestReviewersNoResult1() throws Exception {
     String changeId = createChange().getChangeId();
-    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, name("u"), 6);
     assertThat(reviewers).isEmpty();
   }
 
@@ -82,7 +86,8 @@
       })
   public void suggestReviewersNoResult2() throws Exception {
     String changeId = createChange().getChangeId();
-    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, name("u"), 6);
     assertThat(reviewers).isEmpty();
   }
 
@@ -90,18 +95,20 @@
   @GerritConfig(name = "suggest.from", value = "2")
   public void suggestReviewersNoResult3() throws Exception {
     String changeId = createChange().getChangeId();
-    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, name("").substring(0, 1), 6);
     assertThat(reviewers).isEmpty();
   }
 
   @Test
   public void suggestReviewersChange() throws Exception {
     String changeId = createChange().getChangeId();
-    List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, name("u"), 6);
     assertThat(reviewers).hasSize(6);
-    reviewers = suggestReviewers(changeId, "u", 5);
+    reviewers = suggestReviewers(changeId, name("u"), 5);
     assertThat(reviewers).hasSize(5);
-    reviewers = suggestReviewers(changeId, "users3", 10);
+    reviewers = suggestReviewers(changeId, group3.getName(), 10);
     assertThat(reviewers).hasSize(1);
   }
 
@@ -111,26 +118,26 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    reviewers = suggestReviewers(changeId, "user2", 2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(
-        "First2 Last2");
+    assertThat(Iterables.getOnlyElement(reviewers).account.name)
+        .isEqualTo(user2.fullName);
 
-    reviewers = suggestReviewers(new RestSession(server, user1),
-        changeId, "user2", 2);
+    setApiUser(user1);
+    reviewers = suggestReviewers(changeId, user2.fullName, 2);
     assertThat(reviewers).isEmpty();
 
-    reviewers = suggestReviewers(new RestSession(server, user2),
-        changeId, "user2", 2);
+    setApiUser(user2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(
-        "First2 Last2");
+    assertThat(Iterables.getOnlyElement(reviewers).account.name)
+        .isEqualTo(user2.fullName);
 
-    reviewers = suggestReviewers(new RestSession(server, user3),
-        changeId, "user2", 2);
+    setApiUser(user3);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(
-        "First2 Last2");
+    assertThat(Iterables.getOnlyElement(reviewers).account.name)
+        .isEqualTo(user2.fullName);
   }
 
   @Test
@@ -139,16 +146,17 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    reviewers = suggestReviewers(new RestSession(server, user1),
-        changeId, "user2", 2);
+    setApiUser(user1);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).isEmpty();
 
-    grantCapability(GlobalCapability.VIEW_ALL_ACCOUNTS, group1);
-    reviewers = suggestReviewers(new RestSession(server, user1),
-        changeId, "user2", 2);
+    setApiUser(user1); // Clear cached group info.
+    allowGlobalCapabilities(group1.getGroupUUID(),
+        GlobalCapability.VIEW_ALL_ACCOUNTS);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo(
-        "First2 Last2");
+    assertThat(Iterables.getOnlyElement(reviewers).account.name)
+        .isEqualTo(user2.fullName);
   }
 
   @Test
@@ -156,7 +164,7 @@
   public void suggestReviewersMaxNbrSuggestions() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(changeId, "user", 5);
+        suggestReviewers(changeId, name("user"), 5);
     assertThat(reviewers).hasSize(2);
   }
 
@@ -193,24 +201,24 @@
     reviewers = suggestReviewers(changeId, "first1 last2", 1);
     assertThat(reviewers).hasSize(0);
 
-    reviewers = suggestReviewers(changeId, "user", 8);
-    assertThat(reviewers).hasSize(7);
+    reviewers = suggestReviewers(changeId, name("user"), 7);
+    assertThat(reviewers).hasSize(6);
 
-    reviewers = suggestReviewers(changeId, "user1", 2);
+    reviewers = suggestReviewers(changeId, user1.username, 2);
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "example.com", 6);
-    assertThat(reviewers).hasSize(5);
+    reviewers = suggestReviewers(changeId, "example.com", 7);
+    assertThat(reviewers).hasSize(6);
 
-    reviewers = suggestReviewers(changeId, "user1@example.com", 2);
+    reviewers = suggestReviewers(changeId, user1.email, 2);
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "user1 example", 2);
+    reviewers = suggestReviewers(changeId, user1.username + " example", 2);
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, "user3@example.com", 2);
+    reviewers = suggestReviewers(changeId, user4.email.toLowerCase(), 2);
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.get(0).account.email).isEqualTo("USER3@example.com");
+    assertThat(reviewers.get(0).account.email).isEqualTo(user4.email);
   }
 
   @Test
@@ -221,60 +229,52 @@
   public void suggestReviewersFullTextSearchLimitMaxMatches() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(changeId, "user", 3);
-    assertThat(reviewers).hasSize(3);
+        suggestReviewers(changeId, name("user"), 2);
+    assertThat(reviewers).hasSize(2);
   }
 
   @Test
   public void suggestReviewersWithoutLimitOptionSpecified() throws Exception {
     String changeId = createChange().getChangeId();
-    String query = "users3";
-    List<SuggestedReviewerInfo> suggestedReviewerInfos = newGson().fromJson(
-        adminSession.get("/changes/"
-            + changeId
-            + "/suggest_reviewers?q="
-            + query)
-            .getReader(),
-        new TypeToken<List<SuggestedReviewerInfo>>() {}
-        .getType());
+    String query = user3.username;
+    List<SuggestedReviewerInfo> suggestedReviewerInfos = gApi.changes()
+        .id(changeId)
+        .suggestReviewers(query)
+        .get();
     assertThat(suggestedReviewerInfos).hasSize(1);
   }
 
-  private List<SuggestedReviewerInfo> suggestReviewers(RestSession session,
-      String changeId, String query, int n) throws IOException {
-    return newGson().fromJson(
-        session.get("/changes/"
-            + changeId
-            + "/suggest_reviewers?q="
-            + Url.encode(query)
-            + "&n="
-            + n)
-        .getReader(),
-        new TypeToken<List<SuggestedReviewerInfo>>() {}
-        .getType());
-  }
-
   private List<SuggestedReviewerInfo> suggestReviewers(String changeId,
-      String query, int n) throws IOException {
-    return suggestReviewers(adminSession, changeId, query, n);
+      String query, int n) throws Exception {
+    return gApi.changes()
+        .id(changeId)
+        .suggestReviewers(query)
+        .withLimit(n)
+        .get();
   }
 
   private AccountGroup group(String name) throws Exception {
-    CreateGroupArgs args = new CreateGroupArgs();
-    args.setGroupName(name);
-    args.initialMembers = Collections.singleton(admin.getId());
-    return createGroupFactory.create(args).createGroup();
+    GroupInfo group = createGroupFactory.create(name(name))
+        .apply(TopLevelResource.INSTANCE, null);
+    GroupDescription.Basic d = groups.parseInternal(Url.decode(group.id));
+    return GroupDescriptions.toAccountGroup(d);
   }
 
-  private void grantCapability(String name, AccountGroup group)
+  private TestAccount user(String name, String fullName, String emailName,
+      AccountGroup... groups) throws Exception {
+    String[] groupNames = FluentIterable.from(Arrays.asList(groups))
+        .transform(new Function<AccountGroup, String>() {
+          @Override
+          public String apply(AccountGroup in) {
+            return in.getName();
+          }
+        }).toArray(String.class);
+    return accounts.create(name(name), name(emailName) + "@example.com",
+        fullName, groupNames);
+  }
+
+  private TestAccount user(String name, String fullName, AccountGroup... groups)
       throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(
-        AccessSection.GLOBAL_CAPABILITIES);
-    Permission p = s.getPermission(name, true);
-    p.add(new PermissionRule(config.resolve(group)));
-    config.commit(md);
-    projectCache.evict(config.getProject());
+    return user(name, fullName, name, groups);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java
new file mode 100644
index 0000000..edf55f7
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/TopicIT.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+public class TopicIT extends AbstractDaemonTest {
+  @Test
+  public void topic() throws Exception {
+    Result result = createChange();
+    String endpoint = "/changes/" + result.getChangeId() + "/topic";
+    RestResponse response = adminSession.put(endpoint, "topic");
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+
+    response = adminSession.delete(endpoint);
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+
+    response = adminSession.put(endpoint, "topic");
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+
+    response = adminSession.put(endpoint, "");
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
index c89da30..0802e7c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-config',
   srcs = glob(['*IT.java']),
   labels = ['rest']
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index 304abc8..7e68a03 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -22,18 +22,12 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.config.PostCaches;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.Util;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Arrays;
 
 public class CacheOperationsIT extends AbstractDaemonTest {
@@ -42,7 +36,7 @@
   public void flushAll() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long) 0);
+    assertThat(cacheInfo.entries.mem).isGreaterThan((long) 0);
 
     r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
@@ -71,11 +65,11 @@
   public void flush() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)0);
+    assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
 
     r = adminSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)1);
+    assertThat(cacheInfo.entries.mem).isGreaterThan((long)1);
 
     r = adminSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list")));
@@ -88,7 +82,7 @@
 
     r = adminSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)1);
+    assertThat(cacheInfo.entries.mem).isGreaterThan((long)1);
   }
 
   @Test
@@ -109,7 +103,7 @@
   public void flush_UnprocessableEntity() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/projects");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)0);
+    assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
 
     r = adminSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable")));
@@ -118,35 +112,25 @@
 
     r = adminSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)0);
+    assertThat(cacheInfo.entries.mem).isGreaterThan((long)0);
   }
 
   @Test
   public void flushWebSessions_Forbidden() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    AccountGroup.UUID registeredUsers =
-        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-    Util.allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
-    Util.allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
-    saveProjectConfig(cfg);
-
-    RestResponse r = userSession.post("/config/server/caches/",
-        new PostCaches.Input(FLUSH, Arrays.asList("projects")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
-
-    r = userSession.post("/config/server/caches/",
-        new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-  }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
+    allowGlobalCapabilities(REGISTERED_USERS,
+        GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
     try {
-      cfg.commit(md);
+      RestResponse r = userSession.post("/config/server/caches/",
+          new PostCaches.Input(FLUSH, Arrays.asList("projects")));
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+      r.consume();
+
+      r = userSession.post("/config/server/caches/",
+          new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")));
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
     } finally {
-      md.close();
+      removeGlobalCapabilities(REGISTERED_USERS,
+          GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
     }
-    projectCache.evict(allProjects);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
new file mode 100644
index 0000000..e51ed08
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 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.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ConfirmEmail;
+import com.google.gerrit.server.mail.EmailTokenVerifier;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gwtjsonrpc.server.SignedToken;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class ConfirmEmailIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setString("auth", null, "registerEmailPrivateKey",
+        SignedToken.generateRandomKey());
+    return cfg;
+  }
+
+  @Inject
+  private EmailTokenVerifier emailTokenVerifier;
+
+  @Test
+  public void confirm() throws Exception {
+    ConfirmEmail.Input in = new ConfirmEmail.Input();
+    in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
+    RestResponse r = adminSession.put("/config/server/email.confirm", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+  }
+
+  @Test
+  public void confirmForOtherUser_UnprocessableEntity() throws Exception {
+    ConfirmEmail.Input in = new ConfirmEmail.Input();
+    in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
+    RestResponse r = adminSession.put("/config/server/email.confirm", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+  }
+
+  @Test
+  public void confirmInvalidToken_UnprocessableEntity() throws Exception {
+    ConfirmEmail.Input in = new ConfirmEmail.Input();
+    in.token = "invalidToken";
+    RestResponse r = adminSession.put("/config/server/email.confirm", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+  }
+
+  @Test
+  public void confirmAlreadyInUse_UnprocessableEntity() throws Exception {
+    ConfirmEmail.Input in = new ConfirmEmail.Input();
+    in.token = emailTokenVerifier.encode(admin.getId(), user.email);
+    RestResponse r = adminSession.put("/config/server/email.confirm", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index 9f7b419..bb63928 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -20,25 +20,18 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.Util;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class FlushCacheIT extends AbstractDaemonTest {
 
   @Test
   public void flushCache() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/groups");
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(result.entries.mem.longValue()).isGreaterThan((long)0);
+    assertThat(result.entries.mem).isGreaterThan((long)0);
 
     r = adminSession.post("/config/server/caches/groups/flush");
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
@@ -75,28 +68,18 @@
 
   @Test
   public void flushWebSessionsCache_Forbidden() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    AccountGroup.UUID registeredUsers =
-        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-    Util.allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
-    Util.allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
-    saveProjectConfig(cfg);
-
-    RestResponse r = userSession.post("/config/server/caches/accounts/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
-
-    r = userSession.post("/config/server/caches/web_sessions/flush");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-  }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
+    allowGlobalCapabilities(REGISTERED_USERS,
+        GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
     try {
-      cfg.commit(md);
+      RestResponse r = userSession.post("/config/server/caches/accounts/flush");
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+      r.consume();
+
+      r = userSession.post("/config/server/caches/web_sessions/flush");
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
     } finally {
-      md.close();
+      removeGlobalCapabilities(REGISTERED_USERS,
+          GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
     }
-    projectCache.evict(allProjects);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
index f59752c..02a1b73 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
@@ -34,7 +34,7 @@
 
     assertThat(result.name).isEqualTo("accounts");
     assertThat(result.type).isEqualTo(CacheType.MEM);
-    assertThat(result.entries.mem.longValue()).isEqualTo(1);
+    assertThat(result.entries.mem).isAtLeast(1L);
     assertThat(result.averageGet).isNotNull();
     assertThat(result.averageGet).endsWith("s");
     assertThat(result.entries.disk).isNull();
@@ -47,7 +47,7 @@
     r = adminSession.get("/config/server/caches/accounts");
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertThat(result.entries.mem.longValue()).isEqualTo(2);
+    assertThat(result.entries.mem).isEqualTo(2);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index 26299e2..11c61c6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -28,8 +28,7 @@
 
 public class KillTaskIT extends AbstractDaemonTest {
 
-  @Test
-  public void killTask() throws Exception {
+  private void killTask() throws Exception {
     RestResponse r = adminSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
@@ -48,8 +47,7 @@
     assertThat(result).hasSize(taskCount - 1);
   }
 
-  @Test
-  public void killTask_NotFound() throws Exception {
+  private void killTask_NotFound() throws Exception {
     RestResponse r = adminSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
@@ -59,4 +57,11 @@
     r = userSession.delete("/config/server/tasks/" + result.get(0).id);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
+
+  @Test
+  public void killTaskTests_inOrder() throws Exception {
+    // As killTask() changes the state of the server, we want to test it last
+    killTask_NotFound();
+    killTask();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
index 0d0a94e..fb89e1b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -45,7 +45,7 @@
     assertThat(result).containsKey("accounts");
     CacheInfo accountsCacheInfo = result.get("accounts");
     assertThat(accountsCacheInfo.type).isEqualTo(CacheType.MEM);
-    assertThat(accountsCacheInfo.entries.mem.longValue()).isEqualTo(1);
+    assertThat(accountsCacheInfo.entries.mem).isAtLeast(1L);
     assertThat(accountsCacheInfo.averageGet).isNotNull();
     assertThat(accountsCacheInfo.averageGet).endsWith("s");
     assertThat(accountsCacheInfo.entries.disk).isNull();
@@ -59,7 +59,7 @@
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = newGson().fromJson(r.getReader(),
         new TypeToken<Map<String, CacheInfo>>() {}.getType());
-    assertThat(result.get("accounts").entries.mem.longValue()).isEqualTo(2);
+    assertThat(result.get("accounts").entries.mem).isEqualTo(2);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
new file mode 100644
index 0000000..a656760
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2015 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.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.GetServerInfo.ServerInfo;
+
+import org.junit.Test;
+
+public class ServerInfoIT extends AbstractDaemonTest {
+
+  @Test
+  @GerritConfigs({
+    // auth
+    @GerritConfig(name = "auth.type", value = "HTTP"),
+    @GerritConfig(name = "auth.contributorAgreements", value = "true"),
+    @GerritConfig(name = "auth.loginUrl", value = "https://example.com/login"),
+    @GerritConfig(name = "auth.loginText", value = "LOGIN"),
+    @GerritConfig(name = "auth.switchAccountUrl", value = "https://example.com/switch"),
+
+    // auth fields ignored when auth == HTTP
+    @GerritConfig(name = "auth.registerUrl", value = "https://example.com/register"),
+    @GerritConfig(name = "auth.registerText", value = "REGISTER"),
+    @GerritConfig(name = "auth.editFullNameUrl", value = "https://example.com/editname"),
+    @GerritConfig(name = "auth.httpPasswordUrl", value = "https://example.com/password"),
+
+    // change
+    @GerritConfig(name = "change.allowDrafts", value = "false"),
+    @GerritConfig(name = "change.largeChange", value = "300"),
+    @GerritConfig(name = "change.replyTooltip", value = "Publish votes and draft comments"),
+    @GerritConfig(name = "change.replyLabel", value = "Vote"),
+    @GerritConfig(name = "change.updateDelay", value = "50s"),
+
+    // download
+    @GerritConfig(name = "download.archive", values = {"tar",
+        "tbz2", "tgz", "txz"}),
+
+    // gerrit
+    @GerritConfig(name = "gerrit.allProjects", value = "Root"),
+    @GerritConfig(name = "gerrit.allUsers", value = "Users"),
+    @GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report"),
+    @GerritConfig(name = "gerrit.reportBugText", value = "REPORT BUG"),
+
+    // suggest
+    @GerritConfig(name = "suggest.from", value = "3"),
+
+    // user
+    @GerritConfig(name = "user.anonymousCoward", value = "Unnamed User"),
+  })
+  public void serverConfig() throws Exception {
+    RestResponse r = adminSession.get("/config/server/info/");
+    ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
+
+    // auth
+    assertThat(i.auth.authType).isEqualTo(AuthType.HTTP);
+    assertThat(i.auth.editableAccountFields).containsExactly(
+        Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME);
+    assertThat(i.auth.useContributorAgreements).isTrue();
+    assertThat(i.auth.loginUrl).isEqualTo("https://example.com/login");
+    assertThat(i.auth.loginText).isEqualTo("LOGIN");
+    assertThat(i.auth.switchAccountUrl).isEqualTo("https://example.com/switch");
+    assertThat(i.auth.registerUrl).isNull();
+    assertThat(i.auth.registerText).isNull();
+    assertThat(i.auth.editFullNameUrl).isNull();
+    assertThat(i.auth.httpPasswordUrl).isNull();
+    assertThat(i.auth.isGitBasicAuth).isNull();
+
+    // change
+    assertThat(i.change.allowDrafts).isNull();
+    assertThat(i.change.largeChange).isEqualTo(300);
+    assertThat(i.change.replyTooltip).startsWith("Publish votes and draft comments");
+    assertThat(i.change.replyLabel).isEqualTo("Vote\u2026");
+    assertThat(i.change.updateDelay).isEqualTo(50);
+
+    // download
+    assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
+    assertThat(i.download.schemes).isEmpty();
+
+    // gerrit
+    assertThat(i.gerrit.allProjects).isEqualTo("Root");
+    assertThat(i.gerrit.allUsers).isEqualTo("Users");
+    assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
+    assertThat(i.gerrit.reportBugText).isEqualTo("REPORT BUG");
+
+    // gitweb
+    assertThat(i.gitweb).isNull();
+
+    // sshd
+    assertThat(i.sshd).isNotNull();
+
+    // suggest
+    assertThat(i.suggest.from).isEqualTo(3);
+
+    // user
+    assertThat(i.user.anonymousCowardName).isEqualTo("Unnamed User");
+  }
+
+  @Test
+  public void serverConfigWithDefaults() throws Exception {
+    RestResponse r = adminSession.get("/config/server/info/");
+    ServerInfo i = newGson().fromJson(r.getReader(), ServerInfo.class);
+
+    // auth
+    assertThat(i.auth.authType).isEqualTo(AuthType.OPENID);
+    assertThat(i.auth.editableAccountFields).containsExactly(
+        Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME,
+        Account.FieldName.USER_NAME);
+    assertThat(i.auth.useContributorAgreements).isNull();
+    assertThat(i.auth.loginUrl).isNull();
+    assertThat(i.auth.loginText).isNull();
+    assertThat(i.auth.switchAccountUrl).isNull();
+    assertThat(i.auth.registerUrl).isNull();
+    assertThat(i.auth.registerText).isNull();
+    assertThat(i.auth.editFullNameUrl).isNull();
+    assertThat(i.auth.httpPasswordUrl).isNull();
+    assertThat(i.auth.isGitBasicAuth).isNull();
+
+    // change
+    assertThat(i.change.allowDrafts).isTrue();
+    assertThat(i.change.largeChange).isEqualTo(500);
+    assertThat(i.change.replyTooltip).startsWith("Reply and score");
+    assertThat(i.change.replyLabel).isEqualTo("Reply\u2026");
+    assertThat(i.change.updateDelay).isEqualTo(30);
+
+    // download
+    assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
+    assertThat(i.download.schemes).isEmpty();
+
+    // gerrit
+    assertThat(i.gerrit.allProjects).isEqualTo(AllProjectsNameProvider.DEFAULT);
+    assertThat(i.gerrit.allUsers).isEqualTo(AllUsersNameProvider.DEFAULT);
+    assertThat(i.gerrit.reportBugUrl).isNull();
+    assertThat(i.gerrit.reportBugText).isNull();
+
+    // gitweb
+    assertThat(i.gitweb).isNull();
+
+    // sshd
+    assertThat(i.sshd).isNotNull();
+
+    // suggest
+    assertThat(i.suggest.from).isEqualTo(0);
+
+    // user
+    assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
new file mode 100644
index 0000000..242e1ee
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 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.group;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+public class AddMemberIT extends AbstractDaemonTest {
+  @Test
+  public void addNonExistingMember_NotFound() throws Exception {
+    int status =
+        adminSession.put("/groups/Administrators/members/non-existing")
+            .getStatusCode();
+    assertThat(status).isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
deleted file mode 100644
index b9778b8..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.server.group.AddIncludedGroups;
-import com.google.gerrit.server.group.AddMembers;
-import com.google.gerrit.server.group.CreateGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class AddRemoveGroupMembersIT extends AbstractDaemonTest {
-  @Test
-  public void addToNonExistingGroup_NotFound() throws Exception {
-    assertThat(PUT("/groups/non-existing/members/admin").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void removeFromNonExistingGroup_NotFound() throws Exception {
-    assertThat(DELETE("/groups/non-existing/members/admin"))
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void addRemoveMember() throws Exception {
-    RestResponse r = PUT("/groups/Administrators/members/user");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    AccountInfo ai = newGson().fromJson(r.getReader(), AccountInfo.class);
-    assertAccountInfo(user, ai);
-    assertMembers("Administrators", admin, user);
-    r.consume();
-
-    assertThat(DELETE("/groups/Administrators/members/user"))
-      .isEqualTo(HttpStatus.SC_NO_CONTENT);
-    assertMembers("Administrators", admin);
-  }
-
-  @Test
-  public void addExistingMember_OK() throws Exception {
-    assertThat(PUT("/groups/Administrators/members/admin").getStatusCode())
-      .isEqualTo(HttpStatus.SC_OK);
-  }
-
-  @Test
-  public void addMultipleMembers() throws Exception {
-    group("users");
-    TestAccount u1 = accounts.create("u1", "u1@example.com", "Full Name 1");
-    TestAccount u2 = accounts.create("u2", "u2@example.com", "Full Name 2");
-    List<String> members = Lists.newLinkedList();
-    members.add(u1.username);
-    members.add(u2.username);
-    AddMembers.Input input = AddMembers.Input.fromMembers(members);
-    RestResponse r = POST("/groups/users/members", input);
-    List<AccountInfo> ai = newGson().fromJson(r.getReader(),
-        new TypeToken<List<AccountInfo>>() {}.getType());
-    assertMembers(ai, u1, u2);
-  }
-
-  @Test
-  public void includeRemoveGroup() throws Exception {
-    group("newGroup");
-    RestResponse r = PUT("/groups/Administrators/groups/newGroup");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    GroupInfo i = newGson().fromJson(r.getReader(), GroupInfo.class);
-    r.consume();
-    assertGroupInfo(groupCache.get(new AccountGroup.NameKey("newGroup")), i);
-    assertIncludes("Administrators", "newGroup");
-
-    assertThat(DELETE("/groups/Administrators/groups/newGroup"))
-      .isEqualTo(HttpStatus.SC_NO_CONTENT);
-    assertNoIncludes("Administrators");
-  }
-
-  @Test
-  public void includeExistingGroup_OK() throws Exception {
-    group("newGroup");
-    PUT("/groups/Administrators/groups/newGroup").consume();
-    assertThat(PUT("/groups/Administrators/groups/newGroup").getStatusCode())
-      .isEqualTo(HttpStatus.SC_OK);
-  }
-
-  @Test
-  public void addMultipleIncludes() throws Exception {
-    group("newGroup1");
-    group("newGroup2");
-    List<String> groups = Lists.newLinkedList();
-    groups.add("newGroup1");
-    groups.add("newGroup2");
-    AddIncludedGroups.Input input = AddIncludedGroups.Input.fromGroups(groups);
-    RestResponse r = POST("/groups/Administrators/groups", input);
-    List<GroupInfo> gi = newGson().fromJson(r.getReader(),
-        new TypeToken<List<GroupInfo>>() {}.getType());
-    assertIncludes(gi, "newGroup1", "newGroup2");
-  }
-
-  private RestResponse PUT(String endpoint) throws IOException {
-    return adminSession.put(endpoint);
-  }
-
-  private int DELETE(String endpoint) throws IOException {
-    RestResponse r = adminSession.delete(endpoint);
-    r.consume();
-    return r.getStatusCode();
-  }
-
-  private RestResponse POST(String endPoint, AddMembers.Input mi)
-      throws IOException {
-    return adminSession.post(endPoint, mi);
-  }
-
-  private RestResponse POST(String endPoint, AddIncludedGroups.Input gi)
-      throws IOException {
-    return adminSession.post(endPoint, gi);
-  }
-
-  private void group(String name) throws IOException {
-    CreateGroup.Input in = new CreateGroup.Input();
-    adminSession.put("/groups/" + name, in).consume();
-  }
-
-  private void assertMembers(String group, TestAccount... members)
-      throws OrmException {
-    AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
-    Set<Account.Id> ids = Sets.newHashSet();
-    ResultSet<AccountGroupMember> all =
-        db.accountGroupMembers().byGroup(g.getId());
-    for (AccountGroupMember m : all) {
-      ids.add(m.getAccountId());
-    }
-    assertThat((Iterable<?>)ids).hasSize(members.length);
-    for (TestAccount a : members) {
-      assertThat((Iterable<?>)ids).contains(a.id);
-    }
-  }
-
-  private void assertMembers(List<AccountInfo> ai, TestAccount... members) {
-    Map<Integer, AccountInfo> infoById = Maps.newHashMap();
-    for (AccountInfo i : ai) {
-      infoById.put(i._accountId, i);
-    }
-
-    for (TestAccount a : members) {
-      AccountInfo i = infoById.get(a.id.get());
-      assertThat(i).isNotNull();
-      assertAccountInfo(a, i);
-    }
-    assertThat(ai).hasSize(members.length);
-  }
-
-  private void assertIncludes(String group, String... includes)
-      throws OrmException {
-    AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
-    Set<AccountGroup.UUID> ids = Sets.newHashSet();
-    ResultSet<AccountGroupById> all =
-        db.accountGroupById().byGroup(g.getId());
-    for (AccountGroupById m : all) {
-      ids.add(m.getIncludeUUID());
-    }
-    assertThat((Iterable<?>)ids).hasSize(includes.length);
-    for (String i : includes) {
-      AccountGroup.UUID id = groupCache.get(
-          new AccountGroup.NameKey(i)).getGroupUUID();
-      assertThat((Iterable<?>)ids).contains(id);
-    }
-  }
-
-  private void assertIncludes(List<GroupInfo> gi, String... includes) {
-    Map<String, GroupInfo> groupsByName = Maps.newHashMap();
-    for (GroupInfo i : gi) {
-      groupsByName.put(i.name, i);
-    }
-
-    for (String name : includes) {
-      GroupInfo i = groupsByName.get(name);
-      assertThat(i).isNotNull();
-      assertGroupInfo(groupCache.get(new AccountGroup.NameKey(name)), i);
-    }
-    assertThat(gi).hasSize(includes.length);
-  }
-
-  private void assertNoIncludes(String group) throws OrmException {
-    AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
-    Iterator<AccountGroupById> it =
-        db.accountGroupById().byGroup(g.getId()).iterator();
-    assertThat(it.hasNext()).isFalse();
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
index 726c7cf..d991417 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -1,24 +1,8 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-group',
   srcs = glob(['*IT.java']),
-  deps = [
-    ':util',
-    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
-  ],
   labels = ['rest']
 )
 
-java_library(
-  name = 'util',
-  srcs = ['GroupAssert.java'],
-  deps = [
-    '//gerrit-extension-api:api',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:junit',
-    '//lib:truth',
-  ],
-)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
deleted file mode 100644
index 7de450b..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.CreateGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-public class CreateGroupIT extends AbstractDaemonTest {
-  @Test
-  public void testCreateGroup() throws Exception {
-    final String newGroupName = "newGroup";
-    RestResponse r = adminSession.put("/groups/" + newGroupName);
-    GroupInfo g = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertThat(g.name).isEqualTo(newGroupName);
-    AccountGroup group = groupCache.get(new AccountGroup.NameKey(newGroupName));
-    assertThat(group).isNotNull();
-    assertGroupInfo(group, g);
-  }
-
-  @Test
-  public void testCreateGroupWithProperties() throws Exception {
-    final String newGroupName = "newGroup";
-    CreateGroup.Input in = new CreateGroup.Input();
-    in.description = "Test description";
-    in.visibleToAll = true;
-    in.ownerId = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID().get();
-    RestResponse r = adminSession.put("/groups/" + newGroupName, in);
-    GroupInfo g = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertThat(g.name).isEqualTo(newGroupName);
-    AccountGroup group = groupCache.get(new AccountGroup.NameKey(newGroupName));
-    assertThat(group.getDescription()).isEqualTo(in.description);
-    assertThat(group.isVisibleToAll()).isEqualTo(in.visibleToAll);
-    assertThat(group.getOwnerGroupUUID().get()).isEqualTo(in.ownerId);
-  }
-
-  @Test
-  public void testCreateGroupWithoutCapability_Forbidden() throws Exception {
-    RestResponse r = (new RestSession(server, user)).put("/groups/newGroup");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-  }
-
-  @Test
-  public void testCreateGroupWhenGroupAlreadyExists_Conflict()
-      throws Exception {
-    RestResponse r = adminSession.put("/groups/Administrators");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
deleted file mode 100644
index 8365d79..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gson.reflect.TypeToken;
-
-import org.junit.Test;
-
-import java.util.Map;
-import java.util.Set;
-
-/**
- * An example test that tests presence of default groups in a newly initialized
- * review site.
- *
- * The test shows how to perform these checks via SSH, REST or using Gerrit
- * internals.
- */
-public class DefaultGroupsIT extends AbstractDaemonTest {
-
-  @Test
-  public void defaultGroupsCreated_ssh() throws Exception {
-    SshSession session = new SshSession(server, admin);
-    String result = session.exec("gerrit ls-groups");
-    assert_().withFailureMessage(session.getError())
-      .that(session.hasError()).isFalse();
-    assertThat(result).contains("Administrators");
-    assertThat(result).contains("Non-Interactive Users");
-    session.close();
-  }
-
-  @Test
-  public void defaultGroupsCreated_rest() throws Exception {
-    RestSession session = new RestSession(server, admin);
-    RestResponse r = session.get("/groups/");
-    Map<String, GroupInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    Set<String> names = result.keySet();
-    assertThat((Iterable<?>)names).contains("Administrators");
-    assertThat((Iterable<?>)names).contains("Non-Interactive Users");
-  }
-
-  @Test
-  public void defaultGroupsCreated_internals() throws Exception {
-    Set<String> names = Sets.newHashSet();
-    for (AccountGroup g : db.accountGroups().all()) {
-      names.add(g.getName());
-    }
-    assertThat((Iterable<?>)names).contains("Administrators");
-    assertThat((Iterable<?>)names).contains("Non-Interactive Users");
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
deleted file mode 100644
index 8dfb5ff..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class GetGroupIT extends AbstractDaemonTest {
-  @Test
-  public void testGetGroup() throws Exception {
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
-
-    // by UUID
-    testGetGroup("/groups/" + adminGroup.getGroupUUID().get(), adminGroup);
-
-    // by name
-    testGetGroup("/groups/" + adminGroup.getName(), adminGroup);
-
-    // by legacy numeric ID
-    testGetGroup("/groups/" + adminGroup.getId().get(), adminGroup);
-  }
-
-  private void testGetGroup(String url, AccountGroup expectedGroup)
-      throws IOException {
-    RestResponse r = adminSession.get(url);
-    GroupInfo group = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertGroupInfo(expectedGroup, group);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
deleted file mode 100644
index e9dd776..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.toBoolean;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gerrit.server.group.GroupOptionsInfo;
-import com.google.gerrit.server.group.PutDescription;
-import com.google.gerrit.server.group.PutName;
-import com.google.gerrit.server.group.PutOptions;
-import com.google.gerrit.server.group.PutOwner;
-import com.google.gerrit.server.group.SystemGroupBackend;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-public class GroupPropertiesIT extends AbstractDaemonTest {
-  @Test
-  public void testGroupName() throws Exception {
-    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
-    String url = "/groups/" + groupCache.get(adminGroupName).getGroupUUID().get() + "/name";
-
-    // get name
-    RestResponse r = adminSession.get(url);
-    String name = newGson().fromJson(r.getReader(), String.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(name).isEqualTo("Administrators");
-    r.consume();
-
-    // set name with name conflict
-    String newGroupName = "newGroup";
-    r = adminSession.put("/groups/" + newGroupName);
-    r.consume();
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    PutName.Input in = new PutName.Input();
-    in.name = newGroupName;
-    r = adminSession.put(url, in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
-    r.consume();
-
-    // set name to same name
-    in = new PutName.Input();
-    in.name = "Administrators";
-    r = adminSession.put(url, in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    r.consume();
-
-    // rename
-    in = new PutName.Input();
-    in.name = "Admins";
-    r = adminSession.put(url, in);
-    String newName = newGson().fromJson(r.getReader(), String.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(groupCache.get(new AccountGroup.NameKey(in.name))).isNotNull();
-    assertThat(groupCache.get(adminGroupName)).isNull();
-    assertThat(newName).isEqualTo(in.name);
-    r.consume();
-  }
-
-  @Test
-  public void testGroupDescription() throws Exception {
-    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
-    AccountGroup adminGroup = groupCache.get(adminGroupName);
-    String url = "/groups/" + adminGroup.getGroupUUID().get() + "/description";
-
-    // get description
-    RestResponse r = adminSession.get(url);
-    String description = newGson().fromJson(r.getReader(), String.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(description).isEqualTo(adminGroup.getDescription());
-    r.consume();
-
-    // set description
-    PutDescription.Input in = new PutDescription.Input();
-    in.description = "All users that can administrate the Gerrit Server.";
-    r = adminSession.put(url, in);
-    String newDescription = newGson().fromJson(r.getReader(), String.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(newDescription).isEqualTo(in.description);
-    adminGroup = groupCache.get(adminGroupName);
-    assertThat(adminGroup.getDescription()).isEqualTo(in.description);
-    r.consume();
-
-    // delete description
-    r = adminSession.delete(url);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    adminGroup = groupCache.get(adminGroupName);
-    assertThat(adminGroup.getDescription()).isNull();
-
-    // set description to empty string
-    in = new PutDescription.Input();
-    in.description = "";
-    r = adminSession.put(url, in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    adminGroup = groupCache.get(adminGroupName);
-    assertThat(adminGroup.getDescription()).isNull();
-  }
-
-  @Test
-  public void testGroupOptions() throws Exception {
-    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
-    AccountGroup adminGroup = groupCache.get(adminGroupName);
-    String url = "/groups/" + adminGroup.getGroupUUID().get() + "/options";
-
-    // get options
-    RestResponse r = adminSession.get(url);
-    GroupOptionsInfo options = newGson().fromJson(r.getReader(), GroupOptionsInfo.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(toBoolean(options.visibleToAll)).isEqualTo(adminGroup.isVisibleToAll());
-    r.consume();
-
-    // set options
-    PutOptions.Input in = new PutOptions.Input();
-    in.visibleToAll = !adminGroup.isVisibleToAll();
-    r = adminSession.put(url, in);
-    GroupOptionsInfo newOptions = newGson().fromJson(r.getReader(), GroupOptionsInfo.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(toBoolean(newOptions.visibleToAll)).isEqualTo(in.visibleToAll);
-    adminGroup = groupCache.get(adminGroupName);
-    assertThat(adminGroup.isVisibleToAll()).isEqualTo(in.visibleToAll);
-    r.consume();
-  }
-
-  @Test
-  public void testGroupOwner() throws Exception {
-    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
-    AccountGroup adminGroup = groupCache.get(adminGroupName);
-    String url = "/groups/" + adminGroup.getGroupUUID().get() + "/owner";
-
-    // get owner
-    RestResponse r = adminSession.get(url);
-    GroupInfo options = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertGroupInfo(groupCache.get(adminGroup.getOwnerGroupUUID()), options);
-    r.consume();
-
-    // set owner by name
-    PutOwner.Input in = new PutOwner.Input();
-    in.owner = "Registered Users";
-    r = adminSession.put(url, in);
-    GroupInfo newOwner = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertThat(newOwner.name).isEqualTo(in.owner);
-    assertThat(newOwner.name).isEqualTo(
-        SystemGroupBackend.getGroup(SystemGroupBackend.REGISTERED_USERS).getName());
-    assertThat(SystemGroupBackend.REGISTERED_USERS.get())
-      .isEqualTo(Url.decode(newOwner.id));
-    r.consume();
-
-    // set owner by UUID
-    in = new PutOwner.Input();
-    in.owner = adminGroup.getGroupUUID().get();
-    r = adminSession.put(url, in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    adminGroup = groupCache.get(adminGroupName);
-    assertThat(groupCache.get(adminGroup.getOwnerGroupUUID()).getGroupUUID().get())
-      .isEqualTo(in.owner);
-    r.consume();
-
-    // set non existing owner
-    in = new PutOwner.Input();
-    in.owner = "Non-Existing Group";
-    r = adminSession.put(url, in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
-    r.consume();
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
deleted file mode 100644
index 5dc49c6..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.group.CreateGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gson.reflect.TypeToken;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.List;
-
-public class ListGroupIncludesIT extends AbstractDaemonTest {
-
-  @Test
-  public void listNonExistingGroupIncludes_NotFound() throws Exception {
-    assertThat(adminSession.get("/groups/non-existing/groups/").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void listEmptyGroupIncludes() throws Exception {
-    assertThat(GET("/groups/Administrators/groups/")).isEmpty();
-  }
-
-  @Test
-  public void listNonEmptyGroupIncludes() throws Exception {
-    group("gx", "Administrators");
-    group("gy", "Administrators");
-    PUT("/groups/Administrators/groups/gx");
-    PUT("/groups/Administrators/groups/gy");
-
-    assertIncludes(GET("/groups/Administrators/groups/"), "gx", "gy");
-  }
-
-  @Test
-  public void listOneIncludeMember() throws Exception {
-    group("gx", "Administrators");
-    group("gy", "Administrators");
-    PUT("/groups/Administrators/groups/gx");
-    PUT("/groups/Administrators/groups/gy");
-
-    assertThat(GET_ONE("/groups/Administrators/groups/gx").name).isEqualTo("gx");
-  }
-
-  private List<GroupInfo> GET(String endpoint) throws IOException {
-    RestResponse r = adminSession.get(endpoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(),
-        new TypeToken<List<GroupInfo>>() {}.getType());
-  }
-
-  private GroupInfo GET_ONE(String endpoint) throws IOException {
-    RestResponse r = adminSession.get(endpoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(), GroupInfo.class);
-  }
-
-  private void PUT(String endpoint) throws IOException {
-    adminSession.put(endpoint).consume();
-  }
-
-  private void group(String name, String ownerGroup) throws IOException {
-    CreateGroup.Input in = new CreateGroup.Input();
-    in.ownerId = ownerGroup;
-    adminSession.put("/groups/" + name, in).consume();
-  }
-
-  private void assertIncludes(List<GroupInfo> includes, String name,
-      String... names) {
-    Collection<String> includeNames = Collections2.transform(includes,
-        new Function<GroupInfo, String>() {
-          @Override
-          public String apply(@Nullable GroupInfo info) {
-            return info.name;
-          }
-        });
-    assertThat((Iterable<?>)includeNames).contains(name);
-    for (String n : names) {
-      assertThat((Iterable<?>)includeNames).contains(n);
-    }
-    assertThat(includes).hasSize(names.length + 1);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
deleted file mode 100644
index 80fb960..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.server.group.CreateGroup;
-import com.google.gson.reflect.TypeToken;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.List;
-
-public class ListGroupMembersIT extends AbstractDaemonTest {
-
-  @Test
-  public void listNonExistingGroupMembers_NotFound() throws Exception {
-    assertThat(adminSession.get("/groups/non-existing/members/").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void listEmptyGroupMembers() throws Exception {
-    group("empty", "Administrators");
-    assertThat(GET("/groups/empty/members/")).isEmpty();
-  }
-
-  @Test
-  public void listNonEmptyGroupMembers() throws Exception {
-    assertMembers(GET("/groups/Administrators/members/"), admin.fullName);
-
-    accounts.create("admin2", "Administrators");
-    assertMembers(GET("/groups/Administrators/members/"),
-        admin.fullName, "admin2");
-  }
-
-  @Test
-  public void listOneGroupMember() throws Exception {
-    assertThat(GET_ONE("/groups/Administrators/members/admin").name)
-      .isEqualTo(admin.fullName);
-  }
-
-  @Test
-  public void listGroupMembersRecursively() throws Exception {
-    group("gx", "Administrators");
-    accounts.create("ux", "gx");
-
-    group("gy", "Administrators");
-    accounts.create("uy", "gy");
-
-    PUT("/groups/Administrators/groups/gx");
-    PUT("/groups/gx/groups/gy");
-    assertMembers(GET("/groups/Administrators/members/?recursive"),
-        admin.fullName, "ux", "uy");
-  }
-
-  private List<AccountInfo> GET(String endpoint) throws IOException {
-    RestResponse r = adminSession.get(endpoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(),
-        new TypeToken<List<AccountInfo>>() {}.getType());
-  }
-
-  private AccountInfo GET_ONE(String endpoint) throws IOException {
-    RestResponse r = adminSession.get(endpoint);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(), AccountInfo.class);
-  }
-
-  private void PUT(String endpoint) throws IOException {
-    adminSession.put(endpoint).consume();
-  }
-
-  private void group(String name, String ownerGroup)
-      throws IOException {
-    CreateGroup.Input in = new CreateGroup.Input();
-    in.ownerId = ownerGroup;
-    adminSession.put("/groups/" + name, in).consume();
-  }
-
-  private void assertMembers(List<AccountInfo> members, String name,
-      String... names) {
-    Collection<String> memberNames = Collections2.transform(members,
-        new Function<AccountInfo, String>() {
-          @Override
-          public String apply(@Nullable AccountInfo info) {
-            return info.name;
-          }
-        });
-
-    assertThat((Iterable<?>)memberNames).contains(name);
-    for (String n : names) {
-      assertThat((Iterable<?>)memberNames).contains(n);
-    }
-    assertThat(members).hasSize(names.length + 1);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
deleted file mode 100644
index 763c36a..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2013 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.group;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroups;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.CreateGroup;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gson.reflect.TypeToken;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.util.Map;
-import java.util.Set;
-
-public class ListGroupsIT extends AbstractDaemonTest {
-  @Test
-  public void testListAllGroups() throws Exception {
-    Iterable<String> expectedGroups = Iterables.transform(groupCache.all(),
-        new Function<AccountGroup, String>() {
-          @Override
-          @Nullable
-          public String apply(@Nullable AccountGroup group) {
-            return group.getName();
-          }
-        });
-    RestResponse r = adminSession.get("/groups/");
-    Map<String, GroupInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    assertGroups(expectedGroups, result.keySet());
-  }
-
-  @Test
-  public void testOnlyVisibleGroupsReturned() throws Exception {
-    String newGroupName = "newGroup";
-    CreateGroup.Input in = new CreateGroup.Input();
-    in.description = "a hidden group";
-    in.visibleToAll = false;
-    in.ownerId = groupCache.get(new AccountGroup.NameKey("Administrators"))
-        .getGroupUUID().get();
-    adminSession.put("/groups/" + newGroupName, in).consume();
-
-    Set<String> expectedGroups = Sets.newHashSet(newGroupName);
-    RestResponse r = userSession.get("/groups/");
-    Map<String, GroupInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    assertThat(result).isEmpty();
-
-    r = adminSession.put(
-        String.format("/groups/%s/members/%s", newGroupName, user.username));
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-
-    r = userSession.get("/groups/");
-    result = newGson().fromJson(r.getReader(),
-        new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    assertGroups(expectedGroups, result.keySet());
-  }
-
-  @Test
-  public void testAllGroupInfoFieldsSetCorrectly() throws Exception {
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
-    RestResponse r = adminSession.get("/groups/?q=" + adminGroup.getName());
-    Map<String, GroupInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    GroupInfo adminGroupInfo = result.get(adminGroup.getName());
-    assertGroupInfo(adminGroup, adminGroupInfo);
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
index 1efaa60..c1618fb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'rest-project',
   srcs = glob(['*IT.java']),
   deps = [
     ':branch',
@@ -15,9 +16,8 @@
     'BranchAssert.java',
   ],
   deps = [
-    '//lib:guava',
-    '//lib:junit',
     '//lib:truth',
+    '//gerrit-extension-api:api',
     '//gerrit-server:server',
   ],
 )
@@ -32,8 +32,6 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//lib:gwtorm',
-    '//lib:guava',
-    '//lib:junit',
     '//lib:truth',
   ],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
index ceed9b6..682059c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
@@ -15,18 +15,16 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.add;
-import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.server.project.BanCommit;
 import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 
 import org.apache.http.HttpStatus;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
@@ -34,20 +32,20 @@
 
   @Test
   public void banCommit() throws Exception {
-    add(git, "a.txt", "some content");
-    Commit c = createCommit(git, admin.getIdent(), "subject");
+    RevCommit c = commitBuilder()
+        .add("a.txt", "some content")
+        .create();
 
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/ban/",
-            BanCommit.Input.fromCommits(c.getCommit().getName()));
+            BanCommit.Input.fromCommits(c.name()));
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
-    assertThat(Iterables.getOnlyElement(info.newlyBanned))
-      .isEqualTo(c.getCommit().getName());
+    assertThat(Iterables.getOnlyElement(info.newlyBanned)).isEqualTo(c.name());
     assertThat(info.alreadyBanned).isNull();
     assertThat(info.ignored).isNull();
 
-    PushResult pushResult = pushHead(git, "refs/heads/master", false);
+    PushResult pushResult = pushHead(testRepo, "refs/heads/master", false);
     assertThat(pushResult.getRemoteUpdate("refs/heads/master").getMessage())
         .startsWith("contains banned commit");
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
index c706e17..c860bf0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
@@ -16,39 +16,44 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.Predicate;
+import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 
 import java.util.List;
 
 public class BranchAssert {
-
   public static void assertBranches(List<BranchInfo> expectedBranches,
       List<BranchInfo> actualBranches) {
-    List<BranchInfo> missingBranches = Lists.newArrayList(actualBranches);
-    for (final BranchInfo b : expectedBranches) {
-      BranchInfo info =
-          Iterables.find(actualBranches, new Predicate<BranchInfo>() {
-            @Override
-            public boolean apply(BranchInfo info) {
-              return info.ref.equals(b.ref);
-            }
-          }, null);
-      assertThat(info).named("branch " + b.ref).isNotNull();
-      assertBranchInfo(b, info);
-      missingBranches.remove(info);
+    assertRefNames(refs(expectedBranches), actualBranches);
+    for (int i = 0; i < expectedBranches.size(); i++) {
+      assertBranchInfo(expectedBranches.get(i), actualBranches.get(i));
     }
-    assertThat(missingBranches).named("" + missingBranches).isEmpty();
+  }
+
+  public static void assertRefNames(Iterable<String> expectedRefs,
+      Iterable<BranchInfo> actualBranches) {
+    Iterable<String> actualNames = refs(actualBranches);
+    assertThat(actualNames).containsExactlyElementsIn(expectedRefs).inOrder();
   }
 
   public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) {
     assertThat(actual.ref).isEqualTo(expected.ref);
     if (expected.revision != null) {
-      assertThat(actual.revision).isEqualTo(expected.revision);
+      assertThat(actual.revision).named("revision of " + actual.ref)
+          .isEqualTo(expected.revision);
     }
-    assertThat(toBoolean(actual.canDelete)).isEqualTo(expected.canDelete);
+    assertThat(toBoolean(actual.canDelete)).named("can delete " + actual.ref)
+        .isEqualTo(toBoolean(expected.canDelete));
+  }
+
+  private static Iterable<String> refs(Iterable<BranchInfo> infos) {
+    return Iterables.transform(infos, new Function<BranchInfo, String>() {
+      @Override
+      public String apply(BranchInfo in) {
+        return in.ref;
+      }
+    });
   }
 
   private static boolean toBoolean(Boolean b) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 3cadf66..62dd729 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -20,15 +20,22 @@
 import static com.google.gerrit.server.project.Util.block;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.ProjectConfig;
 
-import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Constants;
 import org.junit.Before;
 import org.junit.Test;
 
+@NoHttpd
 public class CreateBranchIT extends AbstractDaemonTest {
   private Branch.NameKey branch;
 
@@ -39,65 +46,32 @@
 
   @Test
   public void createBranch_Forbidden() throws Exception {
-    RestResponse r =
-        userSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    setApiUser(user);
+    assertCreateFails(AuthException.class);
   }
 
   @Test
   public void createBranchByAdmin() throws Exception {
-    RestResponse r =
-        adminSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertCreateSucceeds();
   }
 
   @Test
   public void branchAlreadyExists_Conflict() throws Exception {
-    RestResponse r =
-        adminSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.put("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    assertCreateSucceeds();
+    assertCreateFails(ResourceConflictException.class);
   }
 
   @Test
   public void createBranchByProjectOwner() throws Exception {
     grantOwner();
-
-    RestResponse r =
-        userSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    setApiUser(user);
+    assertCreateSucceeds();
   }
 
   @Test
   public void createBranchByAdminCreateReferenceBlocked() throws Exception {
     blockCreateReference();
-    RestResponse r =
-        adminSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertCreateSucceeds();
   }
 
   @Test
@@ -105,19 +79,35 @@
       throws Exception {
     grantOwner();
     blockCreateReference();
-    RestResponse r =
-        userSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    setApiUser(user);
+    assertCreateFails(AuthException.class);
   }
 
   private void blockCreateReference() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     block(cfg, Permission.CREATE, ANONYMOUS_USERS, "refs/*");
-    saveProjectConfig(allProjects, cfg);
+    saveProjectConfig(project, cfg);
   }
 
   private void grantOwner() throws Exception {
     allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
+
+  private BranchApi branch() throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get());
+  }
+
+  private void assertCreateSucceeds() throws Exception {
+    BranchInfo created = branch().create(new BranchInput()).get();
+    assertThat(created.ref)
+        .isEqualTo(Constants.R_HEADS + branch.getShortName());
+  }
+
+  private void assertCreateFails(Class<? extends RestApiException> errType)
+      throws Exception {
+    exception.expect(errType);
+    branch().create(new BranchInput());
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 68c9a5e..d14ea44 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -20,12 +20,20 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.net.HttpHeaders;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -33,6 +41,7 @@
 import com.google.gerrit.server.project.ProjectState;
 
 import org.apache.http.HttpStatus;
+import org.apache.http.message.BasicHeader;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
@@ -47,30 +56,8 @@
 
 public class CreateProjectIT extends AbstractDaemonTest {
   @Test
-  public void testCreateProjectApi() throws Exception {
-    final String newProjectName = "newProject";
-    ProjectInfo p = gApi.projects().name(newProjectName).create().get();
-    assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertThat(projectState).isNotNull();
-    assertProjectInfo(projectState.getProject(), p);
-    assertHead(newProjectName, "refs/heads/master");
-  }
-
-  @Test
-  public void testCreateProjectApiWithGitSuffix() throws Exception {
-    final String newProjectName = "newProject";
-    ProjectInfo p = gApi.projects().name(newProjectName + ".git").create().get();
-    assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertThat(projectState).isNotNull();
-    assertProjectInfo(projectState.getProject(), p);
-    assertHead(newProjectName, "refs/heads/master");
-  }
-
-  @Test
-  public void testCreateProject() throws Exception {
-    final String newProjectName = "newProject";
+  public void testCreateProjectHttp() throws Exception {
+    String newProjectName = name("newProject");
     RestResponse r = adminSession.put("/projects/" + newProjectName);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
@@ -82,11 +69,49 @@
   }
 
   @Test
-  public void testCreateProjectWithGitSuffix() throws Exception {
-    final String newProjectName = "newProject";
-    RestResponse r = adminSession.put("/projects/" + newProjectName + ".git");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+  public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict()
+      throws Exception {
+    RestResponse r = adminSession.put("/projects/" + allProjects.get());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+  }
+
+  @Test
+  public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed()
+      throws Exception {
+    RestResponse r = adminSession.putWithHeader("/projects/" + allProjects.get(),
+        new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_PRECONDITION_FAILED);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testCreateProjectHttpWithUnreasonableName_BadRequest()
+      throws Exception {
+    RestResponse r = adminSession.put("/projects/" + Url.encode(name("invalid/../name")));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = name("otherName");
+    RestResponse r = adminSession.put("/projects/" + name("someName"), in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProjectHttpWithInvalidRefName_BadRequest()
+      throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.branches = Collections.singletonList(name("invalid ref name"));
+    RestResponse r = adminSession.put("/projects/" + name("newProject"), in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProject() throws Exception {
+    String newProjectName = name("newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName).get();
     assertThat(p.name).isEqualTo(newProjectName);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNotNull();
@@ -95,25 +120,28 @@
   }
 
   @Test
-  public void testCreateProjectWithNameMismatch_BadRequest() throws Exception {
-    ProjectInput in = new ProjectInput();
-    in.name = "otherName";
-    RestResponse r = adminSession.put("/projects/someName", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  public void testCreateProjectWithGitSuffix() throws Exception {
+    String newProjectName = name("newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName + ".git").get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
   }
 
   @Test
   public void testCreateProjectWithProperties() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.description = "Test description";
     in.submitType = SubmitType.CHERRY_PICK;
     in.useContributorAgreements = InheritableBoolean.TRUE;
     in.useSignedOffBy = InheritableBoolean.TRUE;
     in.useContentMerge = InheritableBoolean.TRUE;
     in.requireChangeId = InheritableBoolean.TRUE;
-    RestResponse r = adminSession.put("/projects/" + newProjectName, in);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    ProjectInfo p = gApi.projects().create(in).get();
     assertThat(p.name).isEqualTo(newProjectName);
     Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
     assertProjectInfo(project, p);
@@ -127,13 +155,16 @@
 
   @Test
   public void testCreateChildProject() throws Exception {
-    final String parentName = "parent";
-    RestResponse r = adminSession.put("/projects/" + parentName);
-    r.consume();
-    final String childName = "child";
+    String parentName = name("parent");
     ProjectInput in = new ProjectInput();
+    in.name = parentName;
+    gApi.projects().create(in);
+
+    String childName = name("child");
+    in = new ProjectInput();
+    in.name = childName;
     in.parent = parentName;
-    r = adminSession.put("/projects/" + childName, in);
+    gApi.projects().create(in);
     Project project = projectCache.get(new Project.NameKey(childName)).getProject();
     assertThat(project.getParentName()).isEqualTo(in.parent);
   }
@@ -142,21 +173,22 @@
   public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
       throws Exception {
     ProjectInput in = new ProjectInput();
+    in.name = name("newProjectName");
     in.parent = "non-existing-project";
-    RestResponse r = adminSession.put("/projects/child", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    assertCreateFails(in, UnprocessableEntityException.class);
   }
 
   @Test
   public void testCreateProjectWithOwner() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.owners = Lists.newArrayListWithCapacity(3);
     in.owners.add("Anonymous Users"); // by name
     in.owners.add(SystemGroupBackend.REGISTERED_USERS.get()); // by UUID
     in.owners.add(Integer.toString(groupCache.get(
         new AccountGroup.NameKey("Administrators")).getId().get())); // by ID
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
     expectedOwnerIds.add(SystemGroupBackend.ANONYMOUS_USERS);
@@ -169,55 +201,97 @@
   public void testCreateProjectWithNonExistingOwner_UnprocessableEntity()
       throws Exception {
     ProjectInput in = new ProjectInput();
+    in.name = name("newProjectName");
     in.owners = Collections.singletonList("non-existing-group");
-    RestResponse r = adminSession.put("/projects/newProject", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    assertCreateFails(in, UnprocessableEntityException.class);
   }
 
   @Test
   public void testCreatePermissionOnlyProject() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.permissionsOnly = true;
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     assertHead(newProjectName, RefNames.REFS_CONFIG);
   }
 
   @Test
   public void testCreateProjectWithEmptyCommit() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.createEmptyCommit = true;
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     assertEmptyCommit(newProjectName, "refs/heads/master");
   }
 
   @Test
   public void testCreateProjectWithBranches() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.createEmptyCommit = true;
     in.branches = Lists.newArrayListWithCapacity(3);
     in.branches.add("refs/heads/test");
     in.branches.add("refs/heads/master");
     in.branches.add("release"); // without 'refs/heads' prefix
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     assertHead(newProjectName, "refs/heads/test");
     assertEmptyCommit(newProjectName, "refs/heads/test", "refs/heads/master",
         "refs/heads/release");
   }
 
   @Test
+  public void testCreateProjectWithCapability() throws Exception {
+    allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS,
+        GlobalCapability.CREATE_PROJECT);
+    try {
+      setApiUser(user);
+      ProjectInput in = new ProjectInput();
+      in.name = name("newProject");
+      ProjectInfo p = gApi.projects().create(in).get();
+      assertThat(p.name).isEqualTo(in.name);
+    } finally {
+      removeGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS,
+          GlobalCapability.CREATE_PROJECT);
+    }
+  }
+
+  @Test
   public void testCreateProjectWithoutCapability_Forbidden() throws Exception {
-    RestResponse r = userSession.put("/projects/newProject");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    setApiUser(user);
+    ProjectInput in = new ProjectInput();
+    in.name = name("newProject");
+    assertCreateFails(in, AuthException.class);
   }
 
   @Test
   public void testCreateProjectWhenProjectAlreadyExists_Conflict()
       throws Exception {
-    RestResponse r = adminSession.put("/projects/All-Projects");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    ProjectInput in = new ProjectInput();
+    in.name = allProjects.get();
+    assertCreateFails(in, ResourceConflictException.class);
+  }
+
+  @Test
+  public void testCreateProjectWithCreateProjectCapabilityAndParentNotVisible()
+      throws Exception {
+    Project parent = projectCache.get(allProjects).getProject();
+    parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
+    allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS,
+        GlobalCapability.CREATE_PROJECT);
+    try {
+      setApiUser(user);
+      ProjectInput in = new ProjectInput();
+      in.name = name("newProject");
+      ProjectInfo p = gApi.projects().create(in).get();
+      assertThat(p.name).isEqualTo(in.name);
+    } finally {
+      parent.setState(com.google.gerrit.extensions.client.ProjectState.ACTIVE);
+      removeGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS,
+          GlobalCapability.CREATE_PROJECT);
+    }
   }
 
   private AccountGroup.UUID groupUuid(String groupName) {
@@ -226,13 +300,10 @@
 
   private void assertHead(String projectName, String expectedRef)
       throws RepositoryNotFoundException, IOException {
-    Repository repo =
-        repoManager.openRepository(new Project.NameKey(projectName));
-    try {
-      assertThat(repo.getRef(Constants.HEAD).getTarget().getName())
+    try (Repository repo =
+        repoManager.openRepository(new Project.NameKey(projectName))) {
+      assertThat(repo.exactRef(Constants.HEAD).getTarget().getName())
         .isEqualTo(expectedRef);
-    } finally {
-      repo.close();
     }
   }
 
@@ -243,7 +314,7 @@
         RevWalk rw = new RevWalk(repo);
         TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
       for (String ref : refs) {
-        RevCommit commit = rw.lookupCommit(repo.getRef(ref).getObjectId());
+        RevCommit commit = rw.lookupCommit(repo.exactRef(ref).getObjectId());
         rw.parseBody(commit);
         tw.addTree(commit.getTree());
         assertThat(tw.next()).isFalse();
@@ -251,4 +322,10 @@
       }
     }
   }
+
+  private void assertCreateFails(ProjectInput in,
+      Class<? extends RestApiException> errType) throws Exception {
+    exception.expect(errType);
+    gApi.projects().create(in);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 8be6c92..c9347cd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,21 +14,24 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
 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.Util.block;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.ProjectConfig;
 
-import org.apache.http.HttpStatus;
 import org.junit.Before;
 import org.junit.Test;
 
+@NoHttpd
 public class DeleteBranchIT extends AbstractDaemonTest {
 
   private Branch.NameKey branch;
@@ -36,62 +39,31 @@
   @Before
   public void setUp() throws Exception {
     branch = new Branch.NameKey(project, "test");
-    adminSession.put("/projects/" + project.get()
-        + "/branches/" + branch.getShortName()).consume();
+    branch().create(new BranchInput());
   }
 
   @Test
   public void deleteBranch_Forbidden() throws Exception {
-    RestResponse r =
-        userSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-    r.consume();
+    setApiUser(user);
+    assertDeleteForbidden();
   }
 
   @Test
   public void deleteBranchByAdmin() throws Exception {
-    RestResponse r =
-        adminSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
-    r.consume();
+    assertDeleteSucceeds();
   }
 
   @Test
   public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
-
-    RestResponse r =
-        userSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    r.consume();
-
-    r = userSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
-    r.consume();
+    setApiUser(user);
+    assertDeleteSucceeds();
   }
 
   @Test
   public void deleteBranchByAdminForcePushBlocked() throws Exception {
     blockForcePush();
-    RestResponse r =
-        adminSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
-    r.consume();
+    assertDeleteSucceeds();
   }
 
   @Test
@@ -99,20 +71,35 @@
       throws Exception {
     grantOwner();
     blockForcePush();
-    RestResponse r =
-        userSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-    r.consume();
+    setApiUser(user);
+    assertDeleteForbidden();
   }
 
   private void blockForcePush() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
-    saveProjectConfig(allProjects, cfg);
+    saveProjectConfig(project, cfg);
   }
 
   private void grantOwner() throws Exception {
     allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
+
+  private BranchApi branch() throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get());
+  }
+
+  private void assertDeleteSucceeds() throws Exception {
+    branch().delete();
+    exception.expect(ResourceNotFoundException.class);
+    branch().get();
+  }
+
+  private void assertDeleteForbidden() throws Exception {
+    exception.expect(AuthException.class);
+    branch().delete();
+    branch().get();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index 6aa3af6..d680541 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
@@ -39,8 +38,7 @@
 
   @Before
   public void setUp() throws Exception {
-    project2 = new Project.NameKey("p2");
-    createProject(sshSession, project2.get());
+    project2 = createProject("p2");
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index f49408e..ea3a5db 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -15,87 +15,66 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
+@NoHttpd
 public class GetChildProjectIT extends AbstractDaemonTest {
 
   @Test
   public void getNonExistingChildProject_NotFound() throws Exception {
-    assertThat(
-        GET("/projects/" + allProjects.get() + "/children/non-existing")
-            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    assertChildNotFound(allProjects, "non-existing");
   }
 
   @Test
   public void getNonChildProject_NotFound() throws Exception {
-    SshSession sshSession = new SshSession(server, admin);
-    Project.NameKey p1 = new Project.NameKey("p1");
-    createProject(sshSession, p1.get());
-    Project.NameKey p2 = new Project.NameKey("p2");
-    createProject(sshSession, p2.get());
-    sshSession.close();
-    assertThat(
-        GET("/projects/" + p1.get() + "/children/" + p2.get()).getStatusCode())
-        .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    Project.NameKey p1 = createProject("p1");
+    Project.NameKey p2 = createProject("p2");
+
+    assertChildNotFound(p1, p2.get());
   }
 
   @Test
   public void getChildProject() throws Exception {
-    SshSession sshSession = new SshSession(server, admin);
-    Project.NameKey child = new Project.NameKey("p1");
-    createProject(sshSession, child.get());
-    sshSession.close();
-    RestResponse r =
-        GET("/projects/" + allProjects.get() + "/children/" + child.get());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    ProjectInfo childInfo =
-        newGson().fromJson(r.getReader(), ProjectInfo.class);
+    Project.NameKey child = createProject("p1");
+    ProjectInfo childInfo = gApi.projects().name(allProjects.get())
+        .child(child.get()).get();
+
     assertProjectInfo(projectCache.get(child).getProject(), childInfo);
   }
 
   @Test
   public void getGrandChildProject_NotFound() throws Exception {
-    SshSession sshSession = new SshSession(server, admin);
-    Project.NameKey child = new Project.NameKey("p1");
-    createProject(sshSession, child.get());
-    Project.NameKey grandChild = new Project.NameKey("p1.1");
-    createProject(sshSession, grandChild.get(), child);
-    sshSession.close();
-    assertThat(
-        GET("/projects/" + allProjects.get() + "/children/" + grandChild.get())
-            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    Project.NameKey child = createProject("p1");
+    Project.NameKey grandChild = createProject("p1.1", child);
+
+    assertChildNotFound(allProjects, grandChild.get());
   }
 
   @Test
   public void getGrandChildProjectWithRecursiveFlag() throws Exception {
-    SshSession sshSession = new SshSession(server, admin);
-    Project.NameKey child = new Project.NameKey("p1");
-    createProject(sshSession, child.get());
-    Project.NameKey grandChild = new Project.NameKey("p1.1");
-    createProject(sshSession, grandChild.get(), child);
-    sshSession.close();
-    RestResponse r =
-        GET("/projects/" + allProjects.get() + "/children/" + grandChild.get()
-            + "?recursive");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    ProjectInfo grandChildInfo =
-        newGson().fromJson(r.getReader(), ProjectInfo.class);
-    assertProjectInfo(projectCache.get(grandChild).getProject(), grandChildInfo);
+    Project.NameKey child = createProject("p1");
+    Project.NameKey grandChild = createProject("p1.1", child);
+
+    ProjectInfo grandChildInfo = gApi.projects().name(allProjects.get())
+        .child(grandChild.get()).get(true);
+    assertProjectInfo(
+        projectCache.get(grandChild).getProject(), grandChildInfo);
   }
 
-  private RestResponse GET(String endpoint) throws IOException {
-    return adminSession.get(endpoint);
+  private void assertChildNotFound(Project.NameKey parent, String child)
+      throws Exception {
+    try {
+      gApi.projects().name(parent.get()).child(child);
+    } catch (ResourceNotFoundException e) {
+      assertThat(e.getMessage()).contains(child);
+    }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 4a20957..7044ad0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -40,13 +39,8 @@
 
   @Before
   public void setUp() throws Exception {
-    repo = new TestRepository<>(repoManager.openRepository(project));
-
-    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
-    for (AccessSection sec : pc.getAccessSections()) {
-      sec.removePermission(Permission.READ);
-    }
-    saveProjectConfig(allProjects, pc);
+    repo = GitUtil.newTestRepository(repoManager.openRepository(project));
+    blockRead(project, "refs/*");
   }
 
   @After
@@ -64,7 +58,7 @@
 
   @Test
   public void getMergedCommit_Found() throws Exception {
-    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+    unblockRead();
     RevCommit commit = repo.parseBody(repo.branch("master")
         .commit()
         .message("Create\n\nNew commit\n")
@@ -98,9 +92,9 @@
 
   @Test
   public void getOpenChange_Found() throws Exception {
-    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
-    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent())
-        .to(git, "refs/for/master");
+    unblockRead();
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/master");
     r.assertOkStatus();
 
     CommitInfo info = getCommit(r.getCommitId());
@@ -108,9 +102,9 @@
     assertThat(info.subject).isEqualTo("test commit");
     assertThat(info.message).isEqualTo(
         "test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-    assertThat(info.author.name).isEqualTo("admin");
+    assertThat(info.author.name).isEqualTo("Administrator");
     assertThat(info.author.email).isEqualTo("admin@example.com");
-    assertThat(info.committer.name).isEqualTo("admin");
+    assertThat(info.committer.name).isEqualTo("Administrator");
     assertThat(info.committer.email).isEqualTo("admin@example.com");
 
     CommitInfo parent = Iterables.getOnlyElement(info.parents);
@@ -123,12 +117,18 @@
 
   @Test
   public void getOpenChange_NotFound() throws Exception {
-    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent())
-        .to(git, "refs/for/master");
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/master");
     r.assertOkStatus();
     assertNotFound(r.getCommitId());
   }
 
+  private void unblockRead() throws Exception {
+    ProjectConfig pc = projectCache.checkedGet(project).getConfig();
+    pc.getAccessSection("refs/*").remove(new Permission(Permission.READ));
+    saveProjectConfig(project, pc);
+  }
+
   private void assertNotFound(ObjectId id) throws Exception {
     RestResponse r = userSession.get(
         "/projects/" + project.get() + "/commits/" + id.name());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
index 761e282..24b1770 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
@@ -17,35 +17,31 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
+@NoHttpd
 public class GetProjectIT extends AbstractDaemonTest {
 
   @Test
   public void getProject() throws Exception {
     String name = project.get();
-    RestResponse r = adminSession.get("/projects/" + name);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    ProjectInfo p = gApi.projects().name(name).get();
     assertThat(p.name).isEqualTo(name);
   }
 
   @Test
   public void getProjectWithGitSuffix() throws Exception {
     String name = project.get();
-    RestResponse r = adminSession.get("/projects/" + name + ".git");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    ProjectInfo p = gApi.projects().name(name).get();
     assertThat(p.name).isEqualTo(name);
   }
 
-  @Test
+  @Test(expected = ResourceNotFoundException.class)
   public void getProjectNotExisting() throws Exception {
-    RestResponse r = adminSession.get("/projects/does-not-exist");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    gApi.projects().name("does-not-exist").get();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 48f9ad89..0799d48 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -14,102 +14,78 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
+import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames;
 
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-
+@NoHttpd
 public class ListBranchesIT extends AbstractDaemonTest {
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws Exception {
-    assertThat(GET("/projects/non-existing/branches").getStatusCode())
-        .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("non-existing").branches().get();
   }
 
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
     blockRead(project, "refs/*");
-    assertThat(
-        userSession.get("/projects/" + project.get() + "/branches")
-            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    setApiUser(user);
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).branches().get();
   }
 
   @Test
+  @TestProjectInput(createEmptyCommit = false)
   public void listBranchesOfEmptyProject() throws Exception {
-    Project.NameKey emptyProject = new Project.NameKey("empty");
-    createProject(sshSession, emptyProject.get(), null, false);
-    RestResponse r = adminSession.get("/projects/" + emptyProject.get() + "/branches");
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("refs/meta/config",  null, false),
-        new BranchInfo[] {
-          new BranchInfo("HEAD", null, false)
-        });
-    assertBranches(expected, toBranchInfoList(r));
+    assertBranches(ImmutableList.of(
+          branch("HEAD", null, false),
+          branch("refs/meta/config",  null, false)),
+        list().get());
   }
 
   @Test
   public void listBranches() throws Exception {
-    pushTo("refs/heads/master");
-    String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    pushTo("refs/heads/dev");
-    String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    RestResponse r = adminSession.get("/projects/" + project.get() + "/branches");
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("refs/meta/config",  null, false),
-        new BranchInfo[] {
-          new BranchInfo("HEAD", "master", false),
-          new BranchInfo("refs/heads/master", masterCommit, false),
-          new BranchInfo("refs/heads/dev", devCommit, true)
-        });
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertBranches(expected, result);
-
-    // verify correct sorting
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/dev");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/master");
+    String master = pushTo("refs/heads/master").getCommit().name();
+    String dev = pushTo("refs/heads/dev").getCommit().name();
+    assertBranches(ImmutableList.of(
+          branch("HEAD", "master", false),
+          branch("refs/meta/config",  null, false),
+          branch("refs/heads/dev", dev, true),
+          branch("refs/heads/master", master, false)),
+        list().get());
   }
 
   @Test
   public void listBranchesSomeHidden() throws Exception {
     blockRead(project, "refs/heads/dev");
-    pushTo("refs/heads/master");
-    String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+    String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
-    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
+    setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("HEAD", "master", false),
-        new BranchInfo[] {
-          new BranchInfo("refs/heads/master", masterCommit, false),
-        });
-    assertBranches(expected, toBranchInfoList(r));
+    assertBranches(ImmutableList.of(
+          branch("HEAD", "master", false),
+          branch("refs/heads/master", master, false)),
+        list().get());
   }
 
   @Test
   public void listBranchesHeadHidden() throws Exception {
     blockRead(project, "refs/heads/master");
     pushTo("refs/heads/master");
-    pushTo("refs/heads/dev");
-    String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
+    String dev = pushTo("refs/heads/dev").getCommit().name();
+    setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    assertBranches(Collections.singletonList(new BranchInfo("refs/heads/dev",
-        devCommit, false)), toBranchInfoList(r));
+    assertBranches(ImmutableList.of(branch("refs/heads/dev", dev, false)),
+        list().get());
   }
 
   @Test
@@ -119,47 +95,40 @@
     pushTo("refs/heads/someBranch2");
     pushTo("refs/heads/someBranch3");
 
-    // using only limit
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches?n=4");
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertThat(result).hasSize(4);
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch1");
+    // Using only limit.
+    assertRefNames(ImmutableList.of(
+          "HEAD",
+          "refs/meta/config",
+          "refs/heads/master",
+          "refs/heads/someBranch1"),
+        list().withLimit(4).get());
 
-    // limit higher than total number of branches
-    r = adminSession.get("/projects/" + project.get() + "/branches?n=25");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(6);
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(4).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(5).ref).isEqualTo("refs/heads/someBranch3");
+    // Limit higher than total number of branches.
+    assertRefNames(ImmutableList.of(
+          "HEAD",
+          "refs/meta/config",
+          "refs/heads/master",
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withLimit(25).get());
 
-    // using skip only
-    r = adminSession.get("/projects/" + project.get() + "/branches?s=2");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(4);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch3");
+    // Using start only.
+    assertRefNames(ImmutableList.of(
+          "refs/heads/master",
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withStart(2).get());
 
-    // skip more branches than the number of available branches
-    r = adminSession.get("/projects/" + project.get() + "/branches?s=7");
-    result = toBranchInfoList(r);
-    assertThat(result).isEmpty();
+    // Skip more branches than the number of available branches.
+    assertRefNames(ImmutableList.<String> of(), list().withStart(7).get());
 
-    // using skip and limit
-    r = adminSession.get("/projects/" + project.get() + "/branches?s=2&n=2");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(2);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch1");
+    // Ssing start and limit.
+    assertRefNames(ImmutableList.of(
+          "refs/heads/master",
+          "refs/heads/someBranch1"),
+        list().withStart(2).withLimit(2).get());
   }
 
   @Test
@@ -169,38 +138,34 @@
     pushTo("refs/heads/someBranch2");
     pushTo("refs/heads/someBranch3");
 
-    //using substring
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches?m=some");
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertThat(result).hasSize(3);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch3");
+    // Using substring.
+    assertRefNames(ImmutableList.of(
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withSubstring("some").get());
 
-    r = adminSession.get("/projects/" + project.get() + "/branches?m=Branch");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(3);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch3");
+    assertRefNames(ImmutableList.of(
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withSubstring("Branch").get());
 
-    //using regex
-    r = adminSession.get("/projects/" + project.get() + "/branches?r=.*ast.*r");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
+    // Using regex.
+    assertRefNames(ImmutableList.of("refs/heads/master"),
+        list().withRegex(".*ast.*r").get());
   }
 
-  private RestResponse GET(String endpoint) throws IOException {
-    return adminSession.get(endpoint);
+  private ListRefsRequest<BranchInfo> list() throws Exception {
+    return gApi.projects().name(project.get()).branches();
   }
 
-  private static List<BranchInfo> toBranchInfoList(RestResponse r)
-      throws IOException {
-    List<BranchInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<BranchInfo>>() {}.getType());
-    return result;
+  private static BranchInfo branch(String ref, String revision,
+        boolean canDelete) {
+    BranchInfo info = new BranchInfo();
+    info.ref = ref;
+    info.revision = revision;
+    info.canDelete = canDelete ? true : null;
+    return info;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index 0d3b467..80ad493 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -15,84 +15,54 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-
+@NoHttpd
 public class ListChildProjectsIT extends AbstractDaemonTest {
 
   @Test
   public void listChildrenOfNonExistingProject_NotFound() throws Exception {
-    assertThat(GET("/projects/non-existing/children/").getStatusCode())
-        .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    try {
+      gApi.projects().name(name("non-existing")).child("children");
+    } catch (ResourceNotFoundException e) {
+      assertThat(e.getMessage()).contains("non-existing");
+    }
   }
 
   @Test
   public void listNoChildren() throws Exception {
-    RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    List<ProjectInfo> projectInfoList = toProjectInfoList(r);
-    // Project 'p' was already created in the base class
-    assertThat(projectInfoList).hasSize(2);
+    assertThatNameList(gApi.projects().name(project.get()).children())
+        .isEmpty();
   }
 
   @Test
   public void listChildren() throws Exception {
-    Project.NameKey existingProject = new Project.NameKey("p");
-    Project.NameKey child1 = new Project.NameKey("p1");
-    createProject(sshSession, child1.get());
-    Project.NameKey child2 = new Project.NameKey("p2");
-    createProject(sshSession, child2.get());
-    createProject(sshSession, "p1.1", child1);
+    Project.NameKey child1 = createProject("p1");
+    Project.NameKey child1_1 = createProject("p1.1", child1);
+    Project.NameKey child1_2 = createProject("p1.2", child1);
 
-    RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertProjects(
-        Arrays.asList(
-            new Project.NameKey("All-Users"),
-            existingProject, child1, child2),
-        toProjectInfoList(r));
+    assertThatNameList(gApi.projects().name(child1.get()).children())
+        .containsExactly(child1_1, child1_2).inOrder();
   }
 
   @Test
   public void listChildrenRecursively() throws Exception {
-    Project.NameKey child1 = new Project.NameKey("p1");
-    createProject(sshSession, child1.get());
-    createProject(sshSession, "p2");
-    Project.NameKey child1_1 = new Project.NameKey("p1.1");
-    createProject(sshSession, child1_1.get(), child1);
-    Project.NameKey child1_2 = new Project.NameKey("p1.2");
-    createProject(sshSession, child1_2.get(), child1);
-    Project.NameKey child1_1_1 = new Project.NameKey("p1.1.1");
-    createProject(sshSession, child1_1_1.get(), child1_1);
-    Project.NameKey child1_1_1_1 = new Project.NameKey("p1.1.1.1");
-    createProject(sshSession, child1_1_1_1.get(), child1_1_1);
+    Project.NameKey child1 = createProject("p1");
+    createProject("p2");
+    Project.NameKey child1_1 = createProject("p1.1", child1);
+    Project.NameKey child1_2 = createProject("p1.2", child1);
+    Project.NameKey child1_1_1 = createProject("p1.1.1", child1_1);
+    Project.NameKey child1_1_1_1 = createProject("p1.1.1.1", child1_1_1);
 
-    RestResponse r = GET("/projects/" + child1.get() + "/children/?recursive");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertProjects(Arrays.asList(child1_1, child1_2,
-        child1_1_1, child1_1_1_1), toProjectInfoList(r));
-  }
-
-  private static List<ProjectInfo> toProjectInfoList(RestResponse r)
-      throws IOException {
-    return newGson().fromJson(r.getReader(),
-        new TypeToken<List<ProjectInfo>>() {}.getType());
-  }
-
-  private RestResponse GET(String endpoint) throws IOException {
-    return adminSession.get(endpoint);
+    assertThatNameList(gApi.projects().name(child1.get()).children(true))
+        .containsExactly(child1_1, child1_1_1, child1_1_1_1, child1_2)
+        .inOrder();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 0f5bed1..1e51571 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -15,25 +15,31 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.Projects.ListRequest;
+import com.google.gerrit.extensions.api.projects.Projects.ListRequest.FilterType;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.Util;
 import com.google.inject.Inject;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
+@NoHttpd
 public class ListProjectsIT extends AbstractDaemonTest {
 
   @Inject
@@ -41,206 +47,174 @@
 
   @Test
   public void listProjects() throws Exception {
-    Project.NameKey someProject = new Project.NameKey("some-project");
-    createProject(sshSession, someProject.get());
+    Project.NameKey someProject = createProject("some-project");
+    assertThatNameList(filter(gApi.projects().list().get()))
+        .containsExactly(allProjects, allUsers, project, someProject).inOrder();
+  }
 
-    RestResponse r = GET("/projects/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertProjects(Arrays.asList(allUsers, someProject, project),
-        result.values());
+  @Test
+  public void listProjectsFiltersInvisibleProjects() throws Exception {
+    setApiUser(user);
+    assertThatNameList(gApi.projects().list().get()).contains(project);
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(project, cfg);
+
+    assertThatNameList(filter(gApi.projects().list().get()))
+        .doesNotContain(project);
   }
 
   @Test
   public void listProjectsWithBranch() throws Exception {
-    RestResponse r = GET("/projects/?b=master");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertThat(result.get(project.get())).isNotNull();
-    assertThat(result.get(project.get()).branches).isNotNull();
-    assertThat(result.get(project.get()).branches).hasSize(1);
-    assertThat(result.get(project.get()).branches.get("master")).isNotNull();
+    Map<String, ProjectInfo> result = gApi.projects().list()
+        .addShowBranch("master").getAsMap();
+    assertThat(result).containsKey(project.get());
+    ProjectInfo info = result.get(project.get());
+    assertThat(info.branches).isNotNull();
+    assertThat(info.branches).hasSize(1);
+    assertThat(info.branches.get("master")).isNotNull();
   }
 
   @Test
+  @TestProjectInput(description = "Description of some-project")
   public void listProjectWithDescription() throws Exception {
-    ProjectInput projectInput = new ProjectInput();
-    projectInput.name = "some-project";
-    projectInput.description = "Description of some-project";
-    gApi.projects().name(projectInput.name).create(projectInput);
-
     // description not be included in the results by default.
-    RestResponse r = GET("/projects/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertThat(result.get(projectInput.name)).isNotNull();
-    assertThat(result.get(projectInput.name).description).isNull();
+    Map<String, ProjectInfo> result = gApi.projects().list().getAsMap();
+    assertThat(result).containsKey(project.get());
+    assertThat(result.get(project.get()).description).isNull();
 
-    r = GET("/projects/?d");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    result = toProjectInfoMap(r);
-    assertThat(result.get(projectInput.name)).isNotNull();
-    assertThat(result.get(projectInput.name).description).isEqualTo(
-        projectInput.description);
+    result = gApi.projects().list().withDescription(true).getAsMap();
+    assertThat(result).containsKey(project.get());
+    assertThat(result.get(project.get()).description).isEqualTo(
+        "Description of some-project");
   }
 
   @Test
   public void listProjectsWithLimit() throws Exception {
     for (int i = 0; i < 5; i++) {
-      createProject(sshSession, new Project.NameKey("someProject" + i).get());
+      createProject("someProject" + i);
     }
 
-    RestResponse r = GET("/projects/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertThat(result).hasSize(7); // 5 plus 2 existing projects: p and
-                                   // All-Users
-
-    r = GET("/projects/?n=2");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    result = toProjectInfoMap(r);
-    assertThat(result).hasSize(2);
+    String p = name("");
+    // 5, plus p which was automatically created.
+    int n = 6;
+    for (int i = 1; i <= n + 2; i++) {
+      assertThatNameList(gApi.projects().list().withPrefix(p)
+              .withLimit(i).get())
+          .hasSize(Math.min(i, n));
+    }
   }
 
   @Test
   public void listProjectsWithPrefix() throws Exception {
-    Project.NameKey someProject = new Project.NameKey("some-project");
-    createProject(sshSession, someProject.get());
-    Project.NameKey someOtherProject =
-        new Project.NameKey("some-other-project");
-    createProject(sshSession, someOtherProject.get());
-    Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
-    createProject(sshSession, projectAwesome.get());
+    Project.NameKey someProject = createProject("some-project");
+    Project.NameKey someOtherProject = createProject("some-other-project");
+    createProject("project-awesome");
 
-    assertThat(GET("/projects/?p=some&r=.*").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
-    assertThat(GET("/projects/?p=some&m=some").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
-
-    RestResponse r = GET("/projects/?p=some");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertProjects(Arrays.asList(someProject, someOtherProject),
-        result.values());
+    String p = name("some");
+    assertBadRequest(gApi.projects().list().withPrefix(p).withRegex(".*"));
+    assertBadRequest(gApi.projects().list().withPrefix(p).withSubstring(p));
+    assertThatNameList(filter(gApi.projects().list().withPrefix(p).get()))
+        .containsExactly(someOtherProject, someProject).inOrder();
   }
 
   @Test
   public void listProjectsWithRegex() throws Exception {
-    Project.NameKey someProject = new Project.NameKey("some-project");
-    createProject(sshSession, someProject.get());
-    Project.NameKey someOtherProject =
-        new Project.NameKey("some-other-project");
-    createProject(sshSession, someOtherProject.get());
-    Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
-    createProject(sshSession, projectAwesome.get());
+    Project.NameKey someProject = createProject("some-project");
+    Project.NameKey someOtherProject = createProject("some-other-project");
+    Project.NameKey projectAwesome = createProject("project-awesome");
 
-    assertThat(GET("/projects/?r=[.*some").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
-    assertThat(GET("/projects/?r=.*&p=s").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
-    assertThat(GET("/projects/?r=.*&m=s").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
+    assertBadRequest(gApi.projects().list().withRegex("[.*"));
+    assertBadRequest(gApi.projects().list().withRegex(".*").withPrefix("p"));
+    assertBadRequest(gApi.projects().list().withRegex(".*").withSubstring("p"));
 
-    RestResponse r = GET("/projects/?r=.*some");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertProjects(Arrays.asList(projectAwesome), result.values());
-
-    r = GET("/projects/?r=some-project$");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    result = toProjectInfoMap(r);
-    assertProjects(Arrays.asList(someProject), result.values());
-
-    r = GET("/projects/?r=.*");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    result = toProjectInfoMap(r);
-    assertProjects(Arrays.asList(someProject, someOtherProject, projectAwesome,
-        project, allUsers), result.values());
+    assertThatNameList(filter(gApi.projects().list().withRegex(".*some").get()))
+        .containsExactly(projectAwesome);
+    String r = name("some-project$").replace(".", "\\.");
+    assertThatNameList(filter(gApi.projects().list().withRegex(r).get()))
+        .containsExactly(someProject);
+    assertThatNameList(filter(gApi.projects().list().withRegex(".*").get()))
+        .containsExactly(allProjects, allUsers, project, projectAwesome,
+            someOtherProject, someProject)
+        .inOrder();
   }
 
   @Test
-  public void listProjectsWithSkip() throws Exception {
+  public void listProjectsWithStart() throws Exception {
     for (int i = 0; i < 5; i++) {
-      createProject(sshSession, new Project.NameKey("someProject" + i).get());
+      createProject(new Project.NameKey("someProject" + i).get());
     }
 
-    RestResponse r = GET("/projects/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertThat(result).hasSize(7); // 5 plus 2 existing projects: p and
-                                   // All-Users
-
-    r = GET("/projects/?S=6");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    result = toProjectInfoMap(r);
-    assertThat(result).hasSize(1);
+    String p = name("");
+    List<ProjectInfo> all = gApi.projects().list().withPrefix(p).get();
+    // 5, plus p which was automatically created.
+    int n = 6;
+    assertThat(all).hasSize(n);
+    assertThatNameList(gApi.projects().list().withPrefix(p)
+            .withStart(n - 1).get())
+        .containsExactly(new Project.NameKey(Iterables.getLast(all).name));
   }
 
   @Test
   public void listProjectsWithSubstring() throws Exception {
-    Project.NameKey someProject = new Project.NameKey("some-project");
-    createProject(sshSession, someProject.get());
-    Project.NameKey someOtherProject =
-        new Project.NameKey("some-other-project");
-    createProject(sshSession, someOtherProject.get());
-    Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
-    createProject(sshSession, projectAwesome.get());
+    Project.NameKey someProject = createProject("some-project");
+    Project.NameKey someOtherProject = createProject("some-other-project");
+    Project.NameKey projectAwesome = createProject("project-awesome");
 
-    assertThat(GET("/projects/?m=some&r=.*").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
-    assertThat(GET("/projects/?m=some&p=some").getStatusCode()).isEqualTo(
-        HttpStatus.SC_BAD_REQUEST);
-
-    RestResponse r = GET("/projects/?m=some");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertProjects(
-        Arrays.asList(someProject, someOtherProject, projectAwesome),
-        result.values());
+    assertBadRequest(gApi.projects().list().withSubstring("some")
+        .withRegex(".*"));
+    assertBadRequest(gApi.projects().list().withSubstring("some")
+        .withPrefix("some"));
+    assertThatNameList(filter(gApi.projects().list().withSubstring("some")
+            .get()))
+        .containsExactly(projectAwesome, someOtherProject, someProject)
+        .inOrder();
   }
 
   @Test
   public void listProjectsWithTree() throws Exception {
-    Project.NameKey someParentProject =
-        new Project.NameKey("some-parent-project");
-    createProject(sshSession, someParentProject.get());
+    Project.NameKey someParentProject = createProject("some-parent-project");
     Project.NameKey someChildProject =
-        new Project.NameKey("some-child-project");
-    createProject(sshSession, someChildProject.get(), someParentProject);
+        createProject("some-child-project", someParentProject);
 
-    RestResponse r = GET("/projects/?tree");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertThat(result.get(someChildProject.get())).isNotNull();
-    assertThat(result.get(someChildProject.get()).parent).isEqualTo(
-        someParentProject.get());
+    Map<String, ProjectInfo> result = gApi.projects().list().withTree(true)
+        .getAsMap();
+    assertThat(result).containsKey(someChildProject.get());
+    assertThat(result.get(someChildProject.get()).parent)
+        .isEqualTo(someParentProject.get());
   }
 
   @Test
   public void listProjectWithType() throws Exception {
-    RestResponse r = GET("/projects/?type=PERMISSIONS");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Map<String, ProjectInfo> result = toProjectInfoMap(r);
+    Map<String, ProjectInfo> result = gApi.projects().list()
+        .withType(FilterType.PERMISSIONS).getAsMap();
     assertThat(result).hasSize(1);
-    assertThat(result.get(allProjects.get())).isNotNull();
+    assertThat(result).containsKey(allProjects.get());
 
-    r = GET("/projects/?type=ALL");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    result = toProjectInfoMap(r);
-    assertThat(result).hasSize(3);
-    assertProjects(Arrays.asList(allProjects, allUsers, project),
-        result.values());
+    assertThatNameList(filter(gApi.projects().list().withType(FilterType.ALL)
+            .get()))
+        .containsExactly(allProjects, allUsers, project).inOrder();
   }
 
-  private static Map<String, ProjectInfo> toProjectInfoMap(RestResponse r)
-      throws IOException {
-    Map<String, ProjectInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<Map<String, ProjectInfo>>() {}.getType());
-    return result;
+  private static void assertBadRequest(ListRequest req) throws Exception {
+    try {
+      req.get();
+    } catch (BadRequestException expected) {
+      // Expected.
+    }
   }
 
-  private RestResponse GET(String endpoint) throws IOException {
-    return adminSession.get(endpoint);
+  private Iterable<ProjectInfo> filter(Iterable<ProjectInfo> infos) {
+    final String prefix = name("");
+    return Iterables.filter(infos, new Predicate<ProjectInfo>() {
+      @Override
+      public boolean apply(ProjectInfo in) {
+        return in.name != null && (
+            in.name.equals(allProjects.get())
+            || in.name.equals(allUsers.get())
+            || in.name.startsWith(prefix));
+      }
+    });
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 95f46e8..db6df95 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -15,35 +15,43 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
-import com.google.common.base.Predicate;
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.truth.IterableSubject;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectState;
 
-import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
 public class ProjectAssert {
-
-  public static void assertProjects(Iterable<Project.NameKey> expected,
-      Collection<ProjectInfo> actual) {
-    for (final Project.NameKey p : expected) {
-      ProjectInfo info = Iterables.find(actual, new Predicate<ProjectInfo>() {
-        @Override
-        public boolean apply(ProjectInfo info) {
-          // 'name' is not set if returned in a map, use the id instead.
-          return new Project.NameKey(info.name != null ? info.name : Url
-              .decode(info.id)).equals(p);
-        }}, null);
-      assertThat(info).isNotNull();
-      actual.remove(info);
+  public static IterableSubject<
+        ? extends IterableSubject<
+            ?, Project.NameKey, Iterable<Project.NameKey>>,
+        Project.NameKey,
+        Iterable<Project.NameKey>>
+      assertThatNameList(Iterable<ProjectInfo> actualIt) {
+    List<ProjectInfo> actual = ImmutableList.copyOf(actualIt);
+    for (ProjectInfo info : actual) {
+      assertWithMessage("missing project name").that(info.name).isNotNull();
+      assertWithMessage("project name does not match id")
+          .that(Url.decode(info.id))
+          .isEqualTo(info.name);
     }
-    assertThat((Iterable<?>)actual).isEmpty();
+    return assertThat(Iterables.transform(actual,
+        new Function<ProjectInfo, Project.NameKey>() {
+          @Override
+          public Project.NameKey apply(ProjectInfo in) {
+            return new Project.NameKey(in.name);
+          }
+        }));
   }
 
   public static void assertProjectInfo(Project project, ProjectInfo info) {
@@ -67,6 +75,6 @@
     for (AccountGroup.UUID g : state.getOwners()) {
       assertThat(expectedOwners.remove(g)).isTrue();
     }
-    assertThat((Iterable<?>)expectedOwners).isEmpty();
+    assertThat(expectedOwners).isEmpty();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index 7e2af65..649534b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -15,16 +15,15 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectState;
 
-import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
@@ -32,8 +31,8 @@
 public class ProjectLevelConfigIT extends AbstractDaemonTest {
   @Before
   public void setUp() throws Exception {
-    fetch(git, RefNames.REFS_CONFIG + ":refs/heads/config");
-    checkout(git, "refs/heads/config");
+    fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
+    testRepo.reset("refs/heads/config");
   }
 
   @Test
@@ -43,9 +42,9 @@
     cfg.setString("s1", null, "k1", "v1");
     cfg.setString("s2", "ss", "k2", "v2");
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), "Create Project Level Config",
+        pushFactory.create(db, admin.getIdent(), testRepo, "Create Project Level Config",
             configName, cfg.toText());
-    push.to(git, RefNames.REFS_CONFIG);
+    push.to(RefNames.REFS_CONFIG);
 
     ProjectState state = projectCache.get(project);
     assertThat(state.getConfig(configName).get().toText()).isEqualTo(
@@ -68,23 +67,28 @@
     parentCfg.setString("s2", "ss", "k3", "parentValue3");
     parentCfg.setString("s2", "ss", "k4", "parentValue4");
 
-    Git parentGit =
-        cloneProject(sshSession.getUrl() + "/" + allProjects.get(), false);
-    fetch(parentGit, RefNames.REFS_CONFIG + ":refs/heads/config");
-    checkout(parentGit, "refs/heads/config");
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), "Create Project Level Config",
-            configName, parentCfg.toText());
-    push.to(parentGit, RefNames.REFS_CONFIG);
+    pushFactory.create(
+          db, admin.getIdent(), testRepo, "Create Project Level Config",
+          configName, parentCfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    Project.NameKey childProject = createProject("child", project);
+    TestRepository<?> childTestRepo = cloneProject(childProject);
+    fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
+    childTestRepo.reset("refs/heads/config");
 
     Config cfg = new Config();
     cfg.setString("s1", null, "k1", "childValue1");
     cfg.setString("s2", "ss", "k3", "childValue2");
-    push = pushFactory.create(db, admin.getIdent(), "Create Project Level Config",
-        configName, cfg.toText());
-    push.to(git, RefNames.REFS_CONFIG);
 
-    ProjectState state = projectCache.get(project);
+    pushFactory.create(
+          db, admin.getIdent(), childTestRepo, "Create Project Level Config",
+          configName, cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    ProjectState state = projectCache.get(childProject);
 
     Config expectedCfg = new Config();
     expectedCfg.setString("s1", null, "k1", "childValue1");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index af45d64..da8ebed 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
@@ -29,8 +28,7 @@
 public class SetParentIT extends AbstractDaemonTest {
   @Test
   public void setParent_Forbidden() throws Exception {
-    String parent = "parent";
-    createProject(sshSession, parent, null, true);
+    String parent = createProject("parent", null, true).get();
     RestResponse r =
         userSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
@@ -40,8 +38,7 @@
 
   @Test
   public void setParent() throws Exception {
-    String parent = "parent";
-    createProject(sshSession, parent, null, true);
+    String parent = createProject("parent", null, true).get();
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
@@ -86,15 +83,13 @@
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
 
-    String child = "child";
-    createProject(sshSession, child, project, true);
+    Project.NameKey child = createProject("child", project, true);
     r = adminSession.put("/projects/" + project.get() + "/parent",
-           newParentInput(child));
+           newParentInput(child.get()));
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
 
-    String grandchild = "grandchild";
-    createProject(sshSession, grandchild, new Project.NameKey(child), true);
+    String grandchild = createProject("grandchild", child, true).get();
     r = adminSession.put("/projects/" + project.get() + "/parent",
            newParentInput(grandchild));
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index eb129d4..b22d26b9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -16,13 +16,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.common.TagInfo;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.PushCommand;
@@ -35,14 +37,29 @@
 import java.util.List;
 
 public class TagsIT extends AbstractDaemonTest {
+  private static final List<String> testTags = ImmutableList.of(
+      "tag-A", "tag-B", "tag-C", "tag-D", "tag-E", "tag-F", "tag-G", "tag-H");
+
   @Test
-  public void listTagsOfNonExistingProject_NotFound() throws Exception {
+  public void listTagsOfNonExistingProject() throws Exception {
     assertThat(adminSession.get("/projects/non-existing/tags").getStatusCode())
         .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void listTagsOfNonVisibleProject_NotFound() throws Exception {
+  public void listTagsOfNonExistingProjectWithApi() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("does-not-exist").tags().get();
+  }
+
+  @Test
+  public void getTagOfNonExistingProjectWithApi() throws Exception {
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name("does-not-exist").tag("tag").get();
+  }
+
+  @Test
+  public void listTagsOfNonVisibleProject() throws Exception {
     blockRead(project, "refs/*");
     assertThat(
         userSession.get("/projects/" + project.get() + "/tags").getStatusCode())
@@ -50,33 +67,47 @@
   }
 
   @Test
+  public void listTagsOfNonVisibleProjectWithApi() throws Exception {
+    blockRead(project, "refs/*");
+    setApiUser(user);
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).tags().get();
+  }
+
+  @Test
+  public void getTagOfNonVisibleProjectWithApi() throws Exception {
+    blockRead(project, "refs/*");
+    exception.expect(ResourceNotFoundException.class);
+    gApi.projects().name(project.get()).tag("tag").get();
+  }
+
+  @Test
   public void listTags() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     grant(Permission.CREATE, project, "refs/tags/*");
     grant(Permission.PUSH, project, "refs/tags/*");
 
     PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
     push1.setTag(tag1);
-    PushOneCommit.Result r1 = push1.to(git, "refs/for/master%submit");
+    PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
     r1.assertOkStatus();
 
     PushOneCommit.AnnotatedTag tag2 =
         new PushOneCommit.AnnotatedTag("v2.0", "annotation", admin.getIdent());
-    PushOneCommit push2 = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo);
     push2.setTag(tag2);
-    PushOneCommit.Result r2 = push2.to(git, "refs/for/master%submit");
+    PushOneCommit.Result r2 = push2.to("refs/for/master%submit");
     r2.assertOkStatus();
 
     String tag3Ref = Constants.R_TAGS + "vLatest";
-    PushCommand pushCmd = git.push();
+    PushCommand pushCmd = testRepo.git().push();
     pushCmd.setRefSpecs(new RefSpec(tag2.name + ":" + tag3Ref));
     Iterable<PushResult> r = pushCmd.call();
     assertThat(Iterables.getOnlyElement(r).getRemoteUpdate(tag3Ref).getStatus())
         .isEqualTo(Status.OK);
 
-    List<TagInfo> result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    List<TagInfo> result = getTags().get();
     assertThat(result).hasSize(3);
 
     TagInfo t = result.get(0);
@@ -98,6 +129,61 @@
     assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
   }
 
+  private void assertTagList(FluentIterable<String> expected, List<TagInfo> actual)
+      throws Exception {
+    assertThat(actual).hasSize(expected.size());
+    for (int i = 0; i < expected.size(); i ++) {
+      assertThat(actual.get(i).ref).isEqualTo("refs/tags/" + expected.get(i));
+    }
+  }
+
+  @Test
+  public void listTagsWithoutOptions() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().get();
+    assertTagList(FluentIterable.from(testTags), result);
+  }
+
+  @Test
+  public void listTagsWithStartOption() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withStart(1).get();
+    assertTagList(FluentIterable.from(testTags).skip(1), result);
+  }
+
+  @Test
+  public void listTagsWithLimitOption() throws Exception {
+    createTags();
+    int limit = testTags.size() - 1;
+    List<TagInfo> result = getTags().withLimit(limit).get();
+    assertTagList(FluentIterable.from(testTags).limit(limit), result);
+  }
+
+  @Test
+  public void listTagsWithLimitAndStartOption() throws Exception {
+    createTags();
+    int limit = testTags.size() - 3;
+    List<TagInfo> result = getTags().withStart(1).withLimit(limit).get();
+    assertTagList(FluentIterable.from(testTags).skip(1).limit(limit), result);
+  }
+
+  @Test
+  public void listTagsWithRegexFilter() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withRegex("^tag-[C|D]$").get();
+    assertTagList(
+        FluentIterable.from(ImmutableList.of("tag-C", "tag-D")), result);
+  }
+
+  @Test
+  public void listTagsWithSubstringFilter() throws Exception {
+    createTags();
+    List<TagInfo> result = getTags().withSubstring("tag-").get();
+    assertTagList(FluentIterable.from(testTags), result);
+    result = getTags().withSubstring("ag-B").get();
+    assertTagList(FluentIterable.from(ImmutableList.of("tag-B")), result);
+  }
+
   @Test
   public void listTagsOfNonVisibleBranch() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
@@ -106,20 +192,19 @@
     grant(Permission.PUSH, project, "refs/tags/*");
 
     PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
     push1.setTag(tag1);
-    PushOneCommit.Result r1 = push1.to(git, "refs/for/master%submit");
+    PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
     r1.assertOkStatus();
 
     pushTo("refs/heads/hidden");
     PushOneCommit.Tag tag2 = new PushOneCommit.Tag("v2.0");
-    PushOneCommit push2 = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo);
     push2.setTag(tag2);
-    PushOneCommit.Result r2 = push2.to(git, "refs/for/hidden%submit");
+    PushOneCommit.Result r2 = push2.to("refs/for/hidden%submit");
     r2.assertOkStatus();
 
-    List<TagInfo> result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    List<TagInfo> result = getTags().get();
     assertThat(result).hasSize(2);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
@@ -127,8 +212,7 @@
     assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
 
     blockRead(project, "refs/heads/hidden");
-    result =
-        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    result = getTags().get();
     assertThat(result).hasSize(1);
     assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
@@ -141,23 +225,34 @@
     grant(Permission.PUSH, project, "refs/tags/*");
 
     PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
-    PushOneCommit push1 = pushFactory.create(db, admin.getIdent());
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo);
     push1.setTag(tag1);
-    PushOneCommit.Result r1 = push1.to(git, "refs/for/master%submit");
+    PushOneCommit.Result r1 = push1.to("refs/for/master%submit");
     r1.assertOkStatus();
 
-    RestResponse response =
-        adminSession.get("/projects/" + project.get() + "/tags/" + tag1.name);
-    TagInfo tagInfo =
-        newGson().fromJson(response.getReader(), TagInfo.class);
+    TagInfo tagInfo = getTag(tag1.name);
     assertThat(tagInfo.ref).isEqualTo("refs/tags/" + tag1.name);
     assertThat(tagInfo.revision).isEqualTo(r1.getCommitId().getName());
   }
 
-  private static List<TagInfo> toTagInfoList(RestResponse r) throws Exception {
-    List<TagInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<TagInfo>>() {}.getType());
-    return result;
+  private void createTags() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+    for (String tagname : testTags) {
+      PushOneCommit.Tag tag = new PushOneCommit.Tag(tagname);
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+      push.setTag(tag);
+      PushOneCommit.Result result = push.to("refs/for/master%submit");
+      result.assertOkStatus();
+    }
+  }
+
+  private ListRefsRequest<TagInfo> getTags() throws Exception {
+    return gApi.projects().name(project.get()).tags();
+  }
+
+  private TagInfo getTag(String ref) throws Exception {
+    return gApi.projects().name(project.get()).tag(ref).get();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
index fce853b..94e69da 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK
@@ -1,6 +1,7 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'server-change',
   srcs = glob(['*IT.java']),
   labels = ['server'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 541d1b8..b784f05 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -15,13 +15,20 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -32,29 +39,21 @@
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Revisions;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.testutil.FakeEmailSender;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.apache.http.HttpStatus;
-import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+@NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
   @Inject
   private Provider<ChangesCollection> changes;
 
@@ -64,7 +63,15 @@
   @Inject
   private Provider<PostReview> postReview;
 
-  private final Integer lines[] = {0, 1};
+  @Inject
+  private FakeEmailSender email;
+
+  private final Integer[] lines = {0, 1};
+
+  @Before
+  public void setUp() {
+    setApiUser(user);
+  }
 
   @Test
   public void createDraft() throws Exception {
@@ -72,8 +79,7 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          "file1", Side.REVISION, line, "comment 1");
+      DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
       addDraft(changeId, revId, comment);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       assertThat(result).hasSize(1);
@@ -87,14 +93,13 @@
     for (Integer line : lines) {
       String file = "file";
       String contents = "contents " + line;
-      PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
           "first subject", file, contents);
-      PushOneCommit.Result r = push.to(git, "refs/for/master");
+      PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       ReviewInput input = new ReviewInput();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          file, Side.REVISION, line, "comment 1");
+      CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
       input.comments = new HashMap<>();
       input.comments.put(comment.path, Lists.newArrayList(comment));
       revision(r).review(input);
@@ -111,8 +116,7 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          "file1", Side.REVISION, line, "comment 1");
+      DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
       addDraft(changeId, revId, comment);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
@@ -132,7 +136,7 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
+      DraftInput comment = newDraft(
           "file1", Side.REVISION, line, "comment 1");
       CommentInfo returned = addDraft(changeId, revId, comment);
       CommentInfo actual = getDraftComment(changeId, revId, returned.id);
@@ -146,9 +150,8 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          "file1", Side.REVISION, line, "comment 1");
-      CommentInfo returned = addDraft(changeId, revId, comment);
+      DraftInput draft = newDraft("file1", Side.REVISION, line, "comment 1");
+      CommentInfo returned = addDraft(changeId, revId, draft);
       deleteDraft(changeId, revId, returned.id);
       Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
       assertThat(drafts).isEmpty();
@@ -161,14 +164,13 @@
     for (Integer line : lines) {
       String file = "file";
       String contents = "contents " + line;
-      PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
           "first subject", file, contents);
-      PushOneCommit.Result r = push.to(git, "refs/for/master");
+      PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       ReviewInput input = new ReviewInput();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          file, Side.REVISION, line, "comment 1");
+      CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
       comment.updated = timestamp;
       input.comments = new HashMap<>();
       input.comments.put(comment.path, Lists.newArrayList(comment));
@@ -186,56 +188,256 @@
     }
   }
 
-  private CommentInfo addDraft(String changeId, String revId,
-      ReviewInput.CommentInput c) throws IOException {
-    RestResponse r = userSession.put(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts", c);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    return newGson().fromJson(r.getReader(), CommentInfo.class);
+  @Test
+  public void addDuplicateComments() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    String changeId = r1.getChangeId();
+    String revId = r1.getCommit().getName();
+    addComment(r1, "nit: trailing whitespace");
+    addComment(r1, "nit: trailing whitespace");
+    Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+    assertThat(result.get(FILE_NAME)).hasSize(2);
+    addComment(r1, "nit: trailing whitespace", true);
+    result = getPublishedComments(changeId, revId);
+    assertThat(result.get(FILE_NAME)).hasSize(2);
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content")
+        .to("refs/for/master");
+    changeId = r2.getChangeId();
+    revId = r2.getCommit().getName();
+    addComment(r2, "nit: trailing whitespace", true);
+    result = getPublishedComments(changeId, revId);
+    assertThat(result.get(FILE_NAME)).hasSize(1);
   }
 
-  private void updateDraft(String changeId, String revId,
-      ReviewInput.CommentInput c, String uuid) throws IOException {
-    RestResponse r = userSession.put(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid, c);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+  @Test
+  public void listChangeDrafts() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content",
+          r1.getChangeId())
+        .to("refs/for/master");
+
+
+    setApiUser(admin);
+    addDraft(r1.getChangeId(), r1.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
+
+    setApiUser(user);
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
+
+    setApiUser(admin);
+    Map<String, List<CommentInfo>> actual =
+        gApi.changes().id(r1.getChangeId()).drafts();
+    assertThat((Iterable<?>) actual.keySet()).containsExactly(FILE_NAME);
+    List<CommentInfo> comments = actual.get(FILE_NAME);
+    assertThat(comments).hasSize(2);
+
+    CommentInfo c1 = comments.get(0);
+    assertThat(c1.author).isNull();
+    assertThat(c1.patchSet).isEqualTo(1);
+    assertThat(c1.message).isEqualTo("nit: trailing whitespace");
+    assertThat(c1.side).isNull();
+    assertThat(c1.line).isEqualTo(1);
+
+    CommentInfo c2 = comments.get(1);
+    assertThat(c2.author).isNull();
+    assertThat(c2.patchSet).isEqualTo(2);
+    assertThat(c2.message).isEqualTo("typo: content");
+    assertThat(c2.side).isNull();
+    assertThat(c2.line).isEqualTo(1);
+  }
+
+  @Test
+  public void listChangeComments() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent",
+          r1.getChangeId())
+        .to("refs/for/master");
+
+    addComment(r1, "nit: trailing whitespace");
+    addComment(r2, "typo: content");
+
+    Map<String, List<CommentInfo>> actual = gApi.changes()
+        .id(r2.getChangeId())
+        .comments();
+    assertThat(actual.keySet()).containsExactly(FILE_NAME);
+
+    List<CommentInfo> comments = actual.get(FILE_NAME);
+    assertThat(comments).hasSize(2);
+
+    CommentInfo c1 = comments.get(0);
+    assertThat(c1.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c1.patchSet).isEqualTo(1);
+    assertThat(c1.message).isEqualTo("nit: trailing whitespace");
+    assertThat(c1.side).isNull();
+    assertThat(c1.line).isEqualTo(1);
+
+    CommentInfo c2 = comments.get(1);
+    assertThat(c2.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c2.patchSet).isEqualTo(2);
+    assertThat(c2.message).isEqualTo("typo: content");
+    assertThat(c2.side).isNull();
+    assertThat(c2.line).isEqualTo(1);
+  }
+
+  @Test
+  public void publishCommentsAllRevisions() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new\ncntent\n",
+          r1.getChangeId())
+        .to("refs/for/master");
+
+    addDraft(r1.getChangeId(), r1.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "join lines"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 2, "typo: content"));
+
+    PushOneCommit.Result other = createChange();
+    // Drafts on other changes aren't returned.
+    addDraft(other.getChangeId(), other.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
+
+    setApiUser(admin);
+    // Drafts by other users aren't returned.
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 2, "oops"));
+    setApiUser(user);
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+    reviewInput.message = "comments";
+    gApi.changes()
+       .id(r2.getChangeId())
+       .current()
+       .review(reviewInput);
+
+    assertThat(gApi.changes()
+          .id(r1.getChangeId())
+          .revision(r1.getCommit().name())
+          .drafts())
+        .isEmpty();
+    Map<String, List<CommentInfo>> ps1Map = gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .comments();
+    assertThat(ps1Map.keySet()).containsExactly(FILE_NAME);
+    List<CommentInfo> ps1List = ps1Map.get(FILE_NAME);
+    assertThat(ps1List).hasSize(1);
+    assertThat(ps1List.get(0).message).isEqualTo("nit: trailing whitespace");
+
+    assertThat(gApi.changes()
+          .id(r2.getChangeId())
+          .revision(r2.getCommit().name())
+          .drafts())
+        .isEmpty();
+    Map<String, List<CommentInfo>> ps2Map = gApi.changes()
+        .id(r2.getChangeId())
+        .revision(r2.getCommit().name())
+        .comments();
+    assertThat(ps2Map.keySet()).containsExactly(FILE_NAME);
+    List<CommentInfo> ps2List = ps2Map.get(FILE_NAME);
+    assertThat(ps2List).hasSize(2);
+    assertThat(ps2List.get(0).message).isEqualTo("join lines");
+    assertThat(ps2List.get(1).message).isEqualTo("typo: content");
+
+    ImmutableList<Message> messages =
+        email.getMessages(r2.getChangeId(), "comment");
+    assertThat(messages).hasSize(1);
+    String url = canonicalWebUrl.get();
+    int c = r1.getChange().getId().get();
+    assertThat(messages.get(0).body()).contains(
+        "\n"
+        + "Patch Set 2:\n"
+        + "\n"
+        + "(3 comments)\n"
+        + "\n"
+        + "comments\n"
+        + "\n"
+        + url + "#/c/" + c + "/1/a.txt\n"
+        + "File a.txt:\n"
+        + "\n"
+        + "PS1, Line 1: ew\n"
+        + "nit: trailing whitespace\n"
+        + "\n"
+        + "\n"
+        + url + "#/c/" + c + "/2/a.txt\n"
+        + "File a.txt:\n"
+        + "\n"
+        + "PS2, Line 1: ew\n"
+        + "join lines\n"
+        + "\n"
+        + "\n"
+        + "PS2, Line 2: nten\n"
+        + "typo: content\n"
+        + "\n"
+        + "\n"
+        + "-- \n");
+  }
+
+  private void addComment(PushOneCommit.Result r, String message)
+      throws Exception {
+    addComment(r, message, false);
+  }
+
+  private void addComment(PushOneCommit.Result r, String message,
+      boolean omitDuplicateComments) throws Exception {
+    CommentInput c = new CommentInput();
+    c.line = 1;
+    c.message = message;
+    c.path = FILE_NAME;
+    ReviewInput in = new ReviewInput();
+    in.comments = ImmutableMap.<String, List<CommentInput>> of(
+        FILE_NAME, ImmutableList.of(c));
+    in.omitDuplicateComments = omitDuplicateComments;
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(in);
+  }
+
+  private CommentInfo addDraft(String changeId, String revId, DraftInput in)
+      throws Exception {
+    return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
+  }
+
+  private void updateDraft(String changeId, String revId, DraftInput in,
+      String uuid) throws Exception {
+    gApi.changes().id(changeId).revision(revId).draft(uuid).update(in);
   }
 
   private void deleteDraft(String changeId, String revId, String uuid)
-      throws IOException {
-    RestResponse r = userSession.delete(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+      throws Exception {
+    gApi.changes().id(changeId).revision(revId).draft(uuid).delete();
   }
 
   private Map<String, List<CommentInfo>> getPublishedComments(String changeId,
-      String revId) throws IOException {
-    RestResponse r = userSession.get(
-        "/changes/" + changeId + "/revisions/" + revId + "/comments/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Type mapType = new TypeToken<Map<String, List<CommentInfo>>>() {}.getType();
-    return newGson().fromJson(r.getReader(), mapType);
+      String revId) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).comments();
   }
 
   private Map<String, List<CommentInfo>> getDraftComments(String changeId,
-      String revId) throws IOException {
-    RestResponse r = userSession.get(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Type mapType = new TypeToken<Map<String, List<CommentInfo>>>() {}.getType();
-    return newGson().fromJson(r.getReader(), mapType);
+      String revId) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).drafts();
   }
 
   private CommentInfo getDraftComment(String changeId, String revId,
-      String uuid) throws IOException {
-    RestResponse r = userSession.get(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(), CommentInfo.class);
+      String uuid) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
   }
 
-  private static void assertCommentInfo(ReviewInput.CommentInput expected,
-      CommentInfo actual) {
+  private static void assertCommentInfo(Comment expected, CommentInfo actual) {
     assertThat(actual.line).isEqualTo(expected.line);
     assertThat(actual.message).isEqualTo(expected.message);
     assertThat(actual.inReplyTo).isEqualTo(expected.inReplyTo);
@@ -258,21 +460,32 @@
     }
   }
 
-  private ReviewInput.CommentInput newCommentInfo(String path,
-      Side side, int line, String message) {
-    ReviewInput.CommentInput input = new ReviewInput.CommentInput();
-    input.path = path;
-    input.side = side;
-    input.line = line != 0 ? line : null;
-    input.message = message;
+  private static CommentInput newComment(String path, Side side, int line,
+      String message) {
+    CommentInput c = new CommentInput();
+    return populate(c, path, side, line, message);
+  }
+
+  private DraftInput newDraft(String path, Side side, int line,
+      String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, side, line, message);
+  }
+
+  private static <C extends Comment> C populate(C c, String path, Side side,
+      int line, String message) {
+    c.path = path;
+    c.side = side;
+    c.line = line != 0 ? line : null;
+    c.message = message;
     if (line != 0) {
       Comment.Range range = new Comment.Range();
-      range.startLine = 1;
+      range.startLine = line;
       range.startCharacter = 1;
-      range.endLine = 1;
+      range.endLine = line;
       range.endCharacter = 5;
-      input.range = range;
+      c.range = range;
     }
-    return input;
+    return c;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
new file mode 100644
index 0000000..272cdcd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -0,0 +1,627 @@
+// Copyright (C) 2014 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.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testutil.TestChanges.newChange;
+import static com.google.gerrit.testutil.TestChanges.newPatchSet;
+import static java.util.Collections.singleton;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.ConsistencyChecker;
+import com.google.gerrit.testutil.TestChanges;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+@NoHttpd
+public class ConsistencyCheckerIT extends AbstractDaemonTest {
+  @Inject
+  private Provider<ConsistencyChecker> checkerProvider;
+
+  private RevCommit tip;
+  private Account.Id adminId;
+  private ConsistencyChecker checker;
+
+  @Before
+  public void setUp() throws Exception {
+    // Ignore client clone of project; repurpose as server-side TestRepository.
+    testRepo = new TestRepository<>(
+        (InMemoryRepository) repoManager.openRepository(project));
+    tip = testRepo.getRevWalk().parseCommit(
+        testRepo.getRepository().exactRef("HEAD").getObjectId());
+    adminId = admin.getId();
+    checker = checkerProvider.get();
+  }
+
+  @Test
+  public void validNewChange() throws Exception {
+    Change c = insertChange();
+    insertPatchSet(c);
+    incrementPatchSet(c);
+    insertPatchSet(c);
+    assertProblems(c);
+  }
+
+  @Test
+  public void validMergedChange() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    insertPatchSet(c);
+    incrementPatchSet(c);
+
+    incrementPatchSet(c);
+    RevCommit commit2 = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, adminId);
+    db.patchSets().insert(singleton(ps2));
+
+    testRepo.branch(c.getDest().get()).update(commit2);
+    assertProblems(c);
+  }
+
+  @Test
+  public void missingOwner() throws Exception {
+    Change c = newChange(project, new Account.Id(2));
+    db.changes().insert(singleton(c));
+    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c, "Missing change owner: 2");
+  }
+
+  @Test
+  public void missingRepo() throws Exception {
+    Change c = newChange(new Project.NameKey("otherproject"), adminId);
+    db.changes().insert(singleton(c));
+    insertMissingPatchSet(c, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertProblems(c, "Destination repository not found: otherproject");
+  }
+
+  @Test
+  public void invalidRevision() throws Exception {
+    Change c = insertChange();
+
+    db.patchSets().insert(singleton(newPatchSet(c.currentPatchSetId(),
+            "fooooooooooooooooooooooooooooooooooooooo", adminId)));
+    incrementPatchSet(c);
+    insertPatchSet(c);
+
+    assertProblems(c,
+        "Invalid revision on patch set 1:"
+        + " fooooooooooooooooooooooooooooooooooooooo");
+  }
+
+  // No test for ref existing but object missing; InMemoryRepository won't let
+  // us do such a thing.
+
+  @Test
+  public void patchSetObjectAndRefMissing() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), adminId);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c,
+        "Ref missing: " + ps.getId().toRefName(),
+        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+  }
+
+  @Test
+  public void patchSetObjectAndRefMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), adminId);
+    db.patchSets().insert(singleton(ps));
+
+    String refName = ps.getId().toRefName();
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + refName);
+    assertThat(p.status).isNull();
+  }
+
+  @Test
+  public void patchSetRefMissing() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = insertPatchSet(c);
+    String refName = ps.getId().toRefName();
+    testRepo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    deleteRef(refName);
+
+    assertProblems(c, "Ref missing: " + refName);
+  }
+
+  @Test
+  public void patchSetRefMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = insertPatchSet(c);
+    String refName = ps.getId().toRefName();
+    testRepo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    deleteRef(refName);
+
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + refName);
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Repaired patch set ref");
+
+    assertThat(testRepo.getRepository().exactRef(refName).getObjectId().name())
+        .isEqualTo(ps.getRevision().get());
+  }
+
+  @Test
+  public void patchSetObjectAndRefMissingWithDeletingPatchSet()
+      throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(2);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
+    assertThat(p.status).isNull();
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps2.getId())).isNull();
+  }
+
+  @Test
+  public void patchSetMultipleObjectsMissingWithDeletingPatchSets()
+      throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    incrementPatchSet(c);
+    PatchSet ps3 = insertPatchSet(c);
+
+    incrementPatchSet(c);
+    PatchSet ps4 = insertMissingPatchSet(c,
+        "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(4);
+
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps4.getId().toRefName());
+    assertThat(p.status).isNull();
+
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 4: c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    p = problems.get(2);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
+    assertThat(p.status).isNull();
+
+    p = problems.get(3);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(3);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps2.getId())).isNull();
+    assertThat(db.patchSets().get(ps3.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps4.getId())).isNull();
+  }
+
+  @Test
+  public void onlyPatchSetObjectMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(2);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps1.getId().toRefName());
+    assertThat(p.status).isNull();
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIX_FAILED);
+    assertThat(p.outcome)
+        .isEqualTo("Cannot delete patch set; no patch sets would remain");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+  }
+
+  @Test
+  public void currentPatchSetMissing() throws Exception {
+    Change c = insertChange();
+    assertProblems(c, "Current patch set 1 not found");
+  }
+
+  @Test
+  public void duplicatePatchSetRevisions() throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+    String rev = ps1.getRevision().get();
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c, rev);
+    updatePatchSetRef(ps2);
+
+    assertProblems(c,
+        "Multiple patch sets pointing to " + rev + ": [1, 2]");
+  }
+
+  @Test
+  public void missingDestRef() throws Exception {
+    String ref = "refs/heads/master";
+    // Detach head so we're allowed to delete ref.
+    testRepo.reset(testRepo.getRepository().exactRef(ref).getObjectId());
+    RefUpdate ru = testRepo.getRepository().updateRef(ref);
+    ru.setForceUpdate(true);
+    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    Change c = insertChange();
+    RevCommit commit = testRepo.commit().create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
+    updatePatchSetRef(ps);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c, "Destination ref not found (may be new branch): " + ref);
+  }
+
+  @Test
+  public void mergedChangeIsNotMerged() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    PatchSet ps = insertPatchSet(c);
+    String rev = ps.getRevision().get();
+
+    assertProblems(c,
+        "Patch set 1 (" + rev + ") is not merged into destination ref"
+        + " refs/heads/master (" + tip.name()
+        + "), but change status is MERGED");
+  }
+
+  @Test
+  public void newChangeIsMerged() throws Exception {
+    Change c = insertChange();
+    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
+    db.patchSets().insert(singleton(ps));
+    testRepo.branch(c.getDest().get()).update(commit);
+
+    assertProblems(c,
+        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
+        + " refs/heads/master (" + commit.name()
+        + "), but change status is NEW");
+  }
+
+  @Test
+  public void newChangeIsMergedWithFix() throws Exception {
+    Change c = insertChange();
+    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
+    db.patchSets().insert(singleton(ps));
+    testRepo.branch(c.getDest().get()).update(commit);
+
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
+        + " refs/heads/master (" + commit.name()
+        + "), but change status is NEW");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Marked change as merged");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
+    assertProblems(c);
+  }
+
+  @Test
+  public void expectedMergedCommitIsLatestPatchSet() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    PatchSet ps = insertPatchSet(c);
+    RevCommit commit = parseCommit(ps);
+    testRepo.update(c.getDest().get(), commit);
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = commit.name();
+    assertThat(checker.check(c, fix).problems()).isEmpty();
+  }
+
+  @Test
+  public void expectedMergedCommitNotMergedIntoDestination() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    PatchSet ps = insertPatchSet(c);
+    RevCommit commit = parseCommit(ps);
+    testRepo.update(c.getDest().get(), commit);
+
+    FixInput fix = new FixInput();
+    RevCommit other =
+        testRepo.commit().message(commit.getFullMessage()).create();
+    fix.expectMergedAs = other.name();
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Expected merged commit " + other.name()
+        + " is not merged into destination ref refs/heads/master"
+        + " (" + commit.name() + ")");
+  }
+
+  @Test
+  public void createNewPatchSetForExpectedMergeCommitWithNoChangeId()
+      throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    RevCommit parent =
+        testRepo.branch(c.getDest().get()).commit().message("parent").create();
+    PatchSet ps = insertPatchSet(c);
+    RevCommit commit = parseCommit(ps);
+
+    RevCommit mergedAs = testRepo.commit().parent(parent)
+        .message(commit.getShortMessage()).create();
+    testRepo.getRevWalk().parseBody(mergedAs);
+    assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)).isEmpty();
+    testRepo.update(c.getDest().get(), mergedAs);
+
+    assertProblems(c, "Patch set 1 (" + commit.name() + ") is not merged into"
+        + " destination ref refs/heads/master (" + mergedAs.name()
+        + "), but change status is MERGED");
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = mergedAs.name();
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "No patch set found for merged commit " + mergedAs.name());
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Inserted as patch set 2");
+
+    c = db.changes().get(c.getId());
+    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
+    assertThat(c.currentPatchSetId()).isEqualTo(psId2);
+    assertThat(db.patchSets().get(psId2).getRevision().get())
+        .isEqualTo(mergedAs.name());
+
+    assertProblems(c);
+  }
+
+  @Test
+  public void createNewPatchSetForExpectedMergeCommitWithChangeId()
+      throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    RevCommit parent =
+        testRepo.branch(c.getDest().get()).commit().message("parent").create();
+    PatchSet ps = insertPatchSet(c);
+    RevCommit commit = parseCommit(ps);
+
+    RevCommit mergedAs = testRepo.commit().parent(parent)
+        .message(commit.getShortMessage() + "\n"
+            + "\n"
+            + "Change-Id: " + c.getKey().get() + "\n").create();
+    testRepo.getRevWalk().parseBody(mergedAs);
+    assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID))
+        .containsExactly(c.getKey().get());
+    testRepo.update(c.getDest().get(), mergedAs);
+
+    assertProblems(c, "Patch set 1 (" + commit.name() + ") is not merged into"
+        + " destination ref refs/heads/master (" + mergedAs.name()
+        + "), but change status is MERGED");
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = mergedAs.name();
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "No patch set found for merged commit " + mergedAs.name());
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Inserted as patch set 2");
+
+    c = db.changes().get(c.getId());
+    PatchSet.Id psId2 = new PatchSet.Id(c.getId(), 2);
+    assertThat(c.currentPatchSetId()).isEqualTo(psId2);
+    assertThat(db.patchSets().get(psId2).getRevision().get())
+        .isEqualTo(mergedAs.name());
+
+    assertProblems(c);
+  }
+
+  @Test
+  public void expectedMergedCommitIsOldPatchSetOfSameChange()
+      throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    PatchSet ps1 = insertPatchSet(c);
+    String rev1 = ps1.getRevision().get();
+    incrementPatchSet(c);
+    PatchSet ps2 = insertPatchSet(c);
+    testRepo.branch(c.getDest().get()).update(parseCommit(ps1));
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = rev1;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Expected merged commit " + rev1 + " corresponds to patch set "
+        + ps1.getId() + ", which is not the current patch set " + ps2.getId());
+  }
+
+  @Test
+  public void expectedMergedCommitWithMismatchedChangeId() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    RevCommit parent =
+        testRepo.branch(c.getDest().get()).commit().message("parent").create();
+    PatchSet ps = insertPatchSet(c);
+    RevCommit commit = parseCommit(ps);
+
+    String badId = "I0000000000000000000000000000000000000000";
+    RevCommit mergedAs = testRepo.commit().parent(parent)
+        .message(commit.getShortMessage() + "\n"
+            + "\n"
+            + "Change-Id: " + badId + "\n")
+        .create();
+    testRepo.getRevWalk().parseBody(mergedAs);
+    testRepo.update(c.getDest().get(), mergedAs);
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = mergedAs.name();
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Expected merged commit " + mergedAs.name() + " has Change-Id: "
+        + badId + ", but expected " + c.getKey().get());
+  }
+
+  @Test
+  public void expectedMergedCommitMatchesMultiplePatchSets()
+      throws Exception {
+    Change c1 = insertChange();
+    c1.setStatus(Change.Status.MERGED);
+    insertPatchSet(c1);
+
+    RevCommit commit = testRepo.branch(c1.getDest().get()).commit().create();
+    Change c2 = insertChange();
+    PatchSet ps2 = newPatchSet(c2.currentPatchSetId(), commit, adminId);
+    updatePatchSetRef(ps2);
+    db.patchSets().insert(singleton(ps2));
+
+    Change c3 = insertChange();
+    PatchSet ps3 = newPatchSet(c3.currentPatchSetId(), commit, adminId);
+    updatePatchSetRef(ps3);
+    db.patchSets().insert(singleton(ps3));
+
+    FixInput fix = new FixInput();
+    fix.expectMergedAs = commit.name();
+    List<ProblemInfo> problems = checker.check(c1, fix).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Multiple patch sets for expected merged commit " + commit.name()
+        + ": [" + ps2 + ", " + ps3 + "]");
+  }
+
+  private Change insertChange() throws Exception {
+    Change c = newChange(project, adminId);
+    db.changes().insert(singleton(c));
+    return c;
+  }
+
+  private void incrementPatchSet(Change c) throws Exception {
+    TestChanges.incrementPatchSet(c);
+    db.changes().upsert(singleton(c));
+  }
+
+  private PatchSet insertPatchSet(Change c) throws Exception {
+    db.changes().upsert(singleton(c));
+    RevCommit commit = testRepo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).message("Change " + c.getId().get()).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, adminId);
+    updatePatchSetRef(ps);
+    db.patchSets().insert(singleton(ps));
+    return ps;
+  }
+
+  private PatchSet insertMissingPatchSet(Change c, String id) throws Exception {
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString(id), adminId);
+    db.patchSets().insert(singleton(ps));
+    return ps;
+  }
+
+  private void updatePatchSetRef(PatchSet ps) throws Exception {
+    testRepo.update(ps.getId().toRefName(),
+        ObjectId.fromString(ps.getRevision().get()));
+  }
+
+  private void deleteRef(String refName) throws Exception {
+    RefUpdate ru = testRepo.getRepository().updateRef(refName, true);
+    ru.setForceUpdate(true);
+    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+  }
+
+  private RevCommit parseCommit(PatchSet ps) throws Exception {
+    RevCommit commit = testRepo.getRevWalk()
+        .parseCommit(ObjectId.fromString(ps.getRevision().get()));
+    testRepo.getRevWalk().parseBody(commit);
+    return commit;
+  }
+
+  private void assertProblems(Change c, String... expected) {
+    assertThat(Lists.transform(checker.check(c).problems(),
+          new Function<ProblemInfo, String>() {
+            @Override
+            public String apply(ProblemInfo in) {
+              checkArgument(in.status == null,
+                  "Status is not null: " + in.message);
+              return in.message;
+            }
+          })).containsExactly((Object[]) expected);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 6cd39ab..c59a3eb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -15,16 +15,14 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.add;
-import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
@@ -35,7 +33,8 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -48,142 +47,550 @@
   @Inject
   private ChangeEditModifier editModifier;
 
+  @Inject
+  private ChangeData.Factory changeDataFactory;
+
   @Test
   public void getRelatedNoResult() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    PatchSet.Id ps = push.to(git, "refs/for/master").getPatchSetId();
-    List<ChangeAndCommit> related = getRelated(ps);
-    assertThat(related).isEmpty();
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    assertRelated(push.to("refs/for/master").getPatchSetId());
   }
 
   @Test
   public void getRelatedLinear() throws Exception {
-    add(git, "a.txt", "1");
-    Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
-    add(git, "b.txt", "2");
-    Commit c2 = createCommit(git, admin.getIdent(), "subject: 2");
-    pushHead(git, "refs/for/master", false);
+    // 1,1---2,1
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
-    for (Commit c : ImmutableList.of(c2, c1)) {
-      List<ChangeAndCommit> related = getRelated(getPatchSetId(c));
-      assertThat(related).hasSize(2);
-      assertThat(related.get(0).changeId)
-          .named("related to " + c.getChangeId()).isEqualTo(c2.getChangeId());
-      assertThat(related.get(1).changeId)
-          .named("related to " + c.getChangeId()).isEqualTo(c1.getChangeId());
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
     }
   }
 
   @Test
   public void getRelatedReorder() throws Exception {
+    // 1,1---2,1
+    //
+    // 2,2---1,2
+
     // Create two commits and push.
-    add(git, "a.txt", "1");
-    Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
-    add(git, "b.txt", "2");
-    Commit c2 = createCommit(git, admin.getIdent(), "subject: 2");
-    pushHead(git, "refs/for/master", false);
-    PatchSet.Id c1ps1 = getPatchSetId(c1);
-    PatchSet.Id c2ps1 = getPatchSetId(c2);
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
     // Swap the order of commits and push again.
-    git.reset().setMode(ResetType.HARD).setRef("HEAD^^").call();
-    git.cherryPick().include(c2.getCommit()).include(c1.getCommit()).call();
-    pushHead(git, "refs/for/master", false);
-    PatchSet.Id c1ps2 = getPatchSetId(c1);
-    PatchSet.Id c2ps2 = getPatchSetId(c2);
+    testRepo.reset("HEAD~2");
+    RevCommit c2_2 = testRepo.cherryPick(c2_1);
+    RevCommit c1_2 = testRepo.cherryPick(c1_1);
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_1);
 
-    for (PatchSet.Id ps : ImmutableList.of(c2ps2, c1ps2)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(2);
-      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
-          c1.getChangeId());
-      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
-          c2.getChangeId());
+    for (PatchSet.Id ps : ImmutableList.of(ps2_2, ps1_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps1_2, c1_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2));
     }
 
-    for (PatchSet.Id ps : ImmutableList.of(c2ps1, c1ps1)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(2);
-      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
-          c2.getChangeId());
-      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
-          c1.getChangeId());
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
     }
   }
 
   @Test
-  public void getRelatedReorderAndExtend() throws Exception {
+  public void getRelatedAmendParentChange() throws Exception {
+    // 1,1---2,1
+    //
+    // 1,2
+
     // Create two commits and push.
-    add(git, "a.txt", "1");
-    Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
-    add(git, "b.txt", "2");
-    Commit c2 = createCommit(git, admin.getIdent(), "subject: 2");
-    pushHead(git, "refs/for/master", false);
-    PatchSet.Id c1ps1 = getPatchSetId(c1);
-    PatchSet.Id c2ps1 = getPatchSetId(c2);
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
-    // Swap the order of commits, create a new commit on top, and push again.
-    git.reset().setMode(ResetType.HARD).setRef("HEAD^^").call();
-    git.cherryPick().include(c2.getCommit()).include(c1.getCommit()).call();
-    add(git, "c.txt", "3");
-    Commit c3 = createCommit(git, admin.getIdent(), "subject: 3");
-    pushHead(git, "refs/for/master", false);
-    PatchSet.Id c1ps2 = getPatchSetId(c1);
-    PatchSet.Id c2ps2 = getPatchSetId(c2);
-    PatchSet.Id c3ps1 = getPatchSetId(c3);
+    // Amend parent change and push.
+    testRepo.reset("HEAD~1");
+    RevCommit c1_2 = amendBuilder()
+        .add("c.txt", "2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
 
-
-    for (PatchSet.Id ps : ImmutableList.of(c3ps1, c2ps2, c1ps2)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(3);
-      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
-          c3.getChangeId());
-      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
-          c1.getChangeId());
-      assertThat(related.get(2).changeId).named("related to " + ps).isEqualTo(
-          c2.getChangeId());
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 2));
     }
 
-    for (PatchSet.Id ps : ImmutableList.of(c2ps1, c1ps1)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(3);
-      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
-          c3.getChangeId());
-      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
-          c2.getChangeId());
-      assertThat(related.get(2).changeId).named("related to " + ps).isEqualTo(
-          c1.getChangeId());
+    assertRelated(ps1_2,
+        changeAndCommit(ps2_1, c2_1, 1),
+        changeAndCommit(ps1_2, c1_2, 2));
+  }
+
+  @Test
+  public void getRelatedReorderAndExtend() throws Exception {
+    // 1,1---2,1
+    //
+    // 2,2---1,2---3,1
+
+    // Create two commits and push.
+    ObjectId initial = repo().exactRef("HEAD").getObjectId();
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+
+    // Swap the order of commits, create a new commit on top, and push again.
+    testRepo.reset(initial);
+    RevCommit c2_2 = testRepo.cherryPick(c2_1);
+    RevCommit c1_2 = testRepo.cherryPick(c1_1);
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "3")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps3_1, ps2_2, ps1_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps1_2, c1_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2));
+    }
+
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedReworkSeries() throws Exception {
+    // 1,1---2,1---3,1
+    //
+    // 1,2---2,2---3,2
+
+    // Create three commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    // Amend all changes change and push.
+    testRepo.reset(c1_1);
+    RevCommit c1_2 = amendBuilder()
+        .add("a.txt", "2")
+        .create();
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message(parseBody(c2_1).getFullMessage())
+        .create();
+    RevCommit c3_2 = commitBuilder()
+        .add("b.txt", "3")
+        .message(parseBody(c3_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_2);
+    PatchSet.Id ps3_2 = getPatchSetId(c3_2);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 2),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_2, ps2_2, ps3_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_2, c3_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2),
+          changeAndCommit(ps1_2, c1_2, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedReworkThenExtendInTheMiddleOfSeries() throws Exception {
+    // 1,1---2,1---3,1
+    //
+    // 1,2---2,2---3,2
+    //   \---4,1
+
+    // Create three commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    // Amend all changes change and push.
+    testRepo.reset(c1_1);
+    RevCommit c1_2 = amendBuilder()
+        .add("a.txt", "2")
+        .create();
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message(parseBody(c2_1).getFullMessage())
+        .create();
+    RevCommit c3_2 = commitBuilder()
+        .add("b.txt", "3")
+        .message(parseBody(c3_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_2);
+    PatchSet.Id ps3_2 = getPatchSetId(c3_2);
+
+    // Add one more commit 4,1 based on 1,2.
+    testRepo.reset(c1_2);
+    RevCommit c4_1 = commitBuilder()
+        .add("d.txt", "4")
+        .message("subject: 4")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps4_1 = getPatchSetId(c4_1);
+
+    // 1,1 is related indirectly to 4,1.
+    assertRelated(ps1_1,
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps3_1, c3_1, 2),
+        changeAndCommit(ps2_1, c2_1, 2),
+        changeAndCommit(ps1_1, c1_1, 2));
+
+    // 2,1 and 3,1 don't include 4,1 since we don't walk forward after walking
+    // backward.
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 2),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    // 1,2 is related directly to 4,1, and the 2-3 parallel branch stays intact.
+    assertRelated(ps1_2,
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps3_2, c3_2, 2),
+        changeAndCommit(ps2_2, c2_2, 2),
+        changeAndCommit(ps1_2, c1_2, 2));
+
+    // 4,1 is only related to 1,2, since we don't walk forward after walking
+    // backward.
+    assertRelated(ps4_1,
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps1_2, c1_2, 2));
+
+    // 2,2 and 3,2 don't include 4,1 since we don't walk forward after walking
+    // backward.
+    for (PatchSet.Id ps : ImmutableList.of(ps2_2, ps3_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_2, c3_2, 2),
+          changeAndCommit(ps2_2, c2_2, 2),
+          changeAndCommit(ps1_2, c1_2, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedCrissCrossDependency() throws Exception {
+    // 1,1---2,1---3,2
+    //
+    // 1,2---2,2---3,1
+
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+
+    // Amend both changes change and push.
+    testRepo.reset(c1_1);
+    RevCommit c1_2 = amendBuilder()
+        .add("a.txt", "2")
+        .create();
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message(parseBody(c2_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_2);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_2);
+
+    // PS 3,1 depends on 2,2.
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "1")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    // PS 3,2 depends on 2,1.
+    testRepo.reset(c2_1);
+    RevCommit c3_2 = commitBuilder()
+        .add("c.txt", "2")
+        .message(parseBody(c3_1).getFullMessage())
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps3_2 = getPatchSetId(c3_2);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_2)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_2, c3_2, 2),
+          changeAndCommit(ps2_1, c2_1, 2),
+          changeAndCommit(ps1_1, c1_1, 2));
+    }
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_2, ps2_2, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 2),
+          changeAndCommit(ps2_2, c2_2, 2),
+          changeAndCommit(ps1_2, c1_2, 2));
+    }
+  }
+
+  @Test
+  public void getRelatedParallelDescendentBranches() throws Exception {
+    // 1,1---2,1---3,1
+    //   \---4,1---5,1
+    //    \--6,1---7,1
+
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "3")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    testRepo.reset(c1_1);
+    RevCommit c4_1 = commitBuilder()
+        .add("d.txt", "4")
+        .message("subject: 4")
+        .create();
+    RevCommit c5_1 = commitBuilder()
+        .add("e.txt", "5")
+        .message("subject: 5")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps4_1 = getPatchSetId(c4_1);
+    PatchSet.Id ps5_1 = getPatchSetId(c5_1);
+
+    testRepo.reset(c1_1);
+    RevCommit c6_1 = commitBuilder()
+        .add("f.txt", "6")
+        .message("subject: 6")
+        .create();
+    RevCommit c7_1 = commitBuilder()
+        .add("g.txt", "7")
+        .message("subject: 7")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps6_1 = getPatchSetId(c6_1);
+    PatchSet.Id ps7_1 = getPatchSetId(c7_1);
+
+    // All changes are related to 1,1, keeping each of the parallel branches
+    // intact.
+    assertRelated(ps1_1,
+        changeAndCommit(ps7_1, c7_1, 1),
+        changeAndCommit(ps6_1, c6_1, 1),
+        changeAndCommit(ps5_1, c5_1, 1),
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps3_1, c3_1, 1),
+        changeAndCommit(ps2_1, c2_1, 1),
+        changeAndCommit(ps1_1, c1_1, 1));
+
+    // The 2-3 branch is only related back to 1, not the other branches.
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+
+    // The 4-5 branch is only related back to 1, not the other branches.
+    for (PatchSet.Id ps : ImmutableList.of(ps4_1, ps5_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps5_1, c5_1, 1),
+          changeAndCommit(ps4_1, c4_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+
+    // The 6-7 branch is only related back to 1, not the other branches.
+    for (PatchSet.Id ps : ImmutableList.of(ps6_1, ps7_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps7_1, c7_1, 1),
+          changeAndCommit(ps6_1, c6_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
     }
   }
 
   @Test
   public void getRelatedEdit() throws Exception {
-    add(git, "a.txt", "1");
-    Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
-    add(git, "b.txt", "2");
-    Commit c2 = createCommit(git, admin.getIdent(), "subject: 2");
-    add(git, "b.txt", "3");
-    Commit c3 = createCommit(git, admin.getIdent(), "subject: 3");
-    pushHead(git, "refs/for/master", false);
+    // 1,1---2,1---3,1
+    //   \---2,E---/
 
-    Change ch2 = getChange(c2).change();
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "3")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+
+    Change ch2 = getChange(c2_1).change();
     editModifier.createEdit(ch2, getPatchSet(ch2));
     editModifier.modifyFile(editUtil.byChange(ch2).get(), "a.txt",
         RestSession.newRawInput(new byte[] {'a'}));
-    String editRev = editUtil.byChange(ch2).get().getRevision().get();
+    ObjectId editRev =
+        ObjectId.fromString(editUtil.byChange(ch2).get().getRevision().get());
 
-    List<ChangeAndCommit> related = getRelated(ch2.getId(), 0);
-    assertThat(related).hasSize(3);
-    assertThat(related.get(0).changeId).named("related to " + c2.getChangeId())
-        .isEqualTo(c3.getChangeId());
-    assertThat(related.get(1).changeId).named("related to " + c2.getChangeId())
-        .isEqualTo(c2.getChangeId());
-    assertThat(related.get(1)._revisionNumber.intValue()).named(
-        "has edit revision number").isEqualTo(0);
-    assertThat(related.get(1).commit.commit).named(
-        "has edit revision " + editRev).isEqualTo(editRev);
-    assertThat(related.get(2).changeId).named("related to " + c2.getChangeId())
-        .isEqualTo(c1.getChangeId());
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps2_edit = new PatchSet.Id(ch2.getId(), 0);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+
+    assertRelated(ps2_edit,
+        changeAndCommit(ps3_1, c3_1, 1),
+        changeAndCommit(new PatchSet.Id(ch2.getId(), 0), editRev, 1),
+        changeAndCommit(ps1_1, c1_1, 1));
+  }
+
+  @Test
+  public void pushNewPatchSetWhenParentHasNullGroup() throws Exception {
+    // 1,1---2,1
+    //   \---2,2
+
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id psId1_1 = getPatchSetId(c1_1);
+    PatchSet.Id psId2_1 = getPatchSetId(c2_1);
+
+    for (PatchSet.Id psId : ImmutableList.of(psId1_1, psId2_1)) {
+      assertRelated(psId,
+          changeAndCommit(psId2_1, c2_1, 1),
+          changeAndCommit(psId1_1, c1_1, 1));
+    }
+
+    // Pretend PS1,1 was pushed before the groups field was added.
+    PatchSet ps1_1 = db.patchSets().get(psId1_1);
+    ps1_1.setGroups(null);
+    db.patchSets().update(ImmutableList.of(ps1_1));
+    indexer.index(changeDataFactory.create(db, psId1_1.getParentKey()));
+
+    if (!cfg.getBoolean("change", null, "getRelatedByAncestors", false)) {
+      // PS1,1 has no groups, so disappeared from related changes.
+      assertRelated(psId2_1);
+    }
+
+    RevCommit c2_2 = testRepo.amend(c2_1)
+        .add("c.txt", "2")
+        .create();
+    testRepo.reset(c2_2);
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id psId2_2 = getPatchSetId(c2_2);
+
+    // Push updated the group for PS1,1, so it shows up in related changes even
+    // though a new patch set was not pushed.
+    assertRelated(psId2_2,
+        changeAndCommit(psId2_2, c2_2, 2),
+        changeAndCommit(psId1_1, c1_1, 1));
   }
 
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
@@ -198,7 +605,12 @@
         RelatedInfo.class).changes;
   }
 
-  private PatchSet.Id getPatchSetId(Commit c) throws OrmException {
+  private RevCommit parseBody(RevCommit c) throws IOException {
+    testRepo.getRevWalk().parseBody(c);
+    return c;
+  }
+
+  private PatchSet.Id getPatchSetId(ObjectId c) throws OrmException {
     return getChange(c).change().currentPatchSetId();
   }
 
@@ -206,8 +618,39 @@
     return db.patchSets().get(c.currentPatchSetId());
   }
 
-  private ChangeData getChange(Commit c) throws OrmException {
-    return Iterables.getOnlyElement(
-        queryProvider.get().byKeyPrefix(c.getChangeId()));
+  private ChangeData getChange(ObjectId c) throws OrmException {
+    return Iterables.getOnlyElement(queryProvider.get().byCommit(c));
+  }
+
+  private static ChangeAndCommit changeAndCommit(
+      PatchSet.Id psId, ObjectId commitId, int currentRevisionNum) {
+    ChangeAndCommit result = new ChangeAndCommit();
+    result._changeNumber = psId.getParentKey().get();
+    result.commit = new CommitInfo();
+    result.commit.commit = commitId.name();
+    result._revisionNumber = psId.get();
+    result._currentRevisionNumber = currentRevisionNum;
+    result.status = "NEW";
+    return result;
+  }
+
+  private void assertRelated(PatchSet.Id psId, ChangeAndCommit... expected)
+      throws Exception {
+    List<ChangeAndCommit> actual = getRelated(psId);
+    assertThat(actual).named("related to " + psId).hasSize(expected.length);
+    for (int i = 0; i < actual.size(); i++) {
+      String name = "index " + i + " related to " + psId;
+      ChangeAndCommit a = actual.get(i);
+      ChangeAndCommit e = expected[i];
+      assertThat(a._changeNumber).named("change ID of " + name)
+          .isEqualTo(e._changeNumber);
+      // Don't bother checking changeId; assume _changeNumber is sufficient.
+      assertThat(a._revisionNumber).named("revision of " + name)
+          .isEqualTo(e._revisionNumber);
+      assertThat(a.commit.commit).named("commit of " + name)
+          .isEqualTo(e.commit.commit);
+      assertThat(a._currentRevisionNumber).named("current revision of " + name)
+          .isEqualTo(e._currentRevisionNumber);
+    }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index 848eeb1..c4a07f2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -15,17 +15,13 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.add;
-import static com.google.gerrit.acceptance.GitUtil.amendCommit;
-import static com.google.gerrit.acceptance.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static com.google.gerrit.acceptance.GitUtil.rm;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -34,8 +30,8 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 import java.util.List;
@@ -55,28 +51,35 @@
 
   @Test
   public void listPatchesAgainstBase() throws Exception {
-    add(git, FILE_D, "4");
-    createCommit(git, admin.getIdent(), SUBJECT_1);
-    pushHead(git, "refs/heads/master", false);
+    commitBuilder()
+        .add(FILE_D, "4")
+        .message(SUBJECT_1)
+        .create();
+    pushHead(testRepo, "refs/heads/master", false);
 
     // Change 1, 1 (+FILE_A, -FILE_D)
-    add(git, FILE_A, "1");
-    rm(git, FILE_D);
-    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
-    pushHead(git, "refs/for/master", false);
+    RevCommit c = commitBuilder()
+        .add(FILE_A, "1")
+        .rm(FILE_D)
+        .message(SUBJECT_2)
+        .insertChangeId()
+        .create();
+    String id = getChangeId(testRepo, c).get();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Compare Change 1,1 with Base (+FILE_A, -FILE_D)
-    List<PatchListEntry> entries = getCurrentPatches(c.getChangeId());
+    List<PatchListEntry> entries = getCurrentPatches(id);
     assertThat(entries).hasSize(3);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
     assertDeleted(FILE_D, entries.get(2));
 
     // Change 1,2 (+FILE_A, +FILE_B, -FILE_D)
-    add(git, FILE_B, "2");
-    c = amendCommit(git, admin.getIdent(), SUBJECT_2, c.getChangeId());
-    pushHead(git, "refs/for/master", false);
-    entries = getCurrentPatches(c.getChangeId());
+    c = amendBuilder()
+        .add(FILE_B, "2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    entries = getCurrentPatches(id);
 
     // Compare Change 1,2 with Base (+FILE_A, +FILE_B, -FILE_D)
     assertThat(entries).hasSize(4);
@@ -88,33 +91,40 @@
 
   @Test
   public void listPatchesAgainstBaseWithRebase() throws Exception {
-    add(git, FILE_D, "4");
-    createCommit(git, admin.getIdent(), SUBJECT_1);
-    pushHead(git, "refs/heads/master", false);
+    commitBuilder()
+        .add(FILE_D, "4")
+        .message(SUBJECT_1)
+        .create();
+    pushHead(testRepo, "refs/heads/master", false);
 
     // Change 1,1 (+FILE_A, -FILE_D)
-    add(git, FILE_A, "1");
-    rm(git, FILE_D);
-    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
-    pushHead(git, "refs/for/master", false);
-    List<PatchListEntry> entries = getCurrentPatches(c.getChangeId());
+    RevCommit c = commitBuilder()
+        .add(FILE_A, "1")
+        .rm(FILE_D)
+        .message(SUBJECT_2)
+        .create();
+    String id = getChangeId(testRepo, c).get();
+    pushHead(testRepo, "refs/for/master", false);
+    List<PatchListEntry> entries = getCurrentPatches(id);
     assertThat(entries).hasSize(3);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
     assertDeleted(FILE_D, entries.get(2));
 
     // Change 2,1 (+FILE_B)
-    git.reset().setMode(ResetType.HARD).setRef("HEAD~1").call();
-    add(git, FILE_B, "2");
-    createCommit(git, admin.getIdent(), SUBJECT_3);
-    pushHead(git, "refs/for/master", false);
+    testRepo.reset("HEAD~1");
+    commitBuilder()
+        .add(FILE_B, "2")
+        .message(SUBJECT_3)
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Change 1,2 (+FILE_A, -FILE_D))
-    git.cherryPick().include(c.getCommit()).call();
-    pushHead(git, "refs/for/master", false);
+    testRepo.cherryPick(c);
+    pushHead(testRepo, "refs/for/master", false);
 
     // Compare Change 1,2 with Base (+FILE_A, -FILE_D))
-    entries = getCurrentPatches(c.getChangeId());
+    entries = getCurrentPatches(id);
     assertThat(entries).hasSize(3);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
@@ -123,24 +133,27 @@
 
   @Test
   public void listPatchesAgainstOtherPatchSet() throws Exception {
-    add(git, FILE_D, "4");
-    createCommit(git, admin.getIdent(), SUBJECT_1);
-    pushHead(git, "refs/heads/master", false);
+    commitBuilder()
+        .add(FILE_D, "4")
+        .message(SUBJECT_1)
+        .create();
+    pushHead(testRepo, "refs/heads/master", false);
 
     // Change 1,1 (+FILE_A, +FILE_C, -FILE_D)
-    add(git, FILE_A, "1");
-    add(git, FILE_C, "3");
-    rm(git, FILE_D);
-    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
-    pushHead(git, "refs/for/master", false);
-    ObjectId a = getCurrentRevisionId(c.getChangeId());
+    RevCommit a = commitBuilder()
+        .add(FILE_A, "1")
+        .add(FILE_C, "3")
+        .rm(FILE_D)
+        .message(SUBJECT_2)
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Change 1,2 (+FILE_A, +FILE_B, -FILE_D)
-    add(git, FILE_B, "2");
-    rm(git, FILE_C);
-    c = amendCommit(git, admin.getIdent(), SUBJECT_2, c.getChangeId());
-    pushHead(git, "refs/for/master", false);
-    ObjectId b = getCurrentRevisionId(c.getChangeId());
+    RevCommit b = amendBuilder()
+        .add(FILE_B, "2")
+        .rm(FILE_C)
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Compare Change 1,1 with Change 1,2 (+FILE_B, -FILE_C)
     List<PatchListEntry>  entries = getPatches(a, b);
@@ -159,29 +172,34 @@
 
   @Test
   public void listPatchesAgainstOtherPatchSetWithRebase() throws Exception {
-    add(git, FILE_D, "4");
-    createCommit(git, admin.getIdent(), SUBJECT_1);
-    pushHead(git, "refs/heads/master", false);
+    commitBuilder()
+        .add(FILE_D, "4")
+        .message(SUBJECT_1)
+        .create();
+    pushHead(testRepo, "refs/heads/master", false);
 
     // Change 1,1 (+FILE_A, -FILE_D)
-    add(git, FILE_A, "1");
-    rm(git, FILE_D);
-    Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
-    pushHead(git, "refs/for/master", false);
-    ObjectId a = getCurrentRevisionId(c.getChangeId());
+    RevCommit a = commitBuilder()
+        .add(FILE_A, "1")
+        .rm(FILE_D)
+        .message(SUBJECT_2)
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Change 2,1 (+FILE_B)
-    git.reset().setMode(ResetType.HARD).setRef("HEAD~1").call();
-    add(git, FILE_B, "2");
-    createCommit(git, admin.getIdent(), SUBJECT_3);
-    pushHead(git, "refs/for/master", false);
+    testRepo.reset("HEAD~1");
+    commitBuilder()
+        .add(FILE_B, "2")
+        .message(SUBJECT_3)
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Change 1,2 (+FILE_A, +FILE_C, -FILE_D)
-    git.cherryPick().include(c.getCommit()).call();
-    add(git, FILE_C, "2");
-    c = amendCommit(git, admin.getIdent(), SUBJECT_2, c.getChangeId());
-    pushHead(git, "refs/for/master", false);
-    ObjectId b = getCurrentRevisionId(c.getChangeId());
+    testRepo.cherryPick(a);
+    RevCommit b = amendBuilder()
+        .add(FILE_C, "2")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
 
     // Compare Change 1,1 with Change 1,2 (+FILE_C)
     List<PatchListEntry>  entries = getPatches(a, b);
@@ -218,16 +236,19 @@
 
   private List<PatchListEntry> getCurrentPatches(String changeId)
       throws PatchListNotAvailableException, RestApiException {
-    return patchListCache.get(getKey(null, getCurrentRevisionId(changeId))).getPatches();
+    return patchListCache
+        .get(getKey(null, getCurrentRevisionId(changeId)), project)
+        .getPatches();
   }
 
   private List<PatchListEntry> getPatches(ObjectId revisionIdA, ObjectId revisionIdB)
       throws PatchListNotAvailableException {
-    return patchListCache.get(getKey(revisionIdA, revisionIdB)).getPatches();
+    return patchListCache.get(getKey(revisionIdA, revisionIdB), project)
+        .getPatches();
   }
 
   private PatchListKey getKey(ObjectId revisionIdA, ObjectId revisionIdB) {
-    return new PatchListKey(project, revisionIdA, revisionIdB, Whitespace.IGNORE_NONE);
+    return new PatchListKey(revisionIdA, revisionIdB, Whitespace.IGNORE_NONE);
   }
 
   private ObjectId getCurrentRevisionId(String changeId) throws RestApiException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
new file mode 100644
index 0000000..4cbf068
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -0,0 +1,219 @@
+// Copyright (C) 2015 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmittedTogetherIT extends AbstractDaemonTest {
+
+  @Test
+  public void returnsAncestors() throws Exception {
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_1);
+    pushHead(testRepo, "refs/for/master", false);
+
+    assertSubmittedTogether(id1);
+    assertSubmittedTogether(id2, id2, id1);
+  }
+
+  @Test
+  public void respectsWholeTopicAndAncestors() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    // Create two independent commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+
+    testRepo.reset(initialHead);
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_1);
+    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertSubmittedTogether(id1, id2, id1);
+      assertSubmittedTogether(id2, id2, id1);
+    } else {
+      assertSubmittedTogether(id1);
+      assertSubmittedTogether(id2);
+    }
+  }
+
+  @Test
+  public void testTopicChaining() throws Exception {
+    RevCommit initialHead = getRemoteHead();
+    // Create two independent commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+
+    testRepo.reset(initialHead);
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_1);
+    pushHead(testRepo, "refs/for/master/" + name("connectingTopic"), false);
+
+    RevCommit c3_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id3 = getChangeId(c3_1);
+    pushHead(testRepo, "refs/for/master/" + name("unrelated-topic"), false);
+
+    if (isSubmitWholeTopicEnabled()) {
+      assertSubmittedTogether(id1, id2, id1);
+      assertSubmittedTogether(id2, id2, id1);
+      assertSubmittedTogether(id3, id3, id2, id1);
+    } else {
+      assertSubmittedTogether(id1);
+      assertSubmittedTogether(id2);
+      assertSubmittedTogether(id3, id3, id2);
+    }
+  }
+
+  @Test
+  public void testNewBranchTwoChangesTogether() throws Exception {
+    Project.NameKey p1 = createProject("a-new-project", null, false);
+    TestRepository<?> repo1 = cloneProject(p1);
+
+    RevCommit c1 = repo1.branch("HEAD").commit().insertChangeId()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = GitUtil.getChangeId(repo1, c1).get();
+    pushHead(repo1, "refs/for/master", false);
+
+    RevCommit c2 = repo1.branch("HEAD").commit().insertChangeId()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = GitUtil.getChangeId(repo1, c2).get();
+    pushHead(repo1, "refs/for/master", false);
+    assertSubmittedTogether(id1);
+    assertSubmittedTogether(id2, id2, id1);
+  }
+
+  @Test
+  @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
+  public void testCherryPickWithoutAncestors() throws Exception {
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_1);
+    pushHead(testRepo, "refs/for/master", false);
+
+    assertSubmittedTogether(id1);
+    assertSubmittedTogether(id2);
+  }
+
+  @Test
+  public void testSubmissionIdSavedOnMergeInOneProject() throws Exception {
+    // Create two commits and push.
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_1);
+    pushHead(testRepo, "refs/for/master", false);
+
+    assertSubmittedTogether(id1);
+    assertSubmittedTogether(id2, id2, id1);
+
+    approve(id1);
+    approve(id2);
+    submit(id2);
+    assertMerged(id1);
+    assertMerged(id2);
+
+    // Prior to submission this was empty, but the post-merge value is what was
+    // actually submitted.
+    assertSubmittedTogether(id1, id2, id1);
+
+    assertSubmittedTogether(id2, id2, id1);
+  }
+
+  private RevCommit getRemoteHead() throws IOException {
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      return rw.parseCommit(repo.exactRef("refs/heads/master").getObjectId());
+    }
+  }
+
+  private String getChangeId(RevCommit c) throws Exception {
+    return GitUtil.getChangeId(testRepo, c).get();
+  }
+
+  private void submit(String changeId) throws Exception {
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit();
+  }
+
+  private void assertMerged(String changeId) throws Exception {
+    assertThat(gApi
+        .changes()
+        .id(changeId)
+        .get()
+        .status).isEqualTo(ChangeStatus.MERGED);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
index fce853b..ad7d597 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK
@@ -1,6 +1,15 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
+FLAKY_TEST_CASES=['ProjectWatchIT.java']
+
 acceptance_tests(
-  srcs = glob(['*IT.java']),
+  group = 'server-project',
+  srcs = glob(['*IT.java'], excludes=FLAKY_TEST_CASES),
   labels = ['server'],
 )
+
+acceptance_tests(
+  group = 'server-project-flaky',
+  srcs = FLAKY_TEST_CASES,
+  labels = ['server', 'flaky'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 64e1966..357f268 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.Util;
@@ -40,7 +39,7 @@
 @NoHttpd
 public class CustomLabelIT extends AbstractDaemonTest {
 
-  private final LabelType Q = category("CustomLabel",
+  private final LabelType label = category("CustomLabel",
       value(1, "Positive"),
       value(0, "No score"),
       value(-1, "Negative"));
@@ -51,24 +50,24 @@
 
   @Before
   public void setUp() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     AccountGroup.UUID anonymousUsers =
         SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-    Util.allow(cfg, Permission.forLabel(Q.getName()), -1, 1, anonymousUsers,
+    Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers,
         "refs/heads/*");
     Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers,
         "refs/heads/*");
-    saveProjectConfig(cfg);
+    saveProjectConfig(project, cfg);
   }
 
   @Test
   public void customLabelNoOp_NegativeVoteNotBlock() throws Exception {
-    Q.setFunctionName("NoOp");
+    label.setFunctionName("NoOp");
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
-    revision(r).review(new ReviewInput().label(Q.getName(), -1));
+    revision(r).review(new ReviewInput().label(label.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
-    LabelInfo q = c.labels.get(Q.getName());
+    LabelInfo q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
     assertThat(q.rejected).isNotNull();
     assertThat(q.blocking).isNull();
@@ -76,12 +75,12 @@
 
   @Test
   public void customLabelNoBlock_NegativeVoteNotBlock() throws Exception {
-    Q.setFunctionName("NoBlock");
+    label.setFunctionName("NoBlock");
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
-    revision(r).review(new ReviewInput().label(Q.getName(), -1));
+    revision(r).review(new ReviewInput().label(label.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
-    LabelInfo q = c.labels.get(Q.getName());
+    LabelInfo q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
     assertThat(q.rejected).isNotNull();
     assertThat(q.blocking).isNull();
@@ -89,12 +88,12 @@
 
   @Test
   public void customLabelMaxNoBlock_NegativeVoteNotBlock() throws Exception {
-    Q.setFunctionName("MaxNoBlock");
+    label.setFunctionName("MaxNoBlock");
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
-    revision(r).review(new ReviewInput().label(Q.getName(), -1));
+    revision(r).review(new ReviewInput().label(label.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
-    LabelInfo q = c.labels.get(Q.getName());
+    LabelInfo q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
     assertThat(q.rejected).isNotNull();
     assertThat(q.blocking).isNull();
@@ -102,12 +101,12 @@
 
   @Test
   public void customLabelAnyWithBlock_NegativeVoteBlock() throws Exception {
-    Q.setFunctionName("AnyWithBlock");
+    label.setFunctionName("AnyWithBlock");
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
-    revision(r).review(new ReviewInput().label(Q.getName(), -1));
+    revision(r).review(new ReviewInput().label(label.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
-    LabelInfo q = c.labels.get(Q.getName());
+    LabelInfo q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
     assertThat(q.disliked).isNull();
     assertThat(q.rejected).isNotNull();
@@ -138,9 +137,9 @@
   public void customLabelMaxWithBlock_NegativeVoteBlock() throws Exception {
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
-    revision(r).review(new ReviewInput().label(Q.getName(), -1));
+    revision(r).review(new ReviewInput().label(label.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
-    LabelInfo q = c.labels.get(Q.getName());
+    LabelInfo q = c.labels.get(label.getName());
     assertThat(q.all).hasSize(1);
     assertThat(q.disliked).isNull();
     assertThat(q.rejected).isNotNull();
@@ -148,19 +147,9 @@
   }
 
   private void saveLabelConfig() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    cfg.getLabelSections().put(Q.getName(), Q);
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().put(label.getName(), label);
     cfg.getLabelSections().put(P.getName(), P);
-    saveProjectConfig(cfg);
-  }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
-    projectCache.evict(allProjects);
+    saveProjectConfig(project, cfg);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index efb4615..564ca23 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.server.project;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -27,34 +26,27 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.server.project.Util;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class LabelTypeIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
-  }
-
   private LabelType codeReview;
 
   @Before
   public void setUp() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    codeReview = checkNotNull(cfg.getLabelSections().get("Code-Review"));
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    codeReview = Util.codeReview();
     codeReview.setDefaultValue((short)-1);
+    cfg.getLabelSections().put(codeReview.getName(), codeReview);
     saveProjectConfig(cfg);
   }
 
   @Test
   public void noCopyMinScoreOnRework() throws Exception {
-    //allProjects only has it true by default
     codeReview.setCopyMinScore(false);
     saveLabelConfig();
 
@@ -143,15 +135,15 @@
     String file = "a.txt";
     String contents = "contents";
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         "first subject", file, contents);
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = push.to("refs/for/master");
     revision(r).review(ReviewInput.recommend());
     assertApproval(r, 1);
 
-    push = pushFactory.create(db, admin.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         "second subject", file, contents, r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = push.to("refs/for/master");
     assertApproval(r, 0);
   }
 
@@ -162,15 +154,15 @@
     codeReview.setCopyAllScoresIfNoCodeChange(true);
     saveLabelConfig();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         "first subject", file, contents);
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = push.to("refs/for/master");
     revision(r).review(ReviewInput.recommend());
     assertApproval(r, 1);
 
-    push = pushFactory.create(db, admin.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         "second subject", file, contents, r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = push.to("refs/for/master");
     assertApproval(r, 1);
   }
 
@@ -180,18 +172,18 @@
     String file = "a.txt";
     String contents = "contents";
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    PushOneCommit.Result r1 = push.to(git, "refs/for/master");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, admin.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         "non-conflicting", "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    PushOneCommit.Result r2 = push.to("refs/for/master");
     merge(r2);
 
-    git.checkout().setName(r1.getCommit().name()).call();
-    push = pushFactory.create(db, admin.getIdent(), subject, file, contents);
-    PushOneCommit.Result r3 = push.to(git, "refs/for/master");
+    testRepo.reset(r1.getCommit());
+    push = pushFactory.create(db, admin.getIdent(), testRepo, subject, file, contents);
+    PushOneCommit.Result r3 = push.to("refs/for/master");
     revision(r3).review(ReviewInput.recommend());
     assertApproval(r3, 1);
 
@@ -207,18 +199,18 @@
     codeReview.setCopyAllScoresOnTrivialRebase(true);
     saveLabelConfig();
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    PushOneCommit.Result r1 = push.to(git, "refs/for/master");
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, admin.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         "non-conflicting", "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    PushOneCommit.Result r2 = push.to("refs/for/master");
     merge(r2);
 
-    git.checkout().setName(r1.getCommit().name()).call();
-    push = pushFactory.create(db, admin.getIdent(), subject, file, contents);
-    PushOneCommit.Result r3 = push.to(git, "refs/for/master");
+    testRepo.reset(r1.getCommit());
+    push = pushFactory.create(db, admin.getIdent(), testRepo, subject, file, contents);
+    PushOneCommit.Result r3 = push.to("refs/for/master");
     revision(r3).review(ReviewInput.recommend());
     assertApproval(r3, 1);
 
@@ -232,11 +224,11 @@
     saveLabelConfig();
 
     PushOneCommit.Result r1 = createChange();
-    git.checkout().setName(r1.getCommit().name()).call();
+    testRepo.reset(r1.getCommit());
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    PushOneCommit.Result r2 = push.to("refs/for/master");
 
     revision(r2).review(ReviewInput.recommend());
 
@@ -262,11 +254,11 @@
 
     PushOneCommit.Result r1 = createChange();
 
-    git.checkout().setName(r1.getCommit().name()).call();
+    testRepo.reset(r1.getCommit());
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, "b.txt", "other contents");
-    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+    PushOneCommit.Result r2 = push.to("refs/for/master");
 
     revision(r2).review(ReviewInput.recommend());
 
@@ -288,38 +280,38 @@
     String file = "a.txt";
     String contents = "contents";
 
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, file, contents);
-    PushOneCommit.Result base = push.to(git, "refs/for/master");
+    PushOneCommit.Result base = push.to("refs/for/master");
     merge(base);
 
-    push = pushFactory.create(db, admin.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, file, contents + "M");
-    PushOneCommit.Result basePlusM = push.to(git, "refs/for/master");
+    PushOneCommit.Result basePlusM = push.to("refs/for/master");
     merge(basePlusM);
 
-    push = pushFactory.create(db, admin.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, file, contents);
-    PushOneCommit.Result basePlusMMinusM = push.to(git, "refs/for/master");
+    PushOneCommit.Result basePlusMMinusM = push.to("refs/for/master");
     merge(basePlusMMinusM);
 
-    git.checkout().setName(base.getCommit().name()).call();
-    push = pushFactory.create(db, admin.getIdent(),
+    testRepo.reset(base.getCommit());
+    push = pushFactory.create(db, admin.getIdent(), testRepo,
         PushOneCommit.SUBJECT, file, contents + "MM");
-    PushOneCommit.Result patchSet = push.to(git, "refs/for/master");
+    PushOneCommit.Result patchSet = push.to("refs/for/master");
     revision(patchSet).review(ReviewInput.recommend());
     return patchSet;
   }
 
   private void saveLabelConfig() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     cfg.getLabelSections().clear();
     cfg.getLabelSections().put(codeReview.getName(), codeReview);
     saveProjectConfig(cfg);
   }
 
   private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
     try {
       cfg.commit(md);
     } finally {
@@ -330,12 +322,9 @@
   private void merge(PushOneCommit.Result r) throws Exception {
     revision(r).review(ReviewInput.approve());
     revision(r).submit();
-    Repository repo = repoManager.openRepository(project);
-    try {
-      assertThat(repo.getRef("refs/heads/master").getObjectId()).isEqualTo(
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(
           r.getCommitId());
-    } finally {
-      repo.close();
     }
   }
 
@@ -356,6 +345,6 @@
     assertThat((int) cr.defaultValue).isEqualTo(-1);
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
-    assertThat(cr.all.get(0).value.intValue()).isEqualTo(expected);
+    assertThat(cr.all.get(0).value).isEqualTo(expected);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
new file mode 100644
index 0000000..0b86ad9
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2015 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.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.git.NotifyConfig;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.testutil.FakeEmailSender;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
+import com.google.inject.Inject;
+
+import org.junit.Test;
+
+import java.util.EnumSet;
+import java.util.List;
+
+@NoHttpd
+public class ProjectWatchIT extends AbstractDaemonTest {
+  @Inject
+  private FakeEmailSender sender;
+
+  /**
+   * Tests message project watches on new patch sets
+   * <p>
+   * As of 2015-06-21 this test is marked flaky for triggering race
+   * conditions between indexing and project watches filters as
+   * of 2015-06-21.
+   * <p>
+   * The test $SOMETIMES fails, stating that 2 emails instead of only
+   * 1 got sent. The root issue is the inserting of two patch sets
+   * (one shortly after the other), where the first patch set would
+   * not match a user's filter while the second one would.
+   * <p>
+   * The test basically:
+   * <ol>
+   *   <li>Sets up a watch on the text 'sekret' in the commit message.</li>
+   *   <li>Pushes a change without sekret in the commit message (no
+   *     email is expected). (We'll refer to this as PS1)</li>
+   *   <li>Push another patch set to the same change with sekret in the
+   *     commit message (1 email is expected). (We'll refer to this as PS2)</li>
+   *   <li>[...]</li>
+   * </ol>
+   * <p>The expected flow of actions for step 2+3 is:
+   * <pre>
+   *    (i) Write PS1 to the index
+   *   (ii) Send out emails for PS1 after checking project watches from
+   *        fresh ChangeData
+   *  (iii) Write PS2 to the index
+   *   (iv) Send out emails for PS2 after checking project watches from
+   *        fresh ChangeData
+   * </pre>
+   * <p>
+   * But as step (ii) and step (iv) happen on separate threads, steps
+   * (ii) and (iii) might get turned around and become:
+   * <pre>
+   *   * Write PS1 to the index
+   *   * Write PS2 to the index
+   *   * Send out emails for PS1 after checking project watches from
+   *     fresh ChangeData
+   *   * Send out emails for PS2 after checking project watches from
+   *     fresh ChangeData
+   * </pre>
+   * <p>
+   * Hence, the filters for project watches for the emails for PS1 query
+   * the index after PS2 has already been written there. Hence, the
+   * filters for PS1 use the commit message of PS2 when filtering on
+   * 'message:sekret'.
+   * <p>
+   * Since in the ProjectWatchIT test, PS2 contains 'sekret', the filters
+   * for sending out emails for PS1 see a commit message containing
+   * 'sekret', and the watches match for both PS1 and PS2, although they
+   * should only match for PS2.
+   * <p>
+   * This explains why the test is only failing sometimes, and also why it
+   * is more likely to occur when the system is under load.
+   * <p>
+   * A demo exposing the race condition is available at
+   * <a href="https://gerrit-review.googlesource.com/#/c/68719/1">https://gerrit-review.googlesource.com/#/c/68719/1</a>.
+   */
+  @Test
+  public void newPatchSetsNotifyConfig() throws Exception {
+    Address addr = new Address("Watcher", "watcher@example.com");
+    NotifyConfig nc = new NotifyConfig();
+    nc.addEmail(addr);
+    nc.setName("new-patch-set");
+    nc.setHeader(NotifyConfig.Header.CC);
+    nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS));
+    nc.setFilter("message:sekret");
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.putNotifyConfig("watch", nc);
+    saveProjectConfig(project, cfg);
+
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), testRepo,
+          "original subject", "a", "a1")
+        .to("refs/for/master");
+    r.assertOkStatus();
+
+    r = pushFactory.create(db, admin.getIdent(), testRepo,
+          "super sekret subject", "a", "a2", r.getChangeId())
+        .to("refs/for/master");
+    r.assertOkStatus();
+
+    r = pushFactory.create(db, admin.getIdent(), testRepo,
+          "back to original subject", "a", "a3")
+        .to("refs/for/master");
+    r.assertOkStatus();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(addr);
+    assertThat(m.body()).contains("Change subject: super sekret subject\n");
+    assertThat(m.body()).contains("Gerrit-PatchSet: 2\n");
+  }
+
+  // TODO(anybody reading this): More tests.
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
index 2ea5dec..0729b68 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -1,6 +1,8 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
+  group = 'ssh',
   srcs = glob(['*IT.java']),
+  deps = ['//lib/commons:compress'],
   labels = ['ssh'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
index e483716..3bef84b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
@@ -16,14 +16,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
-import static com.google.gerrit.acceptance.GitUtil.add;
-import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.NoHttpd;
 
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
@@ -34,17 +32,17 @@
 
   @Test
   public void banCommit() throws Exception {
-    add(git, "a.txt", "some content");
-    Commit c = createCommit(git, admin.getIdent(), "subject");
+    RevCommit c = commitBuilder()
+        .add("a.txt", "some content")
+        .create();
 
     String response =
-        sshSession.exec("gerrit ban-commit " + project.get() + " "
-            + c.getCommit().getName());
+        sshSession.exec("gerrit ban-commit " + project.get() + " " + c.name());
     assert_().withFailureMessage(sshSession.getError())
         .that(sshSession.hasError()).isFalse();
     assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
 
-    PushResult pushResult = pushHead(git, "refs/heads/master", false);
+    PushResult pushResult = pushHead(testRepo, "refs/heads/master", false);
     assertThat(pushResult.getRemoteUpdate("refs/heads/master").getMessage())
         .startsWith("contains banned commit");
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 2bdd894..b2c4df8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
@@ -52,11 +51,8 @@
 
   @Before
   public void setUp() throws Exception {
-    project2 = new Project.NameKey("p2");
-    createProject(sshSession, project2.get());
-
-    project3 = new Project.NameKey("p3");
-    createProject(sshSession, project3.get());
+    project2 = createProject("p2");
+    project3 = createProject("p3");
   }
 
   @Test
@@ -86,7 +82,8 @@
   public void testGcWithoutCapability_Error() throws Exception {
     SshSession s = new SshSession(server, user);
     s.exec("gerrit gc --all");
-    assertError("Capability runGC is required to access this resource", s.getError());
+    assertError("One of the following capabilities is required to access this"
+        + " resource: [runGC, maintainServer]", s.getError());
     s.close();
   }
 
@@ -97,7 +94,7 @@
     GarbageCollectionResult result = garbageCollectionFactory.create().run(
         Arrays.asList(allProjects, project, project2, project3));
     assertThat(result.hasErrors()).isTrue();
-    assertThat(result.getErrors().size()).isEqualTo(1);
+    assertThat(result.getErrors()).hasSize(1);
     GarbageCollectionResult.Error error = result.getErrors().get(0);
     assertThat(error.getType()).isEqualTo(
         GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
index c9e0a89..7fb99b6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.reviewdb.client.Project;
 
 import org.junit.Ignore;
 import org.junit.Test;
@@ -50,8 +50,8 @@
       public Void call() throws Exception {
         for (int i = 1; i < 100; i++) {
           String p = "p" + i;
-          createProject(sshSession, p);
-          cloneProject(sshSession.getUrl() + "/" + p);
+          createProject(p);
+          GitUtil.cloneProject(new Project.NameKey(p), sshSession);
         }
         return null;
       }
@@ -62,6 +62,6 @@
     for (Future<Void> future : futures) {
       future.get();
     }
-    assertThat(futures.size()).isEqualTo(threads);
+    assertThat(futures).hasSize(threads);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
new file mode 100644
index 0000000..88821ce
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2015 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.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.eclipse.jgit.transport.PacketLineIn;
+import org.eclipse.jgit.transport.PacketLineOut;
+import org.eclipse.jgit.util.IO;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+import java.util.TreeSet;
+
+@NoHttpd
+public class UploadArchiveIT extends AbstractDaemonTest {
+
+  @Test
+  @GerritConfig(name = "download.archive", value = "off")
+  public void archiveFeatureOff() throws Exception {
+    archiveNotPermitted();
+  }
+
+  @Test
+  @GerritConfig(name = "download.archive", values = {"tar", "tbz2", "tgz", "txz"})
+  public void zipFormatDisabled() throws Exception {
+    archiveNotPermitted();
+  }
+
+  @Test
+  public void zipFormat() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String c = command(r, abbreviated);
+
+    InputStream out =
+        sshSession.exec2("git-upload-archive " + project.get(),
+            argumentsToInputStream(c));
+
+    // Wrap with PacketLineIn to read ACK bytes from output stream
+    PacketLineIn in = new PacketLineIn(out);
+    String tmp = in.readString();
+    assertThat(tmp).isEqualTo("ACK");
+    tmp = in.readString();
+
+    // Skip length (4 bytes) + 1 byte
+    // to position the output stream to the raw zip stream
+    byte[] buffer = new byte[5];
+    IO.readFully(out, buffer, 0, 5);
+    Set<String> entryNames = new TreeSet<>();
+    try (ZipArchiveInputStream zip = new ZipArchiveInputStream(out)) {
+      ZipArchiveEntry zipEntry = zip.getNextZipEntry();
+      while (zipEntry != null) {
+        String name = zipEntry.getName();
+        entryNames.add(name);
+        zipEntry = zip.getNextZipEntry();
+      }
+    }
+
+    assertThat(entryNames.size()).isEqualTo(1);
+    assertThat(Iterables.getOnlyElement(entryNames)).isEqualTo(
+        String.format("%s/%s", abbreviated, PushOneCommit.FILE_NAME));
+  }
+
+  private String command(PushOneCommit.Result r, String abbreviated) {
+    String c = "-f=zip "
+        + "-9 "
+        + "--prefix=" + abbreviated + "/ "
+        + r.getCommit().name() + " "
+        + PushOneCommit.FILE_NAME;
+    return c;
+  }
+
+  private void archiveNotPermitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String c = command(r, abbreviated);
+
+    InputStream out =
+        sshSession.exec2("git-upload-archive " + project.get(),
+            argumentsToInputStream(c));
+
+    // Wrap with PacketLineIn to read ACK bytes from output stream
+    PacketLineIn in = new PacketLineIn(out);
+    String tmp = in.readString();
+    assertThat(tmp).isEqualTo("ACK");
+    tmp = in.readString();
+    tmp = in.readString();
+    tmp = tmp.substring(1);
+    assertThat(tmp).isEqualTo("fatal: upload-archive not permitted");
+  }
+
+  private InputStream argumentsToInputStream(String c) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    PacketLineOut pctOut = new PacketLineOut(out);
+    for (String arg : Splitter.on(' ').split(c)) {
+      pctOut.writeString("argument " + arg);
+    }
+    pctOut.end();
+    return new ByteArrayInputStream(out.toByteArray());
+  }
+}
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
index 7bd2430..940b1cc 100644
--- a/gerrit-acceptance-tests/tests.defs
+++ b/gerrit-acceptance-tests/tests.defs
@@ -5,6 +5,7 @@
 ]
 
 def acceptance_tests(
+    group,
     srcs,
     deps = [],
     labels = [],
@@ -16,19 +17,18 @@
   if path.exists('/dev/urandom'):
     vm_args = vm_args + ['-Djava.security.egd=file:/dev/./urandom']
 
-  for j in srcs:
-    java_test(
-      name = j[:-len('.java')],
-      srcs = [j],
-      deps = ['//gerrit-acceptance-tests:lib'] + deps,
-      source_under_test = [
-        '//gerrit-httpd:httpd',
-        '//gerrit-sshd:sshd',
-        '//gerrit-server:server',
-      ] + source_under_test,
-      labels = labels + [
-        'acceptance',
-        'slow',
-      ],
-      vm_args = vm_args,
-    )
+  java_test(
+    name = group,
+    srcs = srcs,
+    deps = ['//gerrit-acceptance-tests:lib'] + deps,
+    source_under_test = [
+      '//gerrit-httpd:httpd',
+      '//gerrit-sshd:sshd',
+      '//gerrit-server:server',
+    ] + source_under_test,
+    labels = labels + [
+      'acceptance',
+      'slow',
+    ],
+    vm_args = vm_args,
+  )
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK
index 03c3c1e..e858a72 100644
--- a/gerrit-antlr/BUCK
+++ b/gerrit-antlr/BUCK
@@ -25,7 +25,6 @@
 genrule(
   name = 'query_link',
   cmd = 'ln -s $(location :lib) $OUT',
-  deps = [':lib'],
   out = 'query_parser.jar',
 )
 
diff --git a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
index 98f1af9..d0b5875 100644
--- a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
+++ b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
@@ -116,7 +116,6 @@
 conditionNot
   : '-' conditionBase -> ^(NOT conditionBase)
   | NOT^ conditionBase
-  | VARIABLE_ASSIGN^ conditionOr ')'!
   | conditionBase
   ;
 conditionBase
@@ -143,13 +142,6 @@
   : ('a'..'z' | '_')+
   ;
 
-VARIABLE_ASSIGN
-  : ('A'..'Z') ('A'..'Z' | 'a'..'Z')* '=' '(' {
-      String s = $text;
-      setText(s.substring(0, s.length() - 2));
-    }
-  ;
-
 EXACT_PHRASE
   : '"' ( ~('"') )* '"' {
       String s = $text;
diff --git a/gerrit-cache-h2/BUCK b/gerrit-cache-h2/BUCK
index c7a2221..37c8b96 100644
--- a/gerrit-cache-h2/BUCK
+++ b/gerrit-cache-h2/BUCK
@@ -13,3 +13,16 @@
   ],
   visibility = ['PUBLIC'],
 )
+
+java_test(
+  name = 'tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':cache-h2',
+    '//gerrit-server:server',
+    '//lib:guava',
+    '//lib:h2',
+    '//lib/guice:guice',
+    '//lib:junit',
+  ],
+)
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 5870f91..fcacf78 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -37,7 +37,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
@@ -51,7 +53,7 @@
 
   private final DefaultCacheFactory defaultFactory;
   private final Config config;
-  private final File cacheDir;
+  private final Path cacheDir;
   private final List<H2CacheImpl<?, ?>> caches;
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final ExecutorService executor;
@@ -65,23 +67,7 @@
       DynamicMap<Cache<?, ?>> cacheMap) {
     defaultFactory = defaultCacheFactory;
     config = cfg;
-
-    File loc = site.resolve(cfg.getString("cache", null, "directory"));
-    if (loc == null) {
-      cacheDir = null;
-    } else if (loc.exists() || loc.mkdirs()) {
-      if (loc.canWrite()) {
-        log.info("Enabling disk cache " + loc.getAbsolutePath());
-        cacheDir = loc;
-      } else {
-        log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
-        cacheDir = null;
-      }
-    } else {
-      log.warn("Can't create disk cache: " + loc.getAbsolutePath());
-      cacheDir = null;
-    }
-
+    cacheDir = getCacheDir(site, cfg.getString("cache", null, "directory"));
     caches = Lists.newLinkedList();
     this.cacheMap = cacheMap;
 
@@ -103,6 +89,27 @@
     }
   }
 
+  private static Path getCacheDir(SitePaths site, String name) {
+    if (name == null) {
+      return null;
+    }
+    Path loc = site.resolve(name);
+    if (!Files.exists(loc)) {
+      try {
+        Files.createDirectories(loc);
+      } catch (IOException e) {
+        log.warn("Can't create disk cache: " + loc.toAbsolutePath());
+        return null;
+      }
+    }
+    if (!Files.isWritable(loc)) {
+      log.warn("Can't write to disk cache: " + loc.toAbsolutePath());
+      return null;
+    }
+    log.info("Enabling disk cache " + loc.toAbsolutePath());
+    return loc;
+  }
+
   @Override
   public void start() {
     if (executor != null) {
@@ -213,8 +220,7 @@
       TypeLiteral<K> keyType,
       long maxSize,
       Long expireAfterWrite) {
-    File db = new File(cacheDir, name).getAbsoluteFile();
-    String url = "jdbc:h2:" + db.toURI().toString();
+    String url = "jdbc:h2:" + cacheDir.resolve(name).toUri();
     return new SqlStore<>(url, keyType, maxSize,
         expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
   }
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 123bb9a..289c8cc 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -1,4 +1,16 @@
-// Copyright 2012 Google Inc. All Rights Reserved.
+// Copyright (C) 2012 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.server.cache.h2;
 
@@ -34,6 +46,7 @@
 import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ScheduledExecutorService;
@@ -116,6 +129,12 @@
   }
 
   @Override
+  public V get(K key, Callable<? extends V> valueLoader)
+      throws ExecutionException {
+    return mem.get(key, new LoadingCallable(key, valueLoader)).value;
+  }
+
+  @Override
   public void put(final K key, V val) {
     final ValueHolder<V> h = new ValueHolder<>(val);
     h.created = TimeUtil.nowMs();
@@ -238,6 +257,36 @@
     }
   }
 
+  private class LoadingCallable implements Callable<ValueHolder<V>> {
+    private final K key;
+    private final Callable<? extends V> loader;
+
+    LoadingCallable(K key, Callable<? extends V> loader) {
+      this.key = key;
+      this.loader = loader;
+    }
+
+    @Override
+    public ValueHolder<V> call() throws Exception {
+      if (store.mightContain(key)) {
+        ValueHolder<V> h = store.getIfPresent(key);
+        if (h != null) {
+          return h;
+        }
+      }
+
+      final ValueHolder<V> h = new ValueHolder<V>(loader.call());
+      h.created = TimeUtil.nowMs();
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          store.put(key, h);
+        }
+      });
+      return h;
+    }
+  }
+
   private static class KeyType<K> {
     String columnType() {
       return "OTHER";
@@ -258,15 +307,10 @@
 
         @Override
         public void funnel(K from, PrimitiveSink into) {
-          try {
-            ObjectOutputStream ser =
-                new ObjectOutputStream(new SinkOutputStream(into));
-            try {
-              ser.writeObject(from);
-              ser.flush();
-            } finally {
-              ser.close();
-            }
+          try (ObjectOutputStream ser =
+              new ObjectOutputStream(new SinkOutputStream(into))) {
+            ser.writeObject(from);
+            ser.flush();
           } catch (IOException err) {
             throw new RuntimeException("Cannot hash as Serializable", err);
           }
diff --git a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
new file mode 100644
index 0000000..3b7e436
--- /dev/null
+++ b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2015 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.server.cache.h2;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.inject.TypeLiteral;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class H2CacheTest {
+  private static int dbCnt;
+
+  private Cache<String, ValueHolder<Boolean>> mem;
+  private H2CacheImpl<String, Boolean> impl;
+
+  @Before
+  public void setUp() {
+    mem = CacheBuilder.newBuilder().build();
+
+    TypeLiteral<String> keyType = new TypeLiteral<String>() {};
+    SqlStore<String, Boolean> store = new SqlStore<>(
+        "jdbc:h2:mem:" + "Test_" + (++dbCnt),
+        keyType,
+        1 << 20,
+        0);
+    impl =
+        new H2CacheImpl<>(MoreExecutors.directExecutor(), store, keyType, mem);
+  }
+
+  @Test
+  public void get() throws ExecutionException {
+    assertNull(impl.getIfPresent("foo"));
+
+    final AtomicBoolean called = new AtomicBoolean();
+    assertTrue(impl.get("foo", new Callable<Boolean>() {
+      @Override
+      public Boolean call() throws Exception {
+        called.set(true);
+        return true;
+      }
+    }));
+    assertTrue("used Callable", called.get());
+    assertTrue("exists in cache", impl.getIfPresent("foo"));
+    mem.invalidate("foo");
+    assertTrue("exists on disk", impl.getIfPresent("foo"));
+
+    called.set(false);
+    assertTrue(impl.get("foo", new Callable<Boolean>() {
+      @Override
+      public Boolean call() throws Exception {
+        called.set(true);
+        return true;
+      }
+    }));
+    assertFalse("did not invoke Callable", called.get());
+  }
+}
\ No newline at end of file
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index 88e503e..a36a9b7 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -24,15 +24,10 @@
   name = 'client',
   srcs = glob([SRC + 'common/**/*.java'], excludes = EXCLUDES),
   gwt_xml = SRC + 'Common.gwt.xml',
-  deps = [
-    ':annotations',
+  exported_deps = [
     '//gerrit-extension-api:client',
-    '//gerrit-patch-jgit:client',
     '//gerrit-prettify:client',
-    '//gerrit-reviewdb:client',
-    '//lib:gwtjsonrpc',
-    '//lib:gwtorm',
-    '//lib/jgit:jgit',
+    '//lib:gwtorm_client',
   ],
   visibility = ['PUBLIC'],
 )
@@ -51,6 +46,7 @@
     '//lib:guava',
     '//lib/jgit:jgit',
     '//lib/joda:joda-time',
+    '//lib/log:api',
   ],
   visibility = ['PUBLIC'],
 )
@@ -73,8 +69,6 @@
   name = 'auto_value_tests',
   srcs = AUTO_VALUE_TEST_SRCS,
   deps = [
-    '//lib:guava',
-    '//lib:junit',
     '//lib:truth',
     '//lib/auto:auto-value',
   ],
diff --git a/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml b/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml
index 468b477..80bd2cb 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml
+++ b/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml
@@ -16,5 +16,6 @@
 <module>
   <inherits name='com.google.gerrit.reviewdb.ReviewDB' />
   <inherits name='com.google.gwtjsonrpc.GWTJSONRPC'/>
+  <inherits name="com.google.gwt.logging.Logging"/>
   <source path='common' />
 </module>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
index bed10d6..ed28ec0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
@@ -21,6 +21,8 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
 
 public class FileUtil {
@@ -42,6 +44,11 @@
     }
   }
 
+  public static void chmod(final int mode, final Path path) {
+    // TODO(dborowitz): Is there a portable way to do this with NIO?
+    chmod(mode, path.toFile());
+  }
+
   public static void chmod(final int mode, final File path) {
     path.setReadable(false, false /* all */);
     path.setWritable(false, false /* all */);
@@ -61,6 +68,35 @@
     }
   }
 
+  /**
+   * Get the last modified time of a path.
+   * <p>
+   * Equivalent to {@code File#lastModified()}, returning 0 on errors, including
+   * file not found. Callers that prefer exceptions can use {@link
+   * Files#getLastModifiedTime(Path, java.nio.file.LinkOption...)}.
+   *
+   * @param p path.
+   * @return last modified time, in milliseconds since epoch.
+   */
+  public static long lastModified(Path p) {
+    try {
+      return Files.getLastModifiedTime(p).toMillis();
+    } catch (IOException e) {
+      return 0;
+    }
+  }
+
+  public static Path mkdirsOrDie(Path p, String errMsg) {
+    try {
+      if (!Files.isDirectory(p)) {
+        Files.createDirectories(p);
+      }
+      return p;
+    } catch (IOException e) {
+      throw new Die(errMsg + ": " + p, e);
+    }
+  }
+
   private FileUtil() {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-common/src/main/java/com/google/gerrit/common/FormatUtil.java
similarity index 62%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-common/src/main/java/com/google/gerrit/common/FormatUtil.java
index 4413603..0f6b37a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/FormatUtil.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.common;
 
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+public class FormatUtil {
+  public static String elide(String s, int max) {
+    if (s == null || s.length() <= max) {
+      return s;
+    }
+    int len = (max - 3) / 2;
+    return s.substring(0, len) + "..." + s.substring(s.length() - len);
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index c45d9f9..9bc2ea5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.Sets;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -25,7 +24,9 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Path;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Set;
 
 public final class IoUtil {
@@ -53,7 +54,7 @@
     }.start();
   }
 
-  public static void loadJARs(File... jars) {
+  public static void loadJARs(Iterable<Path> jars) {
     ClassLoader cl = IoUtil.class.getClassLoader();
     if (!(cl instanceof URLClassLoader)) {
       throw noAddURL("Not loaded by URLClassLoader", null);
@@ -71,9 +72,9 @@
     }
 
     Set<URL> have = Sets.newHashSet(Arrays.asList(urlClassLoader.getURLs()));
-    for (File path : jars) {
+    for (Path path : jars) {
       try {
-        URL url = path.toURI().toURL();
+        URL url = path.toUri().toURL();
         if (have.add(url)) {
           addURL.invoke(cl, url);
         }
@@ -86,6 +87,10 @@
     }
   }
 
+  public static void loadJARs(Path jar) {
+    loadJARs(Collections.singleton(jar));
+  }
+
   private static UnsupportedOperationException noAddURL(String m, Throwable why) {
     String prefix = "Cannot extend classpath: ";
     return new UnsupportedOperationException(prefix + m, why);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index b804607..28e0d24 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -25,7 +25,10 @@
 public class PageLinks {
   public static final String SETTINGS = "/settings/";
   public static final String SETTINGS_PREFERENCES = "/settings/preferences";
+  public static final String SETTINGS_DIFF_PREFERENCES = "/settings/diff-preferences";
+  public static final String SETTINGS_EDIT_PREFERENCES = "/settings/edit-preferences";
   public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys";
+  public static final String SETTINGS_GPGKEYS = "/settings/gpg-keys";
   public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password";
   public static final String SETTINGS_WEBIDENT = "/settings/web-identities";
   public static final String SETTINGS_MYGROUPS = "/settings/group-memberships";
@@ -33,6 +36,7 @@
   public static final String SETTINGS_CONTACT = "/settings/contact";
   public static final String SETTINGS_PROJECTS = "/settings/projects";
   public static final String SETTINGS_NEW_AGREEMENT = "/settings/new-agreement";
+  public static final String SETTINGS_EXTENSION = "/settings/x/";
   public static final String REGISTER = "/register";
 
   public static final String MINE = "/";
@@ -87,6 +91,10 @@
     return "/admin/projects/" + p.get() + ",branches";
   }
 
+  public static String toProjectTags(Project.NameKey p) {
+    return "/admin/projects/" + p.get() + ",tags";
+  }
+
   public static String toAccountQuery(String fullname, Status status) {
     return toChangeQuery(op("owner", fullname) + " " + status(status));
   }
@@ -128,6 +136,10 @@
     return ADMIN_GROUPS + "uuid-" + uuid;
   }
 
+  public static String toSettings(String pluginName, String path) {
+    return SETTINGS_EXTENSION + pluginName + "/" + path;
+  }
+
   private static String status(Status status) {
     switch (status) {
       case ABANDONED:
@@ -135,7 +147,6 @@
       case MERGED:
         return "status:merged";
       case NEW:
-      case SUBMITTED:
       default:
         return "status:open";
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
index ffdae9d..27dc639 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.common;
 
-import java.io.File;
+import java.nio.file.Path;
 import java.util.Objects;
 
 public class PluginData {
   public final String name;
   public final String version;
-  public final File pluginFile;
+  public final Path pluginPath;
 
-  public PluginData(String name, String version, File pluginFile) {
+  public PluginData(String name, String version, Path pluginPath) {
     this.name = name;
     this.version = version;
-    this.pluginFile = pluginFile;
+    this.pluginPath = pluginPath;
   }
 
   @Override
@@ -33,13 +33,13 @@
     if (obj instanceof PluginData) {
       PluginData o = (PluginData) obj;
       return Objects.equals(name, o.name) && Objects.equals(version, o.version)
-          && Objects.equals(pluginFile, o.pluginFile);
+          && Objects.equals(pluginPath, o.pluginPath);
     }
     return super.equals(obj);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(name, version, pluginFile);
+    return Objects.hash(name, version, pluginPath);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index a98e0a5..2511a51 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -14,41 +14,57 @@
 
 package com.google.gerrit.common;
 
-import java.io.File;
-import java.io.FileFilter;
-import java.util.Arrays;
-import java.util.Comparator;
+import static com.google.gerrit.common.FileUtil.lastModified;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.List;
 
 public final class SiteLibraryLoaderUtil {
+  private static final Logger log =
+      LoggerFactory.getLogger(SiteLibraryLoaderUtil.class);
 
-  public static void loadSiteLib(File libdir) {
-    File[] jars = listJars(libdir);
-    if (jars != null && 0 < jars.length) {
-      Arrays.sort(jars, new Comparator<File>() {
-        @Override
-        public int compare(File a, File b) {
-          // Sort by reverse last-modified time so newer JARs are first.
-          int cmp = Long.compare(b.lastModified(), a.lastModified());
-          if (cmp != 0) {
-            return cmp;
-          }
-          return a.getName().compareTo(b.getName());
-        }
-      });
-      IoUtil.loadJARs(jars);
+  public static void loadSiteLib(Path libdir) {
+    try {
+      IoUtil.loadJARs(listJars(libdir));
+    } catch (IOException e) {
+      log.error("Error scanning lib directory " + libdir, e);
     }
   }
 
-  public static File[] listJars(File libdir) {
-    File[] jars = libdir.listFiles(new FileFilter() {
+  public static List<Path> listJars(Path dir) throws IOException {
+    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
       @Override
-      public boolean accept(File path) {
-        String name = path.getName();
-        return (name.endsWith(".jar") || name.endsWith(".zip"))
-            && path.isFile();
+      public boolean accept(Path entry) throws IOException {
+          String name = entry.getFileName().toString();
+          return (name.endsWith(".jar") || name.endsWith(".zip"))
+              && Files.isRegularFile(entry);
       }
-    });
-    return jars;
+    };
+    try (DirectoryStream<Path> jars = Files.newDirectoryStream(dir, filter)) {
+      return new Ordering<Path>() {
+        @Override
+        public int compare(Path a, Path b) {
+          // Sort by reverse last-modified time so newer JARs are first.
+          return ComparisonChain.start()
+              .compare(lastModified(b), lastModified(a))
+              .compare(a, b)
+              .result();
+        }
+      }.sortedCopy(jars);
+    } catch (NoSuchFileException nsfe) {
+      return ImmutableList.of();
+    }
   }
 
   private SiteLibraryLoaderUtil() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index f382d49..752f0d2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
@@ -30,10 +29,6 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface AccountSecurity extends RemoteJsonService {
-  @Audit
-  @SignInRequired
-  void changeUserName(String newName, AsyncCallback<VoidResult> callback);
-
   @SignInRequired
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
@@ -45,14 +40,10 @@
   @Audit
   @SignInRequired
   void updateContact(String fullName, String emailAddr,
-      ContactInformation info, AsyncCallback<Account> callback);
+      AsyncCallback<Account> callback);
 
   @Audit
   @SignInRequired
   void enterAgreement(String agreementName,
       AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void validateEmail(String token, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 54a573d..533dfa2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -16,9 +16,8 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -36,12 +35,7 @@
 
   @Audit
   @SignInRequired
-  void changePreferences(AccountGeneralPreferences pref,
-      AsyncCallback<VoidResult> gerritCallback);
-
-  @Audit
-  @SignInRequired
-  void changeDiffPreferences(AccountDiffPreference diffPref,
+  void changeDiffPreferences(DiffPreferencesInfo diffPref,
       AsyncCallback<VoidResult> callback);
 
   @SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
index 5824415..6d9c2cd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -29,5 +29,5 @@
 
   @Audit
   void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
-      AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback);
+      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
index a744122..d0f8cd3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
@@ -107,7 +107,7 @@
     return latest;
   }
 
-  public java.sql.Timestamp getLastUpdatedOn() {
+  public Timestamp getLastUpdatedOn() {
     return lastUpdatedOn;
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
index db78c4d..1b98b09 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
@@ -136,7 +136,9 @@
         parentMap.put(parentUuid, l);
       }
       l.add(c);
-      if (parentUuid == null) rootComments.add(c);
+      if (parentUuid == null) {
+        rootComments.add(c);
+      }
     }
 
     // Add the comments in the list, starting with the head and then going through all the
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
index d475980..17f640d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -25,7 +25,6 @@
   protected String name;
   protected String description;
   protected List<PermissionRule> accepted;
-  protected boolean requireContactInformation;
   protected GroupReference autoVerify;
   protected String agreementUrl;
 
@@ -63,14 +62,6 @@
     this.accepted = accepted;
   }
 
-  public boolean isRequireContactInformation() {
-    return requireContactInformation;
-  }
-
-  public void setRequireContactInformation(boolean requireContactInformation) {
-    this.requireContactInformation = requireContactInformation;
-  }
-
   public GroupReference getAutoVerify() {
     return autoVerify;
   }
@@ -101,7 +92,6 @@
     ContributorAgreement ca = new ContributorAgreement(name);
     ca.description = description;
     ca.accepted = Collections.emptyList();
-    ca.requireContactInformation = requireContactInformation;
     if (autoVerify != null) {
       ca.autoVerify = new GroupReference();
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
deleted file mode 100644
index d911390..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ /dev/null
@@ -1,319 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.client.Project;
-
-import java.util.List;
-import java.util.Set;
-
-public class GerritConfig implements Cloneable {
-  protected String registerUrl;
-  protected String registerText;
-  protected String loginUrl;
-  protected String loginText;
-  protected String switchAccountUrl;
-  protected String httpPasswordUrl;
-  protected String reportBugUrl;
-  protected String reportBugText;
-  protected boolean httpPasswordSettingsEnabled = true;
-
-  protected GitwebConfig gitweb;
-  protected boolean useContributorAgreements;
-  protected boolean useContactInfo;
-  protected boolean allowRegisterNewEmail;
-  protected AuthType authType;
-  protected Set<DownloadScheme> downloadSchemes;
-  protected Set<DownloadCommand> downloadCommands;
-  protected String gitDaemonUrl;
-  protected String gitHttpUrl;
-  protected String sshdAddress;
-  protected String editFullNameUrl;
-  protected Project.NameKey wildProject;
-  protected Set<Account.FieldName> editableAccountFields;
-  protected boolean documentationAvailable;
-  protected String anonymousCowardName;
-  protected int suggestFrom;
-  protected int changeUpdateDelay;
-  protected List<String> archiveFormats;
-  protected int largeChangeSize;
-  protected String replyLabel;
-  protected String replyTitle;
-  protected boolean allowDraftChanges;
-
-  public String getLoginUrl() {
-    return loginUrl;
-  }
-
-  public void setLoginUrl(final String u) {
-    loginUrl = u;
-  }
-
-  public String getLoginText() {
-    return loginText;
-  }
-
-  public void setLoginText(String signinText) {
-    this.loginText = signinText;
-  }
-
-  public String getRegisterUrl() {
-    return registerUrl;
-  }
-
-  public void setRegisterUrl(final String u) {
-    registerUrl = u;
-  }
-
-  public String getSwitchAccountUrl() {
-    return switchAccountUrl;
-  }
-
-  public void setSwitchAccountUrl(String u) {
-    switchAccountUrl = u;
-  }
-
-  public String getRegisterText() {
-    return registerText;
-  }
-
-  public void setRegisterText(final String t) {
-    registerText = t;
-  }
-
-  public String getReportBugUrl() {
-    return reportBugUrl;
-  }
-
-  public void setReportBugUrl(String u) {
-    reportBugUrl = u;
-  }
-
-  public String getReportBugText() {
-    return reportBugText;
-  }
-
-  public void setReportBugText(String t) {
-    reportBugText = t;
-  }
-
-  public boolean isHttpPasswordSettingsEnabled() {
-    return httpPasswordSettingsEnabled;
-  }
-
-  public void setHttpPasswordSettingsEnabled(boolean httpPasswordSettingsEnabled) {
-    this.httpPasswordSettingsEnabled = httpPasswordSettingsEnabled;
-  }
-
-  public String getEditFullNameUrl() {
-    return editFullNameUrl;
-  }
-
-  public void setEditFullNameUrl(String u) {
-    editFullNameUrl = u;
-  }
-
-  public String getHttpPasswordUrl() {
-    return httpPasswordUrl;
-  }
-
-  public void setHttpPasswordUrl(String url) {
-    httpPasswordUrl = url;
-  }
-
-  public AuthType getAuthType() {
-    return authType;
-  }
-
-  public void setAuthType(final AuthType t) {
-    authType = t;
-  }
-
-  public Set<DownloadScheme> getDownloadSchemes() {
-    return downloadSchemes;
-  }
-
-  public void setDownloadSchemes(final Set<DownloadScheme> s) {
-    downloadSchemes = s;
-  }
-
-  public Set<DownloadCommand> getDownloadCommands() {
-    return downloadCommands;
-  }
-
-  public void setDownloadCommands(final Set<DownloadCommand> downloadCommands) {
-    this.downloadCommands = downloadCommands;
-  }
-
-  public GitwebConfig getGitwebLink() {
-    return gitweb;
-  }
-
-  public void setGitwebLink(final GitwebConfig w) {
-    gitweb = w;
-  }
-
-  public boolean isUseContributorAgreements() {
-    return useContributorAgreements;
-  }
-
-  public void setUseContributorAgreements(final boolean r) {
-    useContributorAgreements = r;
-  }
-
-  public boolean isUseContactInfo() {
-    return useContactInfo;
-  }
-
-  public void setUseContactInfo(final boolean r) {
-    useContactInfo = r;
-  }
-
-  public String getGitDaemonUrl() {
-    return gitDaemonUrl;
-  }
-
-  public void setGitDaemonUrl(String url) {
-    if (url != null && !url.endsWith("/")) {
-      url += "/";
-    }
-    gitDaemonUrl = url;
-  }
-
-  public String getGitHttpUrl() {
-    return gitHttpUrl;
-  }
-
-  public void setGitHttpUrl(String url) {
-    if (url != null && !url.endsWith("/")) {
-      url += "/";
-    }
-    gitHttpUrl = url;
-  }
-
-  public String getSshdAddress() {
-    return sshdAddress;
-  }
-
-  public void setSshdAddress(final String addr) {
-    sshdAddress = addr;
-  }
-
-  public Project.NameKey getWildProject() {
-    return wildProject;
-  }
-
-  public void setWildProject(final Project.NameKey wp) {
-    wildProject = wp;
-  }
-
-  public boolean canEdit(final Account.FieldName f) {
-    return editableAccountFields.contains(f);
-  }
-
-  public Set<Account.FieldName> getEditableAccountFields() {
-    return editableAccountFields;
-  }
-
-  public void setEditableAccountFields(final Set<Account.FieldName> af) {
-    editableAccountFields = af;
-  }
-
-  public boolean isDocumentationAvailable() {
-    return documentationAvailable;
-  }
-
-  public void setDocumentationAvailable(final boolean available) {
-    documentationAvailable = available;
-  }
-
-  public String getAnonymousCowardName() {
-    return anonymousCowardName;
-  }
-
-  public void setAnonymousCowardName(final String anonymousCowardName) {
-    this.anonymousCowardName = anonymousCowardName;
-  }
-
-  public int getSuggestFrom() {
-    return suggestFrom;
-  }
-
-  public void setSuggestFrom(final int suggestFrom) {
-    this.suggestFrom = suggestFrom;
-  }
-
-  public boolean siteHasUsernames() {
-    if (getAuthType() == AuthType.CUSTOM_EXTENSION
-        && getHttpPasswordUrl() != null
-        && !canEdit(FieldName.USER_NAME)) {
-      return false;
-    }
-    return true;
-  }
-
-  public int getChangeUpdateDelay() {
-    return changeUpdateDelay;
-  }
-
-  public void setChangeUpdateDelay(int seconds) {
-    changeUpdateDelay = seconds;
-  }
-
-  public int getLargeChangeSize() {
-    return largeChangeSize;
-  }
-
-  public void setLargeChangeSize(int largeChangeSize) {
-    this.largeChangeSize = largeChangeSize;
-  }
-
-  public List<String> getArchiveFormats() {
-    return archiveFormats;
-  }
-
-  public void setArchiveFormats(List<String> formats) {
-    archiveFormats = formats;
-  }
-
-  public String getReplyTitle() {
-    return replyTitle;
-  }
-
-  public void setReplyTitle(String r) {
-    replyTitle = r;
-  }
-
-  public String getReplyLabel() {
-    return replyLabel;
-  }
-
-  public void setReplyLabel(String r) {
-    replyLabel = r;
-  }
-
-  public boolean isAllowDraftChanges() {
-    return allowDraftChanges;
-  }
-
-  public void setAllowDraftChanges(boolean b) {
-    allowDraftChanges = b;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
deleted file mode 100644
index 8219d27..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
+++ /dev/null
@@ -1,279 +0,0 @@
-// 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.
-
-package com.google.gerrit.common.data;
-
-/** Class to store information about different gitweb types. */
-public class GitWebType {
-  /**
-   * Get a GitWebType based on the given name.
-   *
-   * @param name Name to look for.
-   * @return GitWebType from the given name, else null if not found.
-   */
-  public static GitWebType fromName(final String name) {
-    final GitWebType type;
-
-    if (name == null || name.isEmpty() || name.equalsIgnoreCase("gitweb")) {
-      type = new GitWebType();
-      type.setLinkName("gitweb");
-      type.setProject("?p=${project}.git;a=summary");
-      type.setRevision("?p=${project}.git;a=commit;h=${commit}");
-      type.setBranch("?p=${project}.git;a=shortlog;h=${branch}");
-      type.setRootTree("?p=${project}.git;a=tree;hb=${commit}");
-      type.setFile("?p=${project}.git;hb=${commit};f=${file}");
-      type.setFileHistory("?p=${project}.git;a=history;hb=${branch};f=${file}");
-    } else if (name.equalsIgnoreCase("cgit")) {
-      type = new GitWebType();
-      type.setLinkName("cgit");
-      type.setProject("${project}.git/summary");
-      type.setRevision("${project}.git/commit/?id=${commit}");
-      type.setBranch("${project}.git/log/?h=${branch}");
-      type.setRootTree("${project}.git/tree/?h=${commit}");
-      type.setFile("${project}.git/tree/${file}?h=${commit}");
-      type.setFileHistory("${project}.git/log/${file}?h=${branch}");
-    } else if (name.equalsIgnoreCase("custom")) {
-      type = new GitWebType();
-      // The custom name is not defined, let's keep the old style of using GitWeb
-      type.setLinkName("gitweb");
-
-    } else if (name.equalsIgnoreCase("disabled")) {
-      type = null;
-
-    } else {
-      type = null;
-    }
-
-    return type;
-  }
-
-  /** name of the type. */
-  private String name;
-
-  /** String for revision view url. */
-  private String revision;
-
-  /** ParameterizedString for project view url. */
-  private String project;
-
-  /** ParameterizedString for branch view url. */
-  private String branch;
-
-  /** ParameterizedString for root tree view url. */
-  private String rootTree;
-
-  /** ParameterizedString for file view url. */
-  private String file;
-
-  /** ParameterizedString for file history view url. */
-  private String fileHistory;
-
-  /** Character to substitute the standard path separator '/' in branch and
-    * project names */
-  private char pathSeparator = '/';
-
-  /** Whether to include links to draft patch sets */
-  private boolean linkDrafts;
-
-  /** Whether to encode URL segments */
-  private boolean urlEncode;
-
-  /** Private default constructor for gson. */
-  protected GitWebType() {
-  }
-
-  /**
-   * Get the String for branch view.
-   *
-   * @return The String for branch view
-   */
-  public String getBranch() {
-    return branch;
-  }
-
-  /**
-   * Get the String for link-name of the type.
-   *
-   * @return The String for link-name of the type
-   */
-  public String getLinkName() {
-    return name;
-  }
-
-  /**
-   * Get the String for project view.
-   *
-   * @return The String for project view
-   */
-  public String getProject() {
-    return project;
-  }
-
-  /**
-   * Get the String for revision view.
-   *
-   * @return The String for revision view
-   */
-  public String getRevision() {
-    return revision;
-  }
-
-  /**
-   * Get the String for root tree view.
-   *
-   * @return The String for root tree view
-   */
-  public String getRootTree() {
-    return rootTree;
-  }
-
-  /**
-   * Get the String for file view.
-   *
-   * @return The String for file view
-   */
-  public String getFile() {
-    return file;
-  }
-
-  /**
-   * Get the String for file history view.
-   *
-   * @return The String for file history view
-   */
-  public String getFileHistory() {
-    return fileHistory;
-  }
-
-  /**
-   * Get whether to link to draft patch sets
-   *
-   * @return True to link
-   */
-  public boolean getLinkDrafts() {
-    return linkDrafts;
-  }
-
-  /**
-   * Set the pattern for branch view.
-   *
-   * @param pattern The pattern for branch view
-   */
-  public void setBranch(final String pattern) {
-    if (pattern != null && !pattern.isEmpty()) {
-      branch = pattern;
-    }
-  }
-
-  /**
-   * Set the pattern for link-name type.
-   *
-   * @param name The link-name type
-   */
-  public void setLinkName(final String name) {
-    if (name != null && !name.isEmpty()) {
-      this.name = name;
-    }
-  }
-
-  /**
-   * Set the pattern for project view.
-   *
-   * @param pattern The pattern for project view
-   */
-  public void setProject(final String pattern) {
-    if (pattern != null && !pattern.isEmpty()) {
-      project = pattern;
-    }
-  }
-
-  /**
-   * Set the pattern for revision view.
-   *
-   * @param pattern The pattern for revision view
-   */
-  public void setRevision(final String pattern) {
-    if (pattern != null && !pattern.isEmpty()) {
-      revision = pattern;
-    }
-  }
-
-  /**
-   * Set the pattern for root tree view.
-   *
-   * @param pattern The pattern for root tree view
-   */
-  public void setRootTree(final String pattern) {
-    if (pattern != null && !pattern.isEmpty()) {
-      rootTree = pattern;
-    }
-  }
-
-  /**
-   * Set the pattern for file view.
-   *
-   * @param pattern The pattern for file view
-   */
-  public void setFile(final String pattern) {
-    if (pattern != null && !pattern.isEmpty()) {
-      file = pattern;
-    }
-  }
-
-  /**
-   * Set the pattern for file history view.
-   *
-   * @param pattern The pattern for file history view
-   */
-  public void setFileHistory(final String pattern) {
-    if (pattern != null && !pattern.isEmpty()) {
-      fileHistory = pattern;
-    }
-  }
-
-  /**
-   * Replace the standard path separator ('/') in a branch name or project
-   * name with a custom path separator configured by the property
-   * gitweb.pathSeparator.
-   * @param urlSegment The branch or project to replace the path separator in
-   * @return the urlSegment with the standard path separator replaced by the
-   * custom path separator
-   */
-  public String replacePathSeparator(String urlSegment) {
-    if ('/' != pathSeparator) {
-      return urlSegment.replace('/', pathSeparator);
-    }
-    return urlSegment;
-  }
-
-  /**
-   * Set the custom path separator
-   * @param separator The custom path separator
-   */
-  public void setPathSeparator(char separator) {
-    this.pathSeparator = separator;
-  }
-
-  public void setLinkDrafts(boolean linkDrafts) {
-    this.linkDrafts = linkDrafts;
-  }
-
-  public boolean isUrlEncode() {
-    return urlEncode;
-  }
-
-  public void setUrlEncode(boolean urlEncode) {
-    this.urlEncode = urlEncode;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebConfig.java
deleted file mode 100644
index 0503c60..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebConfig.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-/** Link to an external gitweb server. */
-public class GitwebConfig {
-  public String baseUrl;
-  public GitWebType type;
-
-  protected GitwebConfig() {
-  }
-
-  public GitwebConfig(final String base, final GitWebType gitWebType) {
-    baseUrl = base;
-    type = gitWebType;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
new file mode 100644
index 0000000..7cdec2f
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebType.java
@@ -0,0 +1,186 @@
+// 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.
+
+package com.google.gerrit.common.data;
+
+/** Class to store information about different source browser types. */
+public class GitwebType {
+  private String name;
+
+  private String branch;
+  private String file;
+  private String fileHistory;
+  private String project;
+  private String revision;
+  private String rootTree;
+
+  private char pathSeparator = '/';
+  private boolean linkDrafts = true;
+  private boolean urlEncode = true;
+
+  /** @return name displayed in links. */
+  public String getLinkName() {
+    return name;
+  }
+
+  /**
+   * Set the name displayed in links.
+   *
+   * @param name new name.
+   */
+  public void setLinkName(String name) {
+    this.name = name;
+  }
+
+  /** @return parameterized string for the branch URL. */
+  public String getBranch() {
+    return branch;
+  }
+
+  /**
+   * Set the parameterized string for the branch URL.
+   *
+   * @param str new string.
+   */
+  public void setBranch(String str) {
+    branch = str;
+  }
+
+  /** @return parameterized string for the file URL. */
+  public String getFile() {
+    return file;
+  }
+
+  /**
+   * Set the parameterized string for the file URL.
+   *
+   * @param str new string.
+   */
+  public void setFile(String str) {
+    file = str;
+  }
+
+  /** @return parameterized string for the file history URL. */
+  public String getFileHistory() {
+    return fileHistory;
+  }
+
+  /**
+   * Set the parameterized string for the file history URL.
+   *
+   * @param str new string.
+   */
+  public void setFileHistory(String str) {
+    fileHistory = str;
+  }
+
+  /** @return parameterized string for the project URL. */
+  public String getProject() {
+    return project;
+  }
+
+  /**
+   * Set the parameterized string for the project URL.
+   *
+   * @param str new string.
+   */
+  public void setProject(String str) {
+    project = str;
+  }
+
+  /** @return parameterized string for the revision URL. */
+  public String getRevision() {
+    return revision;
+  }
+
+  /**
+   * Set the parameterized string for the revision URL.
+   *
+   * @param str new string.
+   */
+  public void setRevision(String str) {
+    revision = str;
+  }
+
+  /** @return parameterized string for the root tree URL. */
+  public String getRootTree() {
+    return rootTree;
+  }
+
+  /**
+   * Set the parameterized string for the root tree URL.
+   *
+   * @param str new string.
+   */
+  public void setRootTree(String str) {
+    rootTree = str;
+  }
+
+  /** @return path separator used for branch and project names. */
+  public char getPathSeparator() {
+    return pathSeparator;
+  }
+
+  /**
+   * Set the custom path separator.
+   *
+   * @param separator new separator.
+   */
+  public void setPathSeparator(char separator) {
+    this.pathSeparator = separator;
+  }
+
+  /** @return whether to generate links to draft patch sets. */
+  public boolean getLinkDrafts() {
+    return linkDrafts;
+  }
+
+  /**
+   * Set whether to generate links to draft patch sets.
+   *
+   * @param linkDrafts new value.
+   */
+  public void setLinkDrafts(boolean linkDrafts) {
+    this.linkDrafts = linkDrafts;
+  }
+
+  /** @return whether to URL encode path segments. */
+  public boolean getUrlEncode() {
+    return urlEncode;
+  }
+
+  /**
+   * Set whether to URL encode path segments.
+   *
+   * @param urlEncode new value.
+   */
+  public void setUrlEncode(boolean urlEncode) {
+    this.urlEncode = urlEncode;
+  }
+
+  /**
+   * Replace standard path separator with custom configured path separator.
+   *
+   * @param urlSegment URL segment (e.g. branch or project name) in which to
+   *     replace the path separator.
+   * @return the segment with the standard path separator replaced by the custom
+   *   {@link #getPathSeparator()}.
+   */
+  public String replacePathSeparator(String urlSegment) {
+    if ('/' != pathSeparator) {
+      return urlSegment.replace('/', pathSeparator);
+    }
+    return urlSegment;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index d50e754..31c2481 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -71,6 +71,16 @@
   /** Can terminate any task using the kill command. */
   public static final String KILL_TASK = "killTask";
 
+  /**
+   * Can perform limited server maintenance.
+   * <p>
+   * Includes tasks such as reindexing changes and flushing caches that may need
+   * to be performed regularly. Does <strong>not</strong> grant arbitrary
+   * read/write/ACL management permissions as does {@link
+   * #ADMINISTRATE_SERVER}.
+   */
+  public static final String MAINTAIN_SERVER = "maintainServer";
+
   /** Can modify any account on the server. */
   public static final String MODIFY_ACCOUNT = "modifyAccount";
 
@@ -121,6 +131,7 @@
     NAMES_ALL.add(EMAIL_REVIEWERS);
     NAMES_ALL.add(FLUSH_CACHES);
     NAMES_ALL.add(KILL_TASK);
+    NAMES_ALL.add(MAINTAIN_SERVER);
     NAMES_ALL.add(MODIFY_ACCOUNT);
     NAMES_ALL.add(PRIORITY);
     NAMES_ALL.add(QUERY_LIMIT);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
index c261fdd..3362ba2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -27,6 +27,14 @@
     return new GroupReference(group.getGroupUUID(), group.getName());
   }
 
+  public static GroupReference fromString(String ref) {
+    String name =
+        ref.substring(ref.indexOf("[") + 1, ref.lastIndexOf("/")).trim();
+    String uuid =
+        ref.substring(ref.lastIndexOf("/") + 1, ref.lastIndexOf("]")).trim();
+    return new GroupReference(new AccountGroup.UUID(uuid), name);
+  }
+
   protected String uuid;
   protected String name;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index 430c23c..16e7e61 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -14,22 +14,31 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 
 import java.util.Date;
 import java.util.List;
 
 /** Data sent as part of the host page, to bootstrap the UI. */
 public class HostPageData {
+  /**
+   * Name of the cookie in which the XSRF token is sent from the server to the
+   * client during host page bootstrapping.
+   */
+  public static final String XSRF_COOKIE_NAME = "XSRF_TOKEN";
+
+  /**
+   * Name of the HTTP header in which the client must send the XSRF token to the
+   * server on each request.
+   */
+  public static final String XSRF_HEADER_NAME = "X-Gerrit-Auth";
+
   public String version;
-  public Account account;
-  public AccountDiffPreference accountDiffPref;
-  public String xGerritAuth;
-  public GerritConfig config;
+  public DiffPreferencesInfo accountDiffPref;
   public Theme theme;
   public List<String> plugins;
   public List<Message> messages;
+  public Integer pluginsLoadTimeout;
   public boolean isNoteDbEnabled;
 
   public static class Theme {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index 4ed296f..a92af2b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -21,7 +21,7 @@
 import java.util.List;
 import java.util.Map;
 
-/** Performs replacements on strings such as {@code Hello ${user}}. */
+/** Performs replacements on strings such as <code>Hello ${user}</code>. */
 public class ParameterizedString {
   /** Obtain a string which has no parameters and always produces the value. */
   public static ParameterizedString asis(final String constant) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
index 19fcbeb..d9601f0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -27,5 +27,5 @@
 public interface PatchDetailService extends RemoteJsonService {
   @Audit
   void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
-      AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
+      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchScript> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 046df1d..f23afb1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -42,7 +42,7 @@
   protected FileMode oldMode;
   protected FileMode newMode;
   protected List<String> header;
-  protected AccountDiffPreference diffPrefs;
+  protected DiffPreferencesInfo diffPrefs;
   protected SparseFileContent a;
   protected SparseFileContent b;
   protected List<Edit> edits;
@@ -57,15 +57,18 @@
   protected boolean intralineFailure;
   protected boolean intralineTimeout;
   protected boolean binary;
+  protected transient String commitIdA;
+  protected transient String commitIdB;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
-      final List<String> h, final AccountDiffPreference dp,
+      final List<String> h, final DiffPreferencesInfo dp,
       final SparseFileContent ca, final SparseFileContent cb,
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final String mta, final String mtb, final CommentDetail cd,
       final List<Patch> hist, final boolean hf, final boolean id,
-      final boolean idf, final boolean idt, boolean bin) {
+      final boolean idf, final boolean idt, boolean bin,
+      final String cma, final String cmb) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -88,6 +91,8 @@
     intralineFailure = idf;
     intralineTimeout = idt;
     binary = bin;
+    commitIdA = cma;
+    commitIdB = cmb;
   }
 
   protected PatchScript() {
@@ -137,11 +142,11 @@
     return history;
   }
 
-  public AccountDiffPreference getDiffPrefs() {
+  public DiffPreferencesInfo getDiffPrefs() {
     return diffPrefs;
   }
 
-  public void setDiffPrefs(AccountDiffPreference dp) {
+  public void setDiffPrefs(DiffPreferencesInfo dp) {
     diffPrefs = dp;
   }
 
@@ -150,7 +155,7 @@
   }
 
   public boolean isIgnoreWhitespace() {
-    return diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE;
+    return diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE;
   }
 
   public boolean hasIntralineDifference() {
@@ -166,7 +171,7 @@
   }
 
   public boolean isExpandAllComments() {
-    return diffPrefs.isExpandAllComments();
+    return diffPrefs.expandAllComments;
   }
 
   public SparseFileContent getA() {
@@ -190,8 +195,8 @@
   }
 
   public Iterable<EditList.Hunk> getHunks() {
-    int ctx = diffPrefs.getContext();
-    if (ctx == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+    int ctx = diffPrefs.context;
+    if (ctx == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
       ctx = Math.max(a.size(), b.size());
     }
     return new EditList(edits, ctx, a.size(), b.size()).getHunks();
@@ -200,4 +205,12 @@
   public boolean isBinary() {
     return binary;
   }
+
+  public String getCommitIdA() {
+    return commitIdA;
+  }
+
+  public String getCommitIdB() {
+    return commitIdB;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
index 7be8e4e..8d09b88 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
@@ -125,11 +125,15 @@
       r.append(' ');
     } else {
       if (getMin() != getMax()) {
-        if (0 <= getMin()) r.append('+');
+        if (0 <= getMin()) {
+          r.append('+');
+        }
         r.append(getMin());
         r.append("..");
       }
-      if (0 <= getMax()) r.append('+');
+      if (0 <= getMax()) {
+        r.append('+');
+      }
       r.append(getMax());
       r.append(' ');
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index 3ba7adf..ec5ca06 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -126,8 +126,12 @@
   @Override
   public int compareTo(PermissionRule o) {
     int cmp = action(this) - action(o);
-    if (cmp == 0) cmp = range(o) - range(this);
-    if (cmp == 0) cmp = group(this).compareTo(group(o));
+    if (cmp == 0) {
+      cmp = range(o) - range(this);
+    }
+    if (cmp == 0) {
+      cmp = group(this).compareTo(group(o));
+    }
     return cmp;
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index 7041dcc..a83f46c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -32,6 +32,7 @@
   protected LabelTypes labelTypes;
   protected Map<String, String> capabilities;
   protected Map<AccountGroup.UUID, GroupInfo> groupInfo;
+  protected List<WebLinkInfoCommon> fileHistoryLinks;
 
   public ProjectAccess() {
   }
@@ -132,4 +133,12 @@
   public void setGroupInfo(Map<AccountGroup.UUID, GroupInfo> m) {
     groupInfo = m;
   }
+
+  public void setFileHistoryLinks(List<WebLinkInfoCommon> links) {
+    fileHistoryLinks = links;
+  }
+
+  public List<WebLinkInfoCommon> getFileHistoryLinks() {
+    return fileHistoryLinks;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 5e80ac5..272801f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -33,6 +33,4 @@
   void contributorAgreements(AsyncCallback<List<ContributorAgreement>> callback);
 
   void clientError(String message, AsyncCallback<VoidResult> callback);
-
-  public void gerritConfig(final AsyncCallback<GerritConfig> callback);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
similarity index 68%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-common/src/main/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
index 4413603..dd0a70a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.common.data;
 
-import com.google.gwt.core.client.JsArray;
+public class WebLinkInfoCommon {
+  public WebLinkInfoCommon() {}
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+  public String name;
+  public String imageUrl;
+  public String url;
+  public String target;
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/ContactInformationStoreException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/ContactInformationStoreException.java
deleted file mode 100644
index 956d010..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/ContactInformationStoreException.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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.
-
-package com.google.gerrit.common.errors;
-
-/** Error indicating the server cannot store contact information. */
-public class ContactInformationStoreException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Cannot store contact information";
-
-  public ContactInformationStoreException() {
-    super(MESSAGE);
-  }
-
-  public ContactInformationStoreException(final Throwable why) {
-    super(MESSAGE, why);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/ProjectCreationFailedException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/ProjectCreationFailedException.java
deleted file mode 100644
index 0437d77..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/ProjectCreationFailedException.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2011 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.common.errors;
-
-/** Error indicating failed to create new project. */
-public class ProjectCreationFailedException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public ProjectCreationFailedException(final String message) {
-    this(message, null);
-  }
-
-  public ProjectCreationFailedException(final String message,
-      final Throwable why) {
-    super(message, why);
-  }
-}
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
index 816f715..ea3721e 100644
--- a/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
+++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
@@ -19,22 +19,16 @@
 import org.junit.Test;
 
 public class EncodePathSeparatorTest {
-
   @Test
   public void testDefaultBehaviour() {
-
-    GitWebType gitWebType = GitWebType.fromName(null);
-
-    assertEquals("a/b", gitWebType.replacePathSeparator("a/b"));
+    assertEquals("a/b", new GitwebType().replacePathSeparator("a/b"));
   }
 
   @Test
   public void testExclamationMark() {
-
-    GitWebType gitWebType = GitWebType.fromName(null);
-    gitWebType.setPathSeparator('!');
-
-    assertEquals("a!b", gitWebType.replacePathSeparator("a/b"));
+    GitwebType gitwebType = new GitwebType();
+    gitwebType.setPathSeparator('!');
+    assertEquals("a!b", gitwebType.replacePathSeparator("a/b"));
   }
 
 }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 77a6b13..67bfdc1 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -28,6 +28,7 @@
   exported_deps = [
     ':api',
     '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
     '//lib:servlet-api-3_1',
   ],
@@ -37,7 +38,10 @@
 java_library(
   name = 'api',
   srcs = glob([SRC + '**/*.java']),
-  provided_deps = ['//lib/guice:guice'],
+  provided_deps = [
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+  ],
   visibility = ['PUBLIC'],
 )
 
@@ -47,6 +51,17 @@
   visibility = ['PUBLIC'],
 )
 
+java_test(
+  name = 'api_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':api',
+    '//lib:truth',
+    '//lib/guice:guice',
+  ],
+  source_under_test = [':api'],
+)
+
 java_doc(
   name = 'extension-api-javadoc',
   title = 'Gerrit Review Extension API Documentation',
@@ -55,7 +70,8 @@
   srcs = SRCS,
   deps = [
     '//lib/guice:javax-inject',
-    '//lib/guice:guice_library'
+    '//lib/guice:guice_library',
+    '//lib/guice:guice-assistedinject',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 6eafda3..413aedc 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,11 +2,11 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <licenses>
     <license>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
index 75238a8..4893beff 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
@@ -18,24 +18,25 @@
 
 import com.google.inject.BindingAnnotation;
 
-import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
 
 /**
  * Local path where a plugin can store its own private data.
  * <p>
  * A plugin or extension may receive this string by Guice injection to discover
  * a directory where it can store configuration or other data that is private:
+ * <p>
+ * This binding is on both {@link java.io.File} and {@link java.nio.file.Path},
+ * pointing to the same location. The {@code File} version should be considered
+ * deprecated and may be removed in a future version.
  *
  * <pre>
  * {@literal @Inject}
- * MyType(@PluginData java.io.File myDir) {
- *   new FileInputStream(new File(myDir, &quot;my.config&quot;));
+ * MyType(@PluginData java.nio.file.Path myDir) {
+ *   this.in = Files.newInputStream(myDir.resolve(&quot;my.config&quot;));
  * }
  * </pre>
  */
-@Target({ElementType.PARAMETER, ElementType.FIELD})
 @Retention(RUNTIME)
 @BindingAnnotation
 public @interface PluginData {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java
new file mode 100644
index 0000000..8f97d77
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation on {@code com.google.gerrit.sshd.SshCommand} or
+ * {@code com.google.gerrit.httpd.restapi.RestApiServlet} declaring a set of
+ * capabilities of which at least one must be granted.
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface RequiresAnyCapability {
+  /** Capabilities at least one of which is required to invoke this action. */
+  String[] value();
+
+  /** Scope of the named capabilities. */
+  CapabilityScope scope() default CapabilityScope.CONTEXT;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index a14779a..511ae0c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -28,7 +28,7 @@
 @Target({ElementType.TYPE})
 @Retention(RUNTIME)
 public @interface RequiresCapability {
-  /**  Name of the capability required to invoke this action. */
+  /** Name of the capability required to invoke this action. */
   String value();
 
   /** Scope of the named capability. */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
index cc5807b..8b9f520 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
@@ -16,12 +16,16 @@
 
 import com.google.gerrit.extensions.api.accounts.Accounts;
 import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.api.config.Config;
+import com.google.gerrit.extensions.api.groups.Groups;
 import com.google.gerrit.extensions.api.projects.Projects;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 
 public interface GerritApi {
   public Accounts accounts();
   public Changes changes();
+  public Config config();
+  public Groups groups();
   public Projects projects();
 
   /**
@@ -40,6 +44,16 @@
     }
 
     @Override
+    public Config config() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Groups groups() {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Projects projects() {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index d571cfd..a356ab6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -15,14 +15,24 @@
 package com.google.gerrit.extensions.api.accounts;
 
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.List;
+import java.util.Map;
+
 public interface AccountApi {
   AccountInfo get() throws RestApiException;
 
   void starChange(String id) throws RestApiException;
   void unstarChange(String id) throws RestApiException;
+  void addEmail(EmailInput input) throws RestApiException;
+
+  Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException;
+  Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove)
+      throws RestApiException;
+  GpgKeyApi gpgKey(String id) throws RestApiException;
 
   /**
    * A default implementation which allows source compatibility
@@ -43,5 +53,26 @@
     public void unstarChange(String id) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void addEmail(EmailInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, GpgKeyInfo> putGpgKeys(List<String> add,
+        List<String> remove) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public GpgKeyApi gpgKey(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 71a93d3..32f8488 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.List;
+
 public interface Accounts {
   /**
    * Look up an account by ID.
@@ -42,6 +45,69 @@
   AccountApi self() throws RestApiException;
 
   /**
+   * Suggest users for a given query.
+   * <p>
+   * Example code:
+   * {@code suggestAccounts().withQuery("Reviewer").withLimit(5).get()}
+   *
+   * @return API for setting parameters and getting result.
+   */
+  SuggestAccountsRequest suggestAccounts() throws RestApiException;
+
+  /**
+   * Suggest users for a given query.
+   * <p>
+   * Shortcut API for {@code suggestAccounts().withQuery(String)}.
+   *
+   * @see #suggestAccounts()
+   */
+  SuggestAccountsRequest suggestAccounts(String query)
+    throws RestApiException;
+
+  /**
+   * API for setting parameters and getting result.
+   * Used for {@code suggestAccounts()}.
+   *
+   * @see #suggestAccounts()
+   */
+  public abstract class SuggestAccountsRequest {
+    private String query;
+    private int limit;
+
+    /**
+     * Executes query and returns a list of accounts.
+     */
+    public abstract List<AccountInfo> get() throws RestApiException;
+
+    /**
+     * Set query.
+     *
+     * @param query needs to be in human-readable form.
+     */
+    public SuggestAccountsRequest withQuery(String query) {
+      this.query = query;
+      return this;
+    }
+
+    /**
+     * Set limit for returned list of accounts.
+     * Optional; server-default is used when not provided.
+     */
+    public SuggestAccountsRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public String getQuery() {
+      return query;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+  }
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -55,5 +121,16 @@
     public AccountApi self() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public SuggestAccountsRequest suggestAccounts() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SuggestAccountsRequest suggestAccounts(String query)
+      throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/EmailInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/EmailInput.java
new file mode 100644
index 0000000..5036731
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/EmailInput.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 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.extensions.api.accounts;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+/** This entity contains information for registering a new email address. */
+public class EmailInput {
+  /* The email address. If provided, must match the email address from the URL. */
+  @DefaultInput
+  public String email;
+
+  /* Whether the new email address should become the preferred email address of
+   * the user. Only supported if {@link #noConfirmation} is set or if the
+   * authentication type is DEVELOPMENT_BECOME_ANY_ACCOUNT.*/
+  public boolean preferred;
+
+  /* Whether the email address should be added without confirmation. In this
+   * case no verification email is sent to the user. Only Gerrit administrators
+   * are allowed to add email addresses without confirmation. */
+  public boolean noConfirmation;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
new file mode 100644
index 0000000..ffdcf87
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 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.extensions.api.accounts;
+
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface GpgKeyApi {
+  GpgKeyInfo get() throws RestApiException;
+  void delete() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   */
+  public class NotImplemented implements GpgKeyApi {
+    @Override
+    public GpgKeyInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void delete() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 06f0a75..ce07098 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -23,6 +24,7 @@
 
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public interface ChangeApi {
@@ -76,6 +78,8 @@
    */
   ChangeApi revert(RevertInput in) throws RestApiException;
 
+  List<ChangeInfo> submittedTogether() throws RestApiException;
+
   String topic() throws RestApiException;
   void topic(String topic) throws RestApiException;
 
@@ -106,6 +110,24 @@
    */
   Set<String> getHashtags() throws RestApiException;
 
+  /**
+   * Get all published comments on a change.
+   *
+   * @return comments in a map keyed by path; comments have the {@code revision}
+   *     field set to indicate their patch set.
+   * @throws RestApiException
+   */
+  Map<String, List<CommentInfo>> comments() throws RestApiException;
+
+  /**
+   * Get all draft comments for the current user on a change.
+   *
+   * @return drafts in a map keyed by path; comments have the {@code revision}
+   *     field set to indicate their patch set.
+   * @throws RestApiException
+   */
+  Map<String, List<CommentInfo>> drafts() throws RestApiException;
+
   ChangeInfo check() throws RestApiException;
   ChangeInfo check(FixInput fix) throws RestApiException;
 
@@ -250,6 +272,16 @@
     }
 
     @Override
+    public Map<String, List<CommentInfo>> comments() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, List<CommentInfo>> drafts() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeInfo check() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -258,5 +290,10 @@
     public ChangeInfo check(FixInput fix) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public List<ChangeInfo> submittedTogether() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index 8ab3080..da8aeb2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -116,6 +116,23 @@
     public EnumSet<ListChangesOption> getOptions() {
       return options;
     }
+
+    @Override
+    public String toString() {
+      StringBuilder sb =  new StringBuilder(getClass().getSimpleName())
+          .append('{')
+          .append(query);
+      if (limit != 0) {
+        sb.append(", limit=").append(limit);
+      }
+      if (start != 0) {
+        sb.append(", start=").append(start);
+      }
+      if (!options.isEmpty()) {
+        sb.append("options=").append(options);
+      }
+      return sb.append('}').toString();
+    }
   }
 
   /**
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
index c8856e7..fa8c1ef 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
@@ -16,4 +16,6 @@
 
 public class FixInput {
   public boolean deletePatchSetIfCommitMissing;
+
+  public String expectMergedAs;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
index bf84ccb0..c007161 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
@@ -22,4 +22,11 @@
   @DefaultInput
   public Set<String> add;
   public Set<String> remove;
+
+  public HashtagsInput(){
+  }
+
+  public HashtagsInput(Set<String> add) {
+    this.add = add;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index dd2ce92..ee043eb 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -21,7 +21,7 @@
 import java.util.List;
 import java.util.Map;
 
-/** Input passed to {@code POST /changes/{id}/revisions/{id}/review}. */
+/** Input passed to {@code POST /changes/[id]/revisions/[id]/review}. */
 public class ReviewInput {
   @DefaultInput
   public String message;
@@ -49,6 +49,12 @@
   public NotifyHandling notify = NotifyHandling.ALL;
 
   /**
+   * If true check to make sure that the comments being posted aren't already
+   * present.
+   */
+  public boolean omitDuplicateComments;
+
+  /**
    * Account ID, name, email address or username of another user. The review
    * will be posted/updated on behalf of this named user instead of the
    * caller. Caller must have the labelAs-$NAME permission granted for each
@@ -61,7 +67,17 @@
   public String onBehalfOf;
 
   public static enum DraftHandling {
-    DELETE, PUBLISH, KEEP
+    /** Delete pending drafts on this revision only. */
+    DELETE,
+
+    /** Publish pending drafts on this revision only. */
+    PUBLISH,
+
+    /** Leave pending drafts alone. */
+    KEEP,
+
+    /** Publish pending drafts on all revisions. */
+    PUBLISH_ALL_REVISIONS
   }
 
   public static enum NotifyHandling {
@@ -106,6 +122,10 @@
     return new ReviewInput().label("Code-Review", -1);
   }
 
+  public static ReviewInput noScore() {
+    return new ReviewInput().label("Code-Review", 0);
+  }
+
   public static ReviewInput approve() {
     return new ReviewInput().label("Code-Review", 2);
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index b940cc9..44c2ba4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -28,7 +30,6 @@
   void delete() throws RestApiException;
   void review(ReviewInput in) throws RestApiException;
 
-  /** {@code submit} with {@link SubmitInput#waitForMerge} set to true. */
   void submit() throws RestApiException;
   void submit(SubmitInput in) throws RestApiException;
   void publish() throws RestApiException;
@@ -49,12 +50,22 @@
   Map<String, List<CommentInfo>> comments() throws RestApiException;
   Map<String, List<CommentInfo>> drafts() throws RestApiException;
 
+  List<CommentInfo> commentsAsList() throws RestApiException;
+  List<CommentInfo> draftsAsList() throws RestApiException;
+
   DraftApi createDraft(DraftInput in) throws RestApiException;
   DraftApi draft(String id) throws RestApiException;
 
   CommentApi comment(String id) throws RestApiException;
 
   /**
+   * Returns patch of revision.
+   */
+  BinaryResult patch() throws RestApiException;
+
+  Map<String, ActionInfo> actions() throws RestApiException;
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -145,6 +156,16 @@
     }
 
     @Override
+    public List<CommentInfo> commentsAsList() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<CommentInfo> draftsAsList() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Map<String, List<CommentInfo>> drafts() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -163,5 +184,15 @@
     public CommentApi comment(String id) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public BinaryResult patch() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, ActionInfo> actions() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
index 742748e..4e08f8d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.api.changes;
 
 public class SubmitInput {
+  @Deprecated
   public boolean waitForMerge;
   public String onBehalfOf;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
new file mode 100644
index 0000000..348cf4b
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Config.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2015 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.extensions.api.config;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+
+public interface Config {
+  /**
+   * @return An API for getting server related configurations.
+   */
+  Server server();
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements Config {
+    @Override
+    public Server server() {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
new file mode 100644
index 0000000..c093b18
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 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.extensions.api.config;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface Server {
+  /**
+   * @return Version of server.
+   */
+  String getVersion() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements Server {
+    @Override
+    public String getVersion() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
new file mode 100644
index 0000000..f5a89c0
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2015 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.extensions.api.groups;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import java.util.List;
+
+public interface GroupApi {
+  /** @return group info with no {@code ListGroupsOption}s set. */
+  GroupInfo get() throws RestApiException;
+
+  /** @return group info with all {@code ListGroupsOption}s set. */
+  GroupInfo detail() throws RestApiException;
+
+  /** @return group name. */
+  String name() throws RestApiException;
+
+  /**
+   * Set group name.
+   *
+   * @param name new name.
+   * @throws RestApiException
+   */
+  void name(String name) throws RestApiException;
+
+  /** @return owning group info. */
+  GroupInfo owner() throws RestApiException;
+
+  /**
+   * Set group owner.
+   *
+   * @param owner identifier of new group owner.
+   * @throws RestApiException
+   */
+  void owner(String owner) throws RestApiException;
+
+  /** @return group description. */
+  String description() throws RestApiException;
+
+  /**
+   * Set group decsription.
+   *
+   * @param description new description.
+   * @throws RestApiException
+   */
+  void description(String description) throws RestApiException;
+
+  /** @return group options. */
+  GroupOptionsInfo options() throws RestApiException;
+
+  /**
+   * Set group options.
+   *
+   * @param options new options.
+   * @throws RestApiException
+   */
+  void options(GroupOptionsInfo options) throws RestApiException;
+
+  /**
+   * List group members, non-recursively.
+   *
+   * @return group members.
+   * @throws RestApiException
+   */
+  List<AccountInfo> members() throws RestApiException;
+
+  /**
+   * List group members.
+   *
+   * @param recursive whether to recursively included groups.
+   * @return group members.
+   * @throws RestApiException
+   */
+  List<AccountInfo> members(boolean recursive) throws RestApiException;
+
+  /**
+   * Add members to a group.
+   *
+   * @param members list of member identifiers, in any format accepted by
+   *     {@link com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
+   * @throws RestApiException
+   */
+  void addMembers(String... members) throws RestApiException;
+
+  /**
+   * Remove members from a group.
+   *
+   * @param members list of member identifiers, in any format accepted by
+   *     {@link com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
+   * @throws RestApiException
+   */
+  void removeMembers(String... members) throws RestApiException;
+
+  /**
+   * List included groups.
+   *
+   * @return included groups.
+   * @throws RestApiException
+   */
+  List<GroupInfo> includedGroups() throws RestApiException;
+
+  /**
+   * Add groups to be included in this one.
+   *
+   * @param groups list of group identifiers, in any format accepted by
+   *     {@link Groups#id(String)}
+   * @throws RestApiException
+   */
+  void addGroups(String... groups) throws RestApiException;
+
+  /**
+   * Remove included groups from this one.
+   *
+   * @param groups list of group identifiers, in any format accepted by
+   *     {@link Groups#id(String)}
+   * @throws RestApiException
+   */
+  void removeGroups(String... groups) throws RestApiException;
+
+  /**
+   * Returns the audit log of the group.
+   *
+   * @return list of audit events of the group.
+   * @throws RestApiException
+   */
+  List<? extends GroupAuditEventInfo> auditLog() throws RestApiException;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
similarity index 69%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
index 4413603..28665fe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.extensions.api.groups;
 
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+public class GroupInput {
+  public String name;
+  public String description;
+  public Boolean visibleToAll;
+  public String ownerId;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
new file mode 100644
index 0000000..b909f31
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -0,0 +1,177 @@
+// Copyright (C) 2015 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.extensions.api.groups;
+
+import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+public interface Groups {
+  /**
+   * Look up a group by ID.
+   * <p>
+   * <strong>Note:</strong> This method eagerly reads the group. Methods that
+   * mutate the group do not necessarily re-read the group. Therefore, calling a
+   * getter method on an instance after calling a mutation method on that same
+   * instance is not guaranteed to reflect the mutation. It is not recommended
+   * to store references to {@code groupApi} instances.
+   *
+   * @param id any identifier supported by the REST API, including group name or
+   *     UUID.
+   * @return API for accessing the group.
+   * @throws RestApiException if an error occurred.
+   */
+  GroupApi id(String id) throws RestApiException;
+
+  /** Create a new group with the given name and default options. */
+  GroupApi create(String name) throws RestApiException;
+
+  /** Create a new group. */
+  GroupApi create(GroupInput input) throws RestApiException;
+
+  /** @return new request for listing groups. */
+  ListRequest list();
+
+  public abstract class ListRequest {
+    private final EnumSet<ListGroupsOption> options =
+        EnumSet.noneOf(ListGroupsOption.class);
+    private final List<String> projects = new ArrayList<>();
+    private final List<String> groups = new ArrayList<>();
+
+    private boolean visibleToAll;
+    private String user;
+    private boolean owned;
+    private int limit;
+    private int start;
+    private String substring;
+    private String suggest;
+
+    public List<GroupInfo> get() throws RestApiException {
+      Map<String, GroupInfo> map = getAsMap();
+      List<GroupInfo> result = new ArrayList<>(map.size());
+      for (Map.Entry<String, GroupInfo> e : map.entrySet()) {
+        // ListGroups "helpfully" nulls out names when converting to a map.
+        e.getValue().name = e.getKey();
+        result.add(e.getValue());
+      }
+      return Collections.unmodifiableList(result);
+    }
+
+    public abstract Map<String, GroupInfo> getAsMap() throws RestApiException;
+
+    public ListRequest addOption(ListGroupsOption option) {
+      options.add(option);
+      return this;
+    }
+
+    public ListRequest addOptions(ListGroupsOption... options) {
+      return addOptions(Arrays.asList(options));
+    }
+
+    public ListRequest addOptions(Iterable<ListGroupsOption> options) {
+      for (ListGroupsOption option : options) {
+        this.options.add(option);
+      }
+      return this;
+    }
+
+    public ListRequest withProject(String project) {
+      projects.add(project);
+      return this;
+    }
+
+    public ListRequest addGroup(String uuid) {
+      groups.add(uuid);
+      return this;
+    }
+
+    public ListRequest withVisibleToAll(boolean visible) {
+      visibleToAll = visible;
+      return this;
+    }
+
+    public ListRequest withUser(String user) {
+      this.user = user;
+      return this;
+    }
+
+    public ListRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public ListRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public ListRequest withSubstring(String substring) {
+      this.substring = substring;
+      return this;
+    }
+
+    public ListRequest withSuggest(String suggest) {
+      this.suggest = suggest;
+      return this;
+    }
+
+    public EnumSet<ListGroupsOption> getOptions() {
+      return options;
+    }
+
+    public List<String> getProjects() {
+      return Collections.unmodifiableList(projects);
+    }
+
+    public List<String> getGroups() {
+      return Collections.unmodifiableList(groups);
+    }
+
+    public boolean getVisibleToAll() {
+      return visibleToAll;
+    }
+
+    public String getUser() {
+      return user;
+    }
+
+    public boolean getOwned() {
+      return owned;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+
+    public String getSubstring() {
+      return substring;
+    }
+
+    public String getSuggest() {
+      return suggest;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index f88a2cb..91cb70e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -20,6 +20,10 @@
 public interface BranchApi {
   BranchApi create(BranchInput in) throws RestApiException;
 
+  BranchInfo get() throws RestApiException;
+
+  void delete() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -29,5 +33,15 @@
     public BranchApi create(BranchInput in) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public BranchInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void delete() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
new file mode 100644
index 0000000..77513a2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2015 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.extensions.api.projects;
+
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+import java.util.List;
+import java.util.Map;
+
+public class BranchInfo extends RefInfo {
+  public Boolean canDelete;
+  public Map<String, ActionInfo> actions;
+  public List<WebLinkInfo> webLinks;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
new file mode 100644
index 0000000..a930f0d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 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.extensions.api.projects;
+
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface ChildProjectApi {
+  ProjectInfo get() throws RestApiException;
+  ProjectInfo get(boolean recursive) throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements ChildProjectApi {
+    @Override
+    public ProjectInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ProjectInfo get(boolean recursive) throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 07a48a1..e3eb4be 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -18,10 +18,67 @@
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.List;
+
 public interface ProjectApi {
   ProjectApi create() throws RestApiException;
   ProjectApi create(ProjectInput in) throws RestApiException;
-  ProjectInfo get();
+  ProjectInfo get() throws RestApiException;
+
+  String description() throws RestApiException;
+  void description(PutDescriptionInput in) throws RestApiException;
+
+  ListRefsRequest<BranchInfo> branches();
+  ListRefsRequest<TagInfo> tags();
+
+  public abstract class ListRefsRequest<T extends RefInfo> {
+    protected int limit;
+    protected int start;
+    protected String substring;
+    protected String regex;
+
+    public abstract List<T> get() throws RestApiException;
+
+    public ListRefsRequest<T> withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public ListRefsRequest<T> withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public ListRefsRequest<T> withSubstring(String substring) {
+      this.substring = substring;
+      return this;
+    }
+
+    public ListRefsRequest<T> withRegex(String regex) {
+      this.regex = regex;
+      return this;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+
+    public String getSubstring() {
+      return substring;
+    }
+
+    public String getRegex() {
+      return regex;
+    }
+  }
+
+  List<ProjectInfo> children() throws RestApiException;
+  List<ProjectInfo> children(boolean recursive) throws RestApiException;
+  ChildProjectApi child(String name) throws RestApiException;
 
   /**
    * Look up a branch by refname.
@@ -33,9 +90,19 @@
    * to store references to {@code BranchApi} instances.
    *
    * @param ref branch name, with or without "refs/heads/" prefix.
+   * @throws RestApiException if a problem occurred reading the project.
    * @return API for accessing the branch.
    */
-  BranchApi branch(String ref);
+  BranchApi branch(String ref) throws RestApiException;
+
+  /**
+   * Look up a tag by refname.
+   * <p>
+   * @param ref tag name, with or without "refs/tags/" prefix.
+   * @throws RestApiException if a problem occurred reading the project.
+   * @return API for accessing the tag.
+   */
+  TagApi tag(String ref) throws RestApiException;
 
   /**
    * A default implementation which allows source compatibility
@@ -53,12 +120,53 @@
     }
 
     @Override
-    public ProjectInfo get() {
+    public ProjectInfo get() throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
-    public BranchApi branch(String ref) {
+    public String description() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void description(PutDescriptionInput in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListRefsRequest<BranchInfo> branches() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ListRefsRequest<TagInfo> tags() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ProjectInfo> children() {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ProjectInfo> children(boolean recursive) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChildProjectApi child(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public BranchApi branch(String ref) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public TagApi tag(String ref) throws RestApiException {
       throw new NotImplementedException();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index 736d375..0e848b9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -18,7 +18,11 @@
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
 
 public interface Projects {
   /**
@@ -36,15 +40,54 @@
    */
   ProjectApi name(String name) throws RestApiException;
 
+  /**
+   * Create a project using the default configuration.
+   *
+   * @param name project name.
+   * @return API for accessing the newly-created project.
+   * @throws RestApiException if an error occurred.
+   */
+  ProjectApi create(String name) throws RestApiException;
+
+  /**
+   * Create a project.
+   *
+   * @param in project creation input; name must be set.
+   * @return API for accessing the newly-created project.
+   * @throws RestApiException if an error occurred.
+   */
+  ProjectApi create(ProjectInput in) throws RestApiException;
+
   ListRequest list();
 
   public abstract class ListRequest {
+    public static enum FilterType {
+      CODE, PARENT_CANDIDATES, PERMISSIONS, ALL
+    }
+
+    private final List<String> branches = new ArrayList<>();
     private boolean description;
     private String prefix;
+    private String substring;
+    private String regex;
     private int limit;
     private int start;
+    private boolean showTree;
+    private FilterType type = FilterType.ALL;
 
-    public abstract List<ProjectInfo> get() throws RestApiException;
+    public List<ProjectInfo> get() throws RestApiException {
+      Map<String, ProjectInfo> map = getAsMap();
+      List<ProjectInfo> result = new ArrayList<>(map.size());
+      for (Map.Entry<String, ProjectInfo> e : map.entrySet()) {
+        // ListProjects "helpfully" nulls out names when converting to a map.
+        e.getValue().name = e.getKey();
+        result.add(e.getValue());
+      }
+      return Collections.unmodifiableList(result);
+    }
+
+    public abstract SortedMap<String, ProjectInfo> getAsMap()
+        throws RestApiException;
 
     public ListRequest withDescription(boolean description) {
       this.description = description;
@@ -56,6 +99,16 @@
       return this;
     }
 
+    public ListRequest withSubstring(String substring) {
+      this.substring = substring;
+      return this;
+    }
+
+    public ListRequest withRegex(String regex) {
+      this.regex = regex;
+      return this;
+    }
+
     public ListRequest withLimit(int limit) {
       this.limit = limit;
       return this;
@@ -66,6 +119,21 @@
       return this;
     }
 
+    public ListRequest addShowBranch(String branch) {
+      branches.add(branch);
+      return this;
+    }
+
+    public ListRequest withTree(boolean show) {
+      showTree = show;
+      return this;
+    }
+
+    public ListRequest withType(FilterType type) {
+      this.type = type != null ? type : FilterType.ALL;
+      return this;
+    }
+
     public boolean getDescription() {
       return description;
     }
@@ -74,6 +142,14 @@
       return prefix;
     }
 
+    public String getSubstring() {
+      return substring;
+    }
+
+    public String getRegex() {
+      return regex;
+    }
+
     public int getLimit() {
       return limit;
     }
@@ -81,6 +157,18 @@
     public int getStart() {
       return start;
     }
+
+    public List<String> getBranches() {
+      return Collections.unmodifiableList(branches);
+    }
+
+    public boolean getShowTree() {
+      return showTree;
+    }
+
+    public FilterType getFilterType() {
+      return type;
+    }
   }
 
   /**
@@ -94,6 +182,16 @@
     }
 
     @Override
+    public ProjectApi create(ProjectInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ProjectApi create(String name) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ListRequest list() {
       throw new NotImplementedException();
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
similarity index 66%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
index 4413603..7ea9fb6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.extensions.api.projects;
 
-import com.google.gwt.core.client.JsArray;
+import com.google.gerrit.extensions.restapi.DefaultInput;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+public class PutDescriptionInput {
+  @DefaultInput
+  public String description;
+  public String commitMessage;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
similarity index 70%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
index 4413603..1844a76 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.extensions.api.projects;
 
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+public class RefInfo {
+  public String ref;
+  public String revision;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
new file mode 100644
index 0000000..6cc1ba4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2015 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.extensions.api.projects;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface TagApi {
+  TagInfo get() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements TagApi {
+    @Override
+    public TagInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
similarity index 87%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index 3e3d8db..b531d67 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.api.projects;
 
-public class TagInfo {
-  public String ref;
-  public String revision;
+import com.google.gerrit.extensions.common.GitPerson;
+
+public class TagInfo extends RefInfo {
   public String object;
   public String message;
   public GitPerson tagger;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
index f3fc887..56ebf9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -28,40 +28,13 @@
    * <p>
    * Changes in the NEW state can be moved to:
    * <ul>
-   * <li>{@link #SUBMITTED} - when the Submit Patch Set action is used;
+   * <li>{@link #MERGED} - when the Submit Patch Set action is used;
    * <li>{@link #ABANDONED} - when the Abandon action is used.
    * </ul>
    */
   NEW,
 
   /**
-   * Change is open, but has been submitted to the merge queue.
-   *
-   * <p>
-   * A change enters the SUBMITTED state when an authorized user presses the
-   * "submit" action through the web UI, requesting that Gerrit merge the
-   * change's current patch set into the destination branch.
-   *
-   * <p>
-   * Typically a change resides in the SUBMITTED for only a brief sub-second
-   * period while the merge queue fires and the destination branch is updated.
-   * However, if a dependency commit (directly or transitively) is not yet
-   * merged into the branch, the change will hang in the SUBMITTED state
-   * indefinitely.
-   *
-   * <p>
-   * Changes in the SUBMITTED state can be moved to:
-   * <ul>
-   * <li>{@link #NEW} - when a replacement patch set is supplied, OR when a
-   * merge conflict is detected;
-   * <li>{@link #MERGED} - when the change has been successfully merged into
-   * the destination branch;
-   * <li>{@link #ABANDONED} - when the Abandon action is used.
-   * </ul>
-   */
-  SUBMITTED,
-
-  /**
    * Change is a draft change that only consists of draft patchsets.
    *
    * <p>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index e79df1c..b9863d7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -17,6 +17,13 @@
 import java.sql.Timestamp;
 
 public abstract class Comment {
+  /**
+   * Patch set number containing this commit.
+   * <p>
+   * Only set in contexts where comments may come from multiple patch sets.
+   */
+  public Integer patchSet;
+
   public String id;
   public String path;
   public Side side;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
new file mode 100644
index 0000000..18555cf
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2014 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.extensions.client;
+
+public class DiffPreferencesInfo {
+
+  /** Default number of lines of context. */
+  public static final int DEFAULT_CONTEXT = 10;
+
+  /** Default tab size. */
+  public static final int DEFAULT_TAB_SIZE = 8;
+
+  /** Default line length. */
+  public static final int DEFAULT_LINE_LENGTH = 100;
+
+  /** Context setting to display the entire file. */
+  public static final short WHOLE_FILE_CONTEXT = -1;
+
+  /** Typical valid choices for the default context setting. */
+  public static final short[] CONTEXT_CHOICES =
+      {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
+
+  public static enum Whitespace {
+    IGNORE_NONE,
+    IGNORE_TRAILING,
+    IGNORE_LEADING_AND_TRAILING,
+    IGNORE_ALL;
+  }
+
+  public Integer context;
+  public Integer tabSize;
+  public Integer lineLength;
+  public Integer cursorBlinkRate;
+  public Boolean expandAllComments;
+  public Boolean intralineDifference;
+  public Boolean manualReview;
+  public Boolean showLineEndings;
+  public Boolean showTabs;
+  public Boolean showWhitespaceErrors;
+  public Boolean syntaxHighlighting;
+  public Boolean hideTopMenu;
+  public Boolean autoHideDiffTableHeader;
+  public Boolean hideLineNumbers;
+  public Boolean renderEntireFile;
+  public Boolean hideEmptyPane;
+  public Boolean matchBrackets;
+  public Boolean lineWrapping;
+  public Theme theme;
+  public Whitespace ignoreWhitespace;
+  public Boolean retainHeader;
+  public Boolean skipDeleted;
+  public Boolean skipUncommented;
+
+  public static DiffPreferencesInfo defaults() {
+    DiffPreferencesInfo i = new DiffPreferencesInfo();
+    i.context = DEFAULT_CONTEXT;
+    i.tabSize = DEFAULT_TAB_SIZE;
+    i.lineLength = DEFAULT_LINE_LENGTH;
+    i.cursorBlinkRate = 0;
+    i.ignoreWhitespace = Whitespace.IGNORE_NONE;
+    i.theme = Theme.DEFAULT;
+    i.expandAllComments = false;
+    i.intralineDifference = true;
+    i.manualReview = false;
+    i.retainHeader = false;
+    i.showLineEndings = true;
+    i.showTabs = true;
+    i.showWhitespaceErrors = true;
+    i.skipDeleted = false;
+    i.skipUncommented = false;
+    i.syntaxHighlighting = true;
+    i.hideTopMenu = false;
+    i.autoHideDiffTableHeader = true;
+    i.hideLineNumbers = false;
+    i.renderEntireFile = false;
+    i.hideEmptyPane = false;
+    i.matchBrackets = false;
+    i.lineWrapping = false;
+    return i;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
new file mode 100644
index 0000000..fe11b32
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2014 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.extensions.client;
+
+/* This class is stored in Git config file. */
+public class EditPreferencesInfo {
+  public Integer tabSize;
+  public Integer lineLength;
+  public Integer cursorBlinkRate;
+  public Boolean hideTopMenu;
+  public Boolean showTabs;
+  public Boolean showWhitespaceErrors;
+  public Boolean syntaxHighlighting;
+  public Boolean hideLineNumbers;
+  public Boolean matchBrackets;
+  public Boolean lineWrapping;
+  public Boolean autoCloseBrackets;
+  public Theme theme;
+  public KeyMapType keyMapType;
+
+  public static EditPreferencesInfo defaults() {
+    EditPreferencesInfo i = new EditPreferencesInfo();
+    i.tabSize = 8;
+    i.lineLength = 100;
+    i.cursorBlinkRate = 0;
+    i.hideTopMenu = false;
+    i.showTabs = true;
+    i.showWhitespaceErrors = false;
+    i.syntaxHighlighting = true;
+    i.hideLineNumbers = false;
+    i.matchBrackets = true;
+    i.lineWrapping = false;
+    i.autoCloseBrackets = false;
+    i.theme = Theme.DEFAULT;
+    i.keyMapType = KeyMapType.DEFAULT;
+    return i;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
similarity index 70%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
index 4413603..261168d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/KeyMapType.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.extensions.client;
 
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
-}
+public enum KeyMapType {
+  DEFAULT,
+  EMACS,
+  VIM
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 54617a7..4c336f7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.client;
 
 import java.util.EnumSet;
+import java.util.Set;
 
 /** Output options available for retrieval change details. */
 public enum ListChangesOption {
@@ -45,7 +46,8 @@
   /** Set the reviewed boolean for the caller. */
   REVIEWED(11),
 
-  /** Include draft comments for the caller. */
+  /** Not used anymore, kept for backward compatibility */
+  @Deprecated
   DRAFT_COMMENTS(12),
 
   /** Include download commands for the caller. */
@@ -58,7 +60,13 @@
   CHECK(15),
 
   /** Include allowed change actions client could perform. */
-  CHANGE_ACTIONS(16);
+  CHANGE_ACTIONS(16),
+
+  /** Include a copy of commit messages including review footers. */
+  COMMIT_FOOTERS(17),
+
+  /** Include push certificate information along with any patch sets. */
+  PUSH_CERTIFICATES(18);
 
   private final int value;
 
@@ -87,7 +95,7 @@
     return r;
   }
 
-  public static int toBits(EnumSet<ListChangesOption> set) {
+  public static int toBits(Set<ListChangesOption> set) {
     int r = 0;
     for (ListChangesOption o : set) {
       r |= 1 << o.value;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/groups/ListGroupsOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
similarity index 96%
rename from gerrit-common/src/main/java/com/google/gerrit/common/groups/ListGroupsOption.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
index 9da7fd5..d87f73e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/groups/ListGroupsOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListGroupsOption.java
@@ -12,11 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.groups;
+package com.google.gerrit.extensions.client;
 
 import java.util.EnumSet;
 
-
 /** Output options available when using {@code /groups/} RPCs. */
 public enum ListGroupsOption {
   /** Return information on the direct group members. */
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
index 5d5af75..3485b8b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
@@ -15,5 +15,16 @@
 package com.google.gerrit.extensions.client;
 
 public enum Side {
-  PARENT, REVISION
-}
\ No newline at end of file
+  PARENT,
+  REVISION;
+
+  public static Side fromShort(short s) {
+    switch (s) {
+      case 0:
+        return PARENT;
+      case 1:
+        return REVISION;
+    }
+    return null;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 13baf6b..cdfe0c6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -35,6 +35,7 @@
   public Boolean starred;
   public Boolean reviewed;
   public Boolean mergeable;
+  public Boolean submittable;
   public Integer insertions;
   public Integer deletions;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
index d55580c..d26ea23 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
@@ -32,5 +32,5 @@
   COPIED,
 
   /** Sufficient amount of content changed to claim the file was rewritten. */
-  REWRITE;
+  REWRITE
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
index 62b1dc7..58b2d39 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -42,6 +42,8 @@
   }
 
   public static class FileMeta {
+    // The ID of the commit containing the file
+    public transient String commitId;
     // The name of the file
     public String name;
     // The content type of the file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
index 58f5494..00d0c18 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -20,4 +20,5 @@
   public String oldPath;
   public Integer linesInserted;
   public Integer linesDeleted;
+  public long sizeDelta;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
new file mode 100644
index 0000000..33adbea
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GpgKeyInfo.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 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.extensions.common;
+
+import java.util.List;
+
+public class GpgKeyInfo {
+  /**
+   * Status of checking an object like a key or signature.
+   * <p>
+   * Order of values in this enum is significant: OK is "better" than BAD, etc.
+   */
+  public enum Status {
+    /** Something is wrong with this key. */
+    BAD,
+
+    /**
+     * Inspecting only this key found no problems, but the system does not fully
+     * trust the key's origin.
+     */
+    OK,
+
+    /**
+     * This key is valid, and the system knows enough about the key and its
+     * origin to trust it.
+     */
+    TRUSTED;
+  }
+
+  public String id;
+  public String fingerprint;
+  public List<String> userIds;
+  public String key;
+
+  public Status status;
+  public List<String> problems;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
new file mode 100644
index 0000000..1d8839f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 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.extensions.common;
+
+import java.sql.Timestamp;
+
+public abstract class GroupAuditEventInfo {
+  public enum Type {
+    ADD_USER, REMOVE_USER, ADD_GROUP, REMOVE_GROUP
+  }
+
+  public Type type;
+  public AccountInfo user;
+  public Timestamp date;
+
+  public static UserMemberAuditEventInfo createAddUserEvent(AccountInfo user,
+      Timestamp date, AccountInfo member) {
+    return new UserMemberAuditEventInfo(Type.ADD_USER, user, date, member);
+  }
+
+  public static UserMemberAuditEventInfo createRemoveUserEvent(
+      AccountInfo user, Timestamp date, AccountInfo member) {
+    return new UserMemberAuditEventInfo(Type.REMOVE_USER, user, date, member);
+  }
+
+  public static GroupMemberAuditEventInfo createAddGroupEvent(AccountInfo user,
+      Timestamp date, GroupInfo member) {
+    return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date, member);
+  }
+
+  public static GroupMemberAuditEventInfo createRemoveGroupEvent(
+      AccountInfo user, Timestamp date, GroupInfo member) {
+    return new GroupMemberAuditEventInfo(Type.REMOVE_GROUP, user, date, member);
+  }
+
+  protected GroupAuditEventInfo(Type type, AccountInfo user,
+      Timestamp date) {
+    this.type = type;
+    this.user = user;
+    this.date = date;
+  }
+
+  public static class UserMemberAuditEventInfo extends GroupAuditEventInfo {
+    public AccountInfo member;
+
+    public UserMemberAuditEventInfo(Type type, AccountInfo user,
+        Timestamp date, AccountInfo member) {
+      super(type, user, date);
+      this.member = member;
+    }
+  }
+
+  public static class GroupMemberAuditEventInfo extends GroupAuditEventInfo {
+    public GroupInfo member;
+
+    public GroupMemberAuditEventInfo(Type type, AccountInfo user,
+        Timestamp date, GroupInfo member) {
+      super(type, user, date);
+      this.member = member;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java
new file mode 100644
index 0000000..f956a03
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 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.extensions.common;
+
+import java.util.List;
+
+public class GroupInfo extends GroupBaseInfo {
+  public String url;
+  public GroupOptionsInfo options;
+
+  // These fields are only supplied for internal groups.
+  public String description;
+  public Integer groupId;
+  public String owner;
+  public String ownerId;
+
+  // These fields are only supplied for internal groups, and only if requested.
+  public List<AccountInfo> members;
+  public List<GroupInfo> includes;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupOptionsInfo.java
similarity index 70%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupOptionsInfo.java
index 4413603..074e1a4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupOptionsInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.extensions.common;
 
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+public class GroupOptionsInfo {
+  public Boolean visibleToAll;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index a117d07..d04b346 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -16,7 +16,7 @@
 
 public class ProblemInfo {
   public static enum Status {
-    FIXED, FIX_FAILED;
+    FIXED, FIX_FAILED
   }
 
   public String message;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
similarity index 70%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
index 4413603..9eed808 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PushCertificateInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.extensions.common;
 
-import com.google.gwt.core.client.JsArray;
-
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+public class PushCertificateInfo {
+  public String certificate;
+  public GpgKeyInfo key;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 4b8eec1..025c623 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -20,7 +20,6 @@
 public class RevisionInfo {
   public transient boolean isCurrent;
   public Boolean draft;
-  public Boolean hasDraftComments;
   public int _number;
   public Timestamp created;
   public AccountInfo uploader;
@@ -29,4 +28,6 @@
   public CommitInfo commit;
   public Map<String, FileInfo> files;
   public Map<String, ActionInfo> actions;
+  public String commitWithFooters;
+  public PushCertificateInfo pushCertificate;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CloneCommand.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CloneCommand.java
new file mode 100644
index 0000000..f773380
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CloneCommand.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 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.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public abstract class CloneCommand {
+  /**
+   * Returns the clone command for the given download scheme and project.
+   *
+   * @param scheme the download scheme for which the command should be returned
+   * @param project the name of the project for which the clone command
+   *        should be returned
+   * @return the clone command
+   */
+  public abstract String getCommand(DownloadScheme scheme, String project);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
new file mode 100644
index 0000000..072799f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 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.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.Collection;
+import java.util.List;
+
+@ExtensionPoint
+public interface ExternalIncludedIn {
+
+  /**
+   * Returns a list of systems that include the given commit.
+   *
+   * The tags and branches in which the commit is included are provided so that
+   * a RevWalk can be avoided when a system runs a certain tag or branch.
+   *
+   * @param project the name of the project
+   * @param commit the ID of the commit for which it should be checked if it is
+   *        included
+   * @param tags the tags that include the commit
+   * @param branches the branches that include the commit
+   * @return a list of systems that contain the given commit, e.g. names of
+   *         servers on which this commit is deployed
+   */
+  List<String> getIncludedIn(String project, String commit,
+      Collection<String> tags, Collection<String> branches);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java
similarity index 96%
rename from gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java
index a0cf2ab..226a6c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.extensions.config;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
new file mode 100644
index 0000000..89d836d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.Properties;
+
+/**
+ * Notified whenever the garbage collector has run successfully on a project.
+ */
+@ExtensionPoint
+public interface GarbageCollectorListener {
+  public interface Event {
+    /** @return The name of the project that has been garbage collected. */
+    String getProjectName();
+
+    /**
+     * @return Properties describing the result of the garbage collection
+     *         performed by JGit.
+     * @see <a href="http://download.eclipse.org/jgit/site/3.7.0.201502260915-r/apidocs/org/eclipse/jgit/api/GarbageCollectCommand.html#call%28%29">GarbageCollectCommand</a>
+     */
+    Properties getStatistics();
+  }
+
+  void onGarbageCollected(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
index 49a697e..a838baf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -25,6 +25,9 @@
     String getRefName();
     String getOldObjectId();
     String getNewObjectId();
+    boolean isCreate();
+    boolean isDelete();
+    boolean isNonFastForward();
   }
 
   void onGitReferenceUpdated(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
index 1388637..da7db17 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -67,7 +67,7 @@
    * <p>
    * Items must be defined in a Guice module before they can be bound:
    * <pre>
-   *   DynamicSet.itemOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   *   DynamicSet.itemOf(binder(), new TypeLiteral&lt;Thing&lt;Foo&gt;&gt;() {});
    * </pre>
    *
    * @param binder a new binder created in the module.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index abf944a..d3db2e9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -67,8 +67,8 @@
    * Maps must be defined in a Guice module before they can be bound:
    *
    * <pre>
-   * DynamicMap.mapOf(binder(), new TypeLiteral<Thing<Bar>>(){});
-   * bind(new TypeLiteral<Thing<Bar>>() {})
+   * DynamicMap.mapOf(binder(), new TypeLiteral&lt;Thing&lt;Bar&gt;&gt;(){});
+   * bind(new TypeLiteral&lt;Thing&lt;Bar&gt;&gt;() {})
    *   .annotatedWith(Exports.named(&quot;foo&quot;))
    *   .to(Impl.class);
    * </pre>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 82613c7..28052ef 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -61,7 +61,7 @@
    * <p>
    * Sets must be defined in a Guice module before they can be bound:
    * <pre>
-   *   DynamicSet.setOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   *   DynamicSet.setOf(binder(), new TypeLiteral&lt;Thing&lt;Foo&gt;&gt;() {});
    * </pre>
    *
    * @param binder a new binder created in the module.
@@ -182,6 +182,23 @@
   }
 
   /**
+   * Returns {@code true} if this set contains the given item.
+   *
+   * @param item item to check whether or not it is contained.
+   * @return {@code true} if this set contains the given item.
+   */
+  public boolean contains(final T item) {
+    Iterator<T> iterator = iterator();
+    while (iterator.hasNext()) {
+      T candidate = iterator.next();
+      if (candidate == item) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
    * Add one new element to the set.
    *
    * @param item the item to add to the collection. Must not be null.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
index 96538e1..683e0b9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -98,10 +98,7 @@
           handles.add(item.set(b.getKey(), b.getProvider(), pluginName));
         }
       }
-    } catch (RuntimeException e) {
-      remove(handles);
-      throw e;
-    } catch (Error e) {
+    } catch (RuntimeException | Error e) {
       remove(handles);
       throw e;
     }
@@ -130,10 +127,7 @@
           }
         }
       }
-    } catch (RuntimeException e) {
-      remove(handles);
-      throw e;
-    } catch (Error e) {
+    } catch (RuntimeException | Error e) {
       remove(handles);
       throw e;
     }
@@ -164,10 +158,7 @@
           }
         }
       }
-    } catch (RuntimeException e) {
-      remove(handles);
-      throw e;
-    } catch (Error e) {
+    } catch (RuntimeException | Error e) {
       remove(handles);
       throw e;
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 18f356b..a21c2d4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
@@ -23,7 +25,6 @@
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
 import java.nio.charset.UnsupportedCharsetException;
 
 /**
@@ -58,7 +59,7 @@
   }
 
   private String contentType = OCTET_STREAM;
-  private String characterEncoding;
+  private Charset characterEncoding;
   private long contentLength = -1;
   private boolean gzip = true;
   private boolean base64 = false;
@@ -66,9 +67,9 @@
 
   /** @return the MIME type of the result, for HTTP clients. */
   public String getContentType() {
-    String enc = getCharacterEncoding();
+    Charset enc = getCharacterEncoding();
     if (enc != null) {
-      return contentType + "; charset=" + enc;
+      return contentType + "; charset=" + enc.name();
     }
     return contentType;
   }
@@ -80,12 +81,18 @@
   }
 
   /** Get the character encoding; null if not known. */
-  public String getCharacterEncoding() {
+  public Charset getCharacterEncoding() {
     return characterEncoding;
   }
 
   /** Set the character set used to encode text data and return {@code this}. */
+  @Deprecated
   public BinaryResult setCharacterEncoding(String encoding) {
+    return setCharacterEncoding(Charset.forName(encoding));
+  }
+
+  /** Set the character set used to encode text data and return {@code this}. */
+  public BinaryResult setCharacterEncoding(Charset encoding) {
     characterEncoding = encoding;
     return this;
   }
@@ -183,11 +190,11 @@
         getContentType());
   }
 
-  private static String decode(byte[] data, String enc) {
+  private static String decode(byte[] data, Charset enc) {
     try {
       Charset cs = enc != null
-          ? Charset.forName(enc)
-          : StandardCharsets.UTF_8;
+          ? enc
+          : UTF_8;
       return cs.newDecoder()
         .onMalformedInput(CodingErrorAction.REPORT)
         .onUnmappableCharacter(CodingErrorAction.REPORT)
@@ -196,8 +203,9 @@
     } catch (UnsupportedCharsetException | CharacterCodingException e) {
       // Fallback to ISO-8850-1 style encoding.
       StringBuilder r = new StringBuilder(data.length);
-      for (byte b : data)
-          r.append((char) (b & 0xff));
+      for (byte b : data) {
+        r.append((char) (b & 0xff));
+      }
       return r.toString();
     }
   }
@@ -225,9 +233,9 @@
     private final String str;
 
     StringResult(String str) {
-      super(str.getBytes(StandardCharsets.UTF_8));
+      super(str.getBytes(UTF_8));
       setContentType("text/plain");
-      setCharacterEncoding("UTF-8");
+      setCharacterEncoding(UTF_8.name());
       this.str = str;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MergeConflictException.java
similarity index 74%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MergeConflictException.java
index 02bc8dc..a67db0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MergeConflictException.java
@@ -12,11 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.extensions.restapi;
 
-/** Indicates that the commit cannot be merged without conflicts. */
-public class MergeConflictException extends Exception {
+/**
+ * Indicates that a commit cannot be merged without conflicts.
+ * <p>
+ * Messages should be viewable by end users.
+ */
+public class MergeConflictException extends ResourceConflictException {
   private static final long serialVersionUID = 1L;
+
   public MergeConflictException(String msg) {
     super(msg, null);
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java
index 10d0a14..566159d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NotImplementedException.java
@@ -19,6 +19,10 @@
   private static final long serialVersionUID = 1L;
 
   public NotImplementedException() {
-    super("Not implemented.");
+    this("Not implemented");
+  }
+
+  public NotImplementedException(String message) {
+    super(message);
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
index 7708a5c..3567300 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
@@ -16,14 +16,14 @@
 
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.extensions.annotations.Exports;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.binder.ScopedBindingBuilder;
 
 /** Guice DSL for binding {@link RestView} implementations. */
-public abstract class RestApiModule extends AbstractModule {
+public abstract class RestApiModule extends FactoryModule {
   protected static final String GET = "GET";
   protected static final String PUT = "PUT";
   protected static final String DELETE = "DELETE";
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
index 8f4d909..debfa20 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -40,7 +42,7 @@
   public static String encode(String component) {
     if (component != null) {
       try {
-        return URLEncoder.encode(component, "UTF-8");
+        return URLEncoder.encode(component, UTF_8.name());
       } catch (UnsupportedEncodingException e) {
         throw new RuntimeException("JVM must support UTF-8", e);
       }
@@ -52,7 +54,7 @@
   public static String decode(String str) {
     if (str != null) {
       try {
-        return URLDecoder.decode(str, "UTF-8");
+        return URLDecoder.decode(str, UTF_8.name());
       } catch (UnsupportedEncodingException e) {
         throw new RuntimeException("JVM must support UTF-8", e);
       }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileHistoryWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileHistoryWebLink.java
new file mode 100644
index 0000000..f9f9e58
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileHistoryWebLink.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2015 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.extensions.webui;
+
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+public interface FileHistoryWebLink extends WebLink {
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a file to an external service displaying
+   * a log for that file.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
+   *
+   * @param projectName Name of the project
+   * @param revision Name of the revision (e.g. branch or commit ID)
+   * @param fileName Name of the file
+   * @return WebLinkInfo that links to a log for the file in external
+   * service, null if there should be no link.
+   */
+  WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName);
+}
diff --git a/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
new file mode 100644
index 0000000..299b9b0
--- /dev/null
+++ b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2015 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.extensions.registration;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.inject.Key;
+import com.google.inject.util.Providers;
+
+import org.junit.Test;
+
+public class DynamicSetTest {
+  // In tests for {@link DynamicSet#contains(Object)}, be sure to avoid
+  // {@code assertThat(ds).contains(...) @} and
+  // {@code assertThat(ds).DoesNotContains(...) @} as (since
+  // {@link DynamicSet@} is not a {@link Collection@}) those boil down to
+  // iterating over the {@link DynamicSet@} and checking equality instead
+  // of calling {@link DynamicSet#contains(Object)}.
+  // To test for {@link DynamicSet#contains(Object)}, use
+  // {@code assertThat(ds.contains(...)).isTrue() @} and
+  // {@code assertThat(ds.contains(...)).isFalse() @} instead.
+
+  @Test
+  public void testContainsWithEmpty() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    assertThat(ds.contains(2)).isFalse(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsTrueWithSingleElement() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+
+    assertThat(ds.contains(2)).isTrue(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsFalseWithSingleElement() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+
+    assertThat(ds.contains(3)).isFalse(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsTrueWithTwoElements() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+    ds.add(4);
+
+    assertThat(ds.contains(4)).isTrue(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsFalseWithTwoElements() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+    ds.add(4);
+
+    assertThat(ds.contains(3)).isFalse(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsDynamic() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+
+    Key<Integer> key = Key.get(Integer.class);
+    ReloadableRegistrationHandle<Integer> handle = ds.add(key, Providers.of(4));
+
+    ds.add(6);
+
+    // At first, 4 is contained.
+    assertThat(ds.contains(4)).isTrue(); //See above comment about ds.contains
+
+    // Then we remove 4.
+    handle.remove();
+
+    // And now 4 should no longer be contained.
+    assertThat(ds.contains(4)).isFalse(); //See above comment about ds.contains
+  }
+}
diff --git a/gerrit-gpg/BUCK b/gerrit-gpg/BUCK
new file mode 100644
index 0000000..2b258a9
--- /dev/null
+++ b/gerrit-gpg/BUCK
@@ -0,0 +1,57 @@
+DEPS = [
+  '//gerrit-common:server',
+  '//gerrit-extension-api:api',
+  '//gerrit-reviewdb:server',
+  '//gerrit-server:server',
+  '//lib:guava',
+  '//lib:gwtorm',
+  '//lib/guice:guice',
+  '//lib/guice:guice-assistedinject',
+  '//lib/guice:guice-servlet',
+  '//lib/jgit:jgit',
+  '//lib/log:api',
+]
+
+java_library(
+  name = 'gpg',
+  srcs = glob(['src/main/java/**/*.java']),
+  provided_deps = DEPS + [
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java'])
+
+java_library(
+  name = 'testutil',
+  srcs = TESTUTIL_SRCS,
+  deps = DEPS + [
+    ':gpg',
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'gpg_tests',
+  srcs = glob(
+    ['src/test/java/**/*.java'],
+    excludes = TESTUTIL_SRCS,
+  ),
+  deps = DEPS + [
+    ':gpg',
+    ':testutil',
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-lucene:lucene',  
+    '//gerrit-server:testutil',
+    '//lib:truth',
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcprov',
+    '//lib/jgit:junit',
+  ],
+  source_under_test = [':gpg'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/BouncyCastleUtil.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/BouncyCastleUtil.java
new file mode 100644
index 0000000..ef065a1
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/BouncyCastleUtil.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 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.gpg;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPPublicKey;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.security.Security;
+
+/** Utility methods for Bouncy Castle. */
+public class BouncyCastleUtil {
+  /**
+   * Check for Bouncy Castle PGP support.
+   * <p>
+   * As a side effect, adds {@link BouncyCastleProvider} as a security provider.
+   *
+   * @return whether Bouncy Castle PGP support is enabled.
+   */
+  public static boolean havePGP() {
+    try {
+      Class.forName(PGPPublicKey.class.getName());
+      addBouncyCastleProvider();
+      return true;
+    } catch (NoClassDefFoundError | ClassNotFoundException | SecurityException
+        | NoSuchMethodException | InstantiationException
+        | IllegalAccessException | InvocationTargetException
+        | ClassCastException noBouncyCastle) {
+      return false;
+    }
+  }
+
+  private static void addBouncyCastleProvider() throws ClassNotFoundException,
+          SecurityException, NoSuchMethodException, InstantiationException,
+          IllegalAccessException, InvocationTargetException {
+    Class<?> clazz = Class.forName(BouncyCastleProvider.class.getName());
+    Constructor<?> constructor = clazz.getConstructor();
+    Security.addProvider((java.security.Provider) constructor.newInstance());
+  }
+
+  private BouncyCastleUtil() {
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java
new file mode 100644
index 0000000..74184bd
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/CheckResult.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2015 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.gpg;
+
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Result of checking an object like a key or signature. */
+public class CheckResult {
+  static CheckResult ok(String... problems) {
+    return create(Status.OK, problems);
+  }
+
+  static CheckResult bad(String... problems) {
+    return create(Status.BAD, problems);
+  }
+
+  static CheckResult trusted() {
+    return new CheckResult(Status.TRUSTED, Collections.<String> emptyList());
+  }
+
+  static CheckResult create(Status status, String... problems) {
+    List<String> problemList = problems.length > 0
+        ? Collections.unmodifiableList(Arrays.asList(problems))
+        : Collections.<String> emptyList();
+    return new CheckResult(status, problemList);
+  }
+
+  static CheckResult create(Status status, List<String> problems) {
+    return new CheckResult(status,
+        Collections.unmodifiableList(new ArrayList<>(problems)));
+  }
+
+  static CheckResult create(List<String> problems) {
+    return new CheckResult(
+        problems.isEmpty() ? Status.OK : Status.BAD,
+        Collections.unmodifiableList(problems));
+  }
+
+  private final Status status;
+  private final List<String> problems;
+
+  private CheckResult(Status status, List<String> problems) {
+    if (status == null) {
+      throw new IllegalArgumentException("status must not be null");
+    }
+    this.status = status;
+    this.problems = problems;
+  }
+
+  /** @return whether the result has status {@link Status#OK} or better. */
+  public boolean isOk() {
+    return status.compareTo(Status.OK) >= 0;
+  }
+
+  /** @return whether the result has status {@link Status#TRUSTED} or better. */
+  public boolean isTrusted() {
+    return status.compareTo(Status.TRUSTED) >= 0;
+  }
+
+  /** @return the status enum value associated with the object. */
+  public Status getStatus() {
+    return status;
+  }
+
+  /** @return any problems encountered during checking. */
+  public List<String> getProblems() {
+    return problems;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(getClass().getSimpleName())
+        .append('[').append(status);
+    for (int i = 0; i < problems.size(); i++) {
+      sb.append(i == 0 ? ": " : ", ").append(problems.get(i));
+    }
+    return sb.append(']').toString();
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
new file mode 100644
index 0000000..fa78f01
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import org.eclipse.jgit.util.NB;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Fingerprint {
+  private final byte[] fp;
+
+  public static String toString(byte[] fp) {
+    checkLength(fp);
+    return String.format(
+        "%04X %04X %04X %04X %04X  %04X %04X %04X %04X %04X",
+        NB.decodeUInt16(fp, 0), NB.decodeUInt16(fp, 2), NB.decodeUInt16(fp, 4),
+        NB.decodeUInt16(fp, 6), NB.decodeUInt16(fp, 8), NB.decodeUInt16(fp, 10),
+        NB.decodeUInt16(fp, 12), NB.decodeUInt16(fp, 14),
+        NB.decodeUInt16(fp, 16), NB.decodeUInt16(fp, 18));
+  }
+
+  public static long getId(byte[] fp) {
+    return NB.decodeInt64(fp, 12);
+  }
+
+  public static Map<Long, Fingerprint> byId(Iterable<Fingerprint> fps) {
+    Map<Long, Fingerprint> result = new HashMap<>();
+    for (Fingerprint fp : fps) {
+      result.put(fp.getId(), fp);
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
+  private static byte[] checkLength(byte[] fp) {
+    checkArgument(fp.length == 20,
+        "fingerprint must be 20 bytes, got %s", fp.length);
+    return fp;
+  }
+
+  /**
+   * Wrap a fingerprint byte array.
+   * <p>
+   * The newly created Fingerprint object takes ownership of the byte array,
+   * which must not be subsequently modified. (Most callers, such as hex
+   * decoders and {@code
+   * org.bouncycastle.openpgp.PGPPublicKey#getFingerprint()}, already produce
+   * fresh byte arrays).
+   *
+   * @param fp 20-byte fingerprint byte array to wrap.
+   */
+  public Fingerprint(byte[] fp) {
+    this.fp = checkLength(fp);
+  }
+
+  /**
+   * Wrap a portion of a fingerprint byte array.
+   * <p>
+   * Unlike {@link #Fingerprint(byte[])}, creates a new copy of the byte array.
+   *
+   * @param buf byte array to wrap; must have at least {@code off + 20} bytes.
+   * @param off offset in buf.
+   */
+  public Fingerprint(byte[] buf, int off) {
+    int expected = 20 + off;
+    checkArgument(buf.length >= expected,
+        "fingerprint buffer must have at least %s bytes, got %s",
+        expected, buf.length);
+    this.fp = new byte[20];
+    System.arraycopy(buf, off, fp, 0, 20);
+  }
+
+  public byte[] get() {
+    return fp;
+  }
+
+  public boolean equalsBytes(byte[] bytes) {
+    return Arrays.equals(fp, bytes);
+  }
+
+  @Override
+  public int hashCode() {
+    // Same hash code as ObjectId: second int word.
+    return NB.decodeInt32(fp, 4);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return (o instanceof Fingerprint) && equalsBytes(((Fingerprint) o).fp);
+  }
+
+  @Override
+  public String toString() {
+    return toString(fp);
+  }
+
+  public long getId() {
+    return getId(fp);
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
new file mode 100644
index 0000000..c3c886f
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -0,0 +1,267 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Checker for GPG public keys including Gerrit-specific checks.
+ * <p>
+ * For Gerrit, keys must contain a self-signed user ID certification matching a
+ * trusted external ID in the database, or an email address thereof.
+ */
+public class GerritPublicKeyChecker extends PublicKeyChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(GerritPublicKeyChecker.class);
+
+  @Singleton
+  public static class Factory {
+    private final Provider<ReviewDb> db;
+    private final String webUrl;
+    private final IdentifiedUser.GenericFactory userFactory;
+    private final int maxTrustDepth;
+    private final ImmutableMap<Long, Fingerprint> trusted;
+
+    @Inject
+    Factory(@GerritServerConfig Config cfg,
+        Provider<ReviewDb> db,
+        IdentifiedUser.GenericFactory userFactory,
+        @CanonicalWebUrl String webUrl) {
+      this.db = db;
+      this.webUrl = webUrl;
+      this.userFactory = userFactory;
+      this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0);
+
+      String[] strs = cfg.getStringList("receive", null, "trustedKey");
+      if (strs.length != 0) {
+        Map<Long, Fingerprint> fps =
+            Maps.newHashMapWithExpectedSize(strs.length);
+        for (String str : strs) {
+          str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
+          Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
+          fps.put(fp.getId(), fp);
+        }
+        trusted = ImmutableMap.copyOf(fps);
+      } else {
+        trusted = null;
+      }
+    }
+
+    public GerritPublicKeyChecker create() {
+      return new GerritPublicKeyChecker(this);
+    }
+
+    public GerritPublicKeyChecker create(IdentifiedUser expectedUser,
+        PublicKeyStore store) {
+      GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
+      checker.setExpectedUser(expectedUser);
+      checker.setStore(store);
+      return checker;
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final String webUrl;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  private IdentifiedUser expectedUser;
+
+  private GerritPublicKeyChecker(Factory factory) {
+    this.db = factory.db;
+    this.webUrl = factory.webUrl;
+    this.userFactory = factory.userFactory;
+    if (factory.trusted != null) {
+      enableTrust(factory.maxTrustDepth, factory.trusted);
+    }
+  }
+
+   /**
+    * Set the expected user for this checker.
+    * <p>
+    * If set, the top-level key passed to {@link #check(PGPPublicKey)} must
+    * belong to the given user. (Other keys checked in the course of verifying
+    * the web of trust are checked against the set of identities in the database
+    * belonging to the same user as the key.)
+    */
+  public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) {
+    this.expectedUser = expectedUser;
+    return this;
+  }
+
+  @Override
+  public CheckResult checkCustom(PGPPublicKey key, int depth) {
+    try {
+      if (depth == 0 && expectedUser != null) {
+        return checkIdsForExpectedUser(key);
+      } else {
+        return checkIdsForArbitraryUser(key);
+      }
+    } catch (PGPException | OrmException e) {
+      String msg = "Error checking user IDs for key";
+      log.warn(msg + " " + keyIdToString(key.getKeyID()), e);
+      return CheckResult.bad(msg);
+    }
+  }
+
+  private CheckResult checkIdsForExpectedUser(PGPPublicKey key)
+      throws PGPException {
+    Set<String> allowedUserIds = getAllowedUserIds(expectedUser);
+    if (allowedUserIds.isEmpty()) {
+      return CheckResult.bad("No identities found for user; check "
+          + webUrl + "#" + PageLinks.SETTINGS_WEBIDENT);
+    }
+    if (hasAllowedUserId(key, allowedUserIds)) {
+      return CheckResult.trusted();
+    }
+    return CheckResult.bad(missingUserIds(allowedUserIds));
+  }
+
+  private CheckResult checkIdsForArbitraryUser(PGPPublicKey key)
+      throws PGPException, OrmException {
+    AccountExternalId extId = db.get().accountExternalIds().get(
+        toExtIdKey(key));
+    if (extId == null) {
+      return CheckResult.bad("Key is not associated with any users");
+    }
+    IdentifiedUser user = userFactory.create(db, extId.getAccountId());
+    Set<String> allowedUserIds = getAllowedUserIds(user);
+    if (allowedUserIds.isEmpty()) {
+      return CheckResult.bad("No identities found for user");
+    }
+    if (hasAllowedUserId(key, allowedUserIds)) {
+      return CheckResult.trusted();
+    }
+    return CheckResult.bad(
+        "Key does not contain any valid certifications for user's identities");
+  }
+
+  private boolean hasAllowedUserId(PGPPublicKey key, Set<String> allowedUserIds)
+      throws PGPException {
+    @SuppressWarnings("unchecked")
+    Iterator<String> userIds = key.getUserIDs();
+    while (userIds.hasNext()) {
+      String userId = userIds.next();
+      if (isAllowed(userId, allowedUserIds)) {
+        Iterator<PGPSignature> sigs = getSignaturesForId(key, userId);
+        while (sigs.hasNext()) {
+          if (isValidCertification(key, sigs.next(), userId)) {
+            return true;
+          }
+        }
+      }
+    }
+
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Iterator<PGPSignature> getSignaturesForId(PGPPublicKey key,
+      String userId) {
+    return MoreObjects.firstNonNull(
+        key.getSignaturesForID(userId),
+        Collections.emptyIterator());
+  }
+
+  private Set<String> getAllowedUserIds(IdentifiedUser user) {
+    Set<String> result = new HashSet<>();
+    result.addAll(user.getEmailAddresses());
+    for (AccountExternalId extId : user.state().getExternalIds()) {
+      if (extId.isScheme(SCHEME_GPGKEY)) {
+        continue; // Omit GPG keys.
+      }
+      result.add(extId.getExternalId());
+    }
+    return result;
+  }
+
+  private static boolean isAllowed(String userId, Set<String> allowedUserIds) {
+    return allowedUserIds.contains(userId)
+        || allowedUserIds.contains(
+            PushCertificateIdent.parse(userId).getEmailAddress());
+  }
+
+  private static boolean isValidCertification(PGPPublicKey key,
+      PGPSignature sig, String userId) throws PGPException {
+    if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION
+        && sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) {
+      return false;
+    }
+    if (sig.getKeyID() != key.getKeyID()) {
+      return false;
+    }
+    // TODO(dborowitz): Handle certification revocations:
+    // - Is there a revocation by either this key or another key trusted by the
+    //   server?
+    // - Does such a revocation postdate all other valid certifications?
+
+    sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+    return sig.verifyCertification(userId, key);
+  }
+
+  private static String missingUserIds(Set<String> allowedUserIds) {
+    StringBuilder sb = new StringBuilder("Key must contain a valid"
+        + " certification for one of the following identities:\n");
+    Iterator<String> sorted = FluentIterable.from(allowedUserIds)
+        .toSortedList(Ordering.natural())
+        .iterator();
+    while (sorted.hasNext()) {
+      sb.append("  ").append(sorted.next());
+      if (sorted.hasNext()) {
+        sb.append('\n');
+      }
+    }
+    return sb.toString();
+  }
+
+  static AccountExternalId.Key toExtIdKey(PGPPublicKey key) {
+    return new AccountExternalId.Key(
+        SCHEME_GPGKEY,
+        BaseEncoding.base16().encode(key.getFingerprint()));
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
new file mode 100644
index 0000000..30983ac
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.gpg;
+
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class GerritPushCertificateChecker extends PushCertificateChecker {
+  public interface Factory {
+    GerritPushCertificateChecker create(IdentifiedUser expectedUser);
+  }
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+
+  @AssistedInject
+  GerritPushCertificateChecker(
+      GerritPublicKeyChecker.Factory keyCheckerFactory,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers,
+      @Assisted IdentifiedUser expectedUser) {
+    super(keyCheckerFactory.create().setExpectedUser(expectedUser));
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+  }
+
+  @Override
+  protected Repository getRepository() throws IOException {
+    return repoManager.openRepository(allUsers);
+  }
+
+  @Override
+  protected boolean shouldClose(Repository repo) {
+    return true;
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
new file mode 100644
index 0000000..bbf61b8
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 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.gpg;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.gpg.api.GpgApiModule;
+import com.google.gerrit.server.EnableSignedPush;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GpgModule extends FactoryModule {
+  private static final Logger log = LoggerFactory.getLogger(GpgModule.class);
+
+  private final Config cfg;
+
+  public GpgModule(Config cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  protected void configure() {
+    boolean configEnableSignedPush =
+        cfg.getBoolean("receive", null, "enableSignedPush", false);
+    boolean configEditGpgKeys =
+        cfg.getBoolean("gerrit", null, "editGpgKeys", true);
+    boolean havePgp = BouncyCastleUtil.havePGP();
+    boolean enableSignedPush = configEnableSignedPush && havePgp;
+    bindConstant().annotatedWith(EnableSignedPush.class).to(enableSignedPush);
+
+    if (configEnableSignedPush && !havePgp) {
+      log.info("Bouncy Castle PGP not installed; signed push verification is"
+          + " disabled");
+    }
+    if (enableSignedPush) {
+      install(new SignedPushModule());
+      factory(GerritPushCertificateChecker.Factory.class);
+    }
+    install(new GpgApiModule(enableSignedPush && configEditGpgKeys));
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
new file mode 100644
index 0000000..e4c81df
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -0,0 +1,489 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_COMPROMISED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_RETIRED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_SUPERSEDED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.NO_REASON;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
+
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+
+import org.bouncycastle.bcpg.SignatureSubpacket;
+import org.bouncycastle.bcpg.SignatureSubpacketTags;
+import org.bouncycastle.bcpg.sig.RevocationKey;
+import org.bouncycastle.bcpg.sig.RevocationReason;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Checker for GPG public keys for use in a push certificate. */
+public class PublicKeyChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublicKeyChecker.class);
+
+  // https://tools.ietf.org/html/rfc4880#section-5.2.3.13
+  private static final int COMPLETE_TRUST = 120;
+
+  private PublicKeyStore store;
+  private Map<Long, Fingerprint> trusted;
+  private int maxTrustDepth;
+  private Date effectiveTime = new Date();
+
+  /**
+   * Enable web-of-trust checks.
+   * <p>
+   * If enabled, a store must be set with {@link #setStore(PublicKeyStore)}.
+   * (These methods are separate since the store is a closeable resource that
+   * may not be available when reading trusted keys from a config.)
+   *
+   * @param maxTrustDepth maximum depth to search while looking for a trusted
+   *     key.
+   * @param trusted ultimately trusted key fingerprints, keyed by fingerprint;
+   *     may not be empty. To construct a map, see {@link
+   *     Fingerprint#byId(Iterable)}.
+   * @return a reference to this object.
+   */
+  public PublicKeyChecker enableTrust(int maxTrustDepth,
+      Map<Long, Fingerprint> trusted) {
+    if (maxTrustDepth <= 0) {
+      throw new IllegalArgumentException(
+          "maxTrustDepth must be positive, got: " + maxTrustDepth);
+    }
+    if (trusted == null || trusted.isEmpty()) {
+        throw new IllegalArgumentException(
+            "at least one trusted key is required");
+    }
+    this.maxTrustDepth = maxTrustDepth;
+    this.trusted = trusted;
+    return this;
+  }
+
+  /** Disable web-of-trust checks. */
+  public PublicKeyChecker disableTrust() {
+    trusted = null;
+    return this;
+  }
+
+  /** Set the public key store for reading keys referenced in signatures. */
+  public PublicKeyChecker setStore(PublicKeyStore store) {
+    if (store == null) {
+      throw new IllegalArgumentException("PublicKeyStore is required");
+    }
+    this.store = store;
+    return this;
+  }
+
+  /**
+   * Set the effective time for checking the key.
+   * <p>
+   * If set, check whether the key should be considered valid (e.g. unexpired)
+   * as of this time.
+   *
+   * @param effectiveTime effective time.
+   * @return a reference to this object.
+   */
+  public PublicKeyChecker setEffectiveTime(Date effectiveTime) {
+    this.effectiveTime = effectiveTime;
+    return this;
+  }
+
+  protected Date getEffectiveTime() {
+    return effectiveTime;
+  }
+
+  /**
+   * Check a public key.
+   *
+   * @param key the public key.
+   * @return the result of the check.
+   */
+  public final CheckResult check(PGPPublicKey key) {
+    if (store == null) {
+      throw new IllegalStateException("PublicKeyStore is required");
+    }
+    return check(key, 0, true,
+        trusted != null ? new HashSet<Fingerprint>() : null);
+  }
+
+  /**
+   * Perform custom checks.
+   * <p>
+   * Default implementation reports no problems, but may be overridden by
+   * subclasses.
+   *
+   * @param key the public key.
+   * @param depth the depth from the initial key passed to {@link #check(
+   *     PGPPublicKey)}: 0 if this was the initial key, up to a maximum of
+   *     {@code maxTrustDepth}.
+   * @return the result of the custom check.
+   */
+  public CheckResult checkCustom(PGPPublicKey key, int depth) {
+    return CheckResult.ok();
+  }
+
+  private CheckResult check(PGPPublicKey key, int depth, boolean expand,
+      Set<Fingerprint> seen) {
+    CheckResult basicResult = checkBasic(key, effectiveTime);
+    CheckResult customResult = checkCustom(key, depth);
+    CheckResult trustResult = checkWebOfTrust(key, store, depth, seen);
+    if (!expand && !trustResult.isTrusted()) {
+      trustResult = CheckResult.create(trustResult.getStatus(),
+          "Key is not trusted");
+    }
+
+    List<String> problems = new ArrayList<>(
+        basicResult.getProblems().size()
+        + customResult.getProblems().size()
+        + trustResult.getProblems().size());
+    problems.addAll(basicResult.getProblems());
+    problems.addAll(customResult.getProblems());
+    problems.addAll(trustResult.getProblems());
+
+    Status status;
+    if (basicResult.getStatus() == BAD
+        || customResult.getStatus() == BAD
+        || trustResult.getStatus() == BAD) {
+      // Any BAD result and the final result is BAD.
+      status = BAD;
+    } else if (trustResult.getStatus() == TRUSTED) {
+      // basicResult is BAD or OK, whereas trustResult is BAD or TRUSTED. If
+      // TRUSTED, we trust the final result.
+      status = TRUSTED;
+    } else {
+      // All results were OK or better, but trustResult was not TRUSTED. Don't
+      // let subclasses bypass checkWebOfTrust by returning TRUSTED; just return
+      // OK here.
+      status = OK;
+    }
+    return CheckResult.create(status, problems);
+  }
+
+  private CheckResult checkBasic(PGPPublicKey key, Date now) {
+    List<String> problems = new ArrayList<>(2);
+    gatherRevocationProblems(key, now, problems);
+
+    long validMs = key.getValidSeconds() * 1000;
+    if (validMs != 0) {
+      long msSinceCreation = now.getTime() - key.getCreationTime().getTime();
+      if (msSinceCreation > validMs) {
+        problems.add("Key is expired");
+      }
+    }
+    return CheckResult.create(problems);
+  }
+
+  private void gatherRevocationProblems(PGPPublicKey key, Date now,
+      List<String> problems) {
+    try {
+      List<PGPSignature> revocations = new ArrayList<>();
+      Map<Long, RevocationKey> revokers = new HashMap<>();
+      PGPSignature selfRevocation =
+          scanRevocations(key, now, revocations, revokers);
+      if (selfRevocation != null) {
+        RevocationReason reason = getRevocationReason(selfRevocation);
+        if (isRevocationValid(selfRevocation, reason, now)) {
+          problems.add(reasonToString(reason));
+        }
+      } else {
+        checkRevocations(key, revocations, revokers, problems);
+      }
+    } catch (PGPException | IOException e) {
+      problems.add("Error checking key revocation");
+    }
+  }
+
+  private static boolean isRevocationValid(PGPSignature revocation,
+      RevocationReason reason, Date now) {
+    // RFC4880 states:
+    // "If a key has been revoked because of a compromise, all signatures
+    // created by that key are suspect. However, if it was merely superseded or
+    // retired, old signatures are still valid."
+    //
+    // Note that GnuPG does not implement this correctly, as it does not
+    // consider the revocation reason and timestamp when checking whether a
+    // signature (data or certification) is valid.
+    return reason.getRevocationReason() == KEY_COMPROMISED
+        || revocation.getCreationTime().before(now);
+  }
+
+  private PGPSignature scanRevocations(PGPPublicKey key, Date now,
+      List<PGPSignature> revocations, Map<Long, RevocationKey> revokers)
+      throws PGPException {
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> allSigs = key.getSignatures();
+    while (allSigs.hasNext()) {
+      PGPSignature sig = allSigs.next();
+      switch (sig.getSignatureType()) {
+        case KEY_REVOCATION:
+          if (sig.getKeyID() == key.getKeyID()) {
+            sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+            if (sig.verifyCertification(key)) {
+              return sig;
+            }
+          } else {
+            RevocationReason reason = getRevocationReason(sig);
+            if (reason != null && isRevocationValid(sig, reason, now)) {
+              revocations.add(sig);
+            }
+          }
+          break;
+        case DIRECT_KEY:
+          RevocationKey r = getRevocationKey(key, sig);
+          if (r != null) {
+            revokers.put(Fingerprint.getId(r.getFingerprint()), r);
+          }
+          break;
+      }
+    }
+    return null;
+  }
+
+  private RevocationKey getRevocationKey(PGPPublicKey key, PGPSignature sig)
+      throws PGPException {
+    if (sig.getKeyID() != key.getKeyID()) {
+      return null;
+    }
+    SignatureSubpacket sub =
+        sig.getHashedSubPackets().getSubpacket(REVOCATION_KEY);
+    if (sub == null) {
+      return null;
+    }
+    sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+    if (!sig.verifyCertification(key)) {
+      return null;
+    }
+
+    return new RevocationKey(sub.isCritical(), sub.getData());
+  }
+
+  private void checkRevocations(PGPPublicKey key,
+      List<PGPSignature> revocations, Map<Long, RevocationKey> revokers,
+      List<String> problems)
+      throws PGPException, IOException {
+    for (PGPSignature revocation : revocations) {
+      RevocationKey revoker = revokers.get(revocation.getKeyID());
+      if (revoker == null) {
+        continue; // Not a designated revoker.
+      }
+      byte[] rfp = revoker.getFingerprint();
+      PGPPublicKeyRing revokerKeyRing = store.get(rfp);
+      if (revokerKeyRing == null) {
+        // Revoker is authorized and there is a revocation signature by this
+        // revoker, but the key is not in the store so we can't verify the
+        // signature.
+        log.info("Key " + Fingerprint.toString(key.getFingerprint())
+            + " is revoked by " + Fingerprint.toString(rfp)
+            + ", which is not in the store. Assuming revocation is valid.");
+        problems.add(reasonToString(getRevocationReason(revocation)));
+        continue;
+      }
+      PGPPublicKey rk = revokerKeyRing.getPublicKey();
+      if (rk.getAlgorithm() != revoker.getAlgorithm()) {
+        continue;
+      }
+      if (!checkBasic(rk, revocation.getCreationTime()).isOk()) {
+        // Revoker's key was expired or revoked at time of revocation, so the
+        // revocation is invalid.
+        continue;
+      }
+      revocation.init(new BcPGPContentVerifierBuilderProvider(), rk);
+      if (revocation.verifyCertification(key)) {
+        problems.add(reasonToString(getRevocationReason(revocation)));
+      }
+    }
+  }
+
+  private static RevocationReason getRevocationReason(PGPSignature sig) {
+    if (sig.getSignatureType() != KEY_REVOCATION) {
+      throw new IllegalArgumentException(
+          "Expected KEY_REVOCATION signature, got " + sig.getSignatureType());
+    }
+    SignatureSubpacket sub =
+        sig.getHashedSubPackets().getSubpacket(REVOCATION_REASON);
+    if (sub == null) {
+      return null;
+    }
+    return new RevocationReason(sub.isCritical(), sub.getData());
+  }
+
+  private static String reasonToString(RevocationReason reason) {
+    StringBuilder r = new StringBuilder("Key is revoked (");
+    if (reason == null) {
+      return r.append("no reason provided)").toString();
+    }
+    switch (reason.getRevocationReason()) {
+      case NO_REASON:
+        r.append("no reason code specified");
+        break;
+      case KEY_SUPERSEDED:
+        r.append("superseded");
+        break;
+      case KEY_COMPROMISED:
+        r.append("key material has been compromised");
+        break;
+      case KEY_RETIRED:
+        r.append("retired and no longer valid");
+        break;
+      default:
+        r.append("reason code ")
+            .append(Integer.toString(reason.getRevocationReason()))
+            .append(')');
+        break;
+    }
+    r.append(')');
+    String desc = reason.getRevocationDescription();
+    if (!desc.isEmpty()) {
+      r.append(": ").append(desc);
+    }
+    return r.toString();
+  }
+
+  private CheckResult checkWebOfTrust(PGPPublicKey key, PublicKeyStore store,
+      int depth, Set<Fingerprint> seen) {
+    if (trusted == null) {
+      // Trust checking not configured, server trusts all OK keys.
+      return CheckResult.trusted();
+    }
+    Fingerprint fp = new Fingerprint(key.getFingerprint());
+    if (seen.contains(fp)) {
+      return CheckResult.ok("Key is trusted in a cycle");
+    }
+    seen.add(fp);
+
+    Fingerprint trustedFp = trusted.get(key.getKeyID());
+    if (trustedFp != null && trustedFp.equals(fp)) {
+      return CheckResult.trusted(); // Directly trusted.
+    } else if (depth >= maxTrustDepth) {
+      return CheckResult.ok(
+          "No path of depth <= " + maxTrustDepth + " to a trusted key");
+    }
+
+    List<CheckResult> signerResults = new ArrayList<>();
+    @SuppressWarnings("unchecked")
+    Iterator<String> userIds = key.getUserIDs();
+    while (userIds.hasNext()) {
+      String userId = userIds.next();
+
+      // Don't check the timestamp of these certifications. This allows admins
+      // to correct untrusted keys by signing them with a trusted key, such that
+      // older signatures created by those keys retroactively appear valid.
+      @SuppressWarnings("unchecked")
+      Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
+
+      while (sigs.hasNext()) {
+        PGPSignature sig = sigs.next();
+        // TODO(dborowitz): Handle CERTIFICATION_REVOCATION.
+        if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION
+            && sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) {
+          continue; // Not a certification.
+        }
+
+        PGPPublicKey signer = getSigner(store, sig, userId, key, signerResults);
+        // TODO(dborowitz): Require self certification.
+        if (signer == null
+            || Arrays.equals(signer.getFingerprint(), key.getFingerprint())) {
+          continue;
+        }
+        String subpacketProblem = checkTrustSubpacket(sig, depth);
+        if (subpacketProblem == null) {
+          CheckResult signerResult = check(signer, depth + 1, false, seen);
+          if (signerResult.isTrusted()) {
+            return CheckResult.trusted();
+          }
+        }
+        signerResults.add(CheckResult.ok(
+            "Certification by " + keyToString(signer)
+            + " is valid, but key is not trusted"));
+      }
+    }
+
+    List<String> problems = new ArrayList<>();
+    problems.add("No path to a trusted key");
+    for (CheckResult signerResult : signerResults) {
+      problems.addAll(signerResult.getProblems());
+    }
+    return CheckResult.create(OK, problems);
+  }
+
+  private static PGPPublicKey getSigner(PublicKeyStore store, PGPSignature sig,
+      String userId, PGPPublicKey key, List<CheckResult> results) {
+    try {
+      PGPPublicKeyRingCollection signers = store.get(sig.getKeyID());
+      if (!signers.getKeyRings().hasNext()) {
+        results.add(CheckResult.ok(
+            "Key " + keyIdToString(sig.getKeyID())
+            + " used for certification is not in store"));
+        return null;
+      }
+      PGPPublicKey signer = PublicKeyStore.getSigner(signers, sig, userId, key);
+      if (signer == null) {
+        results.add(CheckResult.ok(
+            "Certification by " + keyIdToString(sig.getKeyID())
+            + " is not valid"));
+        return null;
+      }
+      return signer;
+    } catch (PGPException | IOException e) {
+      results.add(CheckResult.ok(
+          "Error checking certification by " + keyIdToString(sig.getKeyID())));
+      return null;
+    }
+  }
+
+  private String checkTrustSubpacket(PGPSignature sig, int depth) {
+    SignatureSubpacket trustSub = sig.getHashedSubPackets().getSubpacket(
+        SignatureSubpacketTags.TRUST_SIG);
+    if (trustSub == null || trustSub.getData().length != 2) {
+      return "Certification is missing trust information";
+    }
+    byte amount = trustSub.getData()[1];
+    if (amount < COMPLETE_TRUST) {
+      return "Certification does not fully trust key";
+    }
+    byte level = trustSub.getData()[0];
+    int required = depth + 1;
+    if (level < required) {
+      return "Certification trusts to depth " + level
+          + ", but depth " + required + " is required";
+    }
+    return null;
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
new file mode 100644
index 0000000..3d939a1
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -0,0 +1,426 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.NB;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Store of GPG public keys in git notes.
+ * <p>
+ * Keys are stored in filenames based on their hex key ID, padded out to 40
+ * characters to match the length of a SHA-1. (This is to easily reuse existing
+ * fanout code in {@link NoteMap}, and may be changed later after an appropriate
+ * transition.)
+ * <p>
+ * The contents of each file is an ASCII armored stream containing one or more
+ * public key rings matching the ID. Multiple keys are supported because forging
+ * a key ID is possible, but such a key cannot be used to verify signatures
+ * produced with the correct key.
+ * <p>
+ * No additional checks are performed on the key after reading; callers should
+ * only trust keys after checking with a {@link PublicKeyChecker}.
+ */
+public class PublicKeyStore implements AutoCloseable {
+  private static final ObjectId EMPTY_TREE =
+      ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
+
+  /** Ref where GPG public keys are stored. */
+  public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
+
+  /**
+   * Choose the public key that produced a signature.
+   * <p>
+   * @param keyRings candidate keys.
+   * @param sig signature object.
+   * @param data signed payload.
+   * @return the key chosen from {@code keyRings} that was able to verify the
+   *     signature, or {@code null} if none was found.
+   * @throws PGPException if an error occurred verifying the signature.
+   */
+  public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
+      PGPSignature sig, byte[] data) throws PGPException {
+    for (PGPPublicKeyRing kr : keyRings) {
+      PGPPublicKey k = kr.getPublicKey();
+      sig.init(new BcPGPContentVerifierBuilderProvider(), k);
+      sig.update(data);
+      if (sig.verify()) {
+        return k;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Choose the public key that produced a certification.
+   * <p>
+   * @param keyRings candidate keys.
+   * @param sig signature object.
+   * @param userId user ID being certified.
+   * @param key key being certified.
+   * @return the key chosen from {@code keyRings} that was able to verify the
+   *     certification, or {@code null} if none was found.
+   * @throws PGPException if an error occurred verifying the certification.
+   */
+  public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
+      PGPSignature sig, String userId, PGPPublicKey key) throws PGPException {
+    for (PGPPublicKeyRing kr : keyRings) {
+      PGPPublicKey k = kr.getPublicKey();
+      sig.init(new BcPGPContentVerifierBuilderProvider(), k);
+      if (sig.verifyCertification(userId, key)) {
+        return k;
+      }
+    }
+    return null;
+  }
+
+  private final Repository repo;
+  private ObjectReader reader;
+  private RevCommit tip;
+  private NoteMap notes;
+  private Map<Fingerprint, PGPPublicKeyRing> toAdd;
+  private Set<Fingerprint> toRemove;
+
+  /** @param repo repository to read keys from. */
+  public PublicKeyStore(Repository repo) {
+    this.repo = repo;
+    toAdd = new HashMap<>();
+    toRemove = new HashSet<>();
+  }
+
+  @Override
+  public void close() {
+    reset();
+  }
+
+  private void reset() {
+    if (reader != null) {
+      reader.close();
+      reader = null;
+      notes = null;
+    }
+  }
+
+  private void load() throws IOException {
+    reset();
+    reader = repo.newObjectReader();
+
+    Ref ref = repo.getRefDatabase().exactRef(REFS_GPG_KEYS);
+    if (ref == null) {
+      return;
+    }
+    try (RevWalk rw = new RevWalk(reader)) {
+      tip = rw.parseCommit(ref.getObjectId());
+      notes = NoteMap.read(reader, tip);
+    }
+  }
+
+  /**
+   * Read public keys with the given key ID.
+   * <p>
+   * Keys should not be trusted unless checked with {@link PublicKeyChecker}.
+   * <p>
+   * Multiple calls to this method use the same state of the key ref; to reread
+   * the ref, call {@link #close()} first.
+   *
+   * @param keyId key ID.
+   * @return any keys found that could be successfully parsed.
+   * @throws PGPException if an error occurred parsing the key data.
+   * @throws IOException if an error occurred reading the repository data.
+   */
+  public PGPPublicKeyRingCollection get(long keyId)
+      throws PGPException, IOException {
+    return new PGPPublicKeyRingCollection(get(keyId, null));
+  }
+
+  /**
+   * Read public key with the given fingerprint.
+   * <p>
+   * Keys should not be trusted unless checked with {@link PublicKeyChecker}.
+   * <p>
+   * Multiple calls to this method use the same state of the key ref; to reread
+   * the ref, call {@link #close()} first.
+   *
+   * @param fingerprint key fingerprint.
+   * @return the key if found, or {@code null}.
+   * @throws PGPException if an error occurred parsing the key data.
+   * @throws IOException if an error occurred reading the repository data.
+   */
+  public PGPPublicKeyRing get(byte[] fingerprint)
+      throws PGPException, IOException {
+    List<PGPPublicKeyRing> keyRings =
+        get(Fingerprint.getId(fingerprint), fingerprint);
+    return !keyRings.isEmpty() ? keyRings.get(0) : null;
+  }
+
+  private List<PGPPublicKeyRing> get(long keyId, byte[] fp) throws IOException {
+    if (reader == null) {
+      load();
+    }
+    if (notes == null) {
+      return Collections.emptyList();
+    }
+    Note note = notes.getNote(keyObjectId(keyId));
+    if (note == null) {
+      return Collections.emptyList();
+    }
+
+    List<PGPPublicKeyRing> keys = new ArrayList<>();
+    try (InputStream in = reader.open(note.getData(), OBJ_BLOB).openStream()) {
+      while (true) {
+        @SuppressWarnings("unchecked")
+        Iterator<Object> it =
+            new BcPGPObjectFactory(new ArmoredInputStream(in)).iterator();
+        if (!it.hasNext()) {
+          break;
+        }
+        Object obj = it.next();
+        if (obj instanceof PGPPublicKeyRing) {
+          PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
+          if (fp == null
+              || Arrays.equals(fp, kr.getPublicKey().getFingerprint())) {
+            keys.add(kr);
+          }
+        }
+        checkState(!it.hasNext(),
+            "expected one PGP object per ArmoredInputStream");
+      }
+      return keys;
+    }
+  }
+
+  /**
+   * Add a public key to the store.
+   * <p>
+   * Multiple calls may be made to buffer keys in memory, and they are not saved
+   * until {@link #save(CommitBuilder)} is called.
+   *
+   * @param keyRing a key ring containing exactly one public master key.
+   */
+  public void add(PGPPublicKeyRing keyRing) {
+    int numMaster = 0;
+    for (PGPPublicKey key : keyRing) {
+      if (key.isMasterKey()) {
+        numMaster++;
+      }
+    }
+    // We could have an additional sanity check to ensure all subkeys belong to
+    // this master key, but that requires doing actual signature verification
+    // here. The alternative is insane but harmless.
+    if (numMaster != 1) {
+      throw new IllegalArgumentException(
+          "Exactly 1 master key is required, found " + numMaster);
+    }
+    Fingerprint fp = new Fingerprint(keyRing.getPublicKey().getFingerprint());
+    toAdd.put(fp, keyRing);
+    toRemove.remove(fp);
+  }
+
+  /**
+   * Remove a public key from the store.
+   * <p>
+   * Multiple calls may be made to buffer deletes in memory, and they are not
+   * saved until {@link #save(CommitBuilder)} is called.
+   *
+   * @param fingerprint the fingerprint of the key to remove.
+   */
+  public void remove(byte[] fingerprint) {
+    Fingerprint fp = new Fingerprint(fingerprint);
+    toAdd.remove(fp);
+    toRemove.add(fp);
+  }
+
+  /**
+   * Save pending keys to the store.
+   * <p>
+   * One commit is created and the ref updated. The pending list is cleared if
+   * and only if the ref update succeeds, which allows for easy retries in case
+   * of lock failure.
+   *
+   * @param cb commit builder with at least author and identity populated; tree
+   *     and parent are ignored.
+   * @return result of the ref update.
+   */
+  public RefUpdate.Result save(CommitBuilder cb)
+      throws PGPException, IOException {
+    if (toAdd.isEmpty() && toRemove.isEmpty()) {
+      return RefUpdate.Result.NO_CHANGE;
+    }
+    if (reader == null) {
+      load();
+    }
+    if (notes == null) {
+      notes = NoteMap.newEmptyMap();
+    }
+    ObjectId newTip;
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      for (PGPPublicKeyRing keyRing : toAdd.values()) {
+        saveToNotes(ins, keyRing);
+      }
+      for (Fingerprint fp : toRemove) {
+        deleteFromNotes(ins, fp);
+      }
+      cb.setTreeId(notes.writeTree(ins));
+      if (cb.getTreeId().equals(
+          tip != null ? tip.getTree() : EMPTY_TREE)) {
+        return RefUpdate.Result.NO_CHANGE;
+      }
+
+      if (tip != null) {
+        cb.setParentId(tip);
+      }
+      if (cb.getMessage() == null) {
+        int n = toAdd.size() + toRemove.size();
+        cb.setMessage(
+            String.format("Update %d public key%s", n, n != 1 ? "s" : ""));
+      }
+      newTip = ins.insert(cb);
+      ins.flush();
+    }
+
+    RefUpdate ru = repo.updateRef(PublicKeyStore.REFS_GPG_KEYS);
+    ru.setExpectedOldObjectId(tip);
+    ru.setNewObjectId(newTip);
+    ru.setRefLogIdent(cb.getCommitter());
+    ru.setRefLogMessage("Store public keys", true);
+    RefUpdate.Result result = ru.update();
+    reset();
+    switch (result) {
+      case FAST_FORWARD:
+      case NEW:
+      case NO_CHANGE:
+        toAdd.clear();
+        toRemove.clear();
+        break;
+      default:
+        break;
+    }
+    return result;
+  }
+
+  private void saveToNotes(ObjectInserter ins, PGPPublicKeyRing keyRing)
+      throws PGPException, IOException {
+    long keyId = keyRing.getPublicKey().getKeyID();
+    PGPPublicKeyRingCollection existing = get(keyId);
+    List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size() + 1);
+    boolean replaced = false;
+    for (PGPPublicKeyRing kr : existing) {
+      if (sameKey(keyRing, kr)) {
+        toWrite.add(keyRing);
+        replaced = true;
+      } else {
+        toWrite.add(kr);
+      }
+    }
+    if (!replaced) {
+      toWrite.add(keyRing);
+    }
+    notes.set(keyObjectId(keyId),
+        ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+  }
+
+  private void deleteFromNotes(ObjectInserter ins, Fingerprint fp)
+      throws PGPException, IOException {
+    long keyId = fp.getId();
+    PGPPublicKeyRingCollection existing = get(keyId);
+    List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size());
+    for (PGPPublicKeyRing kr : existing) {
+      if (!fp.equalsBytes(kr.getPublicKey().getFingerprint())) {
+        toWrite.add(kr);
+      }
+    }
+    if (toWrite.size() == existing.size()) {
+      return;
+    } else if (!toWrite.isEmpty()) {
+      notes.set(keyObjectId(keyId),
+          ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
+    } else {
+      notes.remove(keyObjectId(keyId));
+    }
+  }
+
+  private static boolean sameKey(PGPPublicKeyRing kr1, PGPPublicKeyRing kr2) {
+    return Arrays.equals(kr1.getPublicKey().getFingerprint(),
+        kr2.getPublicKey().getFingerprint());
+  }
+
+  private static byte[] keysToArmored(List<PGPPublicKeyRing> keys)
+      throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream(4096 * keys.size());
+    for (PGPPublicKeyRing kr : keys) {
+      try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
+        kr.encode(aout);
+      }
+    }
+    return out.toByteArray();
+  }
+
+  public static String keyToString(PGPPublicKey key) {
+    @SuppressWarnings("unchecked")
+    Iterator<String> it = key.getUserIDs();
+    return String.format(
+        "%s %s(%s)",
+        keyIdToString(key.getKeyID()),
+        it.hasNext() ? it.next() + " " : "",
+        Fingerprint.toString(key.getFingerprint()));
+  }
+
+  public static String keyIdToString(long keyId) {
+    // Match key ID format from gpg --list-keys.
+    return String.format("%08X", (int) keyId);
+  }
+
+  static ObjectId keyObjectId(long keyId) {
+    byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
+    NB.encodeInt64(buf, 0, keyId);
+    return ObjectId.fromRaw(buf);
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
new file mode 100644
index 0000000..0a0fff7
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -0,0 +1,225 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Checker for push certificates. */
+public abstract class PushCertificateChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(PushCertificateChecker.class);
+
+  public static class Result {
+    private final PGPPublicKey key;
+    private final CheckResult checkResult;
+
+    private Result(PGPPublicKey key, CheckResult checkResult) {
+      this.key = key;
+      this.checkResult = checkResult;
+    }
+
+    public PGPPublicKey getPublicKey() {
+      return key;
+    }
+
+    public CheckResult getCheckResult() {
+      return checkResult;
+    }
+  }
+
+  private final PublicKeyChecker publicKeyChecker;
+
+  private boolean checkNonce;
+
+  protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
+    this.publicKeyChecker = publicKeyChecker;
+    checkNonce = true;
+  }
+
+  /** Set whether to check the status of the nonce; defaults to true. */
+  public PushCertificateChecker setCheckNonce(boolean checkNonce) {
+    this.checkNonce = checkNonce;
+    return this;
+  }
+
+  /**
+   * Check a push certificate.
+   *
+   * @return result of the check.
+   */
+  public final Result check(PushCertificate cert) {
+    if (checkNonce && cert.getNonceStatus() != NonceStatus.OK) {
+      return new Result(null, CheckResult.bad("Invalid nonce"));
+    }
+    List<CheckResult> results = new ArrayList<>(2);
+    Result sigResult = null;
+    try {
+      PGPSignature sig = readSignature(cert);
+      if (sig != null) {
+        @SuppressWarnings("resource")
+        Repository repo = getRepository();
+        try (PublicKeyStore store = new PublicKeyStore(repo)) {
+          sigResult = checkSignature(sig, cert, store);
+          results.add(checkCustom(repo));
+        } finally {
+          if (shouldClose(repo)) {
+            repo.close();
+          }
+        }
+      } else {
+        results.add(CheckResult.bad("Invalid signature format"));
+      }
+    } catch (PGPException | IOException e) {
+      String msg = "Internal error checking push certificate";
+      log.error(msg, e);
+      results.add(CheckResult.bad(msg));
+    }
+
+    return combine(sigResult, results);
+  }
+
+  private static Result combine(Result sigResult, List<CheckResult> results) {
+    // Combine results:
+    //  - If any input result is BAD, the final result is bad.
+    //  - If sigResult is TRUSTED and no other result is BAD, the final result
+    //    is TRUSTED.
+    //  - Otherwise, the result is OK.
+    List<String> problems = new ArrayList<>();
+    boolean bad = false;
+    for (CheckResult result : results) {
+      problems.addAll(result.getProblems());
+      bad |= result.getStatus() == BAD;
+    }
+    Status status = bad ? BAD : OK;
+
+    PGPPublicKey key;
+    if (sigResult != null) {
+      key = sigResult.getPublicKey();
+      CheckResult cr = sigResult.getCheckResult();
+      problems.addAll(cr.getProblems());
+      if (cr.getStatus() == BAD) {
+        status = BAD;
+      } else if (!bad && cr.getStatus() == TRUSTED) {
+        status = TRUSTED;
+      }
+    } else {
+      key = null;
+    }
+    return new Result(key, CheckResult.create(status, problems));
+  }
+
+  /**
+   * Get the repository that this checker should operate on.
+   * <p>
+   * This method is called once per call to {@link #check(PushCertificate)}.
+   *
+   * @return the repository.
+   * @throws IOException if an error occurred reading the repository.
+   */
+  protected abstract Repository getRepository() throws IOException;
+
+  /**
+   * @param repo a repository previously returned by {@link #getRepository()}.
+   * @return whether this repository should be closed before returning from
+   *     {@link #check(PushCertificate)}.
+   */
+  protected abstract boolean shouldClose(Repository repo);
+
+  /**
+   * Perform custom checks.
+   * <p>
+   * Default implementation reports no problems, but may be overridden by
+   * subclasses.
+   *
+   * @param repo a repository previously returned by {@link #getRepository()}.
+   * @return the result of the custom check.
+   */
+  protected CheckResult checkCustom(Repository repo) {
+    return CheckResult.ok();
+  }
+
+  private PGPSignature readSignature(PushCertificate cert) throws IOException {
+    ArmoredInputStream in = new ArmoredInputStream(
+        new ByteArrayInputStream(Constants.encode(cert.getSignature())));
+    PGPObjectFactory factory = new BcPGPObjectFactory(in);
+    Object obj;
+    while ((obj = factory.nextObject()) != null) {
+      if (obj instanceof PGPSignatureList) {
+        PGPSignatureList sigs = (PGPSignatureList) obj;
+        if (!sigs.isEmpty()) {
+          return sigs.get(0);
+        }
+      }
+    }
+    return null;
+  }
+
+  private Result checkSignature(PGPSignature sig, PushCertificate cert,
+      PublicKeyStore store) throws PGPException, IOException {
+    PGPPublicKeyRingCollection keys = store.get(sig.getKeyID());
+    if (!keys.getKeyRings().hasNext()) {
+      return new Result(null,
+          CheckResult.bad("No public keys found for key ID "
+              + keyIdToString(sig.getKeyID())));
+    }
+    PGPPublicKey signer =
+        PublicKeyStore.getSigner(keys, sig, Constants.encode(cert.toText()));
+    if (signer == null) {
+      return new Result(null,
+          CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID())
+              + " is not valid"));
+    }
+    CheckResult result = publicKeyChecker
+        .setStore(store)
+        .setEffectiveTime(sig.getCreationTime())
+        .check(signer);
+    if (!result.getProblems().isEmpty()) {
+      StringBuilder err = new StringBuilder("Invalid public key ")
+          .append(keyToString(signer))
+          .append(":\n  ")
+          .append(Joiner.on("\n  ").join(result.getProblems()));
+      return new Result(
+          signer, CheckResult.create(result.getStatus(), err.toString()));
+    }
+    return new Result(signer, result);
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
new file mode 100644
index 0000000..bc027cd
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2015 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.gpg;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.EnableSignedPush;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.PreReceiveHookChain;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.SignedPushConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+class SignedPushModule extends AbstractModule {
+  private static final Logger log =
+      LoggerFactory.getLogger(SignedPushModule.class);
+
+  @Override
+  protected void configure() {
+    if (!BouncyCastleUtil.havePGP()) {
+      throw new ProvisionException("Bouncy Castle PGP not installed");
+    }
+    bind(PublicKeyStore.class).toProvider(StoreProvider.class);
+    DynamicSet.bind(binder(), ReceivePackInitializer.class)
+        .to(Initializer.class);
+  }
+
+  @Singleton
+  private static class Initializer implements ReceivePackInitializer {
+    private final SignedPushConfig signedPushConfig;
+    private final SignedPushPreReceiveHook hook;
+    private final ProjectCache projectCache;
+
+    @Inject
+    Initializer(@GerritServerConfig Config cfg,
+        @EnableSignedPush boolean enableSignedPush,
+        SignedPushPreReceiveHook hook,
+        ProjectCache projectCache) {
+      this.hook = hook;
+      this.projectCache = projectCache;
+
+      if (enableSignedPush) {
+        String seed = cfg.getString("receive", null, "certNonceSeed");
+        if (Strings.isNullOrEmpty(seed)) {
+          seed = randomString(64);
+        }
+        signedPushConfig = new SignedPushConfig();
+        signedPushConfig.setCertNonceSeed(seed);
+        signedPushConfig.setCertNonceSlopLimit(
+            cfg.getInt("receive", null, "certNonceSlop", 5 * 60));
+      } else {
+        signedPushConfig = null;
+      }
+    }
+
+    @Override
+    public void init(Project.NameKey project, ReceivePack rp) {
+      ProjectState ps = projectCache.get(project);
+      if (!ps.isEnableSignedPush()) {
+        rp.setSignedPushConfig(null);
+        return;
+      } else if (signedPushConfig == null) {
+        log.error("receive.enableSignedPush is true for project {} but"
+            + " false in gerrit.config, so signed push verification is"
+            + " disabled", project.get());
+        rp.setSignedPushConfig(null);
+        return;
+      }
+      rp.setSignedPushConfig(signedPushConfig);
+
+      List<PreReceiveHook> hooks = new ArrayList<>(3);
+      if (ps.isRequireSignedPush()) {
+        hooks.add(SignedPushPreReceiveHook.Required.INSTANCE);
+      }
+      hooks.add(hook);
+      hooks.add(rp.getPreReceiveHook());
+      rp.setPreReceiveHook(PreReceiveHookChain.newChain(hooks));
+    }
+  }
+
+  @Singleton
+  private static class StoreProvider implements Provider<PublicKeyStore> {
+    private final GitRepositoryManager repoManager;
+    private final AllUsersName allUsers;
+
+    @Inject
+    StoreProvider(GitRepositoryManager repoManager,
+        AllUsersName allUsers) {
+      this.repoManager = repoManager;
+      this.allUsers = allUsers;
+    }
+
+    @Override
+    public PublicKeyStore get() {
+      final Repository repo;
+      try {
+        repo = repoManager.openRepository(allUsers);
+      } catch (IOException e) {
+        throw new ProvisionException("Cannot open " + allUsers, e);
+      }
+      return new PublicKeyStore(repo) {
+        @Override
+        public void close() {
+          try {
+            super.close();
+          } finally {
+            repo.close();
+          }
+        }
+      };
+    }
+  }
+
+  private static String randomString(int len) {
+    Random random;
+    try {
+      random = SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalStateException(e);
+    }
+    StringBuilder sb = new StringBuilder(len);
+    for (int i = 0; i < len; i++) {
+      sb.append((char) random.nextInt());
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
new file mode 100644
index 0000000..cdc3c62
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2015 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.gpg;
+
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+
+import java.util.Collection;
+
+/**
+ * Pre-receive hook to check signed pushes.
+ * <p>
+ * If configured, prior to processing any push using
+ * {@link com.google.gerrit.server.git.ReceiveCommits}, requires that any push
+ * certificate present must be valid.
+ */
+@Singleton
+public class SignedPushPreReceiveHook implements PreReceiveHook {
+  public static class Required implements PreReceiveHook {
+    public static final Required INSTANCE = new Required();
+
+    @Override
+    public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+      if (rp.getPushCertificate() == null) {
+        rp.sendMessage("ERROR: Signed push is required");
+        reject(commands, "push cert error");
+      }
+    }
+
+    private Required() {
+    }
+  }
+
+  private final Provider<IdentifiedUser> user;
+  private final GerritPushCertificateChecker.Factory checkerFactory;
+
+  @Inject
+  public SignedPushPreReceiveHook(
+      Provider<IdentifiedUser> user,
+      GerritPushCertificateChecker.Factory checkerFactory) {
+    this.user = user;
+    this.checkerFactory = checkerFactory;
+  }
+
+  @Override
+  public void onPreReceive(ReceivePack rp,
+      Collection<ReceiveCommand> commands) {
+    PushCertificate cert = rp.getPushCertificate();
+    if (cert == null) {
+      return;
+    }
+    CheckResult result = checkerFactory.create(user.get())
+        .setCheckNonce(true)
+        .check(cert)
+        .getCheckResult();
+    if (!isAllowed(result, commands)) {
+      for (String problem : result.getProblems()) {
+        rp.sendMessage(problem);
+      }
+      reject(commands, "invalid push cert");
+    }
+  }
+
+  private static boolean isAllowed(CheckResult result,
+      Collection<ReceiveCommand> commands) {
+    if (onlyMagicBranches(commands)) {
+      // Only pushing magic branches: allow a valid push certificate even if the
+      // key is not ultimately trusted. Assume anyone with Submit permission to
+      // the branch is able to verify during review that the code is legitimate.
+      return result.isOk();
+    } else {
+      // Directly updating one or more refs: require a trusted key.
+      return result.isTrusted();
+    }
+  }
+
+  private static boolean onlyMagicBranches(Iterable<ReceiveCommand> commands) {
+    for (ReceiveCommand c : commands) {
+      if (!MagicBranch.isMagicBranch(c.getRefName())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static void reject(Collection<ReceiveCommand> commands,
+      String reason) {
+    for (ReceiveCommand cmd : commands) {
+      if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+        cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
+      }
+    }
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
new file mode 100644
index 0000000..e6720db
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2015 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.gpg.api;
+
+import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.gpg.GerritPushCertificateChecker;
+import com.google.gerrit.gpg.PushCertificateChecker;
+import com.google.gerrit.gpg.server.GpgKeys;
+import com.google.gerrit.gpg.server.PostGpgKeys;
+import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificateParser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public class GpgApiAdapterImpl implements GpgApiAdapter {
+  private final PostGpgKeys postGpgKeys;
+  private final GpgKeys gpgKeys;
+  private final GpgKeyApiImpl.Factory gpgKeyApiFactory;
+  private final GerritPushCertificateChecker.Factory pushCertCheckerFactory;
+
+  @Inject
+  GpgApiAdapterImpl(
+      PostGpgKeys postGpgKeys,
+      GpgKeys gpgKeys,
+      GpgKeyApiImpl.Factory gpgKeyApiFactory,
+      GerritPushCertificateChecker.Factory pushCertCheckerFactory) {
+    this.postGpgKeys = postGpgKeys;
+    this.gpgKeys = gpgKeys;
+    this.gpgKeyApiFactory = gpgKeyApiFactory;
+    this.pushCertCheckerFactory = pushCertCheckerFactory;
+  }
+
+  @Override
+  public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
+      throws RestApiException, GpgException {
+    try {
+      return gpgKeys.list().apply(account);
+    } catch (OrmException | PGPException | IOException e) {
+      throw new GpgException(e);
+    }
+  }
+
+  @Override
+  public Map<String, GpgKeyInfo> putGpgKeys(AccountResource account,
+      List<String> add, List<String> delete)
+      throws RestApiException, GpgException {
+    PostGpgKeys.Input in = new PostGpgKeys.Input();
+    in.add = add;
+    in.delete = delete;
+    try {
+      return postGpgKeys.apply(account, in);
+    } catch (PGPException | OrmException | IOException e) {
+      throw new GpgException(e);
+    }
+  }
+
+  @Override
+  public GpgKeyApi gpgKey(AccountResource account, IdString idStr)
+      throws RestApiException, GpgException {
+    try {
+      return gpgKeyApiFactory.create(gpgKeys.parse(account, idStr));
+    } catch (PGPException | OrmException | IOException e) {
+      throw new GpgException(e);
+    }
+  }
+
+  @Override
+  public PushCertificateInfo checkPushCertificate(String certStr,
+      IdentifiedUser expectedUser) throws GpgException {
+    try {
+      PushCertificate cert = PushCertificateParser.fromString(certStr);
+      PushCertificateChecker.Result result = pushCertCheckerFactory
+          .create(expectedUser)
+          .setCheckNonce(false)
+          .check(cert);
+      PushCertificateInfo info = new PushCertificateInfo();
+      info.certificate = certStr;
+      info.key = GpgKeys.toJson(result.getPublicKey(), result.getCheckResult());
+      return info;
+    } catch (IOException e) {
+      throw new GpgException(e);
+    }
+  }
+
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
new file mode 100644
index 0000000..932f439
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiModule.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2015 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.gpg.api;
+
+import static com.google.gerrit.gpg.server.GpgKey.GPG_KEY_KIND;
+import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
+
+import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.gpg.server.DeleteGpgKey;
+import com.google.gerrit.gpg.server.GpgKeys;
+import com.google.gerrit.gpg.server.PostGpgKeys;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+
+import java.util.List;
+import java.util.Map;
+
+public class GpgApiModule extends RestApiModule {
+  private final boolean enabled;
+
+  public GpgApiModule(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  @Override
+  protected void configure() {
+    if (!enabled) {
+      bind(GpgApiAdapter.class).to(NoGpgApi.class);
+      return;
+    }
+    bind(GpgApiAdapter.class).to(GpgApiAdapterImpl.class);
+    factory(GpgKeyApiImpl.Factory.class);
+
+    DynamicMap.mapOf(binder(), GPG_KEY_KIND);
+
+    child(ACCOUNT_KIND, "gpgkeys").to(GpgKeys.class);
+    post(ACCOUNT_KIND, "gpgkeys").to(PostGpgKeys.class);
+    get(GPG_KEY_KIND).to(GpgKeys.Get.class);
+    delete(GPG_KEY_KIND).to(DeleteGpgKey.class);
+  }
+
+  private static class NoGpgApi implements GpgApiAdapter {
+    private static final String MSG = "GPG key APIs disabled";
+
+    @Override
+    public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account) {
+      throw new NotImplementedException(MSG);
+    }
+
+    @Override
+    public Map<String, GpgKeyInfo> putGpgKeys(AccountResource account,
+        List<String> add, List<String> delete) {
+      throw new NotImplementedException(MSG);
+    }
+
+    @Override
+    public GpgKeyApi gpgKey(AccountResource account, IdString idStr) {
+      throw new NotImplementedException(MSG);
+    }
+
+    @Override
+    public PushCertificateInfo checkPushCertificate(String certStr,
+        IdentifiedUser expectedUser) {
+      throw new NotImplementedException(MSG);
+    }
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
new file mode 100644
index 0000000..ab30184
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2015 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.gpg.api;
+
+import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.gpg.server.DeleteGpgKey;
+import com.google.gerrit.gpg.server.GpgKey;
+import com.google.gerrit.gpg.server.GpgKeys;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.bouncycastle.openpgp.PGPException;
+
+import java.io.IOException;
+
+public class GpgKeyApiImpl implements GpgKeyApi {
+  public interface Factory {
+    GpgKeyApiImpl create(GpgKey rsrc);
+  }
+
+  private final GpgKeys.Get get;
+  private final DeleteGpgKey delete;
+  private final GpgKey rsrc;
+
+  @AssistedInject
+  GpgKeyApiImpl(
+      GpgKeys.Get get,
+      DeleteGpgKey delete,
+      @Assisted GpgKey rsrc) {
+    this.get = get;
+    this.delete = delete;
+    this.rsrc = rsrc;
+  }
+
+  @Override
+  public GpgKeyInfo get() throws RestApiException {
+    try {
+      return get.apply(rsrc);
+    } catch (IOException e) {
+      throw new RestApiException("Cannot get GPG key", e);
+    }
+  }
+
+  @Override
+  public void delete() throws RestApiException {
+    try {
+      delete.apply(rsrc, new DeleteGpgKey.Input());
+    } catch (PGPException | OrmException | IOException e) {
+      throw new RestApiException("Cannot delete GPG key", e);
+    }
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
new file mode 100644
index 0000000..baac714
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2015 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.gpg.server;
+
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+
+import com.google.common.io.BaseEncoding;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.gpg.server.DeleteGpgKey.Input;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+
+import java.io.IOException;
+import java.util.Collections;
+
+public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
+  public static class Input {
+  }
+
+  private final Provider<PersonIdent> serverIdent;
+  private final Provider<ReviewDb> db;
+  private final Provider<PublicKeyStore> storeProvider;
+
+  @Inject
+  DeleteGpgKey(@GerritPersonIdent Provider<PersonIdent> serverIdent,
+      Provider<ReviewDb> db,
+      Provider<PublicKeyStore> storeProvider) {
+    this.serverIdent = serverIdent;
+    this.db = db;
+    this.storeProvider = storeProvider;
+  }
+
+  @Override
+  public Response<?> apply(GpgKey rsrc, Input input)
+      throws ResourceConflictException, PGPException, OrmException,
+      IOException {
+    PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
+    AccountExternalId.Key extIdKey = new AccountExternalId.Key(
+        AccountExternalId.SCHEME_GPGKEY,
+        BaseEncoding.base16().encode(key.getFingerprint()));
+    db.get().accountExternalIds().deleteKeys(Collections.singleton(extIdKey));
+
+    try (PublicKeyStore store = storeProvider.get()) {
+      store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint());
+
+      CommitBuilder cb = new CommitBuilder();
+      PersonIdent committer = serverIdent.get();
+      cb.setAuthor(rsrc.getUser().newCommitterIdent(
+          committer.getWhen(), committer.getTimeZone()));
+      cb.setCommitter(committer);
+      cb.setMessage("Delete public key " + keyIdToString(key.getKeyID()));
+
+      RefUpdate.Result saveResult = store.save(cb);
+      switch (saveResult) {
+        case NO_CHANGE:
+        case FAST_FORWARD:
+          break;
+        default:
+          throw new ResourceConflictException(
+              "Failed to delete public key: " + saveResult);
+      }
+    }
+    return Response.none();
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKey.java
new file mode 100644
index 0000000..2fe7eb6
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKey.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2015 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.gpg.server;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.inject.TypeLiteral;
+
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+
+public class GpgKey extends AccountResource {
+  public static final TypeLiteral<RestView<GpgKey>> GPG_KEY_KIND =
+      new TypeLiteral<RestView<GpgKey>>() {};
+
+  private final PGPPublicKeyRing keyRing;
+
+  public GpgKey(IdentifiedUser user, PGPPublicKeyRing keyRing) {
+    super(user);
+    this.keyRing = keyRing;
+  }
+
+  public PGPPublicKeyRing getKeyRing() {
+    return keyRing;
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
new file mode 100644
index 0000000..a136007
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -0,0 +1,271 @@
+// Copyright (C) 2015 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.gpg.server;
+
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.gpg.BouncyCastleUtil;
+import com.google.gerrit.gpg.CheckResult;
+import com.google.gerrit.gpg.Fingerprint;
+import com.google.gerrit.gpg.GerritPublicKeyChecker;
+import com.google.gerrit.gpg.PublicKeyChecker;
+import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.util.NB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+@Singleton
+public class GpgKeys implements
+    ChildCollection<AccountResource, GpgKey> {
+  private static final Logger log = LoggerFactory.getLogger(GpgKeys.class);
+
+  public static String MIME_TYPE = "application/pgp-keys";
+
+  private final DynamicMap<RestView<GpgKey>> views;
+  private final Provider<ReviewDb> db;
+  private final Provider<PublicKeyStore> storeProvider;
+  private final GerritPublicKeyChecker.Factory checkerFactory;
+
+  @Inject
+  GpgKeys(DynamicMap<RestView<GpgKey>> views,
+      Provider<ReviewDb> db,
+      Provider<PublicKeyStore> storeProvider,
+      GerritPublicKeyChecker.Factory checkerFactory) {
+    this.views = views;
+    this.db = db;
+    this.storeProvider = storeProvider;
+    this.checkerFactory = checkerFactory;
+  }
+
+  @Override
+  public ListGpgKeys list()
+      throws ResourceNotFoundException, AuthException {
+    checkEnabled();
+    return new ListGpgKeys();
+  }
+
+  @Override
+  public GpgKey parse(AccountResource parent, IdString id)
+      throws ResourceNotFoundException, PGPException, OrmException,
+      IOException {
+    checkEnabled();
+    String str = CharMatcher.WHITESPACE.removeFrom(id.get()).toUpperCase();
+    if ((str.length() != 8 && str.length() != 40)
+        || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    byte[] fp = parseFingerprint(id.get(), getGpgExtIds(parent));
+    try (PublicKeyStore store = storeProvider.get()) {
+      long keyId = keyId(fp);
+      for (PGPPublicKeyRing keyRing : store.get(keyId)) {
+        PGPPublicKey key = keyRing.getPublicKey();
+        if (Arrays.equals(key.getFingerprint(), fp)) {
+          return new GpgKey(parent.getUser(), keyRing);
+        }
+      }
+    }
+
+    throw new ResourceNotFoundException(id);
+  }
+
+  static byte[] parseFingerprint(String str,
+      Iterable<AccountExternalId> existingExtIds)
+      throws ResourceNotFoundException {
+    str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
+    if ((str.length() != 8 && str.length() != 40)
+        || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
+      throw new ResourceNotFoundException(str);
+    }
+    byte[] fp = null;
+    for (AccountExternalId extId : existingExtIds) {
+      String fpStr = extId.getSchemeRest();
+      if (!fpStr.endsWith(str)) {
+        continue;
+      } else if (fp != null) {
+        throw new ResourceNotFoundException("Multiple keys found for " + str);
+      }
+      fp = BaseEncoding.base16().decode(fpStr);
+      if (str.length() == 40) {
+        break;
+      }
+    }
+    if (fp == null) {
+      throw new ResourceNotFoundException(str);
+    }
+    return fp;
+  }
+
+  @Override
+  public DynamicMap<RestView<GpgKey>> views() {
+    return views;
+  }
+
+  public class ListGpgKeys implements RestReadView<AccountResource> {
+    @Override
+    public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
+        throws OrmException, PGPException, IOException {
+      Map<String, GpgKeyInfo> keys = new HashMap<>();
+      try (PublicKeyStore store = storeProvider.get()) {
+        for (AccountExternalId extId : getGpgExtIds(rsrc)) {
+          String fpStr = extId.getSchemeRest();
+          byte[] fp = BaseEncoding.base16().decode(fpStr);
+          boolean found = false;
+          for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
+            if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
+              found = true;
+              GpgKeyInfo info = toJson(
+                  keyRing.getPublicKey(),
+                  checkerFactory.create(rsrc.getUser(), store),
+                  store);
+              keys.put(info.id, info);
+              info.id = null;
+              break;
+            }
+          }
+          if (!found) {
+            log.warn("No public key stored for fingerprint {}",
+                Fingerprint.toString(fp));
+          }
+        }
+      }
+      return keys;
+    }
+  }
+
+  @Singleton
+  public static class Get implements RestReadView<GpgKey> {
+    private final Provider<PublicKeyStore> storeProvider;
+    private final GerritPublicKeyChecker.Factory checkerFactory;
+
+    @Inject
+    Get(Provider<PublicKeyStore> storeProvider,
+        GerritPublicKeyChecker.Factory checkerFactory) {
+      this.storeProvider = storeProvider;
+      this.checkerFactory = checkerFactory;
+    }
+
+    @Override
+    public GpgKeyInfo apply(GpgKey rsrc) throws IOException {
+      try (PublicKeyStore store = storeProvider.get()) {
+        return toJson(
+            rsrc.getKeyRing().getPublicKey(),
+            checkerFactory.create().setExpectedUser(rsrc.getUser()),
+            store);
+      }
+    }
+  }
+
+  @VisibleForTesting
+  public static FluentIterable<AccountExternalId> getGpgExtIds(ReviewDb db,
+      Account.Id accountId) throws OrmException {
+    return FluentIterable
+        .from(db.accountExternalIds().byAccount(accountId))
+        .filter(new Predicate<AccountExternalId>() {
+          @Override
+          public boolean apply(AccountExternalId in) {
+            return in.isScheme(SCHEME_GPGKEY);
+          }
+        });
+  }
+
+  private Iterable<AccountExternalId> getGpgExtIds(AccountResource rsrc)
+      throws OrmException {
+    return getGpgExtIds(db.get(), rsrc.getUser().getAccountId());
+  }
+
+  private static long keyId(byte[] fp) {
+    return NB.decodeInt64(fp, fp.length - 8);
+  }
+
+  static void checkEnabled() throws ResourceNotFoundException {
+    if (!BouncyCastleUtil.havePGP()) {
+      throw new ResourceNotFoundException("GPG not enabled");
+    }
+  }
+
+  public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult)
+      throws IOException {
+    GpgKeyInfo info = new GpgKeyInfo();
+
+    if (key != null) {
+      info.id = PublicKeyStore.keyIdToString(key.getKeyID());
+      info.fingerprint = Fingerprint.toString(key.getFingerprint());
+      @SuppressWarnings("unchecked")
+      Iterator<String> userIds = key.getUserIDs();
+      info.userIds = ImmutableList.copyOf(userIds);
+
+      try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+          ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
+        // This is not exactly the key stored in the store, but is equivalent. In
+        // particular, it will have a Bouncy Castle version string. The armored
+        // stream reader in PublicKeyStore doesn't give us an easy way to extract
+        // the original ASCII armor.
+        key.encode(aout);
+        info.key = new String(out.toByteArray(), UTF_8);
+      }
+    }
+
+    info.status = checkResult.getStatus();
+    info.problems = checkResult.getProblems();
+
+    return info;
+  }
+
+  static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker,
+      PublicKeyStore store) throws IOException {
+    return toJson(key, checker.setStore(store).check(key));
+  }
+
+  public static void toJson(GpgKeyInfo info, CheckResult checkResult) {
+    info.status = checkResult.getStatus();
+    info.problems = checkResult.getProblems();
+  }
+}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
new file mode 100644
index 0000000..91c4494
--- /dev/null
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -0,0 +1,266 @@
+// Copyright (C) 2015 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.gpg.server;
+
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.gpg.CheckResult;
+import com.google.gerrit.gpg.Fingerprint;
+import com.google.gerrit.gpg.GerritPublicKeyChecker;
+import com.google.gerrit.gpg.PublicKeyChecker;
+import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.gpg.server.PostGpgKeys.Input;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.mail.AddKeySender;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Singleton
+public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+    public List<String> add;
+    public List<String> delete;
+  }
+
+  private final Logger log = LoggerFactory.getLogger(getClass());
+  private final Provider<PersonIdent> serverIdent;
+  private final Provider<ReviewDb> db;
+  private final Provider<PublicKeyStore> storeProvider;
+  private final GerritPublicKeyChecker.Factory checkerFactory;
+  private final AddKeySender.Factory addKeyFactory;
+
+  @Inject
+  PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
+      Provider<ReviewDb> db,
+      Provider<PublicKeyStore> storeProvider,
+      GerritPublicKeyChecker.Factory checkerFactory,
+      AddKeySender.Factory addKeyFactory) {
+    this.serverIdent = serverIdent;
+    this.db = db;
+    this.storeProvider = storeProvider;
+    this.checkerFactory = checkerFactory;
+    this.addKeyFactory = addKeyFactory;
+  }
+
+  @Override
+  public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
+      throws ResourceNotFoundException, BadRequestException,
+      ResourceConflictException, PGPException, OrmException, IOException {
+    GpgKeys.checkEnabled();
+
+    List<AccountExternalId> existingExtIds =
+        GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
+
+    try (PublicKeyStore store = storeProvider.get()) {
+      Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
+      List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
+      List<AccountExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
+
+      for (PGPPublicKeyRing keyRing : newKeys) {
+        PGPPublicKey key = keyRing.getPublicKey();
+        AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
+        AccountExternalId existing = db.get().accountExternalIds().get(extIdKey);
+        if (existing != null) {
+          if (!existing.getAccountId().equals(rsrc.getUser().getAccountId())) {
+            throw new ResourceConflictException(
+                "GPG key already associated with another account");
+          }
+        } else {
+          newExtIds.add(
+              new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
+        }
+      }
+
+      storeKeys(rsrc, newKeys, toRemove);
+      if (!newExtIds.isEmpty()) {
+        db.get().accountExternalIds().insert(newExtIds);
+      }
+      db.get().accountExternalIds().deleteKeys(Iterables.transform(toRemove,
+          new Function<Fingerprint, AccountExternalId.Key>() {
+            @Override
+            public AccountExternalId.Key apply(Fingerprint fp) {
+              return toExtIdKey(fp.get());
+            }
+          }));
+      return toJson(newKeys, toRemove, store, rsrc.getUser());
+    }
+  }
+
+  private Set<Fingerprint> readKeysToRemove(Input input,
+      List<AccountExternalId> existingExtIds) {
+    if (input.delete == null || input.delete.isEmpty()) {
+      return ImmutableSet.of();
+    }
+    Set<Fingerprint> fingerprints =
+        Sets.newHashSetWithExpectedSize(input.delete.size());
+    for (String id : input.delete) {
+      try {
+        fingerprints.add(new Fingerprint(
+            GpgKeys.parseFingerprint(id, existingExtIds)));
+      } catch (ResourceNotFoundException e) {
+        // Skip removal.
+      }
+    }
+    return fingerprints;
+  }
+
+  private List<PGPPublicKeyRing> readKeysToAdd(Input input,
+      Set<Fingerprint> toRemove)
+      throws BadRequestException, IOException {
+    if (input.add == null || input.add.isEmpty()) {
+      return ImmutableList.of();
+    }
+    List<PGPPublicKeyRing> keyRings = new ArrayList<>(input.add.size());
+    for (String armored : input.add) {
+      try (InputStream in = new ByteArrayInputStream(armored.getBytes(UTF_8));
+          ArmoredInputStream ain = new ArmoredInputStream(in)) {
+        @SuppressWarnings("unchecked")
+        List<Object> objs = Lists.newArrayList(new BcPGPObjectFactory(ain));
+        if (objs.size() != 1 || !(objs.get(0) instanceof PGPPublicKeyRing)) {
+          throw new BadRequestException("Expected exactly one PUBLIC KEY BLOCK");
+        }
+        PGPPublicKeyRing keyRing = (PGPPublicKeyRing) objs.get(0);
+        if (toRemove.contains(
+            new Fingerprint(keyRing.getPublicKey().getFingerprint()))) {
+          throw new BadRequestException("Cannot both add and delete key: "
+              + keyToString(keyRing.getPublicKey()));
+        }
+        keyRings.add(keyRing);
+      }
+    }
+    return keyRings;
+  }
+
+  private void storeKeys(AccountResource rsrc, List<PGPPublicKeyRing> keyRings,
+      Set<Fingerprint> toRemove) throws BadRequestException,
+      ResourceConflictException, PGPException, IOException {
+    try (PublicKeyStore store = storeProvider.get()) {
+      List<String> addedKeys = new ArrayList<>();
+      for (PGPPublicKeyRing keyRing : keyRings) {
+        PGPPublicKey key = keyRing.getPublicKey();
+        // Don't check web of trust; admins can fill in certifications later.
+        CheckResult result = checkerFactory.create(rsrc.getUser(), store)
+            .disableTrust()
+            .check(key);
+        if (!result.isOk()) {
+          throw new BadRequestException(String.format(
+              "Problems with public key %s:\n%s",
+              keyToString(key), Joiner.on('\n').join(result.getProblems())));
+        }
+        addedKeys.add(PublicKeyStore.keyToString(key));
+        store.add(keyRing);
+      }
+      for (Fingerprint fp : toRemove) {
+        store.remove(fp.get());
+      }
+      CommitBuilder cb = new CommitBuilder();
+      PersonIdent committer = serverIdent.get();
+      cb.setAuthor(rsrc.getUser().newCommitterIdent(
+          committer.getWhen(), committer.getTimeZone()));
+      cb.setCommitter(committer);
+
+      RefUpdate.Result saveResult = store.save(cb);
+      switch (saveResult) {
+        case NEW:
+        case FAST_FORWARD:
+        case FORCED:
+          try {
+            addKeyFactory.create(rsrc.getUser(), addedKeys).send();
+          } catch (EmailException e) {
+            log.error("Cannot send GPG key added message to "
+                + rsrc.getUser().getAccount().getPreferredEmail(), e);
+          }
+          break;
+        case NO_CHANGE:
+          break;
+        default:
+          // TODO(dborowitz): Backoff and retry on LOCK_FAILURE.
+          throw new ResourceConflictException(
+              "Failed to save public keys: " + saveResult);
+      }
+    }
+  }
+
+  private final AccountExternalId.Key toExtIdKey(byte[] fp) {
+    return new AccountExternalId.Key(
+        AccountExternalId.SCHEME_GPGKEY,
+        BaseEncoding.base16().encode(fp));
+  }
+
+  private Map<String, GpgKeyInfo> toJson(
+      Collection<PGPPublicKeyRing> keys,
+      Set<Fingerprint> deleted, PublicKeyStore store, IdentifiedUser user)
+      throws IOException {
+    // Unlike when storing keys, include web-of-trust checks when producing
+    // result JSON, so the user at least knows of any issues.
+    PublicKeyChecker checker = checkerFactory.create(user, store);
+    Map<String, GpgKeyInfo> infos =
+        Maps.newHashMapWithExpectedSize(keys.size() + deleted.size());
+    for (PGPPublicKeyRing keyRing : keys) {
+      PGPPublicKey key = keyRing.getPublicKey();
+      CheckResult result = checker.check(key);
+      GpgKeyInfo info = GpgKeys.toJson(key, result);
+      infos.put(info.id, info);
+      info.id = null;
+    }
+    for (Fingerprint fp : deleted) {
+      infos.put(keyIdToString(fp.getId()), new GpgKeyInfo());
+    }
+    return infos;
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
new file mode 100644
index 0000000..4df9d37
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -0,0 +1,461 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.gpg.GerritPublicKeyChecker.toExtIdKey;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
+import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
+import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED;
+import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+import com.google.gerrit.gpg.testutil.TestKey;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Unit tests for {@link GerritPublicKeyChecker}. */
+public class GerritPublicKeyCheckerTest {
+  @Inject
+  private AccountCache accountCache;
+
+  @Inject
+  private AccountManager accountManager;
+
+  @Inject
+  private GerritPublicKeyChecker.Factory checkerFactory;
+
+  @Inject
+  private IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  private InMemoryDatabase schemaFactory;
+
+  @Inject
+  private SchemaCreator schemaCreator;
+
+  @Inject
+  private ThreadLocalRequestContext requestContext;
+
+  private LifecycleManager lifecycle;
+  private ReviewDb db;
+  private Account.Id userId;
+  private IdentifiedUser user;
+  private Repository storeRepo;
+  private PublicKeyStore store;
+
+  @Before
+  public void setUpInjector() throws Exception {
+    Config cfg = InMemoryModule.newDefaultConfig();
+    cfg.setInt("receive", null, "maxTrustDepth", 2);
+    cfg.setStringList("receive", null, "trustedKey", ImmutableList.of(
+        Fingerprint.toString(keyB().getPublicKey().getFingerprint()),
+        Fingerprint.toString(keyD().getPublicKey().getFingerprint())));
+    Injector injector = Guice.createInjector(new InMemoryModule(cfg));
+
+    lifecycle = new LifecycleManager();
+    lifecycle.add(injector);
+    injector.injectMembers(this);
+    lifecycle.start();
+
+    db = schemaFactory.open();
+    schemaCreator.create(db);
+    userId =
+        accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+    Account userAccount = db.accounts().get(userId);
+    // Note: does not match any key in TestKeys.
+    userAccount.setPreferredEmail("user@example.com");
+    db.accounts().update(ImmutableList.of(userAccount));
+    user = reloadUser();
+
+    requestContext.setContext(new RequestContext() {
+      @Override
+      public CurrentUser getUser() {
+        return user;
+      }
+
+      @Override
+      public Provider<ReviewDb> getReviewDbProvider() {
+        return Providers.of(db);
+      }
+    });
+
+    storeRepo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(storeRepo);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    store.close();
+    storeRepo.close();
+  }
+
+  private IdentifiedUser addUser(String name) throws Exception {
+    AuthRequest req = AuthRequest.forUser(name);
+    Account.Id id = accountManager.authenticate(req).getAccountId();
+    return userFactory.create(Providers.of(db), id);
+  }
+
+  private IdentifiedUser reloadUser() {
+    accountCache.evict(userId);
+    user = userFactory.create(Providers.of(db), userId);
+    return user;
+  }
+
+  @After
+  public void tearDownInjector() {
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
+    if (db != null) {
+      db.close();
+    }
+    InMemoryDatabase.drop(schemaFactory);
+  }
+
+  @Test
+  public void defaultGpgCertificationMatchesEmail() throws Exception {
+    TestKey key = validKeyWithSecondUserId();
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertProblems(
+        checker.check(key.getPublicKey()), Status.BAD,
+        "Key must contain a valid certification for one of the following "
+          + "identities:\n"
+          + "  gerrit:user\n"
+          + "  username:user");
+
+    addExternalId("test", "test", "test5@example.com");
+    checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertNoProblems(checker.check(key.getPublicKey()));
+  }
+
+  @Test
+  public void defaultGpgCertificationDoesNotMatchEmail() throws Exception {
+    addExternalId("test", "test", "nobody@example.com");
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertProblems(
+        checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
+        "Key must contain a valid certification for one of the following "
+          + "identities:\n"
+          + "  gerrit:user\n"
+          + "  nobody@example.com\n"
+          + "  test:test\n"
+          + "  username:user");
+  }
+
+  @Test
+  public void manualCertificationMatchesExternalId() throws Exception {
+    addExternalId("foo", "myId", null);
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertNoProblems(checker.check(validKeyWithSecondUserId().getPublicKey()));
+  }
+
+  @Test
+  public void manualCertificationDoesNotMatchExternalId() throws Exception {
+    addExternalId("foo", "otherId", null);
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertProblems(
+        checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
+        "Key must contain a valid certification for one of the following "
+          + "identities:\n"
+          + "  foo:otherId\n"
+          + "  gerrit:user\n"
+          + "  username:user");
+  }
+
+  @Test
+  public void noExternalIds() throws Exception {
+    db.accountExternalIds().delete(
+        db.accountExternalIds().byAccount(user.getAccountId()));
+    reloadUser();
+
+    TestKey key = validKeyWithSecondUserId();
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertProblems(
+        checker.check(key.getPublicKey()), Status.BAD,
+        "No identities found for user; check"
+          + " http://test/#/settings/web-identities");
+
+    checker = checkerFactory.create()
+        .setStore(store)
+        .disableTrust();
+    assertProblems(
+        checker.check(key.getPublicKey()), Status.BAD,
+        "Key is not associated with any users");
+
+    db.accountExternalIds().insert(Collections.singleton(
+        new AccountExternalId(
+            user.getAccountId(), toExtIdKey(key.getPublicKey()))));
+    reloadUser();
+    assertProblems(
+        checker.check(key.getPublicKey()), Status.BAD,
+        "No identities found for user");
+  }
+
+  @Test
+  public void checkValidTrustChainAndCorrectExternalIds() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // The server ultimately trusts B and D.
+    // D and E trust C to be a valid introducer of depth 2.
+    IdentifiedUser userB = addUser("userB");
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), userB);
+    add(keyC(), addUser("userC"));
+    add(keyD(), addUser("userD"));
+    add(keyE(), addUser("userE"));
+
+    // Checker for A, checking A.
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertNoProblems(checkerA.check(keyA.getPublicKey()));
+
+    // Checker for B, checking B. Trust chain and IDs are correct, so the only
+    // problem is with the key itself.
+    PublicKeyChecker checkerB = checkerFactory.create(userB, store);
+    assertProblems(
+        checkerB.check(keyB.getPublicKey()), Status.BAD,
+        "Key is expired");
+  }
+
+  @Test
+  public void checkWithValidKeyButWrongExpectedUserInChecker()
+      throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // The server ultimately trusts B and D.
+    // D and E trust C to be a valid introducer of depth 2.
+    IdentifiedUser userB = addUser("userB");
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), userB);
+    add(keyC(), addUser("userC"));
+    add(keyD(), addUser("userD"));
+    add(keyE(), addUser("userE"));
+
+    // Checker for A, checking B.
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertProblems(
+        checkerA.check(keyB.getPublicKey()), Status.BAD,
+        "Key is expired",
+        "Key must contain a valid certification for one of the following"
+            + " identities:\n"
+            + "  gerrit:user\n"
+            + "  mailto:testa@example.com\n"
+            + "  testa@example.com\n"
+            + "  username:user");
+
+    // Checker for B, checking A.
+    PublicKeyChecker checkerB = checkerFactory.create(userB, store);
+    assertProblems(
+        checkerB.check(keyA.getPublicKey()), Status.BAD,
+        "Key must contain a valid certification for one of the following"
+            + " identities:\n"
+            + "  gerrit:userB\n"
+            + "  mailto:testb@example.com\n"
+            + "  testb@example.com\n"
+            + "  username:userB");
+  }
+
+  @Test
+  public void checkTrustChainWithExpiredKey() throws Exception {
+    // A---Bx
+    //
+    // The server ultimately trusts B.
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), addUser("userB"));
+
+    PublicKeyChecker checker = checkerFactory.create(user, store);
+    assertProblems(
+        checker.check(keyA.getPublicKey()), Status.OK,
+        "No path to a trusted key",
+        "Certification by " + keyToString(keyB.getPublicKey())
+            + " is valid, but key is not trusted",
+        "Key D24FE467 used for certification is not in store");
+  }
+
+  @Test
+  public void checkTrustChainUsingCheckerWithoutExpectedKey() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // The server ultimately trusts B and D.
+    // D and E trust C to be a valid introducer of depth 2.
+    TestKey keyA = add(keyA(), user);
+    TestKey keyB = add(keyB(), addUser("userB"));
+    TestKey keyC = add(keyC(), addUser("userC"));
+    TestKey keyD = add(keyD(), addUser("userD"));
+    TestKey keyE = add(keyE(), addUser("userE"));
+
+    // This checker can check any key, so the only problems come from issues
+    // with the keys themselves, not having invalid user IDs.
+    PublicKeyChecker checker = checkerFactory.create()
+        .setStore(store);
+    assertNoProblems(checker.check(keyA.getPublicKey()));
+    assertProblems(
+        checker.check(keyB.getPublicKey()), Status.BAD,
+        "Key is expired");
+    assertNoProblems(checker.check(keyC.getPublicKey()));
+    assertNoProblems(checker.check(keyD.getPublicKey()));
+    assertProblems(
+        checker.check(keyE.getPublicKey()), Status.BAD,
+        "Key is expired",
+        "No path to a trusted key");
+  }
+
+  @Test
+  public void keyLaterInTrustChainMissingUserId() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C
+    //
+    // The server ultimately trusts B.
+    // C signed A's key but is not in the store.
+    TestKey keyA = add(keyA(), user);
+
+    PGPPublicKeyRing keyRingB = keyB().getPublicKeyRing();
+    PGPPublicKey keyB = keyRingB.getPublicKey();
+    keyB = PGPPublicKey.removeCertification(
+        keyB, (String) keyB.getUserIDs().next());
+    keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB);
+    add(keyRingB, addUser("userB"));
+
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertProblems(checkerA.check(keyA.getPublicKey()), Status.OK,
+        "No path to a trusted key",
+        "Certification by " + keyToString(keyB)
+            + " is valid, but key is not trusted",
+        "Key D24FE467 used for certification is not in store");
+  }
+
+  private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
+    Account.Id id = user.getAccountId();
+    List<AccountExternalId> newExtIds = new ArrayList<>(2);
+    newExtIds.add(new AccountExternalId(id, toExtIdKey(kr.getPublicKey())));
+
+    @SuppressWarnings("unchecked")
+    String userId = (String) Iterators.getOnlyElement(
+        kr.getPublicKey().getUserIDs(), null);
+    if (userId != null) {
+      String email = PushCertificateIdent.parse(userId).getEmailAddress();
+      assertThat(email).contains("@");
+      AccountExternalId mailto = new AccountExternalId(
+          id, new AccountExternalId.Key(SCHEME_MAILTO, email));
+      mailto.setEmailAddress(email);
+      newExtIds.add(mailto);
+    }
+
+    store.add(kr);
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
+
+    db.accountExternalIds().insert(newExtIds);
+    accountCache.evict(user.getAccountId());
+  }
+
+  private TestKey add(TestKey k, IdentifiedUser user) throws Exception {
+    add(k.getPublicKeyRing(), user);
+    return k;
+  }
+
+  private void assertProblems(CheckResult result, Status expectedStatus,
+      String first, String... rest) throws Exception {
+    List<String> expectedProblems = new ArrayList<>();
+    expectedProblems.add(first);
+    expectedProblems.addAll(Arrays.asList(rest));
+    assertThat(result.getStatus()).isEqualTo(expectedStatus);
+    assertThat(result.getProblems())
+        .containsExactlyElementsIn(expectedProblems)
+        .inOrder();
+  }
+
+  private void assertNoProblems(CheckResult result) {
+    assertThat(result.getStatus()).isEqualTo(Status.TRUSTED);
+    assertThat(result.getProblems()).isEmpty();
+  }
+
+  private void addExternalId(String scheme, String id, String email)
+      throws Exception {
+    AccountExternalId extId = new AccountExternalId(user.getAccountId(),
+        new AccountExternalId.Key(scheme, id));
+    if (email != null) {
+      extId.setEmailAddress(email);
+    }
+    db.accountExternalIds().insert(Collections.singleton(extId));
+    reloadUser();
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
new file mode 100644
index 0000000..99e96a2
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -0,0 +1,397 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyAfterExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyBeforeExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedCompromisedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedNoLongerUsedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.selfRevokedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyF;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyG;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyH;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyI;
+import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyJ;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.gpg.testutil.TestKey;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class PublicKeyCheckerTest {
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  private InMemoryRepository repo;
+  private PublicKeyStore store;
+
+  @Before
+  public void setUp() {
+    repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(repo);
+  }
+
+  @After
+  public void tearDown() {
+    if (store != null) {
+      store.close();
+      store = null;
+    }
+    if (repo != null) {
+      repo.close();
+      repo = null;
+    }
+  }
+
+  @Test
+  public void validKey() throws Exception {
+    assertNoProblems(validKeyWithoutExpiration());
+  }
+
+  @Test
+  public void keyExpiringInFuture() throws Exception {
+    TestKey k = validKeyWithExpiration();
+
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store);
+    assertNoProblems(checker, k);
+
+    checker.setEffectiveTime(parseDate("2015-07-10 12:00:00 -0400"));
+    assertNoProblems(checker, k);
+
+    checker.setEffectiveTime(parseDate("2075-07-10 12:00:00 -0400"));
+    assertProblems(checker, k, "Key is expired");
+  }
+
+  @Test
+  public void expiredKeyIsExpired() throws Exception {
+    assertProblems(expiredKey(), "Key is expired");
+  }
+
+  @Test
+  public void selfRevokedKeyIsRevoked() throws Exception {
+    assertProblems(selfRevokedKey(),
+        "Key is revoked (key material has been compromised)");
+  }
+
+  // Test keys specific to this test are at the bottom of this class. Each test
+  // has a diagram of the trust network, where:
+  //  - The notation M---N indicates N trusts M.
+  //  - An 'x' indicates the key is expired.
+
+  @Test
+  public void trustValidPathLength2() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // D and E trust C to be a valid introducer of depth 2.
+    TestKey ka = add(keyA());
+    TestKey kb = add(keyB());
+    TestKey kc = add(keyC());
+    TestKey kd = add(keyD());
+    TestKey ke = add(keyE());
+    save();
+
+    PublicKeyChecker checker = newChecker(2, kb, kd);
+    assertNoProblems(checker, ka);
+    assertProblems(checker, kb, "Key is expired");
+    assertNoProblems(checker, kc);
+    assertNoProblems(checker, kd);
+    assertProblems(checker, ke, "Key is expired", "No path to a trusted key");
+  }
+
+  @Test
+  public void trustValidPathLength1() throws Exception {
+    // A---Bx
+    //  \
+    //   \---C---D
+    //        \
+    //         \---Ex
+    //
+    // D and E trust C to be a valid introducer of depth 2.
+    TestKey ka = add(keyA());
+    TestKey kb = add(keyB());
+    TestKey kc = add(keyC());
+    TestKey kd = add(keyD());
+    add(keyE());
+    save();
+
+    PublicKeyChecker checker = newChecker(1, kd);
+    assertProblems(checker, ka,
+        "No path to a trusted key", notTrusted(kb), notTrusted(kc));
+  }
+
+  @Test
+  public void trustCycle() throws Exception {
+    // F---G---F, in a cycle.
+    TestKey kf = add(keyF());
+    TestKey kg = add(keyG());
+    save();
+
+    PublicKeyChecker checker = newChecker(10, keyA());
+    assertProblems(checker, kf,
+        "No path to a trusted key", notTrusted(kg));
+    assertProblems(checker, kg,
+        "No path to a trusted key", notTrusted(kf));
+  }
+
+  @Test
+  public void trustInsufficientDepthInSignature() throws Exception {
+    // H---I---J, but J is only trusted to length 1.
+    TestKey kh = add(keyH());
+    TestKey ki = add(keyI());
+    add(keyJ());
+    save();
+
+    PublicKeyChecker checker = newChecker(10, keyJ());
+
+    // J trusts I to a depth of 1, so I itself is valid, but I's certification
+    // of K is not valid.
+    assertNoProblems(checker, ki);
+    assertProblems(checker, kh,
+        "No path to a trusted key", notTrusted(ki));
+  }
+
+  @Test
+  public void revokedKeyDueToCompromise() throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (key material has been compromised):"
+          + " test6 compromised");
+
+    PGPPublicKeyRing kr = removeRevokers(k.getPublicKeyRing());
+    store.add(kr);
+    save();
+
+    // Key no longer specified as revoker.
+    assertNoProblems(kr.getPublicKey());
+  }
+
+  @Test
+  public void revokedKeyDueToCompromiseRevokesKeyRetroactively()
+      throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    String problem =
+        "Key is revoked (key material has been compromised): test6 compromised";
+    assertProblems(k, problem);
+
+    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store)
+        .setEffectiveTime(df.parse("2010-01-01 12:00:00"));
+    assertProblems(checker, k, problem);
+  }
+
+  @Test
+  public void revokedByKeyNotPresentInStore() throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (key material has been compromised):"
+          + " test6 compromised");
+  }
+
+  @Test
+  public void revokedKeyDueToNoLongerBeingUsed() throws Exception {
+    TestKey k = add(revokedNoLongerUsedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (retired and no longer valid): test7 not used");
+  }
+
+  @Test
+  public void revokedKeyDueToNoLongerBeingUsedDoesNotRevokeKeyRetroactively()
+      throws Exception {
+    TestKey k = add(revokedNoLongerUsedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (retired and no longer valid): test7 not used");
+
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store)
+        .setEffectiveTime(parseDate("2010-01-01 12:00:00 -0400"));
+    assertNoProblems(checker, k);
+  }
+
+  @Test
+  public void keyRevokedByExpiredKeyAfterExpirationIsNotRevoked()
+      throws Exception {
+    TestKey k = add(keyRevokedByExpiredKeyAfterExpiration());
+    add(expiredKey());
+    save();
+
+    PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
+    assertNoProblems(checker, k);
+  }
+
+  @Test
+  public void keyRevokedByExpiredKeyBeforeExpirationIsRevoked()
+      throws Exception {
+    TestKey k = add(keyRevokedByExpiredKeyBeforeExpiration());
+    add(expiredKey());
+    save();
+
+    PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
+    assertProblems(checker, k,
+        "Key is revoked (retired and no longer valid): test9 not used");
+
+    // Set time between key creation and revocation.
+    checker.setEffectiveTime(parseDate("2005-08-01 13:00:00 -0400"));
+    assertNoProblems(checker, k);
+  }
+
+  private PGPPublicKeyRing removeRevokers(PGPPublicKeyRing kr) {
+    PGPPublicKey k = kr.getPublicKey();
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> sigs = k.getSignaturesOfType(DIRECT_KEY);
+    while (sigs.hasNext()) {
+      PGPSignature sig = sigs.next();
+      if (sig.getHashedSubPackets().hasSubpacket(REVOCATION_KEY)) {
+        k = PGPPublicKey.removeCertification(k, sig);
+      }
+    }
+    return PGPPublicKeyRing.insertPublicKey(kr, k);
+  }
+
+  private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
+    Map<Long, Fingerprint> fps = new HashMap<>();
+    for (TestKey k : trusted) {
+      Fingerprint fp = new Fingerprint(k.getPublicKey().getFingerprint());
+      fps.put(fp.getId(), fp);
+    }
+    return new PublicKeyChecker()
+        .enableTrust(maxTrustDepth, fps)
+        .setStore(store);
+  }
+
+  private TestKey add(TestKey k) {
+    store.add(k.getPublicKeyRing());
+    return k;
+  }
+
+  private void save() throws Exception {
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    RefUpdate.Result result = store.save(cb);
+    switch (result) {
+      case NEW:
+      case FAST_FORWARD:
+      case FORCED:
+        break;
+      default:
+        throw new AssertionError(result);
+    }
+  }
+
+  private void assertProblems(PublicKeyChecker checker, TestKey k,
+      String first, String... rest) {
+    CheckResult result = checker.setStore(store)
+        .check(k.getPublicKey());
+    assertEquals(list(first, rest), result.getProblems());
+  }
+
+  private void assertNoProblems(PublicKeyChecker checker, TestKey k) {
+    CheckResult result = checker.setStore(store)
+        .check(k.getPublicKey());
+    assertEquals(Collections.emptyList(), result.getProblems());
+  }
+
+  private void assertProblems(TestKey tk, String first, String... rest) {
+    assertProblems(tk.getPublicKey(), first, rest);
+  }
+
+  private void assertNoProblems(TestKey tk) {
+    assertNoProblems(tk.getPublicKey());
+  }
+
+  private void assertProblems(PGPPublicKey k, String first, String... rest) {
+    CheckResult result = new PublicKeyChecker()
+        .setStore(store)
+        .check(k);
+    assertEquals(list(first, rest), result.getProblems());
+  }
+
+  private void assertNoProblems(PGPPublicKey k) {
+    CheckResult result = new PublicKeyChecker()
+        .setStore(store)
+        .check(k);
+    assertEquals(Collections.emptyList(), result.getProblems());
+  }
+
+  private static String notTrusted(TestKey k) {
+    return "Certification by " + keyToString(k.getPublicKey())
+        + " is valid, but key is not trusted";
+  }
+
+  private static Date parseDate(String str) throws Exception {
+    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(str);
+  }
+
+  private static List<String> list(String first, String[] rest) {
+    List<String> all = new ArrayList<>();
+    all.add(first);
+    all.addAll(Arrays.asList(rest));
+    return all;
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
new file mode 100644
index 0000000..5a1cd45
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -0,0 +1,255 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyObjectId;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.gpg.testutil.TestKey;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class PublicKeyStoreTest {
+  private TestRepository<?> tr;
+  private PublicKeyStore store;
+
+  @Before
+  public void setUp() throws Exception {
+    tr = new TestRepository<>(new InMemoryRepository(
+        new DfsRepositoryDescription("pubkeys")));
+    store = new PublicKeyStore(tr.getRepository());
+  }
+
+  @Test
+  public void testKeyIdToString() throws Exception {
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
+    assertEquals("46328A8C", keyIdToString(key.getKeyID()));
+  }
+
+  @Test
+  public void testKeyToString() throws Exception {
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
+    assertEquals("46328A8C Testuser One <test1@example.com>"
+          + " (04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C)",
+        keyToString(key));
+  }
+
+  @Test
+  public void testKeyObjectId() throws Exception {
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
+    String objId = keyObjectId(key.getKeyID()).name();
+    assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
+    assertEquals(keyIdToString(key.getKeyID()).toLowerCase(),
+        objId.substring(8, 16));
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    tr.branch(REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key1.getKeyId()).name(),
+          key1.getPublicKeyArmored())
+        .create();
+    TestKey key2 = validKeyWithExpiration();
+    tr.branch(REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key2.getKeyId()).name(),
+          key2.getPublicKeyArmored())
+        .create();
+
+    assertKeys(key1.getKeyId(), key1);
+    assertKeys(key2.getKeyId(), key2);
+  }
+
+  @Test
+  public void testGetMultiple() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
+    tr.branch(REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key1.getKeyId()).name(),
+            key1.getPublicKeyArmored()
+              // Mismatched for this key ID, but we can still read it out.
+              + key2.getPublicKeyArmored())
+        .create();
+    assertKeys(key1.getKeyId(), key1, key2);
+  }
+
+  @Test
+  public void save() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
+    store.add(key1.getPublicKeyRing());
+    store.add(key2.getPublicKeyRing());
+
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    assertKeys(key1.getKeyId(), key1);
+    assertKeys(key2.getKeyId(), key2);
+  }
+
+  @Test
+  public void saveAppendsToExistingList() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
+    tr.branch(REFS_GPG_KEYS)
+        .commit()
+        // Mismatched for this key ID, but we can still read it out.
+        .add(keyObjectId(key1.getKeyId()).name(), key2.getPublicKeyArmored())
+        .create();
+
+    store.add(key1.getPublicKeyRing());
+    assertEquals(RefUpdate.Result.FAST_FORWARD, store.save(newCommitBuilder()));
+
+    assertKeys(key1.getKeyId(), key1, key2);
+
+    try (ObjectReader reader = tr.getRepository().newObjectReader();
+        RevWalk rw = new RevWalk(reader)) {
+      NoteMap notes = NoteMap.read(
+          reader, tr.getRevWalk().parseCommit(
+            tr.getRepository().exactRef(REFS_GPG_KEYS).getObjectId()));
+      String contents = new String(
+          reader.open(notes.get(keyObjectId(key1.getKeyId()))).getBytes(),
+          UTF_8);
+      String header = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
+      int i1 = contents.indexOf(header);
+      assertTrue(i1 >= 0);
+      int i2 = contents.indexOf(header, i1 + header.length());
+      assertTrue(i2 >= 0);
+    }
+  }
+
+  @Test
+  public void updateExisting() throws Exception {
+    TestKey key5 = validKeyWithSecondUserId();
+    PGPPublicKeyRing keyRing = key5.getPublicKeyRing();
+    PGPPublicKey key = keyRing.getPublicKey();
+    store.add(keyRing);
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    assertUserIds(store.get(key5.getKeyId()).iterator().next(),
+        "Testuser Five <test5@example.com>",
+        "foo:myId");
+
+    keyRing = PGPPublicKeyRing.removePublicKey(keyRing, key);
+    key = PGPPublicKey.removeCertification(key, "foo:myId");
+    keyRing = PGPPublicKeyRing.insertPublicKey(keyRing, key);
+    store.add(keyRing);
+    assertEquals(RefUpdate.Result.FAST_FORWARD, store.save(newCommitBuilder()));
+
+    Iterator<PGPPublicKeyRing> keyRings = store.get(key.getKeyID()).iterator();
+    keyRing = keyRings.next();
+    assertFalse(keyRings.hasNext());
+    assertUserIds(keyRing, "Testuser Five <test5@example.com>");
+  }
+
+  @Test
+  public void remove() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    store.add(key1.getPublicKeyRing());
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+    assertKeys(key1.getKeyId(), key1);
+
+    store.remove(key1.getPublicKey().getFingerprint());
+    assertEquals(RefUpdate.Result.FAST_FORWARD, store.save(newCommitBuilder()));
+    assertKeys(key1.getKeyId());
+  }
+
+  @Test
+  public void removeNonexisting() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    store.add(key1.getPublicKeyRing());
+    assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
+
+    TestKey key2 = validKeyWithExpiration();
+    store.remove(key2.getPublicKey().getFingerprint());
+    assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
+    assertKeys(key1.getKeyId(), key1);
+  }
+
+  @Test
+  public void addThenRemove() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    store.add(key1.getPublicKeyRing());
+    store.remove(key1.getPublicKey().getFingerprint());
+    assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
+    assertKeys(key1.getKeyId());
+  }
+
+  private void assertKeys(long keyId, TestKey... expected)
+      throws Exception {
+    Set<String> expectedStrings = new TreeSet<>();
+    for (TestKey k : expected) {
+      expectedStrings.add(keyToString(k.getPublicKey()));
+    }
+    PGPPublicKeyRingCollection actual = store.get(keyId);
+    Set<String> actualStrings = new TreeSet<>();
+    for (PGPPublicKeyRing k : actual) {
+      actualStrings.add(keyToString(k.getPublicKey()));
+    }
+    assertEquals(expectedStrings, actualStrings);
+  }
+
+  private void assertUserIds(PGPPublicKeyRing keyRing, String... expected)
+      throws Exception {
+    List<String> actual = new ArrayList<>();
+    @SuppressWarnings("unchecked")
+    Iterator<String> userIds = store.get(keyRing.getPublicKey().getKeyID())
+        .iterator().next().getPublicKey().getUserIDs();
+    while (userIds.hasNext()) {
+      actual.add(userIds.next());
+    }
+
+    assertEquals(actual, Arrays.asList(expected));
+  }
+
+  private CommitBuilder newCommitBuilder() {
+    CommitBuilder cb = new CommitBuilder();
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    return cb;
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
new file mode 100644
index 0000000..12a911e
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -0,0 +1,212 @@
+// Copyright (C) 2015 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.gpg;
+
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.gpg.testutil.TestKey;
+
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.BCPGOutputStream;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureGenerator;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.eclipse.jgit.transport.PushCertificateParser;
+import org.eclipse.jgit.transport.SignedPushConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+public class PushCertificateCheckerTest {
+  private InMemoryRepository repo;
+  private PublicKeyStore store;
+  private SignedPushConfig signedPushConfig;
+  private PushCertificateChecker checker;
+
+  @Before
+  public void setUp() throws Exception {
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key3 = expiredKey();
+    repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(repo);
+    store.add(key1.getPublicKeyRing());
+    store.add(key3.getPublicKeyRing());
+
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    assertEquals(RefUpdate.Result.NEW, store.save(cb));
+
+    signedPushConfig = new SignedPushConfig();
+    signedPushConfig.setCertNonceSeed("sekret");
+    signedPushConfig.setCertNonceSlopLimit(60 * 24);
+    checker = newChecker(true);
+  }
+
+  private PushCertificateChecker newChecker(boolean checkNonce) {
+    PublicKeyChecker keyChecker = new PublicKeyChecker().setStore(store);
+    return new PushCertificateChecker(keyChecker) {
+      @Override
+      protected Repository getRepository() {
+        return repo;
+      }
+
+      @Override
+      protected boolean shouldClose(Repository repo) {
+        return false;
+      }
+    }.setCheckNonce(checkNonce);
+  }
+
+  @Test
+  public void validCert() throws Exception {
+    PushCertificate cert =
+        newSignedCert(validNonce(), validKeyWithoutExpiration());
+    assertNoProblems(cert);
+  }
+
+  @Test
+  public void invalidNonce() throws Exception {
+    PushCertificate cert =
+        newSignedCert("invalid-nonce", validKeyWithoutExpiration());
+    assertProblems(cert, "Invalid nonce");
+  }
+
+  @Test
+  public void invalidNonceNotChecked() throws Exception {
+    checker = newChecker(false);
+    PushCertificate cert =
+        newSignedCert("invalid-nonce", validKeyWithoutExpiration());
+    assertNoProblems(cert);
+  }
+
+  @Test
+  public void missingKey() throws Exception {
+    TestKey key2 = validKeyWithExpiration();
+    PushCertificate cert = newSignedCert(validNonce(), key2);
+    assertProblems(cert,
+        "No public keys found for key ID " + keyIdToString(key2.getKeyId()));
+  }
+
+  @Test
+  public void invalidKey() throws Exception {
+    TestKey key3 = expiredKey();
+    PushCertificate cert = newSignedCert(validNonce(), key3);
+    assertProblems(cert,
+        "Invalid public key " + keyToString(key3.getPublicKey())
+          + ":\n  Key is expired");
+  }
+
+  @Test
+  public void signatureByExpiredKeyBeforeExpiration() throws Exception {
+    TestKey key3 = expiredKey();
+    Date now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z")
+        .parse("2005-07-10 12:00:00 -0400");
+    PushCertificate cert = newSignedCert(validNonce(), key3, now);
+    assertNoProblems(cert);
+  }
+
+  private String validNonce() {
+    return signedPushConfig.getNonceGenerator()
+        .createNonce(repo, System.currentTimeMillis() / 1000);
+  }
+
+  private PushCertificate newSignedCert(String nonce, TestKey signingKey)
+      throws Exception {
+    return newSignedCert(nonce, signingKey, null);
+  }
+
+  private PushCertificate newSignedCert(String nonce, TestKey signingKey,
+      Date now) throws Exception {
+    PushCertificateIdent ident = new PushCertificateIdent(
+        signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60);
+    String payload = "certificate version 0.1\n"
+      + "pusher " + ident.getRaw() + "\n"
+      + "pushee test://localhost/repo.git\n"
+      + "nonce " + nonce + "\n"
+      + "\n"
+      + "0000000000000000000000000000000000000000"
+      + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
+      + " refs/heads/master\n";
+    PGPSignatureGenerator gen = new PGPSignatureGenerator(
+        new BcPGPContentSignerBuilder(
+          signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1));
+
+    if (now != null) {
+      PGPSignatureSubpacketGenerator subGen =
+          new PGPSignatureSubpacketGenerator();
+      subGen.setSignatureCreationTime(false, now);
+      gen.setHashedSubpackets(subGen.generate());
+    }
+
+    gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey());
+    gen.update(payload.getBytes(UTF_8));
+    PGPSignature sig = gen.generate();
+
+    ByteArrayOutputStream bout = new ByteArrayOutputStream();
+    try (BCPGOutputStream out = new BCPGOutputStream(
+        new ArmoredOutputStream(bout))) {
+      sig.encode(out);
+    }
+
+    String cert = payload + new String(bout.toByteArray(), UTF_8);
+    Reader reader =
+        new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
+    PushCertificateParser parser =
+        new PushCertificateParser(repo, signedPushConfig);
+    return parser.parse(reader);
+  }
+
+  private void assertProblems(PushCertificate cert, String first,
+      String... rest) throws Exception {
+    List<String> expected = new ArrayList<>();
+    expected.add(first);
+    expected.addAll(Arrays.asList(rest));
+    CheckResult result = checker.check(cert).getCheckResult();
+    assertEquals(expected, result.getProblems());
+  }
+
+  private void assertNoProblems(PushCertificate cert) {
+    CheckResult result = checker.check(cert).getCheckResult();
+    assertEquals(Collections.emptyList(), result.getProblems());
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java
new file mode 100644
index 0000000..494cb2d
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2015 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.gpg.testutil;
+
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
+import org.eclipse.jgit.lib.Constants;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+public class TestKey {
+  private final String pubArmored;
+  private final String secArmored;
+  private final PGPPublicKeyRing pubRing;
+  private final PGPSecretKeyRing secRing;
+
+  public TestKey(String pubArmored, String secArmored) {
+    this.pubArmored = pubArmored;
+    this.secArmored = secArmored;
+    BcKeyFingerprintCalculator fc = new BcKeyFingerprintCalculator();
+    try {
+      this.pubRing = new PGPPublicKeyRing(newStream(pubArmored), fc);
+      this.secRing = new PGPSecretKeyRing(newStream(secArmored), fc);
+    } catch (PGPException | IOException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  public String getPublicKeyArmored() {
+    return pubArmored;
+  }
+
+  public String getSecretKeyArmored() {
+    return secArmored;
+  }
+
+  public PGPPublicKeyRing getPublicKeyRing() {
+    return pubRing;
+  }
+
+  public PGPPublicKey getPublicKey() {
+    return pubRing.getPublicKey();
+  }
+
+  public PGPSecretKey getSecretKey() {
+    return secRing.getSecretKey();
+  }
+
+  public long getKeyId() {
+    return getPublicKey().getKeyID();
+  }
+
+  public String getKeyIdString() {
+    return keyIdToString(getPublicKey().getKeyID());
+  }
+
+  public String getFirstUserId() {
+    return (String) getPublicKey().getUserIDs().next();
+  }
+
+  public PGPPrivateKey getPrivateKey() throws PGPException {
+    return getSecretKey().extractPrivateKey(
+        new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider())
+          // All test keys have no passphrase.
+          .build(new char[0]));
+  }
+
+  private static ArmoredInputStream newStream(String armored)
+      throws IOException {
+    return new ArmoredInputStream(
+        new ByteArrayInputStream(Constants.encode(armored)));
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
new file mode 100644
index 0000000..ad944c5
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
@@ -0,0 +1,1028 @@
+// Copyright (C) 2015 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.gpg.testutil;
+
+import com.google.common.collect.ImmutableList;
+
+/** Common test keys used by a variety of tests. */
+public class TestKeys {
+  public static ImmutableList<TestKey> allValidKeys() {
+    return ImmutableList.of(
+        validKeyWithoutExpiration(),
+        validKeyWithExpiration(),
+        validKeyWithSecondUserId());
+  }
+
+  /**
+   * A valid key with no expiration.
+   *
+   * <pre>
+   * pub   2048R/46328A8C 2015-07-08
+   *       Key fingerprint = 04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C
+   * uid                  Testuser One &lt;test1@example.com&gt;
+   * sub   2048R/F0AF69C0 2015-07-08
+   * </pre>
+   */
+  public static TestKey validKeyWithoutExpiration() {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
+        + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
+        + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
+        + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
+        + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
+        + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8\n"
+        + "dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ\n"
+        + "CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq\n"
+        + "yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg\n"
+        + "RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx\n"
+        + "jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX\n"
+        + "7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy\n"
+        + "W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN\n"
+        + "BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN\n"
+        + "vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE\n"
+        + "fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb\n"
+        + "nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m\n"
+        + "rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl\n"
+        + "IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA\n"
+        + "CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF\n"
+        + "rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj\n"
+        + "Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/\n"
+        + "FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB\n"
+        + "PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA\n"
+        + "0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
+        + "=o/aU\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
+        + "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
+        + "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
+        + "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
+        + "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
+        + "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAEAB/wLoOXEJ+Buo+OZHjpb\n"
+        + "SSZf8GdGs+mOJoKbSJvR6zT/rFsrikUvOPmgt8B9qWjKmJVXO5L09+/Wd/MuX0L1\n"
+        + "7plhdvowP1bl2/j5VyLvZx2qwKXkiCGStFzrBGp9nKtJp4Z8O69pb//ZXaiAtDJC\n"
+        + "HFa1kYT4VgFTevrXtg/z/C0np4Yjx0mZpw4nfISEeHCiYCyRa/B8R1+Pc4uIcoSo\n"
+        + "G3aq6Ow9m/LGvw0MRO5qHvqoF41TLPQpGKjKEsCBKHF1qh0tOOUHnLGrvbmdFnGr\n"
+        + "UXJpRkLdRTnj8ufvA4XVZhImzL+lD+ALtjlV14xh8nsNKYL42880GFl5Cl0OtBcE\n"
+        + "lgQBBADPJ6kHdvUYOe0zugRdukBSYLkZcYwRiphom7dZuavYICIu6B14ljEONzVD\n"
+        + "mPhi2lDOawZOURKwYd9S4K11XWLsTYe7XEwkc+1Fpvu4L/JqnJTTnnvbx05ZsqD5\n"
+        + "j9tybPlrTuLrf2ctfcC03Z55wfo6azsbf89yrr6QX0+l9dlkYQQA/xcMdQJ0Z5vm\n"
+        + "kvyaCPsQzJc/8noVO9PMv7xJm14gJWK7Px3y2eBidzpCbVVFnGWW6CPb3qKerB5U\n"
+        + "pwcF4gCFWyP9C2YtnB0hgqixIPfR+UO8gpqdY6MP8NPspoXouffRn+Zic/P6Cxje\n"
+        + "/MGxNQBeRtqb2IGh1xZ8v/8tmmmxHIkEAP74HkGETcXmlj3/6RlwTBUAovPARSn7\n"
+        + "LDtOCPezg6mQmble1BvnTnAwOHKJVqjx+3qsGqMe8OGGXAxZPSU1xSmOShBFrpDp\n"
+        + "xArE67arE17pT1lyD/gmHRuqnNMvgRrwz1mDm3G2ohWkCVixEiB+8vPQfbZrJBgQ\n"
+        + "WxOF4RCo2WWyRKa0IFRlc3R1c2VyIE9uZSA8dGVzdDFAZXhhbXBsZS5jb20+iQE4\n"
+        + "BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDtBiXc\n"
+        + "RjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8LqyUpBrDp3P06QDGpKGFMAovBuh+NL\n"
+        + "H76VKNIzQLQC8rdTj651fLcLMuJ1enQ3RblgRKr1oc+wqqtFHr4QyOQjE/N3C9GQ\n"
+        + "jEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMxjRcHbM9KQnsE5Z4fh4wmN5ynG+5n\n"
+        + "baF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX7Qkzze+scAlc9E/EWRJQIFcxnxV/\n"
+        + "SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjyW0lGHnh/ZqH6XGVcGUaJZZ2uHTck\n"
+        + "1+czuVVShNcXPW1W20T6E9UqzHbJHN0gnQOYBFWdTIkBCACoLVdPr3gpQwzI+2NG\n"
+        + "XjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjNvYkS/+/oGtVEmiYOiAVTwmkjCYkK\n"
+        + "GDgNcCiJVekiPAN6JryVv488wRc999b5LpFEfhLGwI0YxjcS4KFFnpMC3wSb6tJU\n"
+        + "nHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIbnuyrk3ydEcS4ZeGD+w+taIxMc9F1\n"
+        + "DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3mrBCo97sE95yKcq98ZMIWuQtTcEcc\n"
+        + "ZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11VlIQ9QFSj6ruqoKrYvNZuDDLD1lHvZ\n"
+        + "PD4/ABEBAAEAB/4kQnJauehcbRpqktjaqSGmP9HFSp+50CyZbLUJJM8m0uyQsZMr\n"
+        + "k9JQOZc+Q3RERNTKj7m41Fbhsj7c0Qd856/eJdp3kdBME0hko8lxN/X4EWGjeLYe\n"
+        + "z41+iPgfZhCF0Oa66TecPQ5RRihGPaDPoVPpkmMWMt9L7KVviBg1eJ6bobVIY5hu\n"
+        + "a7KFJHZQcCI1OvdJ0cx89KDSbnH8iMM6Kmw1bE3D2FEaWctuKLBo5PNRgyTJvdBd\n"
+        + "PSf56/Rc6csPqmOntQi2Yn8n47eCOTclHNuygSTJeHPpymVuWbhMq6fhJat/xA+V\n"
+        + "kyT8I2c45RQb0dKId+wEytjbKw8AI6Q3GXqhBADOhsr9M+JWc4MpD43mCDZACN4v\n"
+        + "RBRxSrJvO/V6HqQPmKYRmr9Gk3vxgF0zCf5zB1QeBiXpTpShxV87RIbUYReOyavp\n"
+        + "87zH6/SkRxQJiBEpQh5Fu5CoAaxGOivxbPqdWHrBY6jvqkrRoMPNiFJ6/ty5w9jx\n"
+        + "i9kGm9PelQGu2SdLNwQA0HbGo8sC8h5TSTEDCkFHRYzVYONx+32AlkCsJX9mEt0E\n"
+        + "nG8d97Ay24JsbnuXSq04FJrqzjOVyHLUffpXnAGELJZVNCIparSyqIaj43UG/oPc\n"
+        + "ICPmR7zI9G49ICUPSzI7+S2+BwjbiHRQcP0zmxbH92G4abYwKfk7dsDpGyVM+TkD\n"
+        + "/2nUiV0CRqnGipeiLWNjW/Md0ufkwqBvCWxrtxj0rQCyvBOVg3B6DocVNzgOOYa1\n"
+        + "ji3We5A9mSP40JBmMfk2veFrDdsGn4G+OpzMxKQtNfYemqjALfZ2zTdax0mXPXy6\n"
+        + "Gl0jUgSGrxGm8QnRLsrRx7G7ZKnvkcS+YsdQ8dbtzvJtQfiJAR8EGAECAAkFAlWd\n"
+        + "TIkCGwwACgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUs\n"
+        + "j/16fGiFrRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl\n"
+        + "+xqsgpEjFhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs\n"
+        + "3YI19Ci/FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnx\n"
+        + "qhH4wfHBPGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1\n"
+        + "H2PPSxrA0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
+        + "=MuAn\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A valid key expiring in 2065.
+   *
+   * <pre>
+   * pub   2048R/378A0AED 2015-07-08 [expires: 2065-06-25]
+   *       Key fingerprint = C378 369A CBCD 34CC 138D  90B1 4531 1A6F 378A 0AED
+   * uid                  Testuser Two &lt;test2@example.com&gt;
+   * sub   2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
+   * </pre>
+   */
+  public static final TestKey validKeyWithExpiration() {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
+        + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
+        + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
+        + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
+        + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
+        + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8\n"
+        + "dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD\n"
+        + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d\n"
+        + "UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6\n"
+        + "bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ\n"
+        + "FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT\n"
+        + "449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T\n"
+        + "8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu\n"
+        + "RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR\n"
+        + "tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe\n"
+        + "qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ\n"
+        + "oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9\n"
+        + "7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP\n"
+        + "d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd\n"
+        + "TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV\n"
+        + "YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9\n"
+        + "NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb\n"
+        + "fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM\n"
+        + "NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v\n"
+        + "EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==\n"
+        + "=1e/A\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
+        + "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
+        + "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
+        + "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
+        + "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
+        + "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAEAB/0WW33OVqzEBwj9b/3X\n"
+        + "i+75I/Gb+yVtDZ/km2NwSJie33PirE4mTNKitTBkt1oxmphw5Yqji4gEkI/rXcqy\n"
+        + "OcY/fCIZ+gVT+yE2MCPF7Se4Tnl7tSvPxoUn6mOQ09AygyYVjlSCY02EAL/WxwUH\n"
+        + "6OCs6VYlNiBlPg7O2vHGzlzAd1aMmlG3ytlhb0SIbilaJn/wlQ2SEGySjIAP1qRH\n"
+        + "UXsTfW7oAjdqAY1CbCWg/0FnMBF+DnChH634dbLrS2OefcB70l61trEfRcHbMNTv\n"
+        + "9nVxDDCpaIdxsOfgWpe0GMG1qddRAxBIOVjNUFOL22xEFyaXnt/uagUtKQ7yejci\n"
+        + "bgTFBADcuhsfQaBX1G095iG2qr8Rx2T5GqNf9oZA+rbweWegqIH7MUXHI1KKwwJx\n"
+        + "C+rR5AgnxTSP614XI/AWB/txdelm8z0jLobpS6B1vzM2vRQ7hpwjJ3UvUkoQ5uYL\n"
+        + "DjaBqQi0w1cPJA79H0Yujc1zgdhATymz0uDL1BC2bHLIMuhelwQA80p07G1w8HLQ\n"
+        + "bTdgNwtDBMKIw39/ZyQy8ppxmpD4J6zf25r95g3er0r+njrHsa+72LnvexbedpKA\n"
+        + "4eiDJPN+l5jJOEWfL2WtGcqJ01bdFBPcl73tuwDJJtieUlKZH0jRjykuuUX8F+tJ\n"
+        + "yrmVoIGtawoeLKq3hMMOK4xi+sh3OrcD+wXIU24eO3YfUde5bhyaQplNMU5smIU0\n"
+        + "+looOEmFsZcTONgoN+FKrnm2TY9d4FHZ+QgtnksWHmmLxQJPtp9rHJ5BgdxMBPcK\n"
+        + "3w5GXRuWlOmqmnAb6vp0Q0yzVDLKCcwba0S23m3tbjZsLDcI7MG/knsp9gtL676D\n"
+        + "AsrpeF2+Apj0OwG0IFRlc3R1c2VyIFR3byA8dGVzdDJAZXhhbXBsZS5jb20+iQE+\n"
+        + "BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\n"
+        + "CRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0dUdvAXeBx7DwOAnAodis9ZVqChb7R\n"
+        + "xcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6bgW+1WOB1tZSDVxwL1PnZFw/SyAD\n"
+        + "RIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZFMTFUr2SPscXk1k7muS+ZfEFwNPD\n"
+        + "4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT449CYoq8XBMBfvyWl/LLpw0r3JI6\n"
+        + "pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T8TKDGwwiuwiiT3SfkFSVdcjKulRu\n"
+        + "XSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iuRHSOnQOYBFWdTP8BCADhhGxAA0pX\n"
+        + "5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnRtBScgKZnP0sjRTYEUIwmZuseHMBo\n"
+        + "htVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIeqCrm/6aejbFcQOpxe6U29KJRCAxu\n"
+        + "wNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZoIvpIe9tZH4aXitCY2MCQH+hTyCy\n"
+        + "NBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+97HCe042GIq65h0apgujyjhJidjch\n"
+        + "5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xPd9MncY5Q/eH+hn96694k5bckottS\n"
+        + "yGm/3f2Ihfj1ABEBAAEAB/wP5H+mcTTrhe+57sEHuo9bQDocG+3fMtesHlRCept6\n"
+        + "vg1VQG4Va2GOtCCs7yMz4aNGz4jxOdB7bUkZJyFiRehG0+ahWi5b9JbSegf46Nm2\n"
+        + "54vt4icH2WtaEB04JaD/91k4yrunnzwVEAVDmhhIzjf4KbEjPLeBA7rF7zb0Gexq\n"
+        + "mdxEGO/6KdeQ6KOxkpWEqIIdl/mAGsYCprHeKL/XL+KXYr92nEbUcltmt59TTnoo\n"
+        + "00BQCPuHCdpcUd5nuaxpCZLM+BEpxtj0sinz0ofuWU9RI4K00R01MKXWMucdOhTZ\n"
+        + "kUy5dMx8wA07xbjkE/nH86N76Mty133OB7G3lBBDfO4PBADulfLzbjXUnS1kTKeP\n"
+        + "j/HF1E9qafzTDS/QD55OVajDq66A6zaOazKbURHNZmIqpLO4715+iNtrZQUEP3e1\n"
+        + "mwngeizvAv9luA9kJ1YDTCfsS5H5cYzavhfwuqBu7fQBm/PQqZplQuPCxgXEIBaY\n"
+        + "M0uvR0I/FSwFrepRN2IA6dAkrwQA8fpJEg8C9OLFzDf0rxV3eWwEelemN4E50Obu\n"
+        + "nxtg9IJWZ+QIWkRVLJ8if5+p85s2ieCw8hzEF0FyNfWUnfW5eoN4/j50loR4EbZS\n"
+        + "qOpUJGwr8ezyQN8PpduDOe9OQnUYAv9FY9Rk46L4937GDF2w5gdxyNdKO8yG+Z3A\n"
+        + "6/0DLZsEAOQsRUXIl1XLjkdugfFQ8V9Fv3AYWJt+8zknwcQ+Z3uOtyY2muCi9hX2\n"
+        + "BtuPojjwmN6x8wntMaUkzYHVSdz/cdx+na7VNS2kZHfnECWZGR6IHyRTJN5612yi\n"
+        + "e4MIdTE+BgL1HPq+VIPlMBehEksC5qM0WSq8baMsacGMYeAL8ntoRuyJASUEGAEC\n"
+        + "AA8FAlWdTP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb\n"
+        + "1nsgRMgVYoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFy\n"
+        + "xo6lLHw9NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q\n"
+        + "3uwvP5fbfSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfq\n"
+        + "lOG7SPvMNmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk1\n"
+        + "3ynADO+vEOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN\n"
+        + "9A==\n"
+        + "=qbV3\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key that expired in 2006.
+   *
+   * <pre>
+   * pub   2048R/17DE1ACD 2005-07-08 [expired: 2006-07-08]
+   *       Key fingerprint = 1D9E EB79 DD38 B049 939D  9CAF 3CEC 781B 17DE 1ACD
+   * uid                  Testuser Three &lt;test3@example.com&gt;
+   * </pre>
+   */
+  public static final TestKey expiredKey() {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
+        + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
+        + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
+        + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
+        + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
+        + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl\n"
+        + "IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI\n"
+        + "BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC\n"
+        + "ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df\n"
+        + "SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu\n"
+        + "R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk\n"
+        + "qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5\n"
+        + "btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/\n"
+        + "2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be\n"
+        + "eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F\n"
+        + "p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw\n"
+        + "dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj\n"
+        + "FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp\n"
+        + "KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC\n"
+        + "Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/\n"
+        + "1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb\n"
+        + "NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE\n"
+        + "ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9\n"
+        + "OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU\n"
+        + "kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb\n"
+        + "=d/Xp\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
+        + "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
+        + "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
+        + "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
+        + "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
+        + "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAEAB/4hGI3ckkLMTjRVa7G1\n"
+        + "YYSv4sr8dHXz0CVpZXKOo+Stef3Z4pZTK/BcXOdROvaXooD+EheAs6Yn4fpnT+/K\n"
+        + "IB7ZAx6C0OL8vz17gbPuBFltMZ/COUwaCi/gFCUfWQgqRp/SdHaOfCIuTxpAkDSS\n"
+        + "tpmWJ8eDDSFudMpgweb+SrF9DkCwp+FgUbzDRzO1aqzuu8PGihCHQt/pkhNHQ63/\n"
+        + "srDDqk6lIxxZHhv9+ucr3plDuijkvAa5/QDudQlucKDLtTPSD40UcqYnpg/V/RJU\n"
+        + "eBK0ZXmCIHpG9beHW/xdlwrK3eY4Z2sVDMm9TeeHmRYOCr5wQCyeLpMdAt0Ijk6a\n"
+        + "nINhBADI2lRodgnLvUKbOvVocz8WQjG1IXlL8iXSNuuHONijPXZiWh7XdkNxr9fm\n"
+        + "jRqzvZzYsWGT6MnirX2eXaEWJsWJHxTxJuiuOk0V/iGnV/d+jFduoKXNmB5k/ZB3\n"
+        + "6zySi7+STKNyIvnMATVsRoI/cNUwfmx53m6trFg581CnSiA82QQA4kSPw9OXmTKj\n"
+        + "ctlHrWsapWu+66pDVZw62lW6lvrd7t+m8liNb6VJuTnwIKVXJOQtUo1+GSMs0+YK\n"
+        + "wnd9FGq4jT8l0qBO4K/8B1HxppLC2S0ntC+CusxWMUDbdC2xg+G2W3oLwq3iamgz\n"
+        + "LvPTy1Pzs9PqDd6FXIdzieFy6J8W1+sEAKS3vjh7Z/PIVULZhdaohAd5Igd67S/Z\n"
+        + "BMWYNbBuJTnnb7DiOllLZSd2lR7IAKPKsUd6UY8uskOxI81hI116zNx17mIGFIIq\n"
+        + "DdDgRbvzMNEgNlOxg/BD01kXOS4fhnT2F6ca3VGTgUtOdcdF3M9MtePWQLBzEDPz\n"
+        + "8nx3O20HDupuQmG0IlRlc3R1c2VyIFRocmVlIDx0ZXN0M0BleGFtcGxlLmNvbT6J\n"
+        + "AT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheA\n"
+        + "AAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFCECWLrcOeimuvwbmkonNzOkvKbGXl\n"
+        + "73GStISAksRWAHBQED1rEPC0NkFCDeVZO7dfSYLlsqKwV6uSh05Ra0F5XeniC12Y\n"
+        + "pAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCuR+8sNu/oecMRcFK4S9NaApi3vdqB\n"
+        + "NhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSkqcPfKZmocNXdgLV5Q80n3hc2y2nr\n"
+        + "l+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5btBW2L0UHtoEyiqkRfD6lX2laSLQ\n"
+        + "mA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/2thO41KdA5gEQs6nRQEIAM/833UH\n"
+        + "K1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3beeE4sh1NG5DbRCdo6iacZLarWr3FD\n"
+        + "z7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5Fp5u2R4WF546bWqX45xPdLfHVTPyW\n"
+        + "B9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihwdxLsxaga+QmaL0bAR+dRcO6ucj7T\n"
+        + "DQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9AjFoumMZ6l+k30sSdjSjpBMsNvPos0\n"
+        + "dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELpKgujZ2sKC9Nm395u6Q4cqUWihzb/\n"
+        + "Y7rIRuNHJarI7vUAEQEAAQAH+gNBKDf7FDzwdM37Sz8Ej7OsPcIbekzPcOpV3mzM\n"
+        + "u/NIuOY0QSvW7KRE8hwFlXjVZocJU/Z4Qqw+12pN55LusiRUrOq8eKuJIbl4QikI\n"
+        + "Dea8XUqM+CKJPV3YZXs6YVdIuzrRBSLgsB/Glff5JlzkEjsRYVmmnto8edETL/MK\n"
+        + "S9ClJqQiFKE4b01+Eh9oB/DfxzsiEf/a+rdRnWRh/jtpEwgeXcfmjhf+0zrzChu2\n"
+        + "ylQQ5QOuwQNKJP6DvRu/W5pOaKH9tPDR31SccDJDdnDUzBD7oSsXl06DcfMNEa8q\n"
+        + "PaNHLDDRNnqTEhwYSJ4r2emDFMxg7Kky+aatUNjAYk9vkgMEANnvumgr6/KCLWKc\n"
+        + "D3fZE09N7BveGBBDQBYNGPFtx60WbKrSY3e2RSfgWbyEXkzwm1VlB2869T1we0rL\n"
+        + "z6eV/TK5rrJQxJFHZ/anMxbQY0sCiOgqi6PKT03RTpA2N803hTym+oypy+5T6BFM\n"
+        + "rtjXvwIZN/BgAE2JjA70crTAd1mvBAD0UFNAU9oE7K7sgDbni4EhxmDyaviBHfxV\n"
+        + "PJP1ICUXAcEzAsz2T/L5TqZUD+LfYIkbf8wk2/mPZFfrCrQgCrzWn7KV1SHXkhf4\n"
+        + "4Sg6Y6p0g0Jl3mWRPiQ6ALlOVQIkp5V8z4b0hTF2c4oct1Pzaeq+ZkahyvrhW06P\n"
+        + "iaucRZb+mwP/aVTpkd4n/FyKCcbf9/KniYJ+Ou1OunsBQr/jzN+r0PKCb8l/ksig\n"
+        + "i/M0NGetemq9CxYsJDAyJs1aO4SWgx5LbfcMmyXDuJ3sL0ztFLOES31Mih3ZJebg\n"
+        + "xPpj2bB/67i2zeYRcjxQ116y23gOa2TWM8EE4TW7F/mQjw4fIPJ93ClBMIkBJQQY\n"
+        + "AQIADwUCQs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs\n"
+        + "7mvEWJI/1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Fe\n"
+        + "yxb2rjtbNrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt1\n"
+        + "0RaYR8VEZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGd\n"
+        + "Ut8U1Kq9OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/\n"
+        + "jPj5FUEUkE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6Q\n"
+        + "HDJb\n"
+        + "=RrXv\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A self-revoked key with no expiration.
+   *
+   * <pre>
+   * pub   2048R/7CA87821 2015-07-08 [revoked: 2015-07-08]
+   *       Key fingerprint = E328 CAB1 1F7E B1BC 1451  ABA5 0855 2A17 7CA8 7821
+   * uid                  Testuser Four &lt;test4@example.com&gt;
+   * </pre>
+   */
+  public static final TestKey selfRevokedKey() {
+    return new TestKey(
+        "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFWdTZwBCAC1jukp5mlitfq2sAmdtx1s1VbWh+buDbBY2kWcxbbssczozFUP\n"
+        + "Ii67wPwjRbn3GM5+jY3GMsqKIrdyDlxeTxGWoU/qa2YkCQzgFGD/XJBqkVpP6osm\n"
+        + "qFYSP0xST1iBkatkMHq5KMjrX2q2bGVLlchLF9eHrWSefMcfff1Vs/Y8F2RCo38y\n"
+        + "gH88mbcvgyC+zq6Q2T3h5RiLK2IaZDNsn3uUoIMYHxI6oYtXXMSXRJlLJvamXVrB\n"
+        + "7QAq8L8cNikJjZMz+bHtLtGDyVVp9tqo4yvMrHe6BYmBUte3tPYQlDVdEo7UqepR\n"
+        + "uT7JbBOGBoD+9ngDrDggPUBAoa0h3VNOTyoDABEBAAGJAR8EIAECAAkFAlWdVXkC\n"
+        + "HQIACgkQCFUqF3yoeCH4lgf/aBdTYqnwL1lreHbQaUXI0/B2zlMuoptoi/x+xjIB\n"
+        + "7RszzaN3w0n4/87kUN2koNtgNymv2ccKTR1PiX+obscJhsWzNbz3/Cjtr/IpEQRd\n"
+        + "E6qRptHDk0U2cHW4BYDSltndOktICdhWCWYLDxJHGjdyXqqqdEEFJ24u2fUJ3yF3\n"
+        + "NF2Bxa6llrmLb2fVeVYBzQSztQopKRWP9nt3ySoeJQqRWjNBN2j7cC93nrLHZTvB\n"
+        + "L/sWuTq5ecbXeeNVzxoBd21jmGrIUPNwGdDKdbTB0CjpLpVHOTwGByeRKQXhMlQB\n"
+        + "pK96wUpxxtShtOjNjN1s9GEyLHwDiHSuHNYs/AxxFzf9nbQhVGVzdHVzZXIgRm91\n"
+        + "ciA8dGVzdDRAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnU2cAhsDBgsJCAcDAgYV\n"
+        + "CAIJCgsEFgIDAQIeAQIXgAAKCRAIVSoXfKh4IXsHCACSm9RIdxxqibAaxh+nm6w5\n"
+        + "F5a6Hju5cdmkk9albDoQYh2eM8E5NdDq+r0qSSe2+ujDaQ4C95DZNJQESvIcHHHb\n"
+        + "9AECrBfS8Yk86rX8hxVeYQczMkB9LdBHximTSoOr8L/eAxBE/VXDwust6EAe6Q1A\n"
+        + "a3tlTTvCfcmw4PipvtP7F6UzFaq+QU6fvARpBATOcvVc2JU4JQOrxuNEQ2PKrSti\n"
+        + "75S5mnVWm0pRebM+EorWBtlA0eOAeLNqCp87UwLdvUyOTRZT4DJ51eTxfrFADXrI\n"
+        + "9/ejs3/YxCPYxaPicAlcldduuajU/s+9ifrUn0Npg2ILl8mQkNzqeerlBeecUV4E\n"
+        + "uQENBFWdTZwBCADEOsK+mFQ/2uds9znkmAqrk24waVBpyPGrTTXtXX0dKhtQAsh6\n"
+        + "QkZGkjLTnKxEsa9syqVckw+1JtCh44SP1gjqDUoShpBz5wIuksZ7q96Hx+F0TVG/\n"
+        + "njS6GrWvwKhL2Lb9hYfdlrZiYtOOi0iiOzud25H/Ms15kC8tuQm7NWtANJJF4Sxo\n"
+        + "Bxor6L/F4zunEkTL0L9/dp4qVrw23fJVKE38cSdxjB0u1qSDzLV/u0QJqlYxJAiE\n"
+        + "ciwQN2uVnTY1/XSpouMy6LvbYU7B2uU/WohNmH3RiN/fQ6jJm4x+fCZ8+zqXMiZn\n"
+        + "G2fPkwmxxK9cl64YnNGcTwsVt6BMbCHk9jHxABEBAAGJAR8EGAECAAkFAlWdTZwC\n"
+        + "GwwACgkQCFUqF3yoeCGOdwf/TmoxH3pFBm/MDhY5Ct5FO0KvsgQk2ZgDa68HyQ8j\n"
+        + "QYi1FUCtyDjsxf5KTfyvzpzcTpS7cyOwcJNtTj6UixwATkcivvYWYoOXghAsTo4f\n"
+        + "1+j/x6ECq1+nYE6NpcAN7VRJpYMk2UO2qlhHCesTPGzsHchL7mwiYdhGrdiWGTpd\n"
+        + "KI9WfOYDZZ9ZSw/QINJUyTRxrDnauOvVbhbAXc7jdKCkRQRZpsNlF//1Stg6nstj\n"
+        + "FJ7SrjVdsMJNlihT6fG5ujmrty1/6b1VCLkIQfW5cWvzRzTBFytq7i4PVKh3u7Oz\n"
+        + "tt9lf8s50zt2uBE/AKMkyE6IJLsBWpJPk7iFKkHGDx044Q==\n"
+        + "=477N\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBFWdTZwBCAC1jukp5mlitfq2sAmdtx1s1VbWh+buDbBY2kWcxbbssczozFUP\n"
+        + "Ii67wPwjRbn3GM5+jY3GMsqKIrdyDlxeTxGWoU/qa2YkCQzgFGD/XJBqkVpP6osm\n"
+        + "qFYSP0xST1iBkatkMHq5KMjrX2q2bGVLlchLF9eHrWSefMcfff1Vs/Y8F2RCo38y\n"
+        + "gH88mbcvgyC+zq6Q2T3h5RiLK2IaZDNsn3uUoIMYHxI6oYtXXMSXRJlLJvamXVrB\n"
+        + "7QAq8L8cNikJjZMz+bHtLtGDyVVp9tqo4yvMrHe6BYmBUte3tPYQlDVdEo7UqepR\n"
+        + "uT7JbBOGBoD+9ngDrDggPUBAoa0h3VNOTyoDABEBAAEAB/4jqeZoOiACaV/Nygeh\n"
+        + "iOpJSiDsNDbrFRpKYdnhwT69APIQ2q5sshi+/dopbZVpkeBiIJk0UR7TAp3JVEPV\n"
+        + "rK92SMqjcCRYuMRkMeyZzMt7e4DjiN17ov6BSBjMZFSs4vnpTNKWk4ngHlaebe15\n"
+        + "6vq0sYK/XpKQxU7yAzQjxR190P/F+QEL98zVG/9uqM8PupfdSm4Smp2cIpfta+JD\n"
+        + "mO23HC6jAEm2RFwklovzgK3rbIjyiMuowIkAKx5xxRvpxMHf1l566b9zJrRi0xau\n"
+        + "vp4J/lnBJtTMzCbsaaFxhrj23xvTXaWR+UkaGPCv7wheXQ9K7NAHwmH8YrR+cZx7\n"
+        + "KbDlBADUTHZ+OhNslx/rkjRWrFuK9p49x7qxQc26kcqlGPbW6KOAMdUpwneQbhCG\n"
+        + "a36E/GAZgsgQ4SUqn37EVCtd2Y9Dp0inPAujcZXSwgDHev6ea7fzbxT9KLtEgvQN\n"
+        + "0vrFJDCPIt0wzGqNDw4wgFjF2rAafBO//Wu5K5QLW4hfzSguRQQA2u6DpVja/FYY\n"
+        + "UHVh2HLiB8th4T+qogOsBe5mKEsGRPXtAh7QzJu36C4PJyHeNlmlMx+15cCFnovj\n"
+        + "6cLpGn6ZP4okLyq2+VsW7wh/Vir+UZHoAO/cZRlOc1PsaQconcxxq30SsbaRQrAd\n"
+        + "YargKlXU7HMFiK34nkidBV6vVW0+P6cD/jYRInM983KXqX5bYvqsM1Zyvvlu6otD\n"
+        + "nG0F/nQYT7oaKKR46quDa+xHMxK8/Vu1+TabzY8XapnoYFaFvrl/d2rUBEZSoury\n"
+        + "z2yfTyeomft9MGGQsCGAJ95bVDT+jBoohnYwfwdC7HG3qk0aK/TxFyUqvMOX7SFe\n"
+        + "YT55n3HlD9InST+0IVRlc3R1c2VyIEZvdXIgPHRlc3Q0QGV4YW1wbGUuY29tPokB\n"
+        + "OAQTAQIAIgUCVZ1NnAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQCFUq\n"
+        + "F3yoeCF7BwgAkpvUSHccaomwGsYfp5usOReWuh47uXHZpJPWpWw6EGIdnjPBOTXQ\n"
+        + "6vq9Kkkntvrow2kOAveQ2TSUBEryHBxx2/QBAqwX0vGJPOq1/IcVXmEHMzJAfS3Q\n"
+        + "R8Ypk0qDq/C/3gMQRP1Vw8LrLehAHukNQGt7ZU07wn3JsOD4qb7T+xelMxWqvkFO\n"
+        + "n7wEaQQEznL1XNiVOCUDq8bjRENjyq0rYu+UuZp1VptKUXmzPhKK1gbZQNHjgHiz\n"
+        + "agqfO1MC3b1Mjk0WU+AyedXk8X6xQA16yPf3o7N/2MQj2MWj4nAJXJXXbrmo1P7P\n"
+        + "vYn61J9DaYNiC5fJkJDc6nnq5QXnnFFeBJ0DmARVnU2cAQgAxDrCvphUP9rnbPc5\n"
+        + "5JgKq5NuMGlQacjxq0017V19HSobUALIekJGRpIy05ysRLGvbMqlXJMPtSbQoeOE\n"
+        + "j9YI6g1KEoaQc+cCLpLGe6veh8fhdE1Rv540uhq1r8CoS9i2/YWH3Za2YmLTjotI\n"
+        + "ojs7nduR/zLNeZAvLbkJuzVrQDSSReEsaAcaK+i/xeM7pxJEy9C/f3aeKla8Nt3y\n"
+        + "VShN/HEncYwdLtakg8y1f7tECapWMSQIhHIsEDdrlZ02Nf10qaLjMui722FOwdrl\n"
+        + "P1qITZh90Yjf30OoyZuMfnwmfPs6lzImZxtnz5MJscSvXJeuGJzRnE8LFbegTGwh\n"
+        + "5PYx8QARAQABAAf8CeTumd6jbN7USXXDyQdzjkguR6mfwN29dcY8YF4U52oOm3+w\n"
+        + "bR23XmqTvoDJXONatZEYOm093wP4hBktP3Vq2KZX5Ew9r2JoBUIoWOcHHvCQqSUW\n"
+        + "6KMJBJNBMv3zXnOscmcPvTgStS5HfYn/XRLAhEqkd2ov2x/OiS8p0vM0F7YYSOdu\n"
+        + "X6/nHeBCM5QSJl00kgcaeQYdIGL0bPv9DnoeAC2/yITEvtvs+MHZ7FjH8A45QjWn\n"
+        + "DwfVoLg7WOc3wJtqJ55/r/2pylrWz0YYM8s6I3gbDilCF+Wb8tEIOaWJEwY73J1/\n"
+        + "KQG5qlO3/hBlO80DtzNmi3ylRUuzGhTxQfvemwQA3EuZ+E48LJ3dwtdJhh5mFlWI\n"
+        + "Ket21e5v1mqMxuLhf5/2CYcifM08u3EsEUdIr7egF25Sea8otqmCYcG8FuB37VY/\n"
+        + "Hd4G/+YVVaaAB8EU6u64YfSswhzr0R2qWVLtkJr0EAephzdPdoUEtKDSdTxnXiDV\n"
+        + "3vSqLWtZekScLa979uMEAOQIodJwxSvveKQWILjK67ZJr56X8YQZWA6rFsr1xMY0\n"
+        + "N0GH+5k0k+tr4wT3H9uk9ZM1Z11G3c01mhzCNg5roFoKtftKUZRKxmbfjjDmAofl\n"
+        + "bA6EZ0WHLdOwDLLTuXK09IsjjSHq0YHOxIlgFzIreuoxtz27bEEGhVFQg7xb0Lgb\n"
+        + "A/9LP8i32L7/CHsuN0q4YjhJkkaB6JWUQMFqWwoAXALG3rnw/CGRYHmHpiAuSeHR\n"
+        + "dSlZzndVi5poNC/d27msTx7ZuWlN7nOyywHBCTWV/nstm2I9rDhrHK7Axgq0Vv0y\n"
+        + "bWAurUmEgDJHU3ZpsNVt4e30FooXIDLR4cnpRM7tILv39D4giQEfBBgBAgAJBQJV\n"
+        + "nU2cAhsMAAoJEAhVKhd8qHghjncH/05qMR96RQZvzA4WOQreRTtCr7IEJNmYA2uv\n"
+        + "B8kPI0GItRVArcg47MX+Sk38r86c3E6Uu3MjsHCTbU4+lIscAE5HIr72FmKDl4IQ\n"
+        + "LE6OH9fo/8ehAqtfp2BOjaXADe1USaWDJNlDtqpYRwnrEzxs7B3IS+5sImHYRq3Y\n"
+        + "lhk6XSiPVnzmA2WfWUsP0CDSVMk0caw52rjr1W4WwF3O43SgpEUEWabDZRf/9UrY\n"
+        + "Op7LYxSe0q41XbDCTZYoU+nxubo5q7ctf+m9VQi5CEH1uXFr80c0wRcrau4uD1So\n"
+        + "d7uzs7bfZX/LOdM7drgRPwCjJMhOiCS7AVqST5O4hSpBxg8dOOE=\n"
+        + "=5aNq\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key with an additional user ID.
+   *
+   * <pre>
+   * pub   2048R/98C51DBF 2015-07-30
+   *       Key fingerprint = 42B3 294D 1924 D7EB AF4A  A99F 5024 BB44 98C5 1DBF
+   * uid                  foo:myId
+   * uid                  Testuser Five <test5@example.com>
+   * sub   2048R/C781A9E3 2015-07-30
+   * </pre>
+   */
+  public static TestKey validKeyWithSecondUserId() {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFW6jd4BCACrf+BZS3lntuWq2DWPOG07/BUWhx3RSoiS3JBuKEDmlsjswKcp\n"
+        + "JHT+p2tqH52XbujMlzNjAQcZjJwfOMt6Fg7zd3F8cwYQdCE/W5dpMs/mqdeEz6GL\n"
+        + "VJDZ0Y5wwz54ZQHp91Xq6uejxt5qffeTQk5cToQZ0RVx3iwBc+2P3iYJqMFmJzj8\n"
+        + "djabEoF4D50iI5tY8moE83VcXJ5Y4xn+5Z5AThmlfrMP6gIdG0b4lEe1tsnJC6AG\n"
+        + "GUU6VkzK6E1Tp93Y0brtWpJKi9Gt6eUqvWhZtPEdFVCFbLTpezUdRFEuaFbGg5pn\n"
+        + "9K/DceahFmquDJOHVgawt6erlq/ie7QEEld/ABEBAAG0IVRlc3R1c2VyIEZpdmUg\n"
+        + "PHRlc3Q1QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCVbqN3gIbAwYLCQgHAwIGFQgC\n"
+        + "CQoLBBYCAwECHgECF4AACgkQUCS7RJjFHb+/MAf9FKZatGcuOIoYqwGQQneyc63v\n"
+        + "3H/PyhvYF1nuKNftmhqIiUHec9RaUHQkgam6LRoonkDfIpNlQVRv2XBV2VOAOFVO\n"
+        + "RyQ/Tv7/xtpqGZqivV0yn2ZXbCceA627Vz7gP4gkO0ZJ0JsYJTc/5wO+nVG5Lohu\n"
+        + "/zdUofEbFAvcXs+Z1uXnUDdeGn47Lf1xZ2XOHOI0aQW4DdNaFoAd+AOTe0W3iB6W\n"
+        + "paCIGno69CyNHNnWjJCSD33oLVaXyvbgw5UoyITvSqRnPyLGIc6dsqDLT59ok0Fk\n"
+        + "t4jtiGu9aze4n59GbtSjmWQgzbLCQWhK9K7UCcSLYNKXVyMha2WapBO156V027QI\n"
+        + "Zm9vOm15SWSJATgEEwECACIFAlW6jwYCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B\n"
+        + "AheAAAoJEFAku0SYxR2/zZUH/1BwPsResHLDSmo6UdQyQGxvV0NcwBqGAPSLHr+S\n"
+        + "PHEaHEIYvOywNfWXquYrECa/5iIrXuTQmCH0q8WRcz1UapDCeD8Ui82r+3O8m6gk\n"
+        + "hIR5VAeza+x/fGWhG342PvtpDU7JycDA3KMCTWtcAM89tFhffzuEQ3f5p5cMTtZk\n"
+        + "/23iegXbHd61vojYO17QYEj+qp9l0VNiyFymPL3qr5bVj/xn/mXFj+asj0L2ypIj\n"
+        + "zC36FkhzW5EX2xgV9Cl9zu7kLMTm+yM+jxbMLskYkG8z/D+xBQsoX8tEIPlxHLhB\n"
+        + "miEmVuZrp91ArRMWa3B7PYz7hQzs+M/bxKXcmWxacggTOvy5AQ0EVbqN3gEIAOlq\n"
+        + "mwdiXW0BQP/iQvIweP1taNypAvdjI2fpnXkUfBT5X/+E/RjYOHQEAzy8nEkS+Y0l\n"
+        + "MLwKt3S0IVRvdeXxlpL6Tl+P8DkcD5H+uvACrg9rtgbbNSoQtc9/3bknG9hea6xi\n"
+        + "6SBH1k9Y2RInIrwWslfKmuNkyZVhxPKypasBsvyhOWLlpCngGiCa74KJ1th1WKa2\n"
+        + "aaDqcbieBTc1mtsXR6kBhJZqK+JYBoHriUQMs7nyXxn2qyv6Lehs/tHlrBZ7j16S\n"
+        + "faQzYoBi1edVrpFr/CuGk6RNKxG9vi/uAA9q2cLCMjjyfMH4g0G2l0HuDPQLA9wi\n"
+        + "BfusEC+OceaeFKtS9ykAEQEAAYkBHwQYAQIACQUCVbqN3gIbDAAKCRBQJLtEmMUd\n"
+        + "vw/DB/9Qx9m1eSdddqz/fk16wJf7Ncr2teVvdQOjRf/qo43KDKxEzeepjgypG1br\n"
+        + "St7U4/MlPygJLBDB4pXp0kaKt+S/aqLpEGSGzQ1FysM8oY6K0e1Kbf6nMaQS8ATG\n"
+        + "aD377FrUJ42NV4JS+NGlwaM9PhpRVm5n8iCzRs9HtlTyfCBkNGDjGOSdWcah2m6T\n"
+        + "fEQdD+XVDN1ZC8zAnc8FW28YOTeTjX079okP6ZCjLJ16VZ7eiHFkrNbS9Dl4SPNK\n"
+        + "eElvsZLBaf8t4RQXFFKwRq4BW+zS8zm9E2H6bZ9yGrmgIREzyRPpwU98g8yrabu0\n"
+        + "54w16Vp/SVViJs7nTMSug0WREyd2\n"
+        + "=ldwB\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBFW6jd4BCACrf+BZS3lntuWq2DWPOG07/BUWhx3RSoiS3JBuKEDmlsjswKcp\n"
+        + "JHT+p2tqH52XbujMlzNjAQcZjJwfOMt6Fg7zd3F8cwYQdCE/W5dpMs/mqdeEz6GL\n"
+        + "VJDZ0Y5wwz54ZQHp91Xq6uejxt5qffeTQk5cToQZ0RVx3iwBc+2P3iYJqMFmJzj8\n"
+        + "djabEoF4D50iI5tY8moE83VcXJ5Y4xn+5Z5AThmlfrMP6gIdG0b4lEe1tsnJC6AG\n"
+        + "GUU6VkzK6E1Tp93Y0brtWpJKi9Gt6eUqvWhZtPEdFVCFbLTpezUdRFEuaFbGg5pn\n"
+        + "9K/DceahFmquDJOHVgawt6erlq/ie7QEEld/ABEBAAEAB/9MIlrQiWb+Gf3fWFh+\n"
+        + "mkg0Bva9p4IfNX1n5S7hGFGnjGzqXaRX6W1e16gh1qM5ZO1IVh9j5kLmnrt4SNhb\n"
+        + "/Irqnq3s14trpoJUBC81bm9JMUESHrLSjdo4OIWJncOP4xd0bG7h+SKYXGLE1+Me\n"
+        + "pqLu65RNebqRcFYM1xAxfCdaxatcz+LrW5ZX+6T/Gh/VCHRkkzzVIZO1dDBbyU2C\n"
+        + "JrNcfHSvNrjzfqYHtwfsk/lwcuY9pqkYcuwZ2IM+iWKit+WyCR2BzOpG/Sva1t8b\n"
+        + "7B7ituQCFMCv5IiaAoaSKX/t/0ucWCoT1ttih8LdwgEE0kgij/ZUfRxCiL9HmtLy\n"
+        + "ad9BBADBGYWv6NiTQiBG7+MZ+twCjlSL7vq8iENhQYZShGHF9z+ju7m8U1dteLny\n"
+        + "pC3NcNfCgWyy+8lRn1e6Oe6m7xL83LL3HJT5nIy9mpsCw/TIrrkzkoE+VpkEIL/o\n"
+        + "Yeoxauah4SU7laVD29aAQZ3TqwSwx0sJwPjsj73WjjqtzJfFkQQA410ghqMbQZN1\n"
+        + "yJzXgVAj162ZwTi961N5iYmqTiBtqGz1UfaNBJWdJMkCmhMTsiOtm1h4zUQRuEH+\n"
+        + "yq1xhKOGf15dB/cLSMj2KpVVlvgLoVmYDugSER8Q23juilY7iaf0bqo9q1sTHpn9\n"
+        + "O7Oin/9J3sz+ic45vDh4aa74sOzfhA8EAJwAFEWLrGSxtnYJR5vQNstHIH1wtQ5G\n"
+        + "ZUZ57y9CbDkKrfCQvd0JOBjfUDz+N8qiamNIqfhQBtlhIDYgtswiG+iGP/2G0l6S\n"
+        + "j9DHNe2CYPUKgy+zQiRnyNGE2XUfcE+HuNDfu3AryPqaD8vLLw8TnsAgis3bRGg+\n"
+        + "hhrAC1NyKfDXTg20IVRlc3R1c2VyIEZpdmUgPHRlc3Q1QGV4YW1wbGUuY29tPokB\n"
+        + "OAQTAQIAIgUCVbqN3gIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQUCS7\n"
+        + "RJjFHb+/MAf9FKZatGcuOIoYqwGQQneyc63v3H/PyhvYF1nuKNftmhqIiUHec9Ra\n"
+        + "UHQkgam6LRoonkDfIpNlQVRv2XBV2VOAOFVORyQ/Tv7/xtpqGZqivV0yn2ZXbCce\n"
+        + "A627Vz7gP4gkO0ZJ0JsYJTc/5wO+nVG5Lohu/zdUofEbFAvcXs+Z1uXnUDdeGn47\n"
+        + "Lf1xZ2XOHOI0aQW4DdNaFoAd+AOTe0W3iB6WpaCIGno69CyNHNnWjJCSD33oLVaX\n"
+        + "yvbgw5UoyITvSqRnPyLGIc6dsqDLT59ok0Fkt4jtiGu9aze4n59GbtSjmWQgzbLC\n"
+        + "QWhK9K7UCcSLYNKXVyMha2WapBO156V0250DmARVuo3eAQgA6WqbB2JdbQFA/+JC\n"
+        + "8jB4/W1o3KkC92MjZ+mdeRR8FPlf/4T9GNg4dAQDPLycSRL5jSUwvAq3dLQhVG91\n"
+        + "5fGWkvpOX4/wORwPkf668AKuD2u2Bts1KhC1z3/duScb2F5rrGLpIEfWT1jZEici\n"
+        + "vBayV8qa42TJlWHE8rKlqwGy/KE5YuWkKeAaIJrvgonW2HVYprZpoOpxuJ4FNzWa\n"
+        + "2xdHqQGElmor4lgGgeuJRAyzufJfGfarK/ot6Gz+0eWsFnuPXpJ9pDNigGLV51Wu\n"
+        + "kWv8K4aTpE0rEb2+L+4AD2rZwsIyOPJ8wfiDQbaXQe4M9AsD3CIF+6wQL45x5p4U\n"
+        + "q1L3KQARAQABAAf8C+2DsJPpPEnFHY5dZ2zssd6mbihA2414YLYCcw6F7Lh1nGQa\n"
+        + "XuulruAJnk/xGJbco8bTv7g4ecE+tsbfWnnG/QnHeYCsgO6bKRXATcWFSYpyidUn\n"
+        + "2VdzQwBAv1ZtSNhCXlPLn/erzvA2X4QadUwfnvbehWJAHt8ZJmHUr3FtyRUHEdCK\n"
+        + "2EXsBWnzPCcqHZOMvcbSINSqBFGzVXkOZsMFvPTNIUYRHz8NbJT/OPiOmyBshXpS\n"
+        + "t8w3QqZhBcTT3NZo3kgxN1RygaTa10ytB2cxTCVuD8hmUBaV9gakdfMYkVJds7/T\n"
+        + "ZY3It68F0vitBnqpppZQ+NFgr/vwVg0p3gbmAQQA79zsWPvyIqYvyJhmiKvLIpev\n"
+        + "569ho8tC9xx+IZ5WnjN8ZADlb9brAdA9cqGfBgZkpZUhngCRVOYUIco+m2NYkEJm\n"
+        + "BsSTTM77dqU55DRloJ3FtBwCPXHkwg9P/FHMMYYGyLpQTSB92hXk8yomo+ozX7kx\n"
+        + "DtUHZIrir/rr0lQe+GkEAPkep9V5jBmfHMArnfji7Nfb1/ZjrSAaK+rtqczgm+6j\n"
+        + "ubY/0DpM/6gm+/8X27WFw2m45ncH3qNvOe4Qm40EmgmHkXsdQyU0Fv7uXc9nBYoo\n"
+        + "G6s7DWLY4VAqWwPsvbqgpSp/qdGn9nlcJjjY1HtfU7HM3xysT7TJ2YVhYHlJdjDB\n"
+        + "A/0alBcYtHvaCJaRLWX4UiashbfETWAf/4oHlERjkXj64qOdsGnD6CD99t9x91Ue\n"
+        + "pClPsLDFvY8/HxWX7STA9pQZAa2ZdJd8b58Rgy9TBShw2mbz2S6Cbw77pP/WEjtJ\n"
+        + "pJuS2gDp70H01fYRaw7YH32CfUr1VeEv7hTjk/SNVteIZkkOiQEfBBgBAgAJBQJV\n"
+        + "uo3eAhsMAAoJEFAku0SYxR2/D8MH/1DH2bV5J112rP9+TXrAl/s1yva15W91A6NF\n"
+        + "/+qjjcoMrETN56mODKkbVutK3tTj8yU/KAksEMHilenSRoq35L9qoukQZIbNDUXK\n"
+        + "wzyhjorR7Upt/qcxpBLwBMZoPfvsWtQnjY1XglL40aXBoz0+GlFWbmfyILNGz0e2\n"
+        + "VPJ8IGQ0YOMY5J1ZxqHabpN8RB0P5dUM3VkLzMCdzwVbbxg5N5ONfTv2iQ/pkKMs\n"
+        + "nXpVnt6IcWSs1tL0OXhI80p4SW+xksFp/y3hFBcUUrBGrgFb7NLzOb0TYfptn3Ia\n"
+        + "uaAhETPJE+nBT3yDzKtpu7TnjDXpWn9JVWImzudMxK6DRZETJ3Y=\n"
+        + "=uND5\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key revoked by a valid key, due to key compromise.
+   * <p>
+   * Revoked by {@link #validKeyWithoutExpiration()}.
+   *
+   * <pre>
+   * pub   2048R/3434B39F 2015-10-20 [revoked: 2015-10-20]
+   *       Key fingerprint = 931F 047D 7D01 DDEF 367A  8D90 8C4F D28E 3434 B39F
+   * uid                  Testuser Six &lt;test6@example.com&gt;
+   * </pre>
+   */
+  public static TestKey revokedCompromisedKey() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFYmpXkBCACqaLz51DcWQmfOJnat9iHSySfSHwbKfVvoN43Ba2cf/D/PadRc\n"
+        + "HLgc+91k2yk1kV1LnMdvUGj5zZ84ZqrQx3f1WeItnzZpqxtmQS/GSxCp9EY/s7w6\n"
+        + "5i86R/k9Tzgvk0B7dKZJXbM/OWxxDkkxHWE3Un9wreX7bDU5b9D2knHRiNFqH9ZJ\n"
+        + "KqDIFZqH9WTUxNZcHz20sTCRIMfvsAwf2vRU5N5xTu4Mbk6JFc7BAj7h1f/mYEPo\n"
+        + "CTyB1jV/DSDVdn1FjJVocSg6W/CvsYF9hKFYjJHl4VXdePTpnOjHhJLL0QWk0TMe\n"
+        + "xYeUi/xDr5DeMxTmi7F7BFaQEF+KmUM46e+9ABEBAAGJATAEIAECABoFAlYmq1gT\n"
+        + "HQJ0ZXN0NiBjb21wcm9taXNlZAAKCRDtBiXcRjKKjIm6B/9YwkyG4w+9KUNESywM\n"
+        + "bxC2WWGWrFcQGoKxixzt0uT251UY8qxa1IED0wnLsIQmffTQcnrK3B9svd4HhQlk\n"
+        + "pheKQ3w5iluLeGmGljhDBdAVyS07jYoFUGTXjwzPAgJ3Dxzul8Q8Zj+fOmRcfsP9\n"
+        + "72kl6g2yEEbevnydWIiOj/vWHVLFb54G8bwXTNwH/FXQsHuPYxXZifwyDwdwEQMq\n"
+        + "0VTZcrukgeJ+VbSSuq+uX4I3+kJw5hL49KYAQltQBmTo3yhuY/Q+LkgcBv/umtY/\n"
+        + "DrUqSCBV1bTnfq5SfaObkUu22HWjrtSFSjnXYyh+wyTG3AXG3N9VPrjGQIJIW1j6\n"
+        + "9QM0iQE3BB8BAgAhBQJWJqYUFwyAAQSup+0vghEz5bEo0e0GJdxGMoqMAgcAAAoJ\n"
+        + "EIxP0o40NLOfYd4H/3GpfxfJ+nMzBChn1JqFrKOqqYiOO4sUwubXzpRO33V2jUrU\n"
+        + "V75PTWG/6NlgDbPfKFcU0qZud6M2EQxSS9/I20i/MpRB7qJnWMM/6HxdMDJ0o/pN\n"
+        + "4ImIGj38QTIWx0DS9n3bwlcobl7ZlM8g2N1kv5jQPEuurffeJRS4ny4pEvCCm2IS\n"
+        + "SGOuB0DVtYHGDrJLQ0k4mDkEJuU8fP5un8mN8I8eAINlsTFpsTswMXMiptZTm5SI\n"
+        + "5QZlG3m5MvvckngYdhynvCWc6JHGt1EHXlI4A5Qetr/4FbNE4uYcEEhyzBy4WQfi\n"
+        + "QCPiIzzm3O4cMnr9N+5HzYqRhu2OveYm86G2Rxq0IFRlc3R1c2VyIFNpeCA8dGVz\n"
+        + "dDZAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJWJqV5AhsDBgsJCAcDAgYVCAIJCgsE\n"
+        + "FgIDAQIeAQIXgAAKCRCMT9KONDSzn2XtB/4wl4ctc3cW9Fwp17cktFi6md8fjRiR\n"
+        + "wE/ruVKIKmAHzeMLBoZn4LZVunyNCRGLZfP+MUs4JhLkp8ioTzUB7xPl9k94FXel\n"
+        + "bObn9F0T7htjFLiFAOMeykneylk2kalTt6IBKtaOPn+V6onBwO+YHbwt+xLMhAWj\n"
+        + "Z/WA0TIC1RIukdzWErhd+9lG8B9kupGC5bPo/AgCPoajPhS1qLrth+lCsNJXT/Rt\n"
+        + "k6Jx5omypxMXPzgzNtULMFONszaRnHnrCHQg/yJZDCw3ffW5ShfyfWdFM65jgEKo\n"
+        + "nMKLzy9XV+BM6IJQlgHCBAP8WHKSf4qMG4/hEWLrwA/bTQ7w0DSV88msuQENBFYm\n"
+        + "pXkBCACzIMFDC6kcV58uvF3XwOrS3DmKNPDNzO/4Ay/iOxZbm+9NP8QWEEm+AzCt\n"
+        + "ZMfYdZ8C3DjuzxkhcacI/E5agZICds6bs0+VS7VKEeNYp/UrTF9pkZNXseCrJPgr\n"
+        + "U31eoGVc5bE5c0TGLhAjbMKtR5LZFMpAXgpA7hXJSSuAXGs8gjkJkYSJYnJwIOyd\n"
+        + "xOi5jmnE/U5QuMjBG0bwxFXxkaDa5mcebJ/6C8mgkKyATbQkCe7YJGl1JLK4vY28\n"
+        + "ybSMhMDtZiwgvKzd+HcQr+xUQvmgSMApJaMxKPHRA1IrP/STXUEAjcGfk/HCz/0j\n"
+        + "7mJG2cvCxeOMAmp/pTzhSoXiqUNlABEBAAGJAR8EGAECAAkFAlYmpXkCGwwACgkQ\n"
+        + "jE/SjjQ0s5/kVAf/QvHOhuoBSlSxPcgvnvCl8V3zbNR1P9lgjYGwMsvLhwCT7Wvm\n"
+        + "mkUKvtT913uER93N8xJD2svGhKabpiPj9/eo0p3p64dicijsP1UQfpmWKPa/V9sv\n"
+        + "zep08cpDl/eczSiLqgcTXCoZeewWXoQGqqoXnwa4lwQv4Zvj7TTCN2wRzoGwbRcm\n"
+        + "G2hmc27uOwA+hXbF+bLe6HOZR/7U93j8a22g2X9OgST/QCsLgyiUSw3YYaEan9tn\n"
+        + "wuEgAEY/rchOvgeXe5Sl0wTFLHH6OS4BBGgc1LRKnSCM2dgZqvhOOxOvuuieBWY6\n"
+        + "tULvIEIjNNP8Qizfc4u2O8h7HP2b3yYSrp9MMQ==\n"
+        + "=Dxr7\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBFYmpXkBCACqaLz51DcWQmfOJnat9iHSySfSHwbKfVvoN43Ba2cf/D/PadRc\n"
+        + "HLgc+91k2yk1kV1LnMdvUGj5zZ84ZqrQx3f1WeItnzZpqxtmQS/GSxCp9EY/s7w6\n"
+        + "5i86R/k9Tzgvk0B7dKZJXbM/OWxxDkkxHWE3Un9wreX7bDU5b9D2knHRiNFqH9ZJ\n"
+        + "KqDIFZqH9WTUxNZcHz20sTCRIMfvsAwf2vRU5N5xTu4Mbk6JFc7BAj7h1f/mYEPo\n"
+        + "CTyB1jV/DSDVdn1FjJVocSg6W/CvsYF9hKFYjJHl4VXdePTpnOjHhJLL0QWk0TMe\n"
+        + "xYeUi/xDr5DeMxTmi7F7BFaQEF+KmUM46e+9ABEBAAEAB/wOspbuA1A3AsY6QRYG\n"
+        + "Xg6/w+rD1Do9N7+4ESaQUqej2hlU1d9jjHSSx2RqgP6WaLG/xkdrQeez9/iuICjG\n"
+        + "dhXSGw0He05xobjswl2RAENxLSjr8KAhAl57a97C23TQoaYzn7WB6Wt+3gCM5bsJ\n"
+        + "WevbHinwuYb2/ve+OvcudSYM+Nhtpv0DoTaizhi9wzc3g/XLbturlpdCffbw4y+h\n"
+        + "gBPd/t3cc/0Ams8Wi2RlmDOoe73ls23nBHcNomgydyIYBn7U5Z3v3YkPNp9VBiXx\n"
+        + "rC4mDtB1ugucMhqjRNAYqinaLP35CiBTU/IB0WLu7ZyytnjY5frly1ShAG8wFL0B\n"
+        + "MOMxBADJjGy1NwGSd/7eMeYyYThyhXDxo5so91/O1+RLnSUVv/Nz6VOPp2TtuVN5\n"
+        + "uTJkpSXtUFyWbf8mkQiFz4++vHW5E/Q6+KomXRalK7JeBzeFMtax64ykQHID9cSu\n"
+        + "TaSHBhOEEeZZuf6BlulYEJEBHYK6EFlPJn+cpZtTFaqDoKh22QQA2HKjfyeppNre\n"
+        + "WRFJ9h1x1hBlSRR+XIPYmDmZUjL37jQUlw8iF+txPclfyNBw2I2Om+Jhcf25peOx\n"
+        + "ow4yvjt8r3qDjNhI2zLE9u4zrQ9xU8CUingT0t4k3NO2vigpKlmp1/w2IHSMctry\n"
+        + "v1v3+BAS8qGIYDY1lgI7QBvle5hxGYUD/00zMyHOIgYg/cM5sR0qafesoj9kRff5\n"
+        + "UMnSy1dw+pGMv6GqKGbcZDoC060hUO9GhQRPZXF8PlYzD30lOLS2Uw4mPXjOmQVv\n"
+        + "lDiyl/vLkfkVfP/alYH0FW6mErDrjtHhrZewqDm3iPLGMVGfGCJsL+N37VBSe+jr\n"
+        + "4rZCnjk/Jo5JRoKJATcEHwECACEFAlYmphQXDIABBK6n7S+CETPlsSjR7QYl3EYy\n"
+        + "iowCBwAACgkQjE/SjjQ0s59h3gf/cal/F8n6czMEKGfUmoWso6qpiI47ixTC5tfO\n"
+        + "lE7fdXaNStRXvk9NYb/o2WANs98oVxTSpm53ozYRDFJL38jbSL8ylEHuomdYwz/o\n"
+        + "fF0wMnSj+k3giYgaPfxBMhbHQNL2fdvCVyhuXtmUzyDY3WS/mNA8S66t994lFLif\n"
+        + "LikS8IKbYhJIY64HQNW1gcYOsktDSTiYOQQm5Tx8/m6fyY3wjx4Ag2WxMWmxOzAx\n"
+        + "cyKm1lOblIjlBmUbebky+9ySeBh2HKe8JZzokca3UQdeUjgDlB62v/gVs0Ti5hwQ\n"
+        + "SHLMHLhZB+JAI+IjPObc7hwyev037kfNipGG7Y695ibzobZHGrQgVGVzdHVzZXIg\n"
+        + "U2l4IDx0ZXN0NkBleGFtcGxlLmNvbT6JATgEEwECACIFAlYmpXkCGwMGCwkIBwMC\n"
+        + "BhUIAgkKCwQWAgMBAh4BAheAAAoJEIxP0o40NLOfZe0H/jCXhy1zdxb0XCnXtyS0\n"
+        + "WLqZ3x+NGJHAT+u5UogqYAfN4wsGhmfgtlW6fI0JEYtl8/4xSzgmEuSnyKhPNQHv\n"
+        + "E+X2T3gVd6Vs5uf0XRPuG2MUuIUA4x7KSd7KWTaRqVO3ogEq1o4+f5XqicHA75gd\n"
+        + "vC37EsyEBaNn9YDRMgLVEi6R3NYSuF372UbwH2S6kYLls+j8CAI+hqM+FLWouu2H\n"
+        + "6UKw0ldP9G2TonHmibKnExc/ODM21QswU42zNpGceesIdCD/IlkMLDd99blKF/J9\n"
+        + "Z0UzrmOAQqicwovPL1dX4EzoglCWAcIEA/xYcpJ/iowbj+ERYuvAD9tNDvDQNJXz\n"
+        + "yaydA5gEVialeQEIALMgwUMLqRxXny68XdfA6tLcOYo08M3M7/gDL+I7Flub700/\n"
+        + "xBYQSb4DMK1kx9h1nwLcOO7PGSFxpwj8TlqBkgJ2zpuzT5VLtUoR41in9StMX2mR\n"
+        + "k1ex4Ksk+CtTfV6gZVzlsTlzRMYuECNswq1HktkUykBeCkDuFclJK4BcazyCOQmR\n"
+        + "hIlicnAg7J3E6LmOacT9TlC4yMEbRvDEVfGRoNrmZx5sn/oLyaCQrIBNtCQJ7tgk\n"
+        + "aXUksri9jbzJtIyEwO1mLCC8rN34dxCv7FRC+aBIwCklozEo8dEDUis/9JNdQQCN\n"
+        + "wZ+T8cLP/SPuYkbZy8LF44wCan+lPOFKheKpQ2UAEQEAAQAH/A1Os+Tb9yiGnuoN\n"
+        + "LuiSKa/YEgNBOxmC7dnuPK6xJpBQNZc200WzWJMf8AwVpl4foNxIyYb+Rjbsl1Ts\n"
+        + "z5JcOWFq+57oE5O7D+EMkqf5tFZO4nC4kqprac41HSW02mW/A0DDRKcIt/WEIwlK\n"
+        + "sWzHmjJ736moAtl/holRYQS0ePgB8bUPDQcFovH6X3SUxlPGTYD1DEX+WNvYRk3r\n"
+        + "pa9YXH65qbG9CEJIFTmwZIRDl+CBtBlN/fKadyMJr9fXtv7Fu9hNsK1K1pUtLqCa\n"
+        + "nc22Zak+o+LCPlZ8vmw/UmOGtp2iZlEragmh2rOywp0dHF7gsdlgoafQf8Q4NIag\n"
+        + "TFyHf1kEAMSOKUUwLBEmPnDVfoEOt5spQLVtlF8sh/Okk9zVazWmw0n/b1Ef72z6\n"
+        + "EZqCW9/XhH5pXfKJeV+08hroHI6a5UESa7/xOIx50TaQdRqjwGciMnH2LJcpIU/L\n"
+        + "f0cGXcnTLKt4Z2GeSPKFTj4VzwmwH5F/RYdc5eiVb7VNoy9DC5RZBADpTVH5pklS\n"
+        + "44VDJIcwSNy1LBEU3oj+Nu+sufCimJ5B7HLokoJtm6q8VQRga5hN1TZkdQcLy+b2\n"
+        + "wzxHYoIsIsYFfG/mqLZ3LJNDFqze1/Kj987DYSUGeNYexMN2Fkzbo35Jf0cpOiao\n"
+        + "390JFOS7qecUak5/yJ/V4xy8/nds37617QP9GWlFBykDoESBC2AIz8wXcpUBVNeH\n"
+        + "BNSthmC+PJPhsS6jTQuipqtXUZBgZBrMHp/bA8gTOkI4rPXycH3+ACbuQMAjbFny\n"
+        + "Kt69lPHD8VWw/82E4EY2J9LmHli+2BcATz89ouC4kqC5zF90qJseviSZPihpnFxA\n"
+        + "1UqMU2ZjsPb4CM9C/YkBHwQYAQIACQUCVialeQIbDAAKCRCMT9KONDSzn+RUB/9C\n"
+        + "8c6G6gFKVLE9yC+e8KXxXfNs1HU/2WCNgbAyy8uHAJPta+aaRQq+1P3Xe4RH3c3z\n"
+        + "EkPay8aEppumI+P396jSnenrh2JyKOw/VRB+mZYo9r9X2y/N6nTxykOX95zNKIuq\n"
+        + "BxNcKhl57BZehAaqqhefBriXBC/hm+PtNMI3bBHOgbBtFyYbaGZzbu47AD6FdsX5\n"
+        + "st7oc5lH/tT3ePxrbaDZf06BJP9AKwuDKJRLDdhhoRqf22fC4SAARj+tyE6+B5d7\n"
+        + "lKXTBMUscfo5LgEEaBzUtEqdIIzZ2Bmq+E47E6+66J4FZjq1Qu8gQiM00/xCLN9z\n"
+        + "i7Y7yHsc/ZvfJhKun0wx\n"
+        + "=M/kw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key revoked by a valid key, due to no longer being used.
+   * <p>
+   * Revoked by {@link #validKeyWithoutExpiration()}.
+   *
+   * <pre>
+   * pub   2048R/3D6C52D0 2015-10-20 [revoked: 2015-10-20]
+   *       Key fingerprint = 32DB 6C31 2ED7 A98D 11B2  43EA FAD2 ABE2 3D6C 52D0
+   * uid                  Testuser Seven &lt;test7@example.com&gt;
+   * </pre>
+   */
+  public static TestKey revokedNoLongerUsedKey() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBFYmq3EBCAC9ssY6QhFsnZqKEPlQrx8Zomblj8qV93/B448isOT2L6OVY7UC\n"
+        + "kKPj6afW5UDkYeyZSmLZfTrpePcbAB8FB3uvd/AS9mHC+6zuBlwlkl9xIXlwUXQP\n"
+        + "KER4LKYNTP21AM+/vTJm4+u26tlZECIZlez31KEeqM30EAm+/pO8VkEp8+1ImfLv\n"
+        + "otndIjMoq9gxJvn6KZeexJT2eKCsSa20vVsmAhuFjLZitU3lEjIROfDiyHUZ2cZ+\n"
+        + "qynfppJCKlHJRu/T9L/yxDFVUFDFSajNzSfjG1g3FEveDITyAhRetVfZbhyJptnV\n"
+        + "jfiHSQkLamPsBmMoKfP+aO5SfsTHTJvxgLUdABEBAAGJAS0EIAECABcFAlYmq8AQ\n"
+        + "HQN0ZXN0NyBub3QgdXNlZAAKCRDtBiXcRjKKjPKqB/sF+ypJZaZ5M4jFdoH/YA3s\n"
+        + "4+VkA/NbLKcrlMI0lbnIrax02jdyTo7rBUJfTwuBs5QeQ25+VfaBcz9fWSv4Z8Bk\n"
+        + "9+w61bQZLQkExZ9W7hnhaapyR0aT0rY48KGtHOPNoMQu9Si+RnRiI024jMUUjrau\n"
+        + "w/exgCteY261VtCPRgyZOlpbX43rsBhF8ott0ZzSfLwaNTHhsjFsD1uH6TSFO8La\n"
+        + "/H1nO31sORlY3+rCGiQVuYIJD1qI7bEjDHYO0nq/f7JjfYKmVBg9grwLsX3h1qZ2\n"
+        + "L3Yz+0eCi7/6T/Sm7PavQ+EGL7+WBXX3qJpwc+EFNHs6VxQp86k6csba0c5mNcaQ\n"
+        + "iQE3BB8BAgAhBQJWJqusFwyAAQSup+0vghEz5bEo0e0GJdxGMoqMAgcAAAoJEPrS\n"
+        + "q+I9bFLQ2BYH/jm+t7pZuv8WqZdb8FiBa9CFfhcSKjYarMHjBw7GxWZJMd5VR4DC\n"
+        + "r4T/ZSAGRKBRKQ2uXrkm9H0NPDp0c/UKCHtQMFDnqTk7B63mwSR1d7W0qaRPXYQ1\n"
+        + "bbatnzkEDOj0e+rX6aiqVRMo/q6uMNUFl6UMrUZPSNB5PVRQWPnQ7K11mw3vg0e5\n"
+        + "ycqJbyFvER6EtyDUXGBo8a5/4bK8VBNBMTAIy6GeGpeSM5b7cpQk7/j4dXugCJAV\n"
+        + "fhFNUOgLduoIKM4u+VcFjk3Km/YxOtGi1dLqCbTX/0LiCRA9mgQpyNVyA+Sm48LM\n"
+        + "LUkbcrN/F3SHX1ao/5lm19r8Biu1ziQnLgC0IlRlc3R1c2VyIFNldmVuIDx0ZXN0\n"
+        + "N0BleGFtcGxlLmNvbT6JATgEEwECACIFAlYmq3ECGwMGCwkIBwMCBhUIAgkKCwQW\n"
+        + "AgMBAh4BAheAAAoJEPrSq+I9bFLQvjQH/0K7aBsGU2U/rm4I+u+uPa6BnFTYQJqg\n"
+        + "034pwdD0WfM3M/XgVh7ERjnR9ZViCMVej+K3kW5d2DNaXu5vVpcD6L6jjWwiJHBw\n"
+        + "LIcmpqQrL0TdoCr4F4FKQnBbcH1fNvP8A/hLDHB3k3ERPvEFIo1AkVuK4s/v7yZY\n"
+        + "HAowX0r4ok4ndu/wAc0HI1FkApkAfh18JDTuui53dkKhnkDp7Xnfm/ElAZYjB7Se\n"
+        + "ivxOD9vdhViWSx1VhttPZo5hSyJrEYaJ5u9hsXNUN85DxgLqCmS1v8n3pN1lVY/Q\n"
+        + "TYXtgocakQgHGEG0Tl6a3xpNkn9ihnyCr80mHCxXTyUUBGfygccelB+5AQ0EViar\n"
+        + "cQEIAKxwXb6HGV9QjepADyWW7GMxc2JVZ7pZM2sdf8wrgnQqV2G1rc9gAgwTX4jt\n"
+        + "OY0vSKT1vBq09ZXS3qpYHi/Wwft0KkaX/a7e6vKabDSfhilxC2LuGz2+56f6UOzj\n"
+        + "ggwf5k4LFTQvkDUZumwPjoeC2hqQO3Q/9PW39C6GnvsCr5L0MRdO3PbVJM7lJaOk\n"
+        + "MbGwgysErWgiZXKlxMpIvffIsLC4BAxnjXaCy6zHuBcPMPaRMs7sDRBzeuTV2wnX\n"
+        + "Sd+IXZgdpd1hF7VkuXenzwOqvBGS66C3ILW0ZTFaOtgrloIkTvtYEcJFWvxqWl2F\n"
+        + "+JQ5V6eu2aJ3HIGyr9L1R8MUA6EAEQEAAYkBHwQYAQIACQUCViarcQIbDAAKCRD6\n"
+        + "0qviPWxS0M0PB/9Rbk4/pNW67+uE1lwtaIG7uFiMbJqu8jK6MkD8GdayflroWEZA\n"
+        + "x0Xow9HL8UaRfeRPTZMrDRpjl+fJIXT5qnlB0FPmzSXAKr3piC8migBcbp5m6hWh\n"
+        + "c3ScAqWOeMt9j0TTWHh4hKS8Q+lK392ht65cI/kpFhxm9EEaXmajplNL/2G3PVrl\n"
+        + "fFUgCdOn2DYdVSgJsfBhkcoiy17G3vqtb+We6ulhziae4SIrkUSqdYmRjiFyvqZz\n"
+        + "tmMEoF6CQNCUb1NK0TsSDeIdDacYjUwyq0Qj6TaXrWcbC3kW0GtWoFTNIiX4q9bN\n"
+        + "+B6paw/s8P7XCWznTBRdlFWWgrhcpzQ8fefC\n"
+        + "=CHer\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBFYmq3EBCAC9ssY6QhFsnZqKEPlQrx8Zomblj8qV93/B448isOT2L6OVY7UC\n"
+        + "kKPj6afW5UDkYeyZSmLZfTrpePcbAB8FB3uvd/AS9mHC+6zuBlwlkl9xIXlwUXQP\n"
+        + "KER4LKYNTP21AM+/vTJm4+u26tlZECIZlez31KEeqM30EAm+/pO8VkEp8+1ImfLv\n"
+        + "otndIjMoq9gxJvn6KZeexJT2eKCsSa20vVsmAhuFjLZitU3lEjIROfDiyHUZ2cZ+\n"
+        + "qynfppJCKlHJRu/T9L/yxDFVUFDFSajNzSfjG1g3FEveDITyAhRetVfZbhyJptnV\n"
+        + "jfiHSQkLamPsBmMoKfP+aO5SfsTHTJvxgLUdABEBAAEAB/9AdCtFJSidcolNKwpC\n"
+        + "/1V+VL9IdYxcWx02CDccjuUkvrgCrL+WcQW2jS/hZMChOKJ2zR78DcBEDr1LF8Xy\n"
+        + "ZAIC8yoHj15VLUUrFM8fVvYFzt1fq9VWxxRIjscW0teLNgzgdYzYB84RtwcFa2Vi\n"
+        + "sx2ycTUTYUClEgP1uLMCtX3rnibJh4vR+lVgnDtKSoh4CLAlW6grAAVdw5sSuV7Q\n"
+        + "i9EJcPezGw1RvBU5PooqNDG6kyw/QqsAS4q3WP4uVJKK1e7S9oqXFEN8k/zfllI0\n"
+        + "SSkoyP2flzz71rJF/wQMfJ8uf/CelKXd+gPO4FbCWiZSTLe20JR23qiOyvZkfCwg\n"
+        + "eFmzBADIJUzspDrg5yaqE+HMc8U3O9G9FHoDSweZTbhiq3aK0BqMAn34u0ps6chy\n"
+        + "VMO6aPWVzgcSHNfTlzpjuN9lwDoimYBH5vZa1HlCHt5eeqTORixkxSerOmILabTi\n"
+        + "QWq5JPdJwYZiSvK45G5k3G37RTd6/QyhTlRYXj59RXYajrYngwQA8qMZRkRYcTop\n"
+        + "aG+5M0x44k6NgIyH7Ap+2vRPpDdUlHs+z+6iRvoutkSfKHeZUYBQjgt+tScfn1hM\n"
+        + "BRB+x146ecmSVh/Dh8yu6uCrhitFlKpyJqNptZo5o+sH41zjefpMd/bc8rtHTw3n\n"
+        + "GiFl57ZbXbze2O8UimUVgRI2DtOebt8EAJHM/8vZahzF0chzL4sNVAb8FcNYxAyn\n"
+        + "95VpnWeAtKX7f0bqUvIN4BNV++o6JdMNvBoYEQpKeQIda7QM59hNiS8f/bxkRikF\n"
+        + "OiHB5YGy2zRX5T1G5rVQ0YqrOu959eEwdGZmOQ8GOqq5B/NoHXUtotV6SGE3R+Tl\n"
+        + "grlV4U5/PT0fM3KJATcEHwECACEFAlYmq6wXDIABBK6n7S+CETPlsSjR7QYl3EYy\n"
+        + "iowCBwAACgkQ+tKr4j1sUtDYFgf+Ob63ulm6/xapl1vwWIFr0IV+FxIqNhqsweMH\n"
+        + "DsbFZkkx3lVHgMKvhP9lIAZEoFEpDa5euSb0fQ08OnRz9QoIe1AwUOepOTsHrebB\n"
+        + "JHV3tbSppE9dhDVttq2fOQQM6PR76tfpqKpVEyj+rq4w1QWXpQytRk9I0Hk9VFBY\n"
+        + "+dDsrXWbDe+DR7nJyolvIW8RHoS3INRcYGjxrn/hsrxUE0ExMAjLoZ4al5Izlvty\n"
+        + "lCTv+Ph1e6AIkBV+EU1Q6At26ggozi75VwWOTcqb9jE60aLV0uoJtNf/QuIJED2a\n"
+        + "BCnI1XID5KbjwswtSRtys38XdIdfVqj/mWbX2vwGK7XOJCcuALQiVGVzdHVzZXIg\n"
+        + "U2V2ZW4gPHRlc3Q3QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCViarcQIbAwYLCQgH\n"
+        + "AwIGFQgCCQoLBBYCAwECHgECF4AACgkQ+tKr4j1sUtC+NAf/QrtoGwZTZT+ubgj6\n"
+        + "7649roGcVNhAmqDTfinB0PRZ8zcz9eBWHsRGOdH1lWIIxV6P4reRbl3YM1pe7m9W\n"
+        + "lwPovqONbCIkcHAshyampCsvRN2gKvgXgUpCcFtwfV828/wD+EsMcHeTcRE+8QUi\n"
+        + "jUCRW4riz+/vJlgcCjBfSviiTid27/ABzQcjUWQCmQB+HXwkNO66Lnd2QqGeQOnt\n"
+        + "ed+b8SUBliMHtJ6K/E4P292FWJZLHVWG209mjmFLImsRhonm72Gxc1Q3zkPGAuoK\n"
+        + "ZLW/yfek3WVVj9BNhe2ChxqRCAcYQbROXprfGk2Sf2KGfIKvzSYcLFdPJRQEZ/KB\n"
+        + "xx6UH50DmARWJqtxAQgArHBdvocZX1CN6kAPJZbsYzFzYlVnulkzax1/zCuCdCpX\n"
+        + "YbWtz2ACDBNfiO05jS9IpPW8GrT1ldLeqlgeL9bB+3QqRpf9rt7q8ppsNJ+GKXEL\n"
+        + "Yu4bPb7np/pQ7OOCDB/mTgsVNC+QNRm6bA+Oh4LaGpA7dD/09bf0Loae+wKvkvQx\n"
+        + "F07c9tUkzuUlo6QxsbCDKwStaCJlcqXEyki998iwsLgEDGeNdoLLrMe4Fw8w9pEy\n"
+        + "zuwNEHN65NXbCddJ34hdmB2l3WEXtWS5d6fPA6q8EZLroLcgtbRlMVo62CuWgiRO\n"
+        + "+1gRwkVa/GpaXYX4lDlXp67ZonccgbKv0vVHwxQDoQARAQABAAf5Ae8xa1mPns1E\n"
+        + "B5yCrvzDl79Dw0F1rED46IWIW/ghpVTzmFHV6ngcvcRFM5TZquxHXSuxLv7YVxRq\n"
+        + "UVszXNJaEwyJYYkDRwAS1E2IKN+gknwapm2eWkchySAajUsQt+XEYHFpDPtQRlA3\n"
+        + "Z6PrCOPJDOLmT9Zcf0R6KurGrhvTGrZkKU6ZCFqZWETfZy5cPfq2qxtw3YEUI+eT\n"
+        + "09AgMmPJ9nDPI3cA69tvy/phVFgpglsS76qgd6uFJ5kcDoIB+YepmJoHnzJeowYt\n"
+        + "lvnmmyGqmVS/KCgvILaD0c73Dp2X0BN64hSZHa3nUU67WbKJzo2OXr+yr0hvofcf\n"
+        + "8vhKJe5+2wQAy+rRKSAOPaFiKT8ZenRucx1pTJLoB8JdediOdR4dtXB2Z59Ze7N3\n"
+        + "sedfrJn1ao+jJEpnKeudlDq7oa9THd7ZojN4gBF/lz0duzfertuQ/MrHaTPeK8YI\n"
+        + "dEPg3SgYVOLDBptaKmo0xr2f6aslGLPHgxCgzOcLuuUNGKJSigZvhdMEANh7VKsX\n"
+        + "nb5shZh+KRET84us/uu74q4iIfc8Q10oXuN9+IPlqfAIclo4uMhvo5rtI9ApFtxs\n"
+        + "oZzqqc+gt+OAbn/fHeb61eT36BA+r61Ka+erxkpWU5r1BPVIqq+biTY/HHchqroJ\n"
+        + "aw81qWudO9h5a0yP1alDiBSwhZWIMCKzp6Q7A/472amrSzgs7u8ToQ/2THDxaMf3\n"
+        + "Se0HgMrIT1/+5es2CWiEoZGSZTXlimDYXJULu/DFC7ia7kXOLrMsO85bEi7SHagA\n"
+        + "eO+mAw3xP3OuNkZDt9x4qtal28fNIz22DH5qg2wtsGdCWXz5C6OdcrtQ736kNxa2\n"
+        + "5QemZ/0VWxHPnvXz40RtiQEfBBgBAgAJBQJWJqtxAhsMAAoJEPrSq+I9bFLQzQ8H\n"
+        + "/1FuTj+k1brv64TWXC1ogbu4WIxsmq7yMroyQPwZ1rJ+WuhYRkDHRejD0cvxRpF9\n"
+        + "5E9NkysNGmOX58khdPmqeUHQU+bNJcAqvemILyaKAFxunmbqFaFzdJwCpY54y32P\n"
+        + "RNNYeHiEpLxD6Urf3aG3rlwj+SkWHGb0QRpeZqOmU0v/Ybc9WuV8VSAJ06fYNh1V\n"
+        + "KAmx8GGRyiLLXsbe+q1v5Z7q6WHOJp7hIiuRRKp1iZGOIXK+pnO2YwSgXoJA0JRv\n"
+        + "U0rROxIN4h0NpxiNTDKrRCPpNpetZxsLeRbQa1agVM0iJfir1s34HqlrD+zw/tcJ\n"
+        + "bOdMFF2UVZaCuFynNDx958I=\n"
+        + "=aoJv\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * Key revoked by an expired key, after that key's expiration.
+   * <p>
+   * Revoked by {@link #expiredKey()}.
+   *
+   * <pre>
+   * pub   2048R/78BF7D7E 2005-08-01 [revoked: 2015-10-20]
+   *       Key fingerprint = 916F AB22 5BE7 7585 F59A  994C 001A DF8B 78BF 7D7E
+   * uid                  Testuser Eight &lt;test8@example.com&gt;
+   * </pre>
+   */
+  public static TestKey keyRevokedByExpiredKeyAfterExpiration() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBELuRwABCAC56yhFKybBtuKT4nyb7RdLE98pZR54aGjcDcKH3VKVyBF8Z4Kx\n"
+        + "ptd7Sre0mLPCQiNWVOmCT+JG7GKVE6YeFmyXDUnhX9w4+HAeDEh23S4u9JvwWaF+\n"
+        + "wlJ6jLq/oe5gdT1F6Y2yqNpQ6CztOw52Ko9KSYz7/1zBMPcCkl/4k15ee4iebVdq\n"
+        + "c7qT5Qt49Poiozh0DI5prPQ624uckHkz2mXshjWQVuHWwrkIkCJZ2I/KQN2kBjKw\n"
+        + "/ALxumaWmiB9lQ0nIwLuGzHCh0Xg5RxuCrK8fJp47Aza3ikVuYlNzSxhJVav3OtK\n"
+        + "gftBihQXUlY3Uy/4QTCeH/BdVs5OALtXL3VhABEBAAGJAS0EIAECABcFAlYmr4kQ\n"
+        + "HQN0ZXN0OCBub3QgdXNlZAAKCRA87HgbF94azQJ5B/0TeQk7TSChNp+NqCKPTuw0\n"
+        + "wpflDyc+5ru/Gcs4r358cWzgiLUb3M0Q1+M8CF13BFQdrxT05vjheI9o5PCn3b//\n"
+        + "AHV8m+QFSnRi2J3QslbvuOqOnipz7vc7lyZ7q1sWNC33YN+ZcGZiMuu5HJi9iadf\n"
+        + "ZL7AdInpUb4Zb+XKphbMokDcN3yw7rqSMMcx+rKytUAqUnt9qvaSLrIH/zeazxlp\n"
+        + "YG4jaN53WPfLCcGG+Rw56mW+eCQD2rmzaNHCw8Qr+19sokXLB7OML+rd1wNwZT4q\n"
+        + "stWnL+nOj8ZkbFV0w3zClDYaARr7H+vTckwVStyDVRbnpRitSAtJwbRDzZBaS4Vx\n"
+        + "iQE3BB8BAgAhBQJC7lUQFwyAAR2e63ndOLBJk52crzzseBsX3hrNAgcAAAoJEAAa\n"
+        + "34t4v31+AS4H/0x3Y9E3q9DR5FCuYTXG4BHyrALo2WKoP0CfUWL98Fw9Txl0hF+9\n"
+        + "5wriNlnmd2zvM0quHs78x4/xehQO88cw0lqPx3RARq/ju5/VbOjoNlcHvfGYZiEd\n"
+        + "yWOwHu7O8sZrenFDjeDglD6NArrjncOcC51XIPSSTLvVQpSauQ1FS4tan5Q4aWMb\n"
+        + "s4DzE+Vqu2xMkO/X9toYAZKzyWP29OckpouMbt3GUnS6/o0A8Z7jVX+XOIk3XolP\n"
+        + "Li9tzTQB12Xl23mgFvearDoguR2Bu2SbmTJtdiXz8L3S54kGvxVqak5uOP2dagzU\n"
+        + "vBiqR4SVoAdGoXt6TI6mpA+qdYmPMG8v21S0IlRlc3R1c2VyIEVpZ2h0IDx0ZXN0\n"
+        + "OEBleGFtcGxlLmNvbT6JATgEEwECACIFAkLuRwACGwMGCwkIBwMCBhUIAgkKCwQW\n"
+        + "AgMBAh4BAheAAAoJEAAa34t4v31+8/sIAIuqd+dU8k9c5VQ12k7IfZGGYQHF2Mk/\n"
+        + "8FNuP7hFP/VOXBK3QIxIfGEOHbDX6uIxudYMaDmn2UJbdIqJd8NuQByh1gqXdX/x\n"
+        + "nteUa+4e7U6uTjkp/Ij5UzRed8suINA3NzVOy6qwCu3DTOXIZcjiOZtOA5GTqG6Z\n"
+        + "naDP0hwDssJp+LXIYTJgsvneJQFGSdQhhJSv19oV0JPSbb6Zc7gEIHtPcaJHjuZQ\n"
+        + "Ev+TRcRrI9HPTF0MvgOYgIDo2sbcSFV+8moKsHMC+j1Hmuuqgm/1yKGIZrt0V75s\n"
+        + "D9HYu0tiS3+Wlsry3y1hg/2XBQbwgh6sT/jWkpWar7+uzNxO5GdFYrC5AQ0EQu5H\n"
+        + "AAEIALPFTedbfyK+9B35Uo9cPsmFa3mT3qp/bAQtnOjiTTTiIO3tu0ALnaBjf6On\n"
+        + "fAV1HmGz6hRMRK4LGyHkNTaGDNNPoXO7+t9DWycSHmsCL5d5zp7VevQE8MPR8zHK\n"
+        + "Il2YQlCzdy5TWSUhunKd4guDNZ9GiOS6NQ9feYZ9DQ1kzC8nnu7jLkR2zNT02sYU\n"
+        + "kuOCZUktQhVNszUlavdIFjvToZo3RPcdb/E3kTTy2R9xi89AXjWZf3lSAZe3igkL\n"
+        + "jhwsd+u3RRx0ptOJym7zYl5ZdUZk4QrS7FPI6zEBpjawbS4/r6uEW89P3QAkanDI\n"
+        + "ridIAZP8awLZU3uSPtMwPIJpao0AEQEAAYkBHwQYAQIACQUCQu5HAAIbDAAKCRAA\n"
+        + "Gt+LeL99fqpHB/wOXhdMNtgeVW38bLk8YhcEB23FW6fDjFjBJb9m/yqRTh5CIeG2\n"
+        + "bm29ofT4PTamPb8Gt+YuDLnQQ3K2jURakxNDcYwiurvR/oHVdxsBRU7Px7UPeZk3\n"
+        + "BG5VnIJRT198dF7MWFJ+x5wHbNXwM8DDvUwTjXLH/TlGl1XIheSTHCYd9Pra4ejE\n"
+        + "ockkrDaZlPCQdTwY+P7K2ieb5tsqNpJkQeBrglF2bemY/CtQHnM9qwa6ZJqkyYNR\n"
+        + "F1nkSYn36BPuNpytYw1CaQV9GbePugPHtshECLwA160QzqISQUcJlKXttUqUGnoO\n"
+        + "0d0PyzZT3676mQwmFoebMR9vACAeHjvDxD4F\n"
+        + "=ihWb\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBELuRwABCAC56yhFKybBtuKT4nyb7RdLE98pZR54aGjcDcKH3VKVyBF8Z4Kx\n"
+        + "ptd7Sre0mLPCQiNWVOmCT+JG7GKVE6YeFmyXDUnhX9w4+HAeDEh23S4u9JvwWaF+\n"
+        + "wlJ6jLq/oe5gdT1F6Y2yqNpQ6CztOw52Ko9KSYz7/1zBMPcCkl/4k15ee4iebVdq\n"
+        + "c7qT5Qt49Poiozh0DI5prPQ624uckHkz2mXshjWQVuHWwrkIkCJZ2I/KQN2kBjKw\n"
+        + "/ALxumaWmiB9lQ0nIwLuGzHCh0Xg5RxuCrK8fJp47Aza3ikVuYlNzSxhJVav3OtK\n"
+        + "gftBihQXUlY3Uy/4QTCeH/BdVs5OALtXL3VhABEBAAEAB/wLr88oGuxsoqIHRQZL\n"
+        + "eGm9jc4aQGmcDMcjpwdGilhrwyfrO6f84hWbQdD+rJcnI8hsH7oOd5ZMGkWfpJyt\n"
+        + "eUAh9iNB5ChYGfDVSLUg6KojqDtprj6vNMihvLkr/OI6xL/hZksikwfnLFMPpgXU\n"
+        + "knwPocQ3nn+egsUSL7CR8/SLiIm4MC0brer6jhDxB5LKweExNlfTe4c0MDeYTsWt\n"
+        + "0WGzNPlvRZQXRotJzqemt3wdNZXUnCKR0n7pSQ8EhZr2O6NXr+mUgp6PIOE/3un2\n"
+        + "YGiBEf5uy3qEFe7FjEGIHz+Z3ySRdUDfHOk82TKAzynoJIxRUvLIYVNw4eFB3l5U\n"
+        + "s1w5BADUzfciG7RVLa8UFKJfqQ/5M06QmdS1v1/hMQXg38+3vKe8RgfSSnMJ08Sc\n"
+        + "eAEsmugwpNXAxgRKHcmWzN3NMBHhE3KiyiogWaMGqmSo6swFpu0+dwMvZSxMlfD+\n"
+        + "ka/BWt8YsUdrqW06ow39aTgCV+icbNRV81C7NKe7u0X1JDx2CQQA36gbdo62h/Wd\n"
+        + "gJI8kdz/se3xrt8x6RoWvOnWPNmsZR5XkDqAMTL1dWiEEA/dQTphMcgAe9z3WaP+\n"
+        + "F1TPAfounbiurGCcS3kxJ5tY7ojyU7nYz4DA/V2OU0C/LUoLXhttG5HM+m/i3qn4\n"
+        + "K9bBoWIQY1ijliS7cTSwNqd6IHaQGpkEAMnp5GwSGhY+kUuLw06hmH4xnsuf6agz\n"
+        + "AfhbPylB2nf/ZaX6dt6/mFEAkvQNahcoWEskfS3LGCD8jHm8PvF8K0mciXPDweq2\n"
+        + "gW3/irE0RXNwn3Oa222VSvcgUlocBm9InkfvpFXh20OYFe3dFH7uYkwUqIHJeXjw\n"
+        + "TjpXUX/vC5QJQOyJATcEHwECACEFAkLuVRAXDIABHZ7red04sEmTnZyvPOx4Gxfe\n"
+        + "Gs0CBwAACgkQABrfi3i/fX4BLgf/THdj0Ter0NHkUK5hNcbgEfKsAujZYqg/QJ9R\n"
+        + "Yv3wXD1PGXSEX73nCuI2WeZ3bO8zSq4ezvzHj/F6FA7zxzDSWo/HdEBGr+O7n9Vs\n"
+        + "6Og2Vwe98ZhmIR3JY7Ae7s7yxmt6cUON4OCUPo0CuuOdw5wLnVcg9JJMu9VClJq5\n"
+        + "DUVLi1qflDhpYxuzgPMT5Wq7bEyQ79f22hgBkrPJY/b05ySmi4xu3cZSdLr+jQDx\n"
+        + "nuNVf5c4iTdeiU8uL23NNAHXZeXbeaAW95qsOiC5HYG7ZJuZMm12JfPwvdLniQa/\n"
+        + "FWpqTm44/Z1qDNS8GKpHhJWgB0ahe3pMjqakD6p1iY8wby/bVLQiVGVzdHVzZXIg\n"
+        + "RWlnaHQgPHRlc3Q4QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCQu5HAAIbAwYLCQgH\n"
+        + "AwIGFQgCCQoLBBYCAwECHgECF4AACgkQABrfi3i/fX7z+wgAi6p351TyT1zlVDXa\n"
+        + "Tsh9kYZhAcXYyT/wU24/uEU/9U5cErdAjEh8YQ4dsNfq4jG51gxoOafZQlt0iol3\n"
+        + "w25AHKHWCpd1f/Ge15Rr7h7tTq5OOSn8iPlTNF53yy4g0Dc3NU7LqrAK7cNM5chl\n"
+        + "yOI5m04DkZOobpmdoM/SHAOywmn4tchhMmCy+d4lAUZJ1CGElK/X2hXQk9Jtvplz\n"
+        + "uAQge09xokeO5lAS/5NFxGsj0c9MXQy+A5iAgOjaxtxIVX7yagqwcwL6PUea66qC\n"
+        + "b/XIoYhmu3RXvmwP0di7S2JLf5aWyvLfLWGD/ZcFBvCCHqxP+NaSlZqvv67M3E7k\n"
+        + "Z0VisJ0DmARC7kcAAQgAs8VN51t/Ir70HflSj1w+yYVreZPeqn9sBC2c6OJNNOIg\n"
+        + "7e27QAudoGN/o6d8BXUeYbPqFExErgsbIeQ1NoYM00+hc7v630NbJxIeawIvl3nO\n"
+        + "ntV69ATww9HzMcoiXZhCULN3LlNZJSG6cp3iC4M1n0aI5Lo1D195hn0NDWTMLyee\n"
+        + "7uMuRHbM1PTaxhSS44JlSS1CFU2zNSVq90gWO9OhmjdE9x1v8TeRNPLZH3GLz0Be\n"
+        + "NZl/eVIBl7eKCQuOHCx367dFHHSm04nKbvNiXll1RmThCtLsU8jrMQGmNrBtLj+v\n"
+        + "q4Rbz0/dACRqcMiuJ0gBk/xrAtlTe5I+0zA8gmlqjQARAQABAAf+JNVkZOcGYaQm\n"
+        + "eI3BMMaBxuCjaMG3ec+p3iFKaR0VHKTIgneXSkQXA+nfGTUT4DpjAznN2GLYH6D+\n"
+        + "6i7MCGPm9NT4C7KUcHJoltTLjrlf7vVyNHEhRCZO/pBh9+2mpO6xh799x+wj88u5\n"
+        + "XAqlah50OjJFkjfk70VsrPWqWvgwLejkaQpGbE+pdL+vjy+ol5FHzidzmJvsXDR1\n"
+        + "I1as0vBu5g2XPpexyVanmHJglZdZX07OPYQBhxQKuPXT/2/IRnXsXEpitk4IyJT0\n"
+        + "U5D/iedEUldhBByep1lBcJnAap0CP7iuu2CYhRp6V2wVvdweNPng5Eo7f7LNyjnX\n"
+        + "UMAeaeCjAQQA1A0iKtg3Grxc9+lpFl1znc2/kO3p6ixM13uUvci+yGFNJJninnxo\n"
+        + "99KXEzqqVD0zerjiyyegQmzpITE/+hFIOJZInxEH08WQwZstV/KYeRSJkXf0Um48\n"
+        + "E+Zrh8fpJVW1w3ZCw9Ee2yE6fEhAA4w66+50pM+vBXanWOrG1HDrkxEEANkHc2Rz\n"
+        + "YJsO4v63xo/7/njLSQ31miOglb99ACKBA0Yl/jvj2KqLcomKILqvK3DKP+BHNq86\n"
+        + "LUBUglyKjKuj0wkSWT0tCnfgLzysUpowcoyFhJ36KzAz8hjqIn3TQpMF21HvkZdG\n"
+        + "Mtkcyhu5UDvbfOuWOBaKIeNQWCWv1rNzMme9A/9zU1+esEhKwGWEqa3/B/Te/xQh\n"
+        + "alk180n74sTZid6lXD8o8cEei0CUq7zBSV0P8v6kk8PP9/XyLRl3Rqa95fESUWrL\n"
+        + "xD6TBY1JlHBZS+N6rN/7Ilf5EXSELmnbDFsVxkNGp4elKxajvZxC6uEWYBu62AYy\n"
+        + "wS0dj8mZR3faCEps90YXiQEfBBgBAgAJBQJC7kcAAhsMAAoJEAAa34t4v31+qkcH\n"
+        + "/A5eF0w22B5VbfxsuTxiFwQHbcVbp8OMWMElv2b/KpFOHkIh4bZubb2h9Pg9NqY9\n"
+        + "vwa35i4MudBDcraNRFqTE0NxjCK6u9H+gdV3GwFFTs/HtQ95mTcEblWcglFPX3x0\n"
+        + "XsxYUn7HnAds1fAzwMO9TBONcsf9OUaXVciF5JMcJh30+trh6MShySSsNpmU8JB1\n"
+        + "PBj4/sraJ5vm2yo2kmRB4GuCUXZt6Zj8K1Aecz2rBrpkmqTJg1EXWeRJiffoE+42\n"
+        + "nK1jDUJpBX0Zt4+6A8e2yEQIvADXrRDOohJBRwmUpe21SpQaeg7R3Q/LNlPfrvqZ\n"
+        + "DCYWh5sxH28AIB4eO8PEPgU=\n"
+        + "=cSfw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * Key revoked by an expired key, before that key's expiration.
+   * <p>
+   * Revoked by {@link #expiredKey()}.
+   *
+   * <pre>
+   * pub   2048R/C43BF2E1 2005-08-01 [revoked: 2005-08-01]
+   *       Key fingerprint = 916D 6AD6 36A5 CBA6 B5A6  7274 6040 8661 C43B F2E1
+   * uid                  Testuser Nine &lt;test9@example.com&gt;
+   * </pre>
+   */
+  public static TestKey keyRevokedByExpiredKeyBeforeExpiration() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBELuRwABCADnf2z5dqp3BMFlpd6iUs5dhROrslfzswak1LmbGirK2IPIl4NX\n"
+        + "arAi76xXK9BcF/Cqcj/X/WqFKBd/qMGxwdvwbSN6PVBP6T1jvuVgrPTjd4x5xPUD\n"
+        + "xZ5VPy9hgQXs+1mugTkHYVTU8GI1eGpZ8Oj3PJIgVyqGxGkjWmcz5APbVIRan6L1\n"
+        + "482bZTidH9Nd9YnYlXNgiJcaOPAVBwO/j/myocQCIohvIo4IT8vc/ODhRgfwA0gD\n"
+        + "GVK+tXwT4f4x3qjG/YRpOOZZjBS09B/gJ9QfEnR6WNxg/Tm3T0uipoISOhR+cP/V\n"
+        + "e5o/73SM+w+WlILk/xpbbOfyCxD4Q3lb8EZFABEBAAGJAS0EIAECABcFAkLuYyAQ\n"
+        + "HQN0ZXN0OSBub3QgdXNlZAAKCRA87HgbF94azV2BB/9Rc1j3XOxKbDyUFAORAGnE\n"
+        + "ezQtpOmQhaSUhFC35GFOdTg4eX53FTFSXLJQleTVzvE+eVkQI5tvUZ+SqHoyjnhU\n"
+        + "DpWlmfRUQy4GTUjUTkpFOK07TVTjhUQwaAxN13UZgByopVKc7hLf+uh1xkRJIqAJ\n"
+        + "Tx6LIFZiSIGwStDO6TJlhl1e8h45J3rAV4N+DsGpMy9S4uYOU7erJDupdXK739/l\n"
+        + "VBsP2SeT85iuAv+4A9Jq3+iq+cjK9q3QZCw1O6iI2v3seAWCI6HH3tVw4THr+M6T\n"
+        + "EdTGmyESjdAl+f7/uK0QNfqIMpvUf+AvMakrLi7WOeDs8mpUIjonpeQVLfz6I0Zo\n"
+        + "iQE3BB8BAgAhBQJC7lUQFwyAAR2e63ndOLBJk52crzzseBsX3hrNAgcAAAoJEGBA\n"
+        + "hmHEO/LhHjUH/R/7+iNBLAfKYbpprkWy/8eXVEJhxfh6DI/ppsKLIA+687gX74R9\n"
+        + "6CM5k6fZDjeND26ZEA0rDZmYrbnGUfsu55aeM0/+jiSOZJ2uTlrLXiHMurbNY0pT\n"
+        + "xv215muhumPBzuL1jsAK2Kc/4oE7Z46jaStsPCvDOcx9PW76wR8/uCPvHVz5H/A7\n"
+        + "3erXAloC43jupXwZB32VZq8L0kZNVfuEsjHUcu3GUoZdGfTb4/Qq5a1FK+CGhwWC\n"
+        + "OwpUWZEIUImwUv4FNE4iNFYEHaHLU9fotmIxIkH8TC4NcO+GvkEyMyJ6NVkBBDP2\n"
+        + "EarncWAJxDBlx1CO4ET+/ULvzDnAcYuTc6G0IVRlc3R1c2VyIE5pbmUgPHRlc3Q5\n"
+        + "QGV4YW1wbGUuY29tPokBOAQTAQIAIgUCQu5HAAIbAwYLCQgHAwIGFQgCCQoLBBYC\n"
+        + "AwECHgECF4AACgkQYECGYcQ78uG78ggA1TjeOZtaXjXNG8Bx2sl4W+ypylWWB6yc\n"
+        + "IeR0suLhVlisZ33yOtV4MsvZw0TJNyYmFXiskPTyOcP8RJjS+a41IHc33i13MUnN\n"
+        + "RI5cqhqsWRhf9chlm7XqXtqv57IjojG9vgSUeZdXSTMdHIDDHAjJ/ryBXflzprSw\n"
+        + "2Sab8OXjLkyo9z6ZytFyfXSc8TNiWU6Duollh/bWIsgPETIe2wGn8LcFiVMfPpsI\n"
+        + "RhkphOdTJb+W/zQwLHUcS22A4xsJtBxIXTH/QSG3lAaw8IRbl25EIpaEAF+gExCr\n"
+        + "QM0haAVMmGgYYWpMHXrDhB7ff3kAiqD2qmhSySA6NLmTO+6qGPYJg7kBDQRC7kcA\n"
+        + "AQgA2wqE3DypQhTcYl26dXc9DZzABRQa6KFRqQbhmUBz95cQpAamQjrwOyl2fg84\n"
+        + "b9o9t+DuZcdLzLF/gPVSznOcNUV9mJNdLAxBPPOMUrP/+Snb83FkNpCscrXhIqSf\n"
+        + "BU5D+FOb3bEI2WTJ7lLe8oCrWPE3JIDVCrpAWgZk9puAk1Z7ZFaHsS6ezsZP0YIM\n"
+        + "qTWdoX0zHMPMnr9GG08c0mniXtvfcgtOCeIRU4WZws28sGYCoLeQXsHVDal+gcLp\n"
+        + "1enPh6dfEWBJuhhBBajzm53fzV2a7khEdffggVVylHPLpvms2nIqoearDQtVNpSK\n"
+        + "uhNiykJSMIUn/Y6g5LMySmL+MwARAQABiQEfBBgBAgAJBQJC7kcAAhsMAAoJEGBA\n"
+        + "hmHEO/LhdwcH/0wAxT1NGaR2boMjpTouVUcnEcEzHc0dSwuu+06mLRggSdAfBC8C\n"
+        + "9fdlAYHQ5tp1sRuPwLfQZjo8wLxJ+wLASnIPLaGrtpEHkIKvDwHqwkOXvXeGD/Bh\n"
+        + "40NbJUa7Ec3Jpo+FPFlM8hDsUyHf8IhUAdRd4d+znOVEaZ6S7c1RrtoVTUqzi59n\n"
+        + "nC6ZewL/Jp+znKZlMTM3X1onAGhd+/XdrS52LM8pE3xRjbTLTYWcjnjyLbm0yoO8\n"
+        + "G3yCfIibAaII4a/jGON2X9ZUwaFNIqJ4iIc8Nme86rD/flXsu6Zv+NXVQWylrIG/\n"
+        + "REW68wsnWjwTtrPG8bqo6cCsOzqGYVt81eU=\n"
+        + "=FnZg\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBELuRwABCADnf2z5dqp3BMFlpd6iUs5dhROrslfzswak1LmbGirK2IPIl4NX\n"
+        + "arAi76xXK9BcF/Cqcj/X/WqFKBd/qMGxwdvwbSN6PVBP6T1jvuVgrPTjd4x5xPUD\n"
+        + "xZ5VPy9hgQXs+1mugTkHYVTU8GI1eGpZ8Oj3PJIgVyqGxGkjWmcz5APbVIRan6L1\n"
+        + "482bZTidH9Nd9YnYlXNgiJcaOPAVBwO/j/myocQCIohvIo4IT8vc/ODhRgfwA0gD\n"
+        + "GVK+tXwT4f4x3qjG/YRpOOZZjBS09B/gJ9QfEnR6WNxg/Tm3T0uipoISOhR+cP/V\n"
+        + "e5o/73SM+w+WlILk/xpbbOfyCxD4Q3lb8EZFABEBAAEAB/9GTcWLkUU9tf0B4LjX\n"
+        + "NSyk7ChIKXZadVEcN9pSR0Udq1mCTrk9kBID2iPNqWmyvjaBnQbUkoqJ+93/EAIa\n"
+        + "+NPRlWOD2SEN07ioFS5WCNCqUAEibfU2+woVu4WpJ+TjzoWy4F2wZxe7P3Gj6Xjq\n"
+        + "7aXih8uc9Lveh8GiUe8rrCCbt+BH1RzuV/khZw+2ZDPMCx7yfcfKobc3NWx75WLh\n"
+        + "pki512fawSC6eJHRI50ilPrqAmmhcccfwPji9P+oPj2S6wlhe5kp3R5yU85fWy3b\n"
+        + "C8AtLTfZIn4v6NAtBaurGEjRjzeNEGMJHxnRPWvFc4iD+xvPg6SNPJM/bbTE+yZ3\n"
+        + "16W1BADxjAQLMuGpemaVmOpZ3K02hcNjwniEK2QPp11BnfoQCIwegON+sUD/6AuZ\n"
+        + "S1vOVvS3//eGbPaMM45FK/SQAVHpC9IOL4Tql0C8B6csRhFL824yPfc3WDb4kayQ\n"
+        + "T5oLjlJ0W2r7tWcBcREEzZT6gNi4KI7C4oFF6tU9lsQJuQyAbwQA9Vl6VW/7oG0W\n"
+        + "CC+lcHJc+4rxUB3yak7d4mEccTNb+crOBRH/7dKZOe7A6Fz+ra++MmucDUzsAx0K\n"
+        + "MGT9Xoi5+CBBaNr+Y2lB9fF20N7eRNzQ3Xrz2OPl4cmU4gfECTZ1vZaKlmB+Vt8C\n"
+        + "E/nn49QGRI+BNBOdW+2aEpPoENczFosEAJXi5Cn2l0jOswDD7FU2PER1wfVY629i\n"
+        + "bICunudOSo64GKQslKkQWktc57DgdOQnH15qW1nVO7Z4H0GBxjSTRCu7Z7q08/qM\n"
+        + "ueWIvJ85HcFhOCl+vITOn0fZV0p8/IwsWz8G9h5bb2QgMAwDSdhnLuK/cXaGM09w\n"
+        + "n6k8O2rCvDtXRjqJATcEHwECACEFAkLuVRAXDIABHZ7red04sEmTnZyvPOx4Gxfe\n"
+        + "Gs0CBwAACgkQYECGYcQ78uEeNQf9H/v6I0EsB8phummuRbL/x5dUQmHF+HoMj+mm\n"
+        + "wosgD7rzuBfvhH3oIzmTp9kON40PbpkQDSsNmZitucZR+y7nlp4zT/6OJI5kna5O\n"
+        + "WsteIcy6ts1jSlPG/bXma6G6Y8HO4vWOwArYpz/igTtnjqNpK2w8K8M5zH09bvrB\n"
+        + "Hz+4I+8dXPkf8Dvd6tcCWgLjeO6lfBkHfZVmrwvSRk1V+4SyMdRy7cZShl0Z9Nvj\n"
+        + "9CrlrUUr4IaHBYI7ClRZkQhQibBS/gU0TiI0VgQdoctT1+i2YjEiQfxMLg1w74a+\n"
+        + "QTIzIno1WQEEM/YRqudxYAnEMGXHUI7gRP79Qu/MOcBxi5NzobQhVGVzdHVzZXIg\n"
+        + "TmluZSA8dGVzdDlAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJC7kcAAhsDBgsJCAcD\n"
+        + "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBgQIZhxDvy4bvyCADVON45m1peNc0bwHHa\n"
+        + "yXhb7KnKVZYHrJwh5HSy4uFWWKxnffI61Xgyy9nDRMk3JiYVeKyQ9PI5w/xEmNL5\n"
+        + "rjUgdzfeLXcxSc1EjlyqGqxZGF/1yGWbtepe2q/nsiOiMb2+BJR5l1dJMx0cgMMc\n"
+        + "CMn+vIFd+XOmtLDZJpvw5eMuTKj3PpnK0XJ9dJzxM2JZToO6iWWH9tYiyA8RMh7b\n"
+        + "AafwtwWJUx8+mwhGGSmE51Mlv5b/NDAsdRxLbYDjGwm0HEhdMf9BIbeUBrDwhFuX\n"
+        + "bkQiloQAX6ATEKtAzSFoBUyYaBhhakwdesOEHt9/eQCKoPaqaFLJIDo0uZM77qoY\n"
+        + "9gmDnQOYBELuRwABCADbCoTcPKlCFNxiXbp1dz0NnMAFFBrooVGpBuGZQHP3lxCk\n"
+        + "BqZCOvA7KXZ+Dzhv2j234O5lx0vMsX+A9VLOc5w1RX2Yk10sDEE884xSs//5Kdvz\n"
+        + "cWQ2kKxyteEipJ8FTkP4U5vdsQjZZMnuUt7ygKtY8TckgNUKukBaBmT2m4CTVntk\n"
+        + "VoexLp7Oxk/RggypNZ2hfTMcw8yev0YbTxzSaeJe299yC04J4hFThZnCzbywZgKg\n"
+        + "t5BewdUNqX6BwunV6c+Hp18RYEm6GEEFqPObnd/NXZruSER19+CBVXKUc8um+aza\n"
+        + "ciqh5qsNC1U2lIq6E2LKQlIwhSf9jqDkszJKYv4zABEBAAEAB/0c76POOw6aazUT\n"
+        + "TZHUnhQ+WHHJefbKuoeWI7w+dD7y+02NzaRoZW7XnJ+fAZW8Dlb5k/O1FayUIEgE\n"
+        + "GjnT336dpE4g5NQkfdifG7Fy5NKGRkWx6viJI3g/OHsYX3+ebNDFMmO0gq7067/9\n"
+        + "WuHsTpvUMRwkF1zi1j4AETjZ7IBXdjuSCSu8OhEwr3d+WXibEmY5ec/d24l/APJx\n"
+        + "c3RMHw9PiDQeAKrByS6N10/yFgRpnouVx3wC7zFmhVewNV476Nyg34OvRoc+lCtk\n"
+        + "ixKdua6KuUJzGRWxgw+q2JD4goXxe0v2qU2KSU63gOYi0kg9tpwpn98lDNQykgmJ\n"
+        + "aQYdNIZJBADdlbkg9qbH1DREs7UF4jXN/SoYRbTh9639GfA4zkbfPmh/RmVIIEKd\n"
+        + "QN7qWK/Xy1bUS9vDzRfFgmoYGtqMmygOOFsVtfm8Y18lSXopN/3vhtai+dn+04Ef\n"
+        + "dl1irmGvm3p7y9Jh3s6uYTEJok0MywA7qBHvgSTVtc1PcZc6j6Bz1QQA/Q+nqyZY\n"
+        + "fLimt4KVYO1y6kSHgEqzggLTxyfGMW5RplTA0V1zCwjM6S+QWNqRxVNdB9Kkzn+S\n"
+        + "YDKHLYs8lXO2zvf8Yk9M7glgqvT4rJ51Zn2rc6lg1YUwFBXup5idTsuZwtqkvvKJ\n"
+        + "eS7L3cSBCqJMRjk47Y3V8zkrrN/HcYmyFecD/A+HPf4eSweUS025Bb+eCk4gTHbR\n"
+        + "uwmnKq7npk2XY4m0A/QdYF9dEWlpadsAr+ZwNQB3f21nQgKG0BudfL4FmpeW9RMt\n"
+        + "35aSIaV7RkxYOt5HEvjFRvLbeL1YYaj+D0dvz8SP1AUPvpWIVlQ03OjRlPyrPW50\n"
+        + "LoqyP8PTb6svnHvmQseJAR8EGAECAAkFAkLuRwACGwwACgkQYECGYcQ78uF3Bwf/\n"
+        + "TADFPU0ZpHZugyOlOi5VRycRwTMdzR1LC677TqYtGCBJ0B8ELwL192UBgdDm2nWx\n"
+        + "G4/At9BmOjzAvEn7AsBKcg8toau2kQeQgq8PAerCQ5e9d4YP8GHjQ1slRrsRzcmm\n"
+        + "j4U8WUzyEOxTId/wiFQB1F3h37Oc5URpnpLtzVGu2hVNSrOLn2ecLpl7Av8mn7Oc\n"
+        + "pmUxMzdfWicAaF379d2tLnYszykTfFGNtMtNhZyOePItubTKg7wbfIJ8iJsBogjh\n"
+        + "r+MY43Zf1lTBoU0ioniIhzw2Z7zqsP9+Vey7pm/41dVBbKWsgb9ERbrzCydaPBO2\n"
+        + "s8bxuqjpwKw7OoZhW3zV5Q==\n"
+        + "=JxsF\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestTrustKeys.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestTrustKeys.java
new file mode 100644
index 0000000..55bb9c2
--- /dev/null
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestTrustKeys.java
@@ -0,0 +1,1047 @@
+// Copyright (C) 2015 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.gpg.testutil;
+
+/**
+ * Test keys specific to web-of-trust checks.
+ * <p>
+ * In the following diagrams, the notation <code>M---N</code> indicates N trusts
+ * M, and an 'x' indicates the key is expired.
+ * <p>
+ *
+ * <pre>
+ *  A---Bx
+ *   \
+ *    \---C---D
+ *         \
+ *          \---Ex
+ *
+ *  D and E trust C to be a valid introducer of depth 2.
+ *
+ * F---G---F, in a cycle.
+ *
+ * H---I---J, but J is only trusted to length 1.
+ * </pre>
+ */
+public class TestTrustKeys {
+  /**
+   * pub   2048R/9FD0D396 2010-08-29
+   *       Key fingerprint = E401 17FC 4BF4 17BD 8F93  DEB1 D25A D07A 9FD0 D396
+   * uid                  Testuser A &lt;testa@example.com&gt;
+   * sub   2048R/F5C099DB 2010-08-29
+   */
+  public static TestKey keyA() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6npoBCACp0vHePNPeLzm0HM35i70bRChyZXu/ARxOHZHNbh6hWJkq5saI\n"
+        + "zuzZoaqXAr3xZwHftTULlkgoJIt40x6VCT8EBnUHTkOqoHTsFnXg2kNuhFvmn0OX\n"
+        + "7RQFlR1SGoQG4fEy6t/GlOwEknpNSIMLkbDMP2FzEkLVWtlIe2hqqawVIgqzyO3k\n"
+        + "HaQxW8gWyyTx0qeSdSi4DyFZIzdyu/aZa7sj/MhO3DB3UwubW6yE+PMcAVrJD+0d\n"
+        + "EToMT7i8Erncc+xEzuXAoQUHaQfOXV4DG5qSgVpKaLxJ/ABWUri0eMPhj0cT4iDx\n"
+        + "eNTL7cZ4h72B1uJs8byDN74PHrypNiVE+IRHABEBAAG0HlRlc3R1c2VyIEEgPHRl\n"
+        + "c3RhQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqemgIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQ0lrQep/Q05ZxMAf+OoRzXWbGfv7kZb7xdrVyAUTAV4bU\n"
+        + "UvLoJZUIQ1ckPBcty2LUvY7l9efgp3c57nvTD6U98dVnsKfaW4PT0CRXlpl1IFyh\n"
+        + "kgbInFS5rO+cJMQn1KyC+FfiwyGNii630SwiHyWRG5+XQ6Iptx9JELwWUMCLJxFp\n"
+        + "B8DZQKlNnvdl+YUgEeQOkWTXfTSaBATdXHiZhskiumnTOGO24jSg8CrZc5O/n6fC\n"
+        + "CgEsAFWL7fnO0ii6EW1JH5btLHPxL9QI+5DJIypgOhGI1lqZW9KrpfmJ3w6N1Gek\n"
+        + "GBda98DmzxxxZ9iyq1cELAAiQMjkvws67cOs/hwXNn9YaK74dzhb49MLGIkBIAQQ\n"
+        + "AQIACgUCTHqf0QMFAXgACgkQV2Bph7AH1JCO/Qf+PBJqeWS7p32+K5r1cA7AeCB2\n"
+        + "pcHs78wLjnSxuimf0l+JItb9JQAKjzcdZTKVGkUivkq3zhsPCCtssgSav2wlG59F\n"
+        + "TaqtpGOxvGjc8TKWHW1TrPhV86wh0yUempKTMWfdZ0RAJVG3krAj60bzUsQNK41/\n"
+        + "0EZi4JI+sm/TRlwQcmEzdaGxhFSJqiJyaBWbPL8AQNA2iRyjMKNeGCrgapEl2IkW\n"
+        + "2ST+/yUPI/485LS0uU1+TLB+NhiJ6j5PoiVqYD+ul8WJ+cy1vvcp1GCQpbRv1yXY\n"
+        + "4GB1mw0JPIinVE1q+eKKQxN38zARPqyupiIuBQaqX9NCHCAdNtFc3kJQ7Nm83YkB\n"
+        + "IAQQAQIACgUCTHqkCwMFAXgACgkQZB8Rk9JP5GfGVQgArMBVQo3AD56p4g5A+DRA\n"
+        + "h0KdQMt4hs/dl+2GLAi+nK0wwuHrHvr9kcZNiQNMtu+YiwvxMpJ/JvXRwOp4wbEx\n"
+        + "6P6Uzp18R2sqbV4agnL5tXFZXfsa3OR2NLm56Ox1ReHnZtAcC6qa1nHqt9z2sTt1\n"
+        + "vh7IfK8GDU/3M3z4XBXPpmpZPAczqujuO/yshz84O6oc3noXfRUJRklbkhNC3WyS\n"
+        + "u5+3nupq4GwIYehQQpxBTD9xXj4hl3KfUnctg/MkgUGweEK3oZ22kObTLJttTP9t\n"
+        + "9q/hLkVyDtFhGorcsYbNZyupm3xhddzYovkReePwOO4WA7VeRqRdiYDU1UjIKvv4\n"
+        + "TrkBDQRMep6aAQgA3NQtBhS8yiEGN8rT4hGtuuprVd5jQVprLz4ImcI2+Gt71+CR\n"
+        + "gv/BZ0zzFp3VPjTGRusungJYkKKOGpEpERiqEG1X/ZyL7EzoyT+iKIMDsVJgmyDN\n"
+        + "cryHTejlKA8Z6GQ1hPlOIws22oLq5zQXxD9pzMDuabHl/s/bYlU5qXc7LhxdtrmT\n"
+        + "b2uBP9a+eneWKrz8OfgtS5m9DgqJ6Bjl0TvbeVJgKHX42pqzJlBTCn3hJjJosy8x\n"
+        + "4qTbqMraENnl9y+qynM7atoHX6TPWsD7vWtWvi+FA5OWGEe3rof8o/sJSj05DQUn\n"
+        + "i8mmSiCYW/tUklPPXOvPRP0GZ/GhBzIUtE3jBwARAQABiQEfBBgBAgAJBQJMep6a\n"
+        + "AhsMAAoJENJa0Hqf0NOW/gkH/jm9FL+S53NjrthdbNjffryhp7KhTmYAsRk3Hc3X\n"
+        + "4TBj3upecarJynpvsz5HlLi/OxDRR6L2yfjKk6/2iKAbV56mdnnu5xG3TG8++naL\n"
+        + "7n/s9TGBhgknb6+vGhSMZ/1dpQ6wkiyuEmgKJo8DzHAh3k3VATHiBeSD7fNSsgtK\n"
+        + "gzK0hi53IFRFDDPYiCca+SS6/pA2zF56JWGETiIa8rSHIQaK4hNJ38vgKOZM80vQ\n"
+        + "fp+CxvJkYY71Yc94oQByaQzrXod7xnukp5SXe/N3BYTFCWoaSTRUI/THRywWwKqa\n"
+        + "rUsttYrqs/EQSy0X3kZ7CAm04uzA8csNyxapEVRvJxbrt5I=\n"
+        + "=DAMW\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx6npoBCACp0vHePNPeLzm0HM35i70bRChyZXu/ARxOHZHNbh6hWJkq5saI\n"
+        + "zuzZoaqXAr3xZwHftTULlkgoJIt40x6VCT8EBnUHTkOqoHTsFnXg2kNuhFvmn0OX\n"
+        + "7RQFlR1SGoQG4fEy6t/GlOwEknpNSIMLkbDMP2FzEkLVWtlIe2hqqawVIgqzyO3k\n"
+        + "HaQxW8gWyyTx0qeSdSi4DyFZIzdyu/aZa7sj/MhO3DB3UwubW6yE+PMcAVrJD+0d\n"
+        + "EToMT7i8Erncc+xEzuXAoQUHaQfOXV4DG5qSgVpKaLxJ/ABWUri0eMPhj0cT4iDx\n"
+        + "eNTL7cZ4h72B1uJs8byDN74PHrypNiVE+IRHABEBAAEAB/9BbaG9Bz9zd0tqjrx2\n"
+        + "u/VQR3qz1FCQXtuqZu8RMC+B5zIf2si71clf8c7ZHnfSxWZt65Ez1SMYwDeyBdje\n"
+        + "/7B1Gw3Ekk00tFxHx0GEL2NSdZE4sbynkHIp0nD4/HlIc41rmh08E405F7wiAWFn\n"
+        + "uCpfDr47SNpR/A4BxHYOvi8r9pBxn/fXiHluqYROit0Z4tfKDCvQ47k+wqVD5nOt\n"
+        + "BEbHDfEwUMibgTuJ1qPyHf6HDlSdTQSfYV8QW1/UbHWus9QikfjGfLJpX0Rv3UG+\n"
+        + "WXHmowpRDVixj74UQCYXQ/AZi/OBlcS8PRY6EZV4RLyEWlZrdzKViNLOTUbJNHvA\n"
+        + "ZAQVBADQND7CIO6z4k8e9Z8Lf4iLWP9iIbH9R7ArTZr2mX1vkwp+sk0BNQurL/BQ\n"
+        + "jUHOJZnouwkc+C3pQi/JvGvAe1fLHPA0+NKe/tcuDXMk+L1HH6XmDgKtByac41AR\n"
+        + "txxqhaECNeK9OKXAXaEvenkGFMcqQV3QMiF2q5VlmFxSSXydEwQA0M8tCowz0iZF\n"
+        + "i3fGuuZDTN3Ut4u6Uf9FiLcR4ye2Aa5ppO8vlNjObNqpHz0UqdDjB+e3O/n7BUx3\n"
+        + "A5PRZNQvcMbhgr2U3zjWvFMHS3YuxbuIaZ1Vj69vpOAGkUc98v4i0/3Lk7Lijpto\n"
+        + "n40S0eCVo+eccHA4HRvS5XSdNGHVJn0EAMzfBt3DalOlHm+PrAiZdVdp5IfbJwJv\n"
+        + "xkyI++0p4VaYTZhOxjswTs6vgv30FBmHAlx1FzoUOKLaOhxPyLgamFd9YG+ab4DK\n"
+        + "chc4TxIj3kkx3/m6JufW8DWhKyAJNZ/MW+Iqop5pUIeTbOBlNyaflK+XxjkP71rP\n"
+        + "2gZx4pjYjK5EPDy0HlRlc3R1c2VyIEEgPHRlc3RhQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTHqemgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ0lrQep/Q\n"
+        + "05ZxMAf+OoRzXWbGfv7kZb7xdrVyAUTAV4bUUvLoJZUIQ1ckPBcty2LUvY7l9efg\n"
+        + "p3c57nvTD6U98dVnsKfaW4PT0CRXlpl1IFyhkgbInFS5rO+cJMQn1KyC+FfiwyGN\n"
+        + "ii630SwiHyWRG5+XQ6Iptx9JELwWUMCLJxFpB8DZQKlNnvdl+YUgEeQOkWTXfTSa\n"
+        + "BATdXHiZhskiumnTOGO24jSg8CrZc5O/n6fCCgEsAFWL7fnO0ii6EW1JH5btLHPx\n"
+        + "L9QI+5DJIypgOhGI1lqZW9KrpfmJ3w6N1GekGBda98DmzxxxZ9iyq1cELAAiQMjk\n"
+        + "vws67cOs/hwXNn9YaK74dzhb49MLGJ0DmARMep6aAQgA3NQtBhS8yiEGN8rT4hGt\n"
+        + "uuprVd5jQVprLz4ImcI2+Gt71+CRgv/BZ0zzFp3VPjTGRusungJYkKKOGpEpERiq\n"
+        + "EG1X/ZyL7EzoyT+iKIMDsVJgmyDNcryHTejlKA8Z6GQ1hPlOIws22oLq5zQXxD9p\n"
+        + "zMDuabHl/s/bYlU5qXc7LhxdtrmTb2uBP9a+eneWKrz8OfgtS5m9DgqJ6Bjl0Tvb\n"
+        + "eVJgKHX42pqzJlBTCn3hJjJosy8x4qTbqMraENnl9y+qynM7atoHX6TPWsD7vWtW\n"
+        + "vi+FA5OWGEe3rof8o/sJSj05DQUni8mmSiCYW/tUklPPXOvPRP0GZ/GhBzIUtE3j\n"
+        + "BwARAQABAAf+KQOPSS3Y0oHHsd0N9VLrPWgEf3JKZPzyI1gWKNiVdRYhbjrbS8VM\n"
+        + "mm8ERxMRY/hRSyKrCdXNtS87zVtgkThPfbWRPh0xL7YpFhenena63Ng78RPqlIDH\n"
+        + "cITs6r/DRBI4jnXvOTr/+R2Pm1llgKF2ePzsSt0rpmPcjyrdBsiKSUnLGxm4tGtW\n"
+        + "wVoEjy3+MRN2ULyTO8Pe4URKTtUkkb23iuQuJZy+k+SfH+H0/3oEb8ERRE3UXNG7\n"
+        + "BIbaj71nsx8+H8+x8ffRm1s5Unn86AJ418oEhxNzQk59NnrrlJ4HH9NNbjjzI3JE\n"
+        + "intSQKhFJsvMARdzX062yartQtnm1v6jwQQA65rpMMHCoh9pxvL6yagw3WjQLEPw\n"
+        + "vOGpD9ossBvcv/SfAe7SgJsx6J6X0IIW6EKIjyRhWTIfK/rVR0cmUFTGStib+y22\n"
+        + "BPcQmt/Oiw9rdUfOmDrnosPC0SB+19tKw1v1AfW5swpJnGBCkGz9UfX4Fr/eTS3e\n"
+        + "2KaMq+r1KALSUVkEAO/x0SWOiBRH3X1ETNE9nLTP6u2W3TAvrd+dXyP7JjXWZPB8\n"
+        + "NOwT7qidvUlhTbxdR7xWNI1W924Ywwgs43cAPGyq95pjdzhvi0Xxab7124UK+MS3\n"
+        + "V4WBvjOYYW8pkdMOydRLETXSkco2mDCRTiVKe3Zi7p+lKlVJj4xrFUPUnetfBADH\n"
+        + "EPwYeeZ8sQnW644J75eoph2e5KLRJaOy5GMPRLNmq+ODtJxdoIGpfQnEA35nSlze\n"
+        + "Ea+1UvLBlWyF+p08bNfnXHp3j5ugucAYbVEs4ptUwTB3vFt7eJ8rkx9GYcuBFiwm\n"
+        + "H47rg7QmS1mWDLyX6v2pI9brsb1SCgBL+oi9CyjypkjqiQEfBBgBAgAJBQJMep6a\n"
+        + "AhsMAAoJENJa0Hqf0NOW/gkH/jm9FL+S53NjrthdbNjffryhp7KhTmYAsRk3Hc3X\n"
+        + "4TBj3upecarJynpvsz5HlLi/OxDRR6L2yfjKk6/2iKAbV56mdnnu5xG3TG8++naL\n"
+        + "7n/s9TGBhgknb6+vGhSMZ/1dpQ6wkiyuEmgKJo8DzHAh3k3VATHiBeSD7fNSsgtK\n"
+        + "gzK0hi53IFRFDDPYiCca+SS6/pA2zF56JWGETiIa8rSHIQaK4hNJ38vgKOZM80vQ\n"
+        + "fp+CxvJkYY71Yc94oQByaQzrXod7xnukp5SXe/N3BYTFCWoaSTRUI/THRywWwKqa\n"
+        + "rUsttYrqs/EQSy0X3kZ7CAm04uzA8csNyxapEVRvJxbrt5I=\n"
+        + "=FLdD\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/B007D490 2010-08-29 [expired: 2011-08-29]
+   *       Key fingerprint = 355D 5B98 FECE 6199 83CD  C91D 5760 6987 B007 D490
+   * uid                  Testuser B &lt;testb@example.com&gt;
+   */
+  public static TestKey keyB() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6ntMBCADG7j/nuI+VvvNbPY9nnfLrAc3KTj0Z+DMxxUMYoZNLjTw1szQ0\n"
+        + "PuKKACiiSA9Oyj4R0aIhWdIR9iYxp6gQdje3yewzoqMwE+t5onYDpdX9QDFXyEzF\n"
+        + "UPWCjA7OSji1G6fyWakiYxKseqyRXOdHXI5TqMikBalmSpwwvmik0cfRGO+l6qvM\n"
+        + "mVJlcn6mkZB0d8WOPV8j8rFxmVSPn9SVP9L8HaFWv1uI9EY3zXbfNeDNgNeTWIMY\n"
+        + "75saINBA2LALBQ54u52GoSbaR8ukZYAjjkif3WIFI8B9xREwjUBLFy3E357aGyLZ\n"
+        + "jE8nsmPk4MDxDaeDNoSHJjcxtDWQJBub3u1zABEBAAG0HlRlc3R1c2VyIEIgPHRl\n"
+        + "c3RiQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqe0wIbAwUJAeEzgAYLCQgHAwIG\n"
+        + "FQgCCQoLBBYCAwECHgECF4AACgkQV2Bph7AH1JD0nQf/Vm+/Mvl99/y3Qw10S6et\n"
+        + "H6NYWDUeAKXe9mfXBJ39HdtlF50jZ5NzSwksAOSQtQZJ3tQQeElXB29cZDvAscva\n"
+        + "RiTtt+KUxDZSYbEHrC0EO7w0Wi5ltwaWdXnoitMOgPZ/grL7UpUbL8rB1evfLbhm\n"
+        + "AqC/6kgHuXeY/7EAzwU3o0wKbmfx1sh8AyQSi4unUwIDCV1RIAP0+ZfJSg0WwGoS\n"
+        + "JB5+lKajtIE6kMn9m8CWM66/zxSCY3XLcoXvjVxCYPwwgYSyje8dDxxOI+x7uj2I\n"
+        + "IjM5RHQ9hTsR7NQ9JUTFmpKZlcdah93NZLKJAFLUtOPjMa5d5t2O0ZOxZ5ftlhHp\n"
+        + "Q7kBDQRMep7TAQgAwOuLBXnACIsd879ld/vLcn8umpKV8MIUjrqOMjR0rNKpCUDw\n"
+        + "LxL4uVh3q/ksESHnQPPqxFYkgeA66SYrx4jwZjbZ5vv9BW99LHe8lSahqrJA9A9g\n"
+        + "5iw5hH+2ZWrGlu3P65UdQUJW+JaDx1IIBt3BbmdGDuKF/ESsy9qxEKq7tKqHI2JL\n"
+        + "Ed+6OIwWblU7ZogfiNpgZJ0lapxTe84mGsD0TowGTu5re/8wIJf1f2q4PuG+L9OZ\n"
+        + "0ZD5i9s1MAxdw4OD+705owPCQnqsr18nH9aUBHWJn9NCXb3jL7QGaId84Yq8SRlK\n"
+        + "wHSRtHLLJoowJ5fXw5UbZcUtRUergxFRwae87wARAQABiQElBBgBAgAPBQJMep7T\n"
+        + "AhsMBQkB4TOAAAoJEFdgaYewB9SQMbsH/iu1HY7OMJxd8EkfxairRNec/v9uEvYQ\n"
+        + "XqfEPw/Hihdef1TY8vB69ymAPd89e1PRDj1m+0/RivO045qFP7lbWMkjKeR9dXXe\n"
+        + "UzIEsTUJ1CNnA7C3fo11NBVg59E0d84bMKQx7n4AZkljgKFKghUb6OJZiWRdh+8W\n"
+        + "0I95JI2R7nMYw3L8/sSGxt+Vjhs9acB1DldbyYbJitYA4fhVZQH9zgeuhQqdCULQ\n"
+        + "ZexpkQqvG0o4iJKO4yeJNHdeM+NwH38wXfzydtEv6Dxz/YZSTwt08p97l6DQ//H7\n"
+        + "wek1LcqeX47YFa9Ftns8Y8fjh4S8Kyi1F6BhZKbsdDqg2hA+0AFv7LA=\n"
+        + "=tmW1\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx6ntMBCADG7j/nuI+VvvNbPY9nnfLrAc3KTj0Z+DMxxUMYoZNLjTw1szQ0\n"
+        + "PuKKACiiSA9Oyj4R0aIhWdIR9iYxp6gQdje3yewzoqMwE+t5onYDpdX9QDFXyEzF\n"
+        + "UPWCjA7OSji1G6fyWakiYxKseqyRXOdHXI5TqMikBalmSpwwvmik0cfRGO+l6qvM\n"
+        + "mVJlcn6mkZB0d8WOPV8j8rFxmVSPn9SVP9L8HaFWv1uI9EY3zXbfNeDNgNeTWIMY\n"
+        + "75saINBA2LALBQ54u52GoSbaR8ukZYAjjkif3WIFI8B9xREwjUBLFy3E357aGyLZ\n"
+        + "jE8nsmPk4MDxDaeDNoSHJjcxtDWQJBub3u1zABEBAAEAB/wPPV1Om92pc9F3jJsZ\n"
+        + "2F3YZxukLfjnA76tnMEWd/pYGrUhdV3AdY4r/aB0njSeApxdXRlLQ3L2cUxdGCJQ\n"
+        + "mzM1ies7IXCC/w5WaShwAG+zpmFL/5+cq3vDc9tb2Q/IasVOVFQYEE2el7SfW5Cp\n"
+        + "mjZFGR8V1wvdNvC0Q0IHrmfdECYSeftzZBEj7CcoGc2pF5zpCG0XQxq7K6cEeSf5\n"
+        + "TKf//UVHgyBCIso6mzgP5k6DGw2d64843CPhhlHEbirUu/wNnbm1SqJ5xFL2VatH\n"
+        + "w7ij4V/hbgnP0GQkbY5+p/PU74P7fx/Ee8D8mF2HmEKRy6ZQY/SAnrjsAURBYR5S\n"
+        + "GF5RBADfhOYEgseWr81lq6Y1oM4YQz+pXRIZk34BagOJsL767B7+uwhvmxBJKIOS\n"
+        + "nRIxfV8GlvT22hrbqsRRyusoIlo2ZUat94IMAL6Oqm6VFm71PT3z9+ukWK43FIXf\n"
+        + "Bsz4swSV001398e3jpSizI6fGW7LRxvnua+NPN+xJLmDVcsPvwQA49ajm48NorD9\n"
+        + "bIWG87+2ScNTVOnHKryR+/LrGWA0f3G6LUsHZPKHNBdFZ4yza2QtEKw95L3K9D4y\n"
+        + "jIeKGwSRYJPb5oh5tSge58pxwP88eI9J4dL+XF1nsG0vYF9B41+qG1TCsPyUJTp6\n"
+        + "ry7NAgWrbpsZpjB0yJ1kFva3iS/hD00EAMu66p1CtsosoDHhekvRZp8a3svd+8uf\n"
+        + "YEKkEKXZuNNmJJktJBSA2FK1RKl9bV8wuG0Pi1/k39egLO3QTjruWUbSggT+aibR\n"
+        + "RW3hU7G+Z5IBOU3p+kTFLat6+TBg0XhCjJ+Eq366nZy1QIfqTCixIaDwrutZd6DC\n"
+        + "BXOjdoG6ZvLcQia0HlRlc3R1c2VyIEIgPHRlc3RiQGV4YW1wbGUuY29tPokBPgQT\n"
+        + "AQIAKAUCTHqe0wIbAwUJAeEzgAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ\n"
+        + "V2Bph7AH1JD0nQf/Vm+/Mvl99/y3Qw10S6etH6NYWDUeAKXe9mfXBJ39HdtlF50j\n"
+        + "Z5NzSwksAOSQtQZJ3tQQeElXB29cZDvAscvaRiTtt+KUxDZSYbEHrC0EO7w0Wi5l\n"
+        + "twaWdXnoitMOgPZ/grL7UpUbL8rB1evfLbhmAqC/6kgHuXeY/7EAzwU3o0wKbmfx\n"
+        + "1sh8AyQSi4unUwIDCV1RIAP0+ZfJSg0WwGoSJB5+lKajtIE6kMn9m8CWM66/zxSC\n"
+        + "Y3XLcoXvjVxCYPwwgYSyje8dDxxOI+x7uj2IIjM5RHQ9hTsR7NQ9JUTFmpKZlcda\n"
+        + "h93NZLKJAFLUtOPjMa5d5t2O0ZOxZ5ftlhHpQ50DmARMep7TAQgAwOuLBXnACIsd\n"
+        + "879ld/vLcn8umpKV8MIUjrqOMjR0rNKpCUDwLxL4uVh3q/ksESHnQPPqxFYkgeA6\n"
+        + "6SYrx4jwZjbZ5vv9BW99LHe8lSahqrJA9A9g5iw5hH+2ZWrGlu3P65UdQUJW+JaD\n"
+        + "x1IIBt3BbmdGDuKF/ESsy9qxEKq7tKqHI2JLEd+6OIwWblU7ZogfiNpgZJ0lapxT\n"
+        + "e84mGsD0TowGTu5re/8wIJf1f2q4PuG+L9OZ0ZD5i9s1MAxdw4OD+705owPCQnqs\n"
+        + "r18nH9aUBHWJn9NCXb3jL7QGaId84Yq8SRlKwHSRtHLLJoowJ5fXw5UbZcUtRUer\n"
+        + "gxFRwae87wARAQABAAf8DAVBKsyswfuFGMB2vpSiVxaEnV3/2LoHFOOb45XwJSqV\n"
+        + "HL3+mThJ5iaUglMqw0CFC7+HA8fIS41grlFSDgNC02OcjS9rUxDg0En/pp17Gks0\n"
+        + "D+D7bSwZQ1+/yi7ug836lBe89GmBSMj8GgnK9T6RBGOL8nZ72b2ftK4CNWMmAfo4\n"
+        + "NZUy+rnnziV5WoYrkFZhl3dMMd3nITILBy9eYUoiKJl8O1b8amhrNkB/PEMAV7jc\n"
+        + "260XEQ9fgzMMe5/oT8pzIOGyrB+QO5rMu9pGVJ1qeMzTiZjjHXE2CEaEbvEk0F4l\n"
+        + "6w2gp5C6O5GoMpCOPwCy7dOYX5ETdO4Ppjnrob2XEQQAwus5q+EFoBVG8vfEf56x\n"
+        + "czkC15+0VcMe/IM8l/ur/oF1NUlAnPCq7WfgdELvGNszW7R+A625yXJJf7LJE/y/\n"
+        + "5GUGHAK60FUa0ElbVEn0A6kDcvll0dM6rKPQvFguaFpBKXre6k17cdOrf9hasfJk\n"
+        + "+lzaHlh9hJgoM30pAwG4+n8EAP1f+TEkEfVFo4Uy84eO6xVkYVndopDU1gCpfW1a\n"
+        + "84SA2PNjU3vkdIoFsEvOmf1xlfYeDYn37dikFPEZDsHBUzELDMewAXRgmVvnMJrj\n"
+        + "8Zq4FbEQSVjyz3qJOGk5V999qqoVMRXdnlQs5IXgZauPsnIqi5TRQZOMhbaiOVBO\n"
+        + "kqWRBAC9FhxypA3t9j1zGTFDppWmcBxpVzGGsgmzGO+WTVyk6szbZgTsf2+R+gTJ\n"
+        + "ZKVVzE6Mu+iZmPbrn/x7LWzKJuavRz0xSrvCYbIxYyheFz5LOPFHLF181h1g79gY\n"
+        + "E5Tz7uwu3jIldM7rY5RhxS6V5GGDVSfA+/Dsk6Iaujs6Hs7y30C0iQElBBgBAgAP\n"
+        + "BQJMep7TAhsMBQkB4TOAAAoJEFdgaYewB9SQMbsH/iu1HY7OMJxd8EkfxairRNec\n"
+        + "/v9uEvYQXqfEPw/Hihdef1TY8vB69ymAPd89e1PRDj1m+0/RivO045qFP7lbWMkj\n"
+        + "KeR9dXXeUzIEsTUJ1CNnA7C3fo11NBVg59E0d84bMKQx7n4AZkljgKFKghUb6OJZ\n"
+        + "iWRdh+8W0I95JI2R7nMYw3L8/sSGxt+Vjhs9acB1DldbyYbJitYA4fhVZQH9zgeu\n"
+        + "hQqdCULQZexpkQqvG0o4iJKO4yeJNHdeM+NwH38wXfzydtEv6Dxz/YZSTwt08p97\n"
+        + "l6DQ//H7wek1LcqeX47YFa9Ftns8Y8fjh4S8Kyi1F6BhZKbsdDqg2hA+0AFv7LA=\n"
+        + "=uFLT\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/D24FE467 2010-08-29
+   *       Key fingerprint = 6C21 10AC F4FC 1C7B F270  C00E 641F 1193 D24F E467
+   * uid                  Testuser C &lt;testc@example.com&gt;
+   * sub   2048R/DBECD4FA 2010-08-29
+   */
+  public static TestKey keyC() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6nuMBCADd077pyfsDGbGhHh+7xzipWihMJRrzQnpbSeVJIxA/Js+Z2MW8\n"
+        + "9J98AgnjONjGVlLqtp11O8Bp9xgdoGYWvFl2CrooQrCe+70JORHE30MJT+61mgLQ\n"
+        + "jm9l2WmIIcuzNwoTOKqWlXuaRIKddXMVbwr++Enl/9znx81FCf1KioDijeeHzVZb\n"
+        + "IjELLCtLlhwhGlYNy6LfNhSY+rNHOomIM9CUXkGZU7JvTe3M1plUzYYIFu3tttZI\n"
+        + "b6e1FSfR60yZ/f88fLacloc3fSrPWA261R/gHuFfLCdTt/I3EcYE+x33LZnSSOgz\n"
+        + "v/JtAuFlCaF/oNRTJHeRbALeri+FxBYule15ABEBAAG0HlRlc3R1c2VyIEMgPHRl\n"
+        + "c3RjQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqe4wIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQZB8Rk9JP5GcEIgf/cMvYBwH8ENrWec366Txaaeh/TO6n\n"
+        + "4v4P2LUR4/hcrNpHx3+9ikznkyF/b8OCsOE+KstvOO6i9vuRGVBPmfoALVv8iCGs\n"
+        + "5MXZJskjACXOqQav0I7ZY5rDJxuOKq6DrxtpHNxK8n0D1PEZllyk/OZVBAcjL2vu\n"
+        + "WC6ujP3jbMKaV0+heFqOVIghQjdA4McLH2u1XLOGEZdp7hLfmTnClmfzbnslFBSQ\n"
+        + "xU2g3jCq2k2zAPhn+jOGCL0987QGj1e6pHRXdUxcfnLRyNadRied0HO/clIb8vdt\n"
+        + "UaexujHjgg+1KDxj4PBAftN2lRtnnsSG9z4T31aTFz5YVG+pq8UXk9ohCokBIAQQ\n"
+        + "AQIACgUCTHqkKQMFAngACgkQqZHi1Q/dNnexiQf/ba9LcR76+tVvos1cxrGO3VkD\n"
+        + "3R1pvIWsb37/NTypWCvrFhsy4OUEy3bVCfJcqfwdY3Q2XixB9kuKo3qCSom1EjGg\n"
+        + "Qhr5ZsrB3qYqaa6S0AeVusmIwArEr9uuMUDjXhKlUALDX8HfXWGy2UmjNJkkT8Jm\n"
+        + "GtISS4KOfXUuZY04DttvbukEnyxAiLU9V0BnzrI9DARh0gEjqjUZAVyP5lOXJJxt\n"
+        + "sau95mOe8E61GELXPkxDLrnCboX7ys2OxcFO6S7q1xJPkki2SVq0y0k5oY/3jktw\n"
+        + "jO8uC3n7NiyW+BYJK6+zj3u3iA+o0YGm+i6F7aneJEaJrFqRj9L1vbojvuH0cYkB\n"
+        + "IAQQAQIACgUCTHqkOwMFAngACgkQOwm5f0tDh+7dSQf+PnEUftNSOuLVLoJ+2tyD\n"
+        + "DPJpcLIavNCyNR3hCGL86NXRUxOrmYgDVVv8pJuYB6aUTm69rFFZlzNwqQN5pBiX\n"
+        + "Zr3NM1jgJT6gKfXddcg1p/X2S9+xn4RN92R0fn0kEjM65fpE1Do+YWHOuHDZEOrx\n"
+        + "L8OaSo8lr19+r27fn09/HBhz2lOyTYzsdTjHeWdxPVQ3JNiVX11k7iKsttdYtM/V\n"
+        + "mAHzzd54Kvt5So/2qLIAcfSmUe9DQAdmcEcJQpQ2veND9uwccX7tH0cH4n9Cp16o\n"
+        + "quJ2pxWzOvKR3zxSw+cRxyIS4VjT6k+UsG3Lw55QZgdb5IEaJfezPj+tOhQlQz0f\n"
+        + "VrkBDQRMep7jAQgAw+67ahlOGnkF6mTtmg6MOGzAbRQ11MNrORnNtGOccNgtlgrO\n"
+        + "Y8TBqw1HkJ56v26E1FxfRh69CUGkYVXx0tMw0QbI+unX35ce5hJD4aWa8bOA1vfw\n"
+        + "474p/NpI+czWsFvcdOu5K6xIGXHShaQQyf2FQ9QeIFrU60qfaBL5jzuLyujCACqU\n"
+        + "46QGgBgeUjaT54LjrWSdn/Jtsbpv0MPv3Ea8fMdtSMkkBsDkF55jaJDFYq+xbs+e\n"
+        + "IKBjTwtSvrUisnLAC0Z9YY21GXGI3DGYqpVXz+Fe5xMTX1a6K3VKEmxmX2m/ebhm\n"
+        + "1p6EqjAJguOjJbJJQHKHMOol0zU6ANB6SgP26wARAQABiQEfBBgBAgAJBQJMep7j\n"
+        + "AhsMAAoJEGQfEZPST+Rn7AcH/32HACPLdxINsi8OSWa8OccMG5XEUvHTZjmdwVT2\n"
+        + "czMss8nwgifU9D4hEVRu1MWpiyxUgegW94wuSh4PWIVOVd18PmzAYc73aYgonakb\n"
+        + "M+MDIqGVvAH8QtHo79sqZ9vrkQaQXB3Y8cq+WxDQZyl8KLXP2icmq1Rl6Q6+i9oS\n"
+        + "pFe88Wr0cGaTblkfDbbWcih3C6tKAfcFwLLg8u4jYfXjZg/E9eAJf0dIFcQSQoHd\n"
+        + "O8hVXaZwx/rYXA8UFwAuROo2nu6SIof1lrH92p+now95d5zUZ5BYnKVd3uXsln0j\n"
+        + "z585UPQKS2J8PUy9IirmahgTyEYFwO64kZ2B4hYOE2g+rYw=\n"
+        + "=LtMR\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx6nuMBCADd077pyfsDGbGhHh+7xzipWihMJRrzQnpbSeVJIxA/Js+Z2MW8\n"
+        + "9J98AgnjONjGVlLqtp11O8Bp9xgdoGYWvFl2CrooQrCe+70JORHE30MJT+61mgLQ\n"
+        + "jm9l2WmIIcuzNwoTOKqWlXuaRIKddXMVbwr++Enl/9znx81FCf1KioDijeeHzVZb\n"
+        + "IjELLCtLlhwhGlYNy6LfNhSY+rNHOomIM9CUXkGZU7JvTe3M1plUzYYIFu3tttZI\n"
+        + "b6e1FSfR60yZ/f88fLacloc3fSrPWA261R/gHuFfLCdTt/I3EcYE+x33LZnSSOgz\n"
+        + "v/JtAuFlCaF/oNRTJHeRbALeri+FxBYule15ABEBAAEAB/sFPLoJDG1eV5QpqEZf\n"
+        + "m/QMOTOn8ZJ9xraQvXFvV7zgVXxJBvTLMbuACrnHnoiCrULS+w8Dt66Nfz7s4yQJ\n"
+        + "5SDtFX2AlMDVWL7wBEPgF1UpN6ox1CzSa6HOaygaUFGeKHO20WDjV4HmBLhQkKIa\n"
+        + "vKbghHA/4Nm1s1z3BHB8GtdGZ1VHc+s1DhPK5w+WHqYpLYjpNmI9yJg3gclEqEG9\n"
+        + "XzBqTZm9mPJRBdDMOD0xLa4nUD3Dkrjimqod3X7EuXE6sT2DuGVa1nuynk/8gIyO\n"
+        + "uS6crY7YJzEQUtQJ2n3y/h+QnZFo9UFuIVpgsxhBDsCnYNFWNR91Q0IM6PohHvqx\n"
+        + "BtFhBADsax1Bc0obP+bIkeAXltGlUYqm3bjOgVZ87XR0qe4TGwXGe8T1Yjfc8rj0\n"
+        + "cfBYCud201r/05CgchojMnTWlFLg308bSIZ9YvN3oOVay8nZ7h62dUIs45zebw3R\n"
+        + "SHwvjE5Sm/VWIdLrUUW1aGfk/VPudNMMMu2C64ev8DF/iwYjoQQA8DM+9oPvFJPA\n"
+        + "kLYg71tP2iIE5GbFqkiIEx59eQUxTsn6ubEfREjI99QliAdcKbyRHc3jc68NopLB\n"
+        + "41L7ny0j6VKuEszOYhhQ0qQK/jlI461aG14qHAylhuQTLrjpsUPE+WelBm9bxli0\n"
+        + "gA8F81WLOvJ2HzuMYVrj3tjGl3AHetkEAI77VKxGCGRzK63qBnmLwQEvqbphpgxH\n"
+        + "ANNAsg5HuWtDUgk85t2nrIgL1kfhu++CfP9duN/qU4dw/bgJaKOamWTfLBwST8qe\n"
+        + "3F8omovi1vLzHVpmvQp6Ly4wggJ4Gl/n0DNFopKw20V8ZTiRYtuLS43H7VsczE+8\n"
+        + "NKjy01EgHDMAP8O0HlRlc3R1c2VyIEMgPHRlc3RjQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTHqe4wIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQZB8Rk9JP\n"
+        + "5GcEIgf/cMvYBwH8ENrWec366Txaaeh/TO6n4v4P2LUR4/hcrNpHx3+9ikznkyF/\n"
+        + "b8OCsOE+KstvOO6i9vuRGVBPmfoALVv8iCGs5MXZJskjACXOqQav0I7ZY5rDJxuO\n"
+        + "Kq6DrxtpHNxK8n0D1PEZllyk/OZVBAcjL2vuWC6ujP3jbMKaV0+heFqOVIghQjdA\n"
+        + "4McLH2u1XLOGEZdp7hLfmTnClmfzbnslFBSQxU2g3jCq2k2zAPhn+jOGCL0987QG\n"
+        + "j1e6pHRXdUxcfnLRyNadRied0HO/clIb8vdtUaexujHjgg+1KDxj4PBAftN2lRtn\n"
+        + "nsSG9z4T31aTFz5YVG+pq8UXk9ohCp0DmARMep7jAQgAw+67ahlOGnkF6mTtmg6M\n"
+        + "OGzAbRQ11MNrORnNtGOccNgtlgrOY8TBqw1HkJ56v26E1FxfRh69CUGkYVXx0tMw\n"
+        + "0QbI+unX35ce5hJD4aWa8bOA1vfw474p/NpI+czWsFvcdOu5K6xIGXHShaQQyf2F\n"
+        + "Q9QeIFrU60qfaBL5jzuLyujCACqU46QGgBgeUjaT54LjrWSdn/Jtsbpv0MPv3Ea8\n"
+        + "fMdtSMkkBsDkF55jaJDFYq+xbs+eIKBjTwtSvrUisnLAC0Z9YY21GXGI3DGYqpVX\n"
+        + "z+Fe5xMTX1a6K3VKEmxmX2m/ebhm1p6EqjAJguOjJbJJQHKHMOol0zU6ANB6SgP2\n"
+        + "6wARAQABAAf9HIsMy8S/92SmE018vQgILrgjwursz1Vgq22HkBNALm2acSnwgzbz\n"
+        + "V8M+0mH5U9ClPSKae+aXzLS+s7IHi++u7uSO0YQmKgZ5PonD+ygFoyxumo0oOfqc\n"
+        + "DJ/oKFaforWJ2jv05S3bRbRVN5l9G0/5jWC7ZXnrXBOqQUkdCLFjXhMPq3zg2Yy3\n"
+        + "XSU83dVteOtrYRZqv33umZNCdk44z6kQOvh9tgSCL/aZ3d7AqjRK99I/IYY1IuVN\n"
+        + "qreFriVcJ0EzlnbPCnva+ReWAd2zt5VEClGu9J0CVnHmZNlwfmbFSiUN1hiMonkr\n"
+        + "sFImlw3adfJ7dsi/GzCC4147ep6jXw7QwQQAzwkeRWR9xc3ndrnXqUbQmgQkAD3D\n"
+        + "p2cwPygyLr0UDBDVX0z+8GKeBhNs3KIFXwUs6GxmDodHh0t4HUJeVLs7ur5ZATqo\n"
+        + "Bx50cSUOoaeSHRFVwicdJRtVgTTQ4UwwmKcLLJe2fWv6hnmyInK7Lp8ThLGQgqo8\n"
+        + "UWg3cdfzCvhKSvsEAPJFYhsFA/E92xUpzP8oYs3AA4mUXB+F0eObe9gqv8lAE6SX\n"
+        + "gB5kWhcd+MGddUGJuJV2LRrgOx3nXu3m3n35AH6iAY4Qi9URPzi/K659oefUU1c5\n"
+        + "BFArHX9bN1k1cOvH28tpQ38eAxaMygLqyR5Q5VbtZ5tYqLKCvHVs3I8lekDRA/4i\n"
+        + "e0vlu34qenppPANPm+Vq/7cSlG3XY4ioxwC/j6Y+92u90DXbbGatOg1SqGSwn1VP\n"
+        + "S034m7bDCNoWOXL0yAcbXrLZV74AyfvVOYOs/WtehehzWeTQRT5lkxX5+xGc1/h6\n"
+        + "9HQvsKKnUK8n1oc5aM5xzRVkU9+kcmqYqXqyOHnIbDbPiQEfBBgBAgAJBQJMep7j\n"
+        + "AhsMAAoJEGQfEZPST+Rn7AcH/32HACPLdxINsi8OSWa8OccMG5XEUvHTZjmdwVT2\n"
+        + "czMss8nwgifU9D4hEVRu1MWpiyxUgegW94wuSh4PWIVOVd18PmzAYc73aYgonakb\n"
+        + "M+MDIqGVvAH8QtHo79sqZ9vrkQaQXB3Y8cq+WxDQZyl8KLXP2icmq1Rl6Q6+i9oS\n"
+        + "pFe88Wr0cGaTblkfDbbWcih3C6tKAfcFwLLg8u4jYfXjZg/E9eAJf0dIFcQSQoHd\n"
+        + "O8hVXaZwx/rYXA8UFwAuROo2nu6SIof1lrH92p+now95d5zUZ5BYnKVd3uXsln0j\n"
+        + "z585UPQKS2J8PUy9IirmahgTyEYFwO64kZ2B4hYOE2g+rYw=\n"
+        + "=5pIh\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/0FDD3677 2010-08-29
+   *       Key fingerprint = C96C 5E9D 669C 448A D1B9  BEB5 A991 E2D5 0FDD 3677
+   * uid                  Testuser D &lt;testd@example.com&gt;
+   * sub   2048R/CAB81AE0 2010-08-29
+   */
+  public static TestKey keyD() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C\n"
+        + "jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg\n"
+        + "DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO\n"
+        + "cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784\n"
+        + "9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx\n"
+        + "JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAG0HlRlc3R1c2VyIEQgPHRl\n"
+        + "c3RkQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQqZHi1Q/dNne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGq\n"
+        + "IDPhZFtPn0p2IAkqr5sAhvZAjd3u9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16\n"
+        + "aBK2ADq2YgPEmTToots1A0Tj+LaCFOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vY\n"
+        + "I/LtvThAk28D8yIfDnW49Mc4GGq+qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7\n"
+        + "Qw70Kqysaoy1KiPRAgwiPQfMCEx6pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhgu\n"
+        + "Q3Qe7xQlAtVObxskcTH2CWggl2dPqSMNieLK0g/ER8PIReGDCBXNSJ4qYbkBDQRM\n"
+        + "ep8JAQgAw/o1nhJPLGlIfEMzOGU0Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJq\n"
+        + "jSo7e9XC9jA2ih0+Gld0vWV7S0LZ84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWX\n"
+        + "QmY76hHIaF8rs6aJB7lRig735VRLxVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsT\n"
+        + "GRHgmydaxZbGXz+Z57jbQgm11CQEHX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNi\n"
+        + "xXHxryH2Jd34pA0cGHYVcTgVjXuZ9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN\n"
+        + "5Pxy5ocR7R2ZoN0pYD5+Cc7oGHjuCQARAQABiQEfBBgBAgAJBQJMep8JAhsMAAoJ\n"
+        + "EKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0KrausBHH161j\n"
+        + "lraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg9a2LWb4z\n"
+        + "rvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayboePRXdfr\n"
+        + "8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5QUig+c3oG\n"
+        + "a5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4C58w0Uvp\n"
+        + "HZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=\n"
+        + "=YDhQ\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx6nwkBCADuztv2tGhjPljwW46qEhth7ZnkdhYXuctZ6lNQuy5LMaEECE3C\n"
+        + "jvVKY+nBrgsLY2Trts+q+mdooBWvxy/qe5PAQTcPR83KjVS4fYwNMBgeRxBEZAZg\n"
+        + "DFwRRCsRrHost+cMgtzLocQ+vL3+9yTRAIe/WmYwbEDXg/c9JSC7kQbZqaAaOshO\n"
+        + "cIOyeB8/QoYee0fEnBzHMmcd0SB1YpwIvRG6v61lXmgpQ9CbovvXO6ZZyEyCX784\n"
+        + "9xprzqP1y03DPrbhuhBAY8EMf3KGJA1dEcU4+lbGEgmlOe2YSbWoLs7mRLFcq5xx\n"
+        + "JroYMtvXF04k4ZHNZAnT3IZc+lJyCqOp4vXpABEBAAEAB/0Yf+FiLHz/HYDbW9FF\n"
+        + "kmj7wXgFz7WRho6dsWQNxr5HmZZWxxFPMgJpONnc9GGOsApFAnLIrDraqX3AFFPO\n"
+        + "nxH36djfuPKcYqZ77Olm2vXGeWzqT0a2KN5zKQawH/1CxDUwe+Zx/60V8KAfXbSJ\n"
+        + "up+ymnAcbKa0VYYSYFI82/KTdthJ1jFMNtXkaLskpM8TrDBCgd38m8Dpb5GCrDVY\n"
+        + "faZgkHokTTrvaTcx7ebGOxlOcbfzOPMJyFiz6lHf4JGr5ZVQXymaAG18kRDFxXHm\n"
+        + "AskOJIxnMdcy2IzNximht2CIgRuGznyPoeh/j8KFONKIKf3N6dVfV12uIvGOVV+D\n"
+        + "/ZQZBAD2dennp3Z4IsOWkgHTG3bloOVcIY5n+WvliQY/5G3psKdKeaGZxt6MhMSj\n"
+        + "sJEiUgveYTt5PxvQc5jmFEyjEQJmDAHo3RbycdFVvICrKIhKFyIlcVFCOSwDvLAW\n"
+        + "aZhu/m47jGnnYZ+bDzZl4X8L7Zu8e3TStEiVhjYTRqJfdEdMVQQA+A0ehIhIa1mJ\n"
+        + "ytGKWQVxn9BwKTP583vf2qPzul7yDEsYdGfoA0QGUicVwV4NNK3vK3FQM9MBSevp\n"
+        + "JFpxh2bRS/tgd5tFDyRqekTcagMqTxnJoIpCPUvj5D+WXsS1Kwrcm7OpWoNHOcjD\n"
+        + "Hbhk/966QALO+T6BTVLx32/72jtQ10UD/RsqQfRDzlQUOd6ZYOlH5qCb1+f8f3qJ\n"
+        + "yUmudrmjj8unBK3QbBVrxZ1h9AyaI5evFmsMlLKdTp0y49CmrSQmgEnUYzvBDjse\n"
+        + "/jYanpRKnt69HeZFilHLIF+HBbQfSM66UVXVoJSNTJIsncVa0IcGoZTpCUVOng3/\n"
+        + "MLfW4sh9NX1yRIi0HlRlc3R1c2VyIEQgPHRlc3RkQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTHqfCQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQqZHi1Q/d\n"
+        + "Nne/0wgApuPzh4J8p2quCK1ScsJHlgGRojGqIDPhZFtPn0p2IAkqr5sAhvZAjd3u\n"
+        + "9A2DqQ7pwOX7gnGRE7dSrK69IAjfbRMc5k16aBK2ADq2YgPEmTToots1A0Tj+LaC\n"
+        + "FOXYUtEkgAC+RfFIkCdt8z86GIr0kg19Q/vYI/LtvThAk28D8yIfDnW49Mc4GGq+\n"
+        + "qvrOytBaGu3dzW0mjYWGEyl0fdSjNqtKyWN7Qw70Kqysaoy1KiPRAgwiPQfMCEx6\n"
+        + "pVaXuAfgRKaJ18kCNOldpajLgQv6yeY7mhguQ3Qe7xQlAtVObxskcTH2CWggl2dP\n"
+        + "qSMNieLK0g/ER8PIReGDCBXNSJ4qYZ0DmARMep8JAQgAw/o1nhJPLGlIfEMzOGU0\n"
+        + "Jjj+DwEyB3QIEEc+WKRvgtGsJ4cbZdaGWBJqjSo7e9XC9jA2ih0+Gld0vWV7S0LZ\n"
+        + "84xXxQeadC+AZBFR+b9ga4aUFIji8Tdi2dWXQmY76hHIaF8rs6aJB7lRig735VRL\n"
+        + "xVHOb194t9KLUzZiEKqd71BvLQyuLqAfTEsTGRHgmydaxZbGXz+Z57jbQgm11CQE\n"
+        + "HX1dtS8uqWb64xrV5GAeuEhRj4R6Yiy7OPNixXHxryH2Jd34pA0cGHYVcTgVjXuZ\n"
+        + "9FFP2SnXuxABONGAIaJuqg7ozYBa2kOdr0DN5Pxy5ocR7R2ZoN0pYD5+Cc7oGHju\n"
+        + "CQARAQABAAf/QiN/k9y+/pB7h4BQWXCCNIIYb6zqGuzUSdYZWuYHwiEL1f05SFmp\n"
+        + "VjDE5+ZAU+8U0Gv+BAeRbWdlfQOyI/ioQJL1DggeXqanUF4uCbjGDBPLhtCZsmmM\n"
+        + "QVLdrOl+v+SHe33e7E7AQSyQMaUSkUEtHycYIasZPQRfw9H/L3u9OEWXkMUbPso5\n"
+        + "L0A0StkcsM1isYfC8ApnF4zSTWHO9uqnc+qE4qChCqsGvaSIyLKEpVe4F0vEkbrq\n"
+        + "3usVp3cxJd9apN+JjMoC9dHJcQahgfJZ1jzgJ3rueRxrGZV+keo8VmyrDGFCerX9\n"
+        + "6Ke3RPMHN/evCHyPMtHC82QKYuy4ZTvldwQAyzbNKIIpNjyHRc/hXLMBUtnW0VYS\n"
+        + "dELA1VBMmT/d6Xx6pI9gg9HCjDx+DuQRych7ShxrYLL1pNQD8jwEJhZIeUpSgIFD\n"
+        + "BXdwkiGbmdrU5N0tBhxp8kRcqcGbL68zC9S0X2hNju6Dxu9hbG8ZAdYaCdAavVy0\n"
+        + "O6E66+T0cLRBinsEAPbiL/0rpV15DdITwD3hvzhYDyURE+yxQZe9ngS1uoui3mGn\n"
+        + "bLc/L/nbHf2Z91ViSsUaqJjpb2/eDsJtGJ9pFlFLTndujkA62CktJytD9DIYLlYD\n"
+        + "huXlsKvZkNZEZNDKLC5Tg8YR/28Opz0/ZFzfVuJAQqg7+iWkxklG3SvN71RLA/9x\n"
+        + "wun1AEw6tLJ2R2j8+yXIt8UaWExqAviT/JgZELVXdCTqcYuOmktsM2z+2D+OyUtP\n"
+        + "7+Yyz7MGQKMAU+V/1uOK4YqwUJrcGy501o9Of+xm+5DASsK1oM5e9sBdmNewdLHL\n"
+        + "ZJEllURrEC6zCE/4zzs7qUfakH4l4ZJgjRL6va+ED0HfiQEfBBgBAgAJBQJMep8J\n"
+        + "AhsMAAoJEKmR4tUP3TZ369QIAKPlfX2TUfhP3otYiaa24zBJ/cvGljGiSfX0Krau\n"
+        + "sBHH161jlraJfLzpe7vSOZhwZwgIY/eKoErAkJwVnX1+dLuOcHaqRDi5gnLqa6Yg\n"
+        + "9a2LWb4zrvgsvbiNUs1o9htOcvcpv7e3UUUcRa8lO+aNkO+VoI6DI8RJ3wIfJayb\n"
+        + "oePRXdfr8g9of0jSdIOzlaaBPxA2wYSWXm4kv7QXzZooxuGqhn0+JKuq2+oO9y5Q\n"
+        + "Uig+c3oGa5mpVblmv5ZL6Gc36kCbeEC8j6JkNT4wnceQwpNUNYtPU186cjy3rAD4\n"
+        + "C58w0UvpHZZSTc0syLOShQr//We39LUNaX6WF3NmyF8K/OM=\n"
+        + "=e1xT\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/4B4387EE 2010-08-29 [expired: 2011-08-29]
+   *       Key fingerprint = F01D 677C 8BDB 854E 1054  406E 3B09 B97F 4B43 87EE
+   * uid                  Testuser E &lt;teste@example.com&gt;
+   */
+  public static TestKey keyE() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx6nxoBCADjYOWOFa7ZBJpRuNspRoXBTK0LiK5zqN894b87LgIYEgUM6q5J\n"
+        + "yLNo43x7V+ow1/7BEq0JUAMSQ3uRn2jqXiJskSXvwlFYcTVFb0gY09CSD0ptHvda\n"
+        + "zqYOuM/MU1l9jqmlM+pDw/z0pLTKYmAHi6pKJ64pqccMHPUZHpLywyzSNX+JM86I\n"
+        + "K5KAsyGArtgpT9vfci3idNeXjhMR8rfLPDFbdGvGFOZrYv0cfgTbBpVEWeHjs2FR\n"
+        + "4vHG133AdjdZcvA9Y9VW34ZLeiyBEeFix7+HPVS82rko2kQxZu1UZRu340maKDAo\n"
+        + "+UVirgo0FQ8nNUR+c9oNKgiZtO39IAPJv/WZABEBAAG0HlRlc3R1c2VyIEUgPHRl\n"
+        + "c3RlQGV4YW1wbGUuY29tPokBPgQTAQIAKAUCTHqfGgIbAwUJAeEzgAYLCQgHAwIG\n"
+        + "FQgCCQoLBBYCAwECHgECF4AACgkQOwm5f0tDh+6Fowf9FZgntlW4qc7BHe8zYJ0q\n"
+        + "zoLZrHwCFcaeO3kz53y5Lz3+plMuqVDjoQDOt8DxsPHrXWKiu0qBTjZ28ztN3ef6\n"
+        + "f0MpguTGclvFroevUct0xiyox5r1DfMT8JRvqsojE1XPscR2zJzIgEg3OCPuksT9\n"
+        + "EsHsF+/3RBbsXbQgDpW38g0GzIJI4AiQ/yvG2ON9awN2kzIWoBkthVCGy54lCTGj\n"
+        + "yPhatE7Zu2ABNcerIDstupWww2Psec6pGbPPci8ojc90fzalk3UMXcXHD7m8cTJS\n"
+        + "kgHScOzTElIQqOA1+w6uiHy2oAn+qW7534j6p9Tj+DrSIzUXBedGjXZevaKaurVy\n"
+        + "KLkBDQRMep8aAQgAn5r6toYnEzwDeig8r+t89vqOFtohYcmtyeLeTiTTdAz/xWBW\n"
+        + "HUlqV8sglQ9aINpGtBf37v13RhtU3WkUv8cZMQoRM8P2H3cKDNwkucFO6uKSEQO5\n"
+        + "FdzTm4C4WaoE7QiTRbiekwh7O54mz4Wup6LHuEFQEcSpdRUp8w/qaJIHG9EJad1q\n"
+        + "UEsKNnITW+mWHY3+ccK1hgqPwOPqO3/8QtaipekKOYAtOb+57c1jtDFBZnYIkant\n"
+        + "oKs+kRw0DykXFTyFOMYqaleBMcVG+u7ljwAq18L8Ev+qVIpBIZ5eQ5+6p1w9B69h\n"
+        + "RH0Ebn50ebpoqKOXhN4/bu/wq596y0o4xDB0GQARAQABiQElBBgBAgAPBQJMep8a\n"
+        + "AhsMBQkB4TOAAAoJEDsJuX9LQ4fu0/wH/35/22xina8ktbvGV/kB0pH2LBqeXN/b\n"
+        + "CLdA+CDzfwMDzqG0kU39EJ3Fbux7fj4uMaeiYfbO9U85+NOuDmeH41B2dM9S1AzE\n"
+        + "H+/OiCp/Zf1fdd1qXhsA4Xe5vc/VD9oso9OrZK5CM5u0TPmYFijfVDPNgag6mPnD\n"
+        + "zd8JCsuEj4VEy6NF1KcoCc8edQ8AZ4L6ZQ6qiV24gxLnh8xImVr5YjBKDUCdrl79\n"
+        + "0u4wekfgapSx9Sw9Ycz5dFOL07OOHPiKZwUG0f8td6oJX4Ddxset5JAm1pPcLQHR\n"
+        + "6PRx0hI/Tz7rsAI6O37/BEM15+MVGIgOSLL/SRIpOa0L8qmuUhhS6Bg=\n"
+        + "=uA5x\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx6nxoBCADjYOWOFa7ZBJpRuNspRoXBTK0LiK5zqN894b87LgIYEgUM6q5J\n"
+        + "yLNo43x7V+ow1/7BEq0JUAMSQ3uRn2jqXiJskSXvwlFYcTVFb0gY09CSD0ptHvda\n"
+        + "zqYOuM/MU1l9jqmlM+pDw/z0pLTKYmAHi6pKJ64pqccMHPUZHpLywyzSNX+JM86I\n"
+        + "K5KAsyGArtgpT9vfci3idNeXjhMR8rfLPDFbdGvGFOZrYv0cfgTbBpVEWeHjs2FR\n"
+        + "4vHG133AdjdZcvA9Y9VW34ZLeiyBEeFix7+HPVS82rko2kQxZu1UZRu340maKDAo\n"
+        + "+UVirgo0FQ8nNUR+c9oNKgiZtO39IAPJv/WZABEBAAEAB/4xKKzYqDVyM/2NN5Mi\n"
+        + "fF3EqegruzRESzlgrqLij5LiU1sGLOLbjunC/pPWMu6t+rTYV0pT3hmb5D0eAcH0\n"
+        + "EcANiuAR0wg1P9yNk36Z54mLWoTzzKMb3dunCSvb+BU8AREKZ4v5dLEGz2lK7DPo\n"
+        + "zbhWaffMiClBpC0VbjfFBo91LrVUVnhRglBYKdPLQm/Lhw5cNCYOw194ZturO+cC\n"
+        + "iQZhGSy52HMoMs4Wr470CeFZvvWaiDCirVLcj4UhMsVANFKsahMARm9c+QrGrkRP\n"
+        + "+654f8M9ptapcQYpGOMmaeZVnpocONXOTkiJd7Hhr4PRUY+QS8C8F0LbmL2ERQbL\n"
+        + "F65RBADkIelztY/8Xy2S0jsW7+xF2ziz9riOR87G6b0wrXDdFz4GHPzLvwsdXOeN\n"
+        + "cODic14d9bf5jtXr9hgbAzx55ANDjOl3jK5qil8Z9qwsrNK9Mz0wT1acQXBwf/5D\n"
+        + "hI/whBK1FsH7Y+wdX64XA3EXmclxB8GZf1JsGXF3jNH30vyS7QQA/ydoMMw8ja9L\n"
+        + "j6MxHtVHcE4A4j6tFljLDuf8icOwwNUfb7SsHTDjUI2+30ZJOv+qISrthsASCSj3\n"
+        + "AN87CGdVR62Xe923DNdW8/moKKDILNaESyOi27qhI5qWrVRgNB5QwbQcSoClUxbj\n"
+        + "V7YZSfrZkiI+GE1gh1QPMOVyCUmqu90D+wc0x0wUj8emX/4xbbujOa5RAvNcNvnD\n"
+        + "mOB2CfPWD10TEeOOlHBhuoy2/GdIl76W0szJaxnzcV82VArllSciCBzpSfkExDZ6\n"
+        + "08hA8GpOsuOmAAPwXWZsb8YZbJeM0ULMgUCGHgvUj1/pGsCVA6c7sPAdkCfAFlmO\n"
+        + "smC9bvpS2VHZPuG0HlRlc3R1c2VyIEUgPHRlc3RlQGV4YW1wbGUuY29tPokBPgQT\n"
+        + "AQIAKAUCTHqfGgIbAwUJAeEzgAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ\n"
+        + "Owm5f0tDh+6Fowf9FZgntlW4qc7BHe8zYJ0qzoLZrHwCFcaeO3kz53y5Lz3+plMu\n"
+        + "qVDjoQDOt8DxsPHrXWKiu0qBTjZ28ztN3ef6f0MpguTGclvFroevUct0xiyox5r1\n"
+        + "DfMT8JRvqsojE1XPscR2zJzIgEg3OCPuksT9EsHsF+/3RBbsXbQgDpW38g0GzIJI\n"
+        + "4AiQ/yvG2ON9awN2kzIWoBkthVCGy54lCTGjyPhatE7Zu2ABNcerIDstupWww2Ps\n"
+        + "ec6pGbPPci8ojc90fzalk3UMXcXHD7m8cTJSkgHScOzTElIQqOA1+w6uiHy2oAn+\n"
+        + "qW7534j6p9Tj+DrSIzUXBedGjXZevaKaurVyKJ0DmARMep8aAQgAn5r6toYnEzwD\n"
+        + "eig8r+t89vqOFtohYcmtyeLeTiTTdAz/xWBWHUlqV8sglQ9aINpGtBf37v13RhtU\n"
+        + "3WkUv8cZMQoRM8P2H3cKDNwkucFO6uKSEQO5FdzTm4C4WaoE7QiTRbiekwh7O54m\n"
+        + "z4Wup6LHuEFQEcSpdRUp8w/qaJIHG9EJad1qUEsKNnITW+mWHY3+ccK1hgqPwOPq\n"
+        + "O3/8QtaipekKOYAtOb+57c1jtDFBZnYIkantoKs+kRw0DykXFTyFOMYqaleBMcVG\n"
+        + "+u7ljwAq18L8Ev+qVIpBIZ5eQ5+6p1w9B69hRH0Ebn50ebpoqKOXhN4/bu/wq596\n"
+        + "y0o4xDB0GQARAQABAAf7Bk9bQCIXo2QJAyhaFd5qh10qhu7CyRnvG/8zKMW98mWd\n"
+        + "KxF+9hNz99qZBCuiNZBLoU0dST6OG6By/3nrDxXxAgZS3cgOj/nl1NJTRWDGHPUu\n"
+        + "LywFgj7Dwu8Y2rqlDTX8lJIS+t8n+BhtkmDHoesGmFtErh8nT/CxQuHLM60qSMgv\n"
+        + "6mSmtOkM+2KfiA5z2o1fDWXjDieW+hdgDPxkaB835wfuDn/Dsn1ch1XHON0xSyTo\n"
+        + "+c35nFXoK1pAXaoalAxZNxcXCAM3NhU37Ih4GejM0K7sSgK72HmgxtNYF77DrTIM\n"
+        + "m5+3960ri1JUuEaJ7ZcqbpKxy/GDldNCYBTx07QMzQQAyYQ+ujT9Pj8zfp1jMLRs\n"
+        + "Xn9GsvYawjo+AIZuHeUmmIXfEoyNmsEUoGHnz9ROLnJzanW5XEStiTys8tHJPIkz\n"
+        + "zL0Ce0oUF93ln0z/jQBIKaSzYB7PMmYCd7ueF94aKqAOrQ/QBb+6JsVjGAtLUoTv\n"
+        + "ey09hGYMogiBV1r0MB2Rsa8EAMrB5VKVQF6+q0XuP6ljFQRaumi4lH7PoQ65E7UD\n"
+        + "6YpyQpLBOE7dV+fHizdUuwsD/wyAOu0EskV1ZLXvXzyk10r3PRoFdpHOvijwZBGt\n"
+        + "jiOiVvK1vkQKDMBczOe74+DaknKn6HzgCsXmLgfk+P8BtLOJnCYsbS9IbnImy2vi\n"
+        + "aJC3A/9wOOK+po8C7JPHVIEfxbe7nwHOoi/h7T4uPrlq/gcQRquqGhQ16nDGYZvX\n"
+        + "ny9aPQ3NcvDR69RM2AaXav03bHVxfhVEyGjP5jLZz7956e4LlnKrsuEhDLfiv30i\n"
+        + "qCC7zNHNA99s5u25vt8AuPVVHfSQ++jifabfv5lU4FHqmK8/4EAoiQElBBgBAgAP\n"
+        + "BQJMep8aAhsMBQkB4TOAAAoJEDsJuX9LQ4fu0/wH/35/22xina8ktbvGV/kB0pH2\n"
+        + "LBqeXN/bCLdA+CDzfwMDzqG0kU39EJ3Fbux7fj4uMaeiYfbO9U85+NOuDmeH41B2\n"
+        + "dM9S1AzEH+/OiCp/Zf1fdd1qXhsA4Xe5vc/VD9oso9OrZK5CM5u0TPmYFijfVDPN\n"
+        + "gag6mPnDzd8JCsuEj4VEy6NF1KcoCc8edQ8AZ4L6ZQ6qiV24gxLnh8xImVr5YjBK\n"
+        + "DUCdrl790u4wekfgapSx9Sw9Ycz5dFOL07OOHPiKZwUG0f8td6oJX4Ddxset5JAm\n"
+        + "1pPcLQHR6PRx0hI/Tz7rsAI6O37/BEM15+MVGIgOSLL/SRIpOa0L8qmuUhhS6Bg=\n"
+        + "=HTKj\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/31FA48C4 2010-09-01
+   *       Key fingerprint = 85CE F045 8113 42DA 14A4  42AA 4A9F AC70 31FA 48C4
+   * uid                  Testuser F &lt;testf@example.com&gt;
+   * sub   2048R/50FF7D5C 2010-09-01
+   */
+  public static TestKey keyF() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx+aQkBCADycHmQ8wC4GzaDIvw4uv6/HiTWOUmuLcT06PpsvNBdR2sQ6Vyy\n"
+        + "w1SAnaPmskdgxE7TXpDrwIWPmIkg8KzSfPAap6qZy5zyE1ABQa9yD9v6wsew+lM5\n"
+        + "3UdBO6HQodpWSJMbeR48mUQ96z72B7Lb2GvhFLxvcn5od9jQhbQXfb2k67l33hgR\n"
+        + "D427lxXa+qnmL9pMRGhRw6QATFX+icHxsPfpKnzuk0aY3feJm4+jr4RgHP4djH3i\n"
+        + "NZbv3ibZ24Dj3CK07PwbhqUhZwMqueWbo3ChYjLkRGT/UosNTN0EbHjqBMl4N9OT\n"
+        + "Pl2CM6kuzuaLz3ZeAf48B29GX4rAXfuJTKBbABEBAAG0HlRlc3R1c2VyIEYgPHRl\n"
+        + "c3RmQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pCQIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQSp+scDH6SMRfqggAh//U/l4JuwFgWx14mo0SB9YWU81L\n"
+        + "EgUYUd2MUzvX4p/HIFQa0c7stj68Za40+O0tG/J0RCMNb7piM9JFii+MQZzOVuza\n"
+        + "4bbO59D9qboc7Anvx9hGlfIdinT+n5rwX9kZvD2D7GMskm8ZgovkvNwNKcW+5W/4\n"
+        + "ciWqCJKE/Fp9XsooJgN94pJfgDQ2WBL5KDx1aGt4wZXhH2Atl6a6oVZJIH4SaizD\n"
+        + "jau7F4vc7hBfbcDhxFcrVX1QMpzpl352cIx6KVw4FRWvQ8VKkga4JiQwosfvCT2Z\n"
+        + "pdMwy3cARynv8BWLc4Uexf88QIeClP9ZhoVeMqvHMfUb3d6Q5362VdZqI4kBIAQQ\n"
+        + "AQIACgUCTH5xcgMFCngACgkQiptSk+LTK6UqsAgAlsEmzC3Xxv4o5ui95AFbWZGi\n"
+        + "es5rI9WoW2P+6OqVUy1E8+5HdlJ8wUbU1H7JAdFTjY9rH3vKXCXsTetF4z0cupER\n"
+        + "Rkx06M9/jl5OSw8i9bPNNJFobHwiiNO00ctC1tT5oUVXVsfPQHlEbMofv8jehfgC\n"
+        + "gMqH/ve/aafKFfYCZkNHugRgLzxeDpXp3IdyXoSAFGiULnGvMDN7n61QOvEYOw2Z\n"
+        + "i63ql+bL2oj4G+/bNOkdYkuIBN4F/P45P7xy80MSOvkMH7IG/aFTKMNQGWSykKwI\n"
+        + "FRkC+y+F5Oqf/WD30GvbSA7q013sb6nHYvsaHS/48cgIJ5TSVd0LTlrF9uv43bkB\n"
+        + "DQRMfmkJAQgAzc1uAF4x16Cx4GtHI0Hvm+v7bUEUtBw2XzyOKu883XC5JmGcY18y\n"
+        + "YItRpchAtmacDpu0/2925/mWF7aS9RMgSYI/1D9LaTeimISM3iGFY35kt78NGZwJ\n"
+        + "DeCPJPI1sbOU0njfrCPTbOQuRDJ6evaBNX9HYArSEp0ygruJdOUYgnepCt4A7W95\n"
+        + "EKp9KPo7XV1K8y86vrKbgpJ+NnEi7dzMqVxnhO4wAWqb6HYcKLrEc2gVnLtzHkBl\n"
+        + "Y/6dOP15jgQKql1/yQIXae/WGT24n/VeaKqrbSmDNkhW5eW5o1Bkgy/M98oNHXd0\n"
+        + "nVrT8Lyf6un5TwMy+vk0l5AjMMtIZKS0GQARAQABiQEfBBgBAgAJBQJMfmkJAhsM\n"
+        + "AAoJEEqfrHAx+kjEvDAH/iO6BHQfFa+kqjfYD3NE+FNosXv3jiXOU7SCD2MG3AwD\n"
+        + "YqM+v1n4UvvMLLdEbtboht1Btys1vuyNM3RAmR45oh9Dfuc4SKtVzSCkKs85jNvH\n"
+        + "7Ik8gxZ9ARzJbawNzTLFyLwDdcdX42Umuvh49Pn7Nc7FDYcZLffEcTh9sZ7KyxLY\n"
+        + "qcjtnblx5oOQnYnpBbM61GvgNXC8Z+g9fg0oHRouKXKE/HDKbsN0siEf9XJFJTKd\n"
+        + "Eg1NgoyKWdaV4+pU/fTzZUvvDqOSRx8he5w64dvW9o7WdARq/3vPvHgy0O8fMTSI\n"
+        + "tmcHxCU8l0jptJz181N36Uhmjyc9oC4dn9ceSn6VDbg=\n"
+        + "=WDx2\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx+aQkBCADycHmQ8wC4GzaDIvw4uv6/HiTWOUmuLcT06PpsvNBdR2sQ6Vyy\n"
+        + "w1SAnaPmskdgxE7TXpDrwIWPmIkg8KzSfPAap6qZy5zyE1ABQa9yD9v6wsew+lM5\n"
+        + "3UdBO6HQodpWSJMbeR48mUQ96z72B7Lb2GvhFLxvcn5od9jQhbQXfb2k67l33hgR\n"
+        + "D427lxXa+qnmL9pMRGhRw6QATFX+icHxsPfpKnzuk0aY3feJm4+jr4RgHP4djH3i\n"
+        + "NZbv3ibZ24Dj3CK07PwbhqUhZwMqueWbo3ChYjLkRGT/UosNTN0EbHjqBMl4N9OT\n"
+        + "Pl2CM6kuzuaLz3ZeAf48B29GX4rAXfuJTKBbABEBAAEAB/4vTP+C5s5snS6ZDlHc\n"
+        + "datvOV/hhgLYn2huiigV4A7dLCp4/bbOz+pkP51zTLQ9bn+coLYwsPq+Bfo3OY3W\n"
+        + "cXbdFHpmEEJaPqdc32ZuICcAuVEBuA1V3FTjJtHO5U02iWleMlbSZurYE9ZQZTch\n"
+        + "yotdulB7hACivENKh9OXw7ok+1GZVvBGA8tpIwzLZo0Pkb2lDQHaL0GXAjlMNzwg\n"
+        + "cCPFtzjNu6K4g58nuYrjGiE+yWPMJgfo4fTGXcapqXgvh1tKIVxwr2YQSyEOqfMH\n"
+        + "8EwgBj5NPwv0UXAivQUkTaguUJXrlJLtS3mp45nCEAlGT4PNoMyPdvPEf62gND7C\n"
+        + "y9K1BAD493ADPAx9pWCSQI9wp4ARUelTzwHgZ6fRVIzmwO6MuZN1PrtiOLCwY5Jw\n"
+        + "r+97VvMmem7Ya3khP4vz0IiN7p1oCR5nJazk2eRaQNuim0aB0lqrTsli8OXtBlgQ\n"
+        + "5WtLcRi5798Jw8coczc5OftZKhu1SbQZ1VdDdmTbMTAsSRtMjQQA+UnU6FYJZBjE\n"
+        + "NHNheV6+k45HXHubcCm4Ka3kJK88zbZzyt+nrBLEtElosxDCqT8WbiAH7qmpnd/r\n"
+        + "ly7ryIX08etuWVYnx0Xa02cKQ6TzNcbxijeGQYGHIE0RK29nRo8zRWVmbCydqJz1\n"
+        + "5cHgcvoTu7DWWjM5QEZlLPQytJeAyocEAM6AiWDXYVZVnCB9w0wwK/9cX0v3tfYv\n"
+        + "QrJZCT3/YKxJWnMZ+LgHYO0w1B0YwGEeVTnmXODDy5mRh9lxV1aZnwKCwMR1tXTx\n"
+        + "G1potBR0GJxI2xpMb/MJPxeJCAZPu8NncRpl/8v0stiGnkpYCNR/k3JV5jEXq0u6\n"
+        + "4pDSzRGehOHnOqu0HlRlc3R1c2VyIEYgPHRlc3RmQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTH5pCQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQSp+scDH6\n"
+        + "SMRfqggAh//U/l4JuwFgWx14mo0SB9YWU81LEgUYUd2MUzvX4p/HIFQa0c7stj68\n"
+        + "Za40+O0tG/J0RCMNb7piM9JFii+MQZzOVuza4bbO59D9qboc7Anvx9hGlfIdinT+\n"
+        + "n5rwX9kZvD2D7GMskm8ZgovkvNwNKcW+5W/4ciWqCJKE/Fp9XsooJgN94pJfgDQ2\n"
+        + "WBL5KDx1aGt4wZXhH2Atl6a6oVZJIH4SaizDjau7F4vc7hBfbcDhxFcrVX1QMpzp\n"
+        + "l352cIx6KVw4FRWvQ8VKkga4JiQwosfvCT2ZpdMwy3cARynv8BWLc4Uexf88QIeC\n"
+        + "lP9ZhoVeMqvHMfUb3d6Q5362VdZqI50DmARMfmkJAQgAzc1uAF4x16Cx4GtHI0Hv\n"
+        + "m+v7bUEUtBw2XzyOKu883XC5JmGcY18yYItRpchAtmacDpu0/2925/mWF7aS9RMg\n"
+        + "SYI/1D9LaTeimISM3iGFY35kt78NGZwJDeCPJPI1sbOU0njfrCPTbOQuRDJ6evaB\n"
+        + "NX9HYArSEp0ygruJdOUYgnepCt4A7W95EKp9KPo7XV1K8y86vrKbgpJ+NnEi7dzM\n"
+        + "qVxnhO4wAWqb6HYcKLrEc2gVnLtzHkBlY/6dOP15jgQKql1/yQIXae/WGT24n/Ve\n"
+        + "aKqrbSmDNkhW5eW5o1Bkgy/M98oNHXd0nVrT8Lyf6un5TwMy+vk0l5AjMMtIZKS0\n"
+        + "GQARAQABAAf/T22JFmhESUnSTOBqeK+Sd/WIOJ7lDCxVScVXwzdJINfIBYmnr2yG\n"
+        + "x18NuHOEkkEg2rx6ixksZZRcurMynZZvoB8+Xj69bpLT1JRXv8VlM0SNP6NjPW6M\n"
+        + "ygfQhzxZv8ck2WRgQxIin8SjHJv0zG9F5+1DEUyrzhZQb8dMYkqm/nbZ1FDnMu4F\n"
+        + "1qUZxKx0hU70tAXfywtpH9NQs8jwenUjiXA00k6A48BF7gartYtcGnEG9mk+Z+lh\n"
+        + "/uD+z5j3/ym9XqOJPpFIWhMYTLueSD5yrCT34VdIc1xBOjjtxBsCCbgSFZaewCpB\n"
+        + "5usRr2I4+CK3vbAMny5Hk+/RYZdFQkCA5wQA2JusdhwqPjfzxtcxz13Vu1ZzKR41\n"
+        + "kkno/boGh5afBlf7kL/5FXDhGVVvHMvXtQntU1kHgOcE8b2Jfy38gNGkd3TAh4Oj\n"
+        + "fLavcYyn+9tEkjRVdOeU0P9fszDA1cW5Gjuv6GkbCUSQrv68TKp/mWiTlYm+FT3a\n"
+        + "RSIz2gEyOZNkTzsEAPM6sU/VOwpJ2ppOa5+290sptjSbRNYjKlQ66nHZnbafzLz5\n"
+        + "tKpRc0BzG/N2lXwlVl5+3oXSSSbWhJscA8EFwSnAx8Id10zW5NAEfxNuqxxEXlJg\n"
+        + "kOhqwJ1JMz32xlZFRZYxSdXSycYrX/AhV7I7RQxgC48X9udMb8LIXYq0lzy7A/9p\n"
+        + "Skd2Me9JotuTN3OaR42hXozLx+yERBBEWuI3WXovWRD8b8gCfWL3P40d2UVnjFmP\n"
+        + "TZ8p9aHAd2srWgaPSZaSsHtIyI6dQGScMEOKEaCJxYvF/wuvx/MABDatcaJhMaAc\n"
+        + "W/0w+gb8Lr2hbuRhBSP754V3Amma6LxsmLRAwB6ioT7NiQEfBBgBAgAJBQJMfmkJ\n"
+        + "AhsMAAoJEEqfrHAx+kjEvDAH/iO6BHQfFa+kqjfYD3NE+FNosXv3jiXOU7SCD2MG\n"
+        + "3AwDYqM+v1n4UvvMLLdEbtboht1Btys1vuyNM3RAmR45oh9Dfuc4SKtVzSCkKs85\n"
+        + "jNvH7Ik8gxZ9ARzJbawNzTLFyLwDdcdX42Umuvh49Pn7Nc7FDYcZLffEcTh9sZ7K\n"
+        + "yxLYqcjtnblx5oOQnYnpBbM61GvgNXC8Z+g9fg0oHRouKXKE/HDKbsN0siEf9XJF\n"
+        + "JTKdEg1NgoyKWdaV4+pU/fTzZUvvDqOSRx8he5w64dvW9o7WdARq/3vPvHgy0O8f\n"
+        + "MTSItmcHxCU8l0jptJz181N36Uhmjyc9oC4dn9ceSn6VDbg=\n"
+        + "=ZLpl\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/E2D32BA5 2010-09-01
+   *       Key fingerprint = CB2B 665B 88DA D56A 7009  C15D 8A9B 5293 E2D3 2BA5
+   * uid                  Testuser G &lt;testg@example.com&gt;
+   * sub   2048R/829DAE8D 2010-09-01
+   */
+  public static TestKey keyG() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx+aRYBCAC77YjBScTjRFHtZvk0yyAy8KAXopbCdMBQs7S7iidFMMxhs0Uu\n"
+        + "D7GeleyVusLFJfEM0Ul0b0pLgfJx9j3cot4BTl71OqnawHp4ktuqFyTjhhYy8kBe\n"
+        + "4mliNP36WW7fYXh+f5SZqDQ6rgyoJCOmiUlosb6CM2yUPH3oDtOKg/9Z0iMUcXfQ\n"
+        + "y+bxRKSQmDtiSIS7hwUZmQoo30iAZNygMBLnYyVau3YFan+xyBMCFLa2/pfE0qaU\n"
+        + "QMy67XP8uP7DXlepfc4Lk/qa/2WnAqmuTT2ty9MG+X8M8LuiPuMWfOEx8ICUWB9s\n"
+        + "kCCMWCagS7EUIPhp6AOqjMqEWGOyLmclkGCJABEBAAG0HlRlc3R1c2VyIEcgPHRl\n"
+        + "c3RnQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pFgIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQiptSk+LTK6VSwQf/WnIYkLZoARZIUfH61EDlkUPv8+6G\n"
+        + "1YY3YgFFMjeOKybu47eU3QtATEaKHphvKqFtxdNyEtmti1Zx7Cq2LzReY1KoQQ5E\n"
+        + "OlKeyxVmXAuAqoRWesxuG318rVTrozCqSdKPCHLcC26M5sO+Gd2sKbA4DjoSyfrE\n"
+        + "zEOVS1NA9dtZ7WBMXr8gjH//ob7dvuptSAlADaLYYaJugcmbzkRGRbfiCQHqv30I\n"
+        + "+81d7RAeSx8XS38YEWm2IvBLpiS/d7A/2AQ25SHxf+QMMWt83+uOuEVa9rEOraid\n"
+        + "ZC6T8vnSRu1TKkX/60LnJvAw9tigmedi21O6Gpz3H3uGyjuk9o18+m8dJokBIAQQ\n"
+        + "AQIACgUCTH5xfAMFCngACgkQSp+scDH6SMT42gf9H7K0jp6PF1vD5t90bcjtnP/t\n"
+        + "CkOXgfL3lJK/l0KMkoDzyO5z898PP8IAnAj1veJ2fNPsRP903/3K8kd9/31kBriC\n"
+        + "poTVPWBmeLut16TgSDxAQPDLsBPcKe2VadhszOQwhfmdsUlCXwXcwbiAjweXwKh+\n"
+        + "00UoW1GLnPw0T387ttCjHsLe972SVUPFxb6NUkA7val62qxDKg+6MRcf6tDs8sN8\n"
+        + "orhYgh9VJcI3Iw8qK1wHI0CenNie0U5xEkZ5U6W4lfhnL5sggjoAeVeAVLiQ4eiP\n"
+        + "sFrq4TOYq9qfuThYiRaSuTLXzuWG5NVs7NyXxOGFSkwzXrQsBo+LuPwjSCERLbkB\n"
+        + "DQRMfmkWAQgA1O0I9vfZNSRuYTx++SkJccXXqL4neVWEnQ4Ws9tzfSG0Rch3Gb/d\n"
+        + "+ckDtJhlQOdaayTVX7h5k8tTGx0myg6OjG2UM6i+aTgFAzwGnBh/N3p5tTaJhRCF\n"
+        + "x1IapX0N7ijq6rQPPCISc3CUZhCVBTnp5dk3c0/hNxsyYXlI1AwuoMabygzTFN/c\n"
+        + "b1bXp0UTTVrdN+Sj5hHVDvpxyaljLa77I0V+lI3bCil9VhQ9h/TP4C2iK3ZdXOMb\n"
+        + "uW7ANhd+I9LWulmExZIiD9RIsHvB3bDu32g1847uT+DUynKETbZWlZS0Q93Aly1N\n"
+        + "lBIkvOCVCBt+VatzZ8oBV8vbk5R41W1HywARAQABiQEfBBgBAgAJBQJMfmkWAhsM\n"
+        + "AAoJEIqbUpPi0yul/doH+wR+o6UCdD6OZxGMx7d0a7yDJqQFkFf2DRsJvY2suug0\n"
+        + "CMJZRWiA+hIin5P6Brn/eb5nTdWgzlrHxkvb68YkevHALdOvmrYNQFXbb9uWGgEf\n"
+        + "3qERdI8ayJsSTqYsTqyuh9YVz21kADxTHN3JkJ4evjHpyz0Xbtq+oDADg+uswj1b\n"
+        + "ihHthFif54vNMEIW9rX9T7ufhXKamr4LuGwKTPTxV8gEPW4h4ZoQwFKV2qOjR+su\n"
+        + "tHnuXVL24kTnv8CHXUVzJXVTNz7i7fAJTgWc9drH6Ktp3XHfLDBwzT5/5ZhyxGJk\n"
+        + "Qq2Jm/Q8mNkXi34H2DeQ3VPtjtMLr9JR9pf6ivmvUag=\n"
+        + "=34GE\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOXBEx+aRYBCAC77YjBScTjRFHtZvk0yyAy8KAXopbCdMBQs7S7iidFMMxhs0Uu\n"
+        + "D7GeleyVusLFJfEM0Ul0b0pLgfJx9j3cot4BTl71OqnawHp4ktuqFyTjhhYy8kBe\n"
+        + "4mliNP36WW7fYXh+f5SZqDQ6rgyoJCOmiUlosb6CM2yUPH3oDtOKg/9Z0iMUcXfQ\n"
+        + "y+bxRKSQmDtiSIS7hwUZmQoo30iAZNygMBLnYyVau3YFan+xyBMCFLa2/pfE0qaU\n"
+        + "QMy67XP8uP7DXlepfc4Lk/qa/2WnAqmuTT2ty9MG+X8M8LuiPuMWfOEx8ICUWB9s\n"
+        + "kCCMWCagS7EUIPhp6AOqjMqEWGOyLmclkGCJABEBAAEAB/QJiwZmylg1MkL2y0Pc\n"
+        + "anQ4If//M0J0nXkmn/mNjHZyDQhT7caVkDZ01ygsck9xs3uKKxaP0xbyvqaRIvAB\n"
+        + "REQBzPkFevUlJqERfmOpP4OgCi8WZzbdmqG/WvGKxP/cWBbGVbQ2GVSNpkj+QNeO\n"
+        + "nWoc5unFstbQsEG0hww2/Hz7EppYoBvDrDLY1EPKzr0r6sk1O5gk3VWOqMEJVCh+\n"
+        + "K7EV4pPGmzMrfZQ0jSwRpr0HhzzhDYR7+QUbxr4OS5PoSJDFh0+A5kqFagyupe7A\n"
+        + "96L3Lh7wJBQJsOe5xjOu3lkFp+3vU+Mq7VzO9Fnp9BCwjb4mEjI39bJdGeeOVCWR\n"
+        + "sYEEAMjmftMhIHrjGRlbZVrLcZY8Du4CFQqImb2Tluo/6siIEurVp4F2swZFm7fw\n"
+        + "B2v09GGJ6zKpauJuxlbwo3CFnxbk24W39F/SixZLggLPtNOXdSrLIQrQ1AXu5ucQ\n"
+        + "oCnXS5FaVkD3Rtd53hSMIf2xJiSRKGp/1X9hga/phScud7URBADveDh1oEmwl3gc\n"
+        + "gorhABLYV7cPrARteQRV13tYWcuAZ6WjqNlbbW2mzBE7KTh4bgTzIX0uQ6SZ7bPl\n"
+        + "RmuKQHrdOO9vFGiSf3zDnIg8fhqSyy2SNrC/e7teuaguGCrg5GrP5izBAsiwvXbt\n"
+        + "ST3OG7c8Ky717JGTiUeTJoe4IaET+QP/SB4uQzVTrbXjBNtq1KqL/CT7l2ABnXsn\n"
+        + "psaVwHOMmY/wP+PiazMEDvLInDAu7R8oLNGqYR+7UYmYeAGmWgrc0L3yFVC01tTG\n"
+        + "bk7Yt/V5KRKVO2I9x+2CP0v0EqW4BNOJzbx5TJ5lBFLMTvbviOdsoDXw0S98HIHB\n"
+        + "T1bFFmhVeulCDLQeVGVzdHVzZXIgRyA8dGVzdGdAZXhhbXBsZS5jb20+iQE4BBMB\n"
+        + "AgAiBQJMfmkWAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCKm1KT4tMr\n"
+        + "pVLBB/9achiQtmgBFkhR8frUQOWRQ+/z7obVhjdiAUUyN44rJu7jt5TdC0BMRooe\n"
+        + "mG8qoW3F03IS2a2LVnHsKrYvNF5jUqhBDkQ6Up7LFWZcC4CqhFZ6zG4bfXytVOuj\n"
+        + "MKpJ0o8IctwLbozmw74Z3awpsDgOOhLJ+sTMQ5VLU0D121ntYExevyCMf/+hvt2+\n"
+        + "6m1ICUANothhom6ByZvOREZFt+IJAeq/fQj7zV3tEB5LHxdLfxgRabYi8EumJL93\n"
+        + "sD/YBDblIfF/5Awxa3zf6464RVr2sQ6tqJ1kLpPy+dJG7VMqRf/rQucm8DD22KCZ\n"
+        + "52LbU7oanPcfe4bKO6T2jXz6bx0mnQOYBEx+aRYBCADU7Qj299k1JG5hPH75KQlx\n"
+        + "xdeovid5VYSdDhaz23N9IbRFyHcZv935yQO0mGVA51prJNVfuHmTy1MbHSbKDo6M\n"
+        + "bZQzqL5pOAUDPAacGH83enm1NomFEIXHUhqlfQ3uKOrqtA88IhJzcJRmEJUFOenl\n"
+        + "2TdzT+E3GzJheUjUDC6gxpvKDNMU39xvVtenRRNNWt035KPmEdUO+nHJqWMtrvsj\n"
+        + "RX6UjdsKKX1WFD2H9M/gLaIrdl1c4xu5bsA2F34j0ta6WYTFkiIP1Eiwe8HdsO7f\n"
+        + "aDXzju5P4NTKcoRNtlaVlLRD3cCXLU2UEiS84JUIG35Vq3NnygFXy9uTlHjVbUfL\n"
+        + "ABEBAAEAB/48KLaaNJ+xhJgNMA797crF0uyiOAumG/PqfeMLMQs5xQ6OktuXsl6Q\n"
+        + "pus9mLsu8c7Zq9//efsbt1xFMmDVwPQkmAdB60DVMKc16T1C2CcFcTy25vBG4Mqz\n"
+        + "bK6rqCAJ9JSe+H2/cy78X8gF6FR6VAkSUGN62IxcyfnbkW1yv/hiowZ5pQpGVjBH\n"
+        + "sjfu+6HGZhdJIyzrjnVjTJhXNCodtKq1lQGuL2t3ZB6osOXEsFtsI6lQF2s6QZZd\n"
+        + "MUOpSO+X1Rb5TCpWpR/Yj43sH6Tq7LZWEml9fV4wKe2PQWmFW+L8eZCwbYEz6GgZ\n"
+        + "w2pMoMxxOZJsOMOq4LFs4r9qaNQI+sU1BADZhx42JjqBIUsq0OhQcCizjCbPURNw\n"
+        + "7HRfPV8SQkldzmccVzGwFIKQqAVglNdT9AQefUQzx84CRqmWaROXaypkulOB79gM\n"
+        + "R/C/aXOdWz9/dGJ9fT/gcgq1vg9zt7dPE5QIYlhmNdfQPt6R50bUTXe22N2UYL98\n"
+        + "n1pQrhAdlsbT3QQA+pWPXQE4k3Hm7pwCycM2d4TmOIfB6YiaxjMNsZiepV4bqWPX\n"
+        + "iaHh0gw1f8Av6zmMncQELKRspA8Zrj3ZzB/OvNwfpgpqmjS0LyH4u8fGttm7y3In\n"
+        + "/NxZO33omf5vdB2yptzE6DegtsvS94ux6zp01SuzgCXjQbiSjb/VDL0/A8cD/1sQ\n"
+        + "PQGP1yrhn8aX/HAxgJv8cdI6ZnrSUW+G8RnhX281dl5a9so8APchhqeXspYFX6DJ\n"
+        + "Br6MqNkX69a7jthdLZCxaa3hGInr+A/nPVkNEHhjQ8a/kI+28ChRWndofme10hje\n"
+        + "QISFfGuMf6ULK9uo4d1MzGlstfcNRecizfniKby3SBmJAR8EGAECAAkFAkx+aRYC\n"
+        + "GwwACgkQiptSk+LTK6X92gf7BH6jpQJ0Po5nEYzHt3RrvIMmpAWQV/YNGwm9jay6\n"
+        + "6DQIwllFaID6EiKfk/oGuf95vmdN1aDOWsfGS9vrxiR68cAt06+atg1AVdtv25Ya\n"
+        + "AR/eoRF0jxrImxJOpixOrK6H1hXPbWQAPFMc3cmQnh6+MenLPRdu2r6gMAOD66zC\n"
+        + "PVuKEe2EWJ/ni80wQhb2tf1Pu5+Fcpqavgu4bApM9PFXyAQ9biHhmhDAUpXao6NH\n"
+        + "6y60ee5dUvbiROe/wIddRXMldVM3PuLt8AlOBZz12sfoq2ndcd8sMHDNPn/lmHLE\n"
+        + "YmRCrYmb9DyY2ReLfgfYN5DdU+2O0wuv0lH2l/qK+a9RqA==\n"
+        + "=T1WV\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/080E5723 2010-09-01
+   *       Key fingerprint = 2957 ABE4 937D A84A 2E5D  31DB 65C4 33C4 080E 5723
+   * uid                  Testuser H &lt;testh@example.com&gt;
+   * sub   2048R/68C7C262 2010-09-01
+   */
+  public static TestKey keyH() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx+aSUBCADzpZ1h9awUQR1ChzrMhtoE1ltyTlJpS1G5HFEov9QNxVDTjpB8\n"
+        + "PMdb20NNdk/7g6E+ETpCBJGPoC4/TPFDiqe+UI7cRrRZJVbInkCbflYycLTUt9qW\n"
+        + "5c7IuyZA1+cSYaKp3jYccFZfIWvfTWDLWyUozTs9t1TsI28s3r5fBPvrZ+F1nYv/\n"
+        + "xpSkx3Zsxnn7QJTnd63rZdp0RdfJmF2rXERwR6XVtuLj5CqrFoLxy6OrSOl4am4J\n"
+        + "C2HRWhskB21LpdRtloY8bz0DOn6W6JUFRmSxQ1kbPClOXaiNhzMI0fD/KFnHImgR\n"
+        + "IKbbQyHHsKHBjNyn+zTIm5zUL6JMZMf9PoSZABEBAAG0HlRlc3R1c2VyIEggPHRl\n"
+        + "c3RoQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pJQIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQZcQzxAgOVyORcAf/QaHVlyhlBnU4edujW2uG/PFrZvwK\n"
+        + "fqOKW0QqQ7kVN8okKhnFv4y11IwLIzL9mOLYe2+Zyv3I3bz4X8Xw+MsBF6sMWLLf\n"
+        + "9ieu4Wz/5ScVu0PxY36kgV0AQRiLXk802Vk4t9jElCp9qx/dDln7f3879LLb3wNt\n"
+        + "fajne8EH0hjR4E3joPoG+IXSvSzWcPoZTmAZOKHPcRS8iqy0Ao8/UuQWYCedI/4R\n"
+        + "S1IJaByk8mmkMkqqV0kuPyDkvGpqhfh9zFYh97LuKcJktRTEBp3YMvuGcBDBwofG\n"
+        + "vYIVEMr7Ci5rowRQO/sxJfI1zNSWterWC46v6tOb9IvenOgP0/dQxlU82YkBIAQQ\n"
+        + "AQIACgUCTH5xmAMFAXgACgkQ0CLaOl6a7dCYuQf/V2i3Ih5Dqze0Rz5zoTD56/J7\n"
+        + "0SA4/SFm5eDUirY5B9BohkyxoMVG04uyjUmVs62ree7N0IASmeiF/wkBUZ/r/rr/\n"
+        + "0ntGj43y+1JpuSEohZOfgZJryDKRqyVWhRbeBj0g/SzxIQ1lEt2iHFvdSlfFVd+a\n"
+        + "SH1uDDjT/ZATKfAXcgeajUirWorJRaldue7O4oFe67fMLy36ewvpaMVZ+SpxH4CC\n"
+        + "Owq4Ls3dIAg2C5GQK8G0G7FwT1M26EPg66C79EGYkaxprgrilWE6l7QHc484TY1L\n"
+        + "ys04qKoPRnBinmrRxgRyyimvDN/+nd1jdM6nMe1gVLL3s5Vgo0fJMwNhDZMtdrkB\n"
+        + "DQRMfmklAQgAyajPVMt+OXO1ow7xzb0aZYNa5Xdv+w50JzVeWI0boPOuOmq6RCc1\n"
+        + "3NhOmBzx3CKH6zbSRoLBCZWM3cs1EQbl+8noaxq6YQVWiaROX8U7CThYA50jONP/\n"
+        + "qEk655QFsP8Bq96Z5AT/MflxEMayOtQywUFREF4/olhXvJOdurZfQPGnIis35NUc\n"
+        + "IaubI+gGVsluqWBohLOgqzyF7GMlv+Y2JZE5JKGSTO7ZosyI+OCNdZ6X2CJdDPZ1\n"
+        + "325QHYkmqiMJtb73AYTXurL7NNTxdxQVOnfvwXXW4mgHwPEHr8PU30+2xgo1ktrr\n"
+        + "rpFsd0o2UFhybTe7w1z2sAO1gP5s1bbGlwARAQABiQEfBBgBAgAJBQJMfmklAhsM\n"
+        + "AAoJEGXEM8QIDlcjqkQIAI78nwAgO5EgrUDoFikH6d36Kie9SHleaYcSX2c95Vqc\n"
+        + "umuiSAhaulGX0gM/jwvZkoawSyWIq+O2sPSc9F7VzdYdEnWVj2J5BpVx83TRPrTu\n"
+        + "72tsJ97op6JZz+Q8HwTLYJBmyW3/TEKh+iRL9CBtfTVywodZa58j41vCkx37NFPw\n"
+        + "plglT/Se1/US1rWYTH3Kfqo5zNARLUYzAdcxEpjwXWOvqnybn86KfMwqiOunz8eq\n"
+        + "MnTQYECfUrhX2WrbEAjCSc6/LfrTv/S+cO0rvulO/R97gG99pZdWSUjZypU5KLbp\n"
+        + "MBh0qq2wQxO2iagNXE6ms3kV/XihvCpXo9RArmldmW0=\n"
+        + "=lddL\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx+aSUBCADzpZ1h9awUQR1ChzrMhtoE1ltyTlJpS1G5HFEov9QNxVDTjpB8\n"
+        + "PMdb20NNdk/7g6E+ETpCBJGPoC4/TPFDiqe+UI7cRrRZJVbInkCbflYycLTUt9qW\n"
+        + "5c7IuyZA1+cSYaKp3jYccFZfIWvfTWDLWyUozTs9t1TsI28s3r5fBPvrZ+F1nYv/\n"
+        + "xpSkx3Zsxnn7QJTnd63rZdp0RdfJmF2rXERwR6XVtuLj5CqrFoLxy6OrSOl4am4J\n"
+        + "C2HRWhskB21LpdRtloY8bz0DOn6W6JUFRmSxQ1kbPClOXaiNhzMI0fD/KFnHImgR\n"
+        + "IKbbQyHHsKHBjNyn+zTIm5zUL6JMZMf9PoSZABEBAAEAB/wPPOigp4d9VcwxbLkz\n"
+        + "8OwiONDLz5OuY6hHCjsWMBcgTFqffI9TQc7bExW8ur1KVuNm+RdaaSQ8ZhF2YobF\n"
+        + "SV7v02R36NEfMStiDSmvv+E+stdQZXY9kT5TRgcgr5ATUXllo9DhCvKP7Qxs0Q9Q\n"
+        + "cJEcoedGVxiv0xCBLyYbVbm2sW+GJYjq0R5loaOy/Swbt5vOKQsajU8iyA4czSE8\n"
+        + "Ryr63OtwZ1TZsxekj//HKcngnptYY/FT5TPe4uzw8g1tJTIg/OZXrm8CahWzpfE3\n"
+        + "q8lGafhd0GjLftA9ffIHF0cAUs7HklMrgIKGdVPXfQmPzqDpmH5FO2y6QmqTG0v6\n"
+        + "JYW9BAD4Iobwh80MT3JZhJ0jGYMdi07cRyFN+hRwVKgNcBTdx3QGpGJatcyumD0C\n"
+        + "Yn/aXAn+XUkewSgYhdj9sSRodnWGoavdWELxUQkktsdiFg2/rnqmpqRXTGfR/tDh\n"
+        + "ohD2JaPrsavmUF6ShT3stGp8nUN+n6Bhd+QosaCZm5TC1CtA7QQA+16rrNNdP8XN\n"
+        + "MvpQRqJM5ljH0haqR/yD8vdCCZjk23hBk3YsXwSrhSbPzMeZC2FcDqkQTraTxrSG\n"
+        + "U0+xK3NjKKtbzCjQFH4cy4zdNMUX04OWopLGOEnnvTYukGtXT4lZQ9qm8ZBPh5a4\n"
+        + "cXfWy3ovjvRbxUuFOWm0gOfIoRcuWN0D/isTjqPmjihCuWkKTfa3xoq+dD7ynYhg\n"
+        + "Yu3UKfCqbNVor59ZrB4AkQiaVIDLKim3E1XDMS+IukmTuNVXpJeqK32tAYbEduHM\n"
+        + "7kwEq7SgVh34QvryKjCC/EUkDcjSQ+xlUaKl8QKYOdwtH97zZYK6QixB4uNQ6CuM\n"
+        + "75dqTZ6iQw7jQA+0HlRlc3R1c2VyIEggPHRlc3RoQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTH5pJQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQZcQzxAgO\n"
+        + "VyORcAf/QaHVlyhlBnU4edujW2uG/PFrZvwKfqOKW0QqQ7kVN8okKhnFv4y11IwL\n"
+        + "IzL9mOLYe2+Zyv3I3bz4X8Xw+MsBF6sMWLLf9ieu4Wz/5ScVu0PxY36kgV0AQRiL\n"
+        + "Xk802Vk4t9jElCp9qx/dDln7f3879LLb3wNtfajne8EH0hjR4E3joPoG+IXSvSzW\n"
+        + "cPoZTmAZOKHPcRS8iqy0Ao8/UuQWYCedI/4RS1IJaByk8mmkMkqqV0kuPyDkvGpq\n"
+        + "hfh9zFYh97LuKcJktRTEBp3YMvuGcBDBwofGvYIVEMr7Ci5rowRQO/sxJfI1zNSW\n"
+        + "terWC46v6tOb9IvenOgP0/dQxlU82Z0DmARMfmklAQgAyajPVMt+OXO1ow7xzb0a\n"
+        + "ZYNa5Xdv+w50JzVeWI0boPOuOmq6RCc13NhOmBzx3CKH6zbSRoLBCZWM3cs1EQbl\n"
+        + "+8noaxq6YQVWiaROX8U7CThYA50jONP/qEk655QFsP8Bq96Z5AT/MflxEMayOtQy\n"
+        + "wUFREF4/olhXvJOdurZfQPGnIis35NUcIaubI+gGVsluqWBohLOgqzyF7GMlv+Y2\n"
+        + "JZE5JKGSTO7ZosyI+OCNdZ6X2CJdDPZ1325QHYkmqiMJtb73AYTXurL7NNTxdxQV\n"
+        + "OnfvwXXW4mgHwPEHr8PU30+2xgo1ktrrrpFsd0o2UFhybTe7w1z2sAO1gP5s1bbG\n"
+        + "lwARAQABAAf8C3vFcrqz0Wm5ajOrqV+fZTB5uJ94jP9htengGYLPk/bMcR8qxD7H\n"
+        + "XnAi6Z6cV0DQJKDWkJVZkMYnY2ny96lA53mz9oVrH6NCLkxg+istFXVT7cDBBLdt\n"
+        + "05N3+z/+ovmiirr+YHG4Zowh2Ca4d4kl6sNhbmEvlnsZY++0B7Hi8ru2KgFBag2g\n"
+        + "wDmeVt2+ANJNfJ4uIHUEG+sDSDL4+rxQlBTMhxfVY5+zjbvzPlTf2jyAgDa5zGN2\n"
+        + "vRjB33Z0lbdZTeW7HsJcDsXaS77lKnQeWMmHSvpOXvFSIjnrWpxcMpg8hGY5e5UC\n"
+        + "zLCk+nucY/Od1NbtFYu/e7fl9/n3YnT7AQQA0v/t43Ut3go9vRlb47NN/KpJYL1N\n"
+        + "hh9F/SRzFwWxS+79CiZkf/bgmdJe4XkkS7QJMv+nXhtcko/gfzoaCrvIWIAyvhYa\n"
+        + "7tEbqH+iZ0eaLrQf7bu89Jmp2UNRT1EHLzm38eJ8gg7eNu+SjIhs3wART1KB7GvT\n"
+        + "YmpN5caJA2t2OaEEAPSq7CbvlPDc0qomQSs+NrDnhAv89mQEeksZRmhVa0o4Z7EO\n"
+        + "84DzM+Vxho5fn9h0LtxthhuKWKT8uYN/Qu4Y42cKQuRgMx09+GGwc4GWSC6gJPeP\n"
+        + "oKVJCdZx0l9u8fWQb37gnyH34WDxPvdQx3e4iw/dvruNzu17zmPndkdcyEU3BACD\n"
+        + "yXo21SEflFcfrO16VsITXWc9yweKTSD8Mq7wg2GG6eJPopgtwCLZSlYjnehxD2w2\n"
+        + "38lyr6jGPyITvalVwH6R//676Q2osbQ948Dv2ZcxaTlyla4RyY6E33hsnV9m8ZmM\n"
+        + "PUoNJvFSkKCuPy1N5zaYgUAPKwbEkc3qG+bZm+x2WU2biQEfBBgBAgAJBQJMfmkl\n"
+        + "AhsMAAoJEGXEM8QIDlcjqkQIAI78nwAgO5EgrUDoFikH6d36Kie9SHleaYcSX2c9\n"
+        + "5VqcumuiSAhaulGX0gM/jwvZkoawSyWIq+O2sPSc9F7VzdYdEnWVj2J5BpVx83TR\n"
+        + "PrTu72tsJ97op6JZz+Q8HwTLYJBmyW3/TEKh+iRL9CBtfTVywodZa58j41vCkx37\n"
+        + "NFPwplglT/Se1/US1rWYTH3Kfqo5zNARLUYzAdcxEpjwXWOvqnybn86KfMwqiOun\n"
+        + "z8eqMnTQYECfUrhX2WrbEAjCSc6/LfrTv/S+cO0rvulO/R97gG99pZdWSUjZypU5\n"
+        + "KLbpMBh0qq2wQxO2iagNXE6ms3kV/XihvCpXo9RArmldmW0=\n"
+        + "=voB9\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/5E9AEDD0 2010-09-01
+   *       Key fingerprint = 818D 5D0B 4AE2 A4FE A4C3  C44D D022 DA3A 5E9A EDD0
+   * uid                  Testuser I &lt;testi@example.com&gt;
+   * sub   2048R/0884E452 2010-09-01
+   */
+  public static TestKey keyI() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx+aTEBCAC6dFperkew4ZowIfEyAjScjPBggcbw5XUXxLCF0nBRjWH+HvuI\n"
+        + "CGwznRyeuTiy5yyB9/CcvTLTkEs8qIyJUJoikm7QpaVVL6imVq1HD1xcOJpV1FV1\n"
+        + "eFu562xCRDUqD6KQf54N04V9TMDyubhPkQYbx1H2gq+uBEo9d1w6AsSMgaUn3xH/\n"
+        + "xYe+INxcP6jFT2OKc36x+8ipP6pc8Hba1X90JwadOcJlwEyJfJKs7hYHTaYn+I6+\n"
+        + "4w0Y//WebhT4ocsYIiOYrENQUcic+vL3fkwwJCloyDBCGxr7w7Gn4Pe3peTCl4Sp\n"
+        + "vIIoYnHPW4h3Nyh8qAlBNDw7dCPS9LP7wRdNABEBAAG0HlRlc3R1c2VyIEkgPHRl\n"
+        + "c3RpQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pMQIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQ0CLaOl6a7dAjNQf/fLmGeKgaesawP53UeioQ8hgDEFKP\n"
+        + "BddNQP248NpReZ1rg3h8Q21PQJVKrtDYn94WJi5NTqUtk9rtx9SiqKaEc3wzIpLc\n"
+        + "nIYrgGLWot5nq+5V1nY9t9QAiJJDrmm2/3tX+jTWW6CpuLih7IsD+gJmpZkY6PfM\n"
+        + "T+teKEeh5E1XBbu10fwDwMJta+043/TiljInjER1f/b41EnSjI6YXFnxnyiLeDgD\n"
+        + "A1QIIzB/W2ccGqphzJriDETDJhKFZIeqvjylZofgCLyMRSyZtsu+b4pfBK3hMpu5\n"
+        + "aaYylaM1BWOpAiqUmGUKqxN/o9EGx4wvsMxK6xgiZe5UdQPaoDcFCsEMg4kBIAQQ\n"
+        + "AQIACgUCTH5xrAMFAXgACgkQoTk8RsLmoZiu2Af8D4PnyWkosYYkcmU4T7CvIHGW\n"
+        + "Qnx4KsnYWaAqYrYrorL6R+f8SZ5caGwj05UOvHnqx/Ij0a1Zv4MpEuzB0se1XkyQ\n"
+        + "eCLdAIKVodfiepsCHyqW6/mc9LV2qKS1HF5x5LwDkI1atOuPt/O14fch4E0beTbl\n"
+        + "FXzGo7YdpH8RunV8l+i3FxxTcUtUkij3Ro4EMwVF/6YG8gBOd08GxWspEQWBH3GK\n"
+        + "k7Repj4IPwXCoEfU1H+XJNPaM5cnt+L87QfbhNOWmHmWhhrOmZg160joODON8w8x\n"
+        + "j3gma9Cp6luPDEQC3XnsEup3BdCdIciG5JS6JA/2GDeulg+eS4x9Xkmmp6nzObkB\n"
+        + "DQRMfmkxAQgAxeT+bUBbADga+lYtkmtYVbuG7uWjwdg9TR6qWKD7n37mcu6OgNNl\n"
+        + "rPaHoClvOL20fcArZ8wT/FbjvDI6ZHn22YA19OvAR+Eqmf3D7qTmebchnCu955Pk\n"
+        + "X7AOOpKfX48qoYq8BoskZDnbFidm5YKfIin3CNDdlQbd3na+ihGCuv0KoGzefuAH\n"
+        + "cITeYEUESh7HLzQ9/pMES9eCgdTEkwYD5NJjfkLnj2kZtDsSiNnENZ0TIlyKOBMn\n"
+        + "ixgsARDjLrkqyTg79thWALiqVBXUKn2NBtMkK5xTDc/7q3nIw4InYMIrLtntSu1w\n"
+        + "pn1gXbdg1HFl5BgqEB9Fp0k02YvrSiiVswARAQABiQEfBBgBAgAJBQJMfmkxAhsM\n"
+        + "AAoJENAi2jpemu3QFPoH/1ynX1j1QWL8TfJFPoB3vXivwGURs3J7LsywHTRjpQVQ\n"
+        + "vxQvKTzB1+woUxtEbdjKgMbvY/ShHSlEZKVV9l3ZihrNewHA1GMHrDtBGXcNRP9B\n"
+        + "RfJHTrDzjUxrEEwu4QIq71o4tS89NvQmlYYi7O4ThtVB4hYSwl436+vAT9ybIQkU\n"
+        + "OjCkYrKye6JHs1K4BnVuWcOVujQwW4H8QFbddcWF49uN6DSqrwDFsjFog6wL7u6V\n"
+        + "UL5upRBP/RZWA4HKJVF2tS0Ptr6xLTmf4Tp5n10CGFYkPcRp9biVyeVRJBW4uZf0\n"
+        + "EDsn9J5rNG0pWtgnhAEi6smoT4fADTOzpOovUiTSQhQ=\n"
+        + "=SiG3\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx+aTEBCAC6dFperkew4ZowIfEyAjScjPBggcbw5XUXxLCF0nBRjWH+HvuI\n"
+        + "CGwznRyeuTiy5yyB9/CcvTLTkEs8qIyJUJoikm7QpaVVL6imVq1HD1xcOJpV1FV1\n"
+        + "eFu562xCRDUqD6KQf54N04V9TMDyubhPkQYbx1H2gq+uBEo9d1w6AsSMgaUn3xH/\n"
+        + "xYe+INxcP6jFT2OKc36x+8ipP6pc8Hba1X90JwadOcJlwEyJfJKs7hYHTaYn+I6+\n"
+        + "4w0Y//WebhT4ocsYIiOYrENQUcic+vL3fkwwJCloyDBCGxr7w7Gn4Pe3peTCl4Sp\n"
+        + "vIIoYnHPW4h3Nyh8qAlBNDw7dCPS9LP7wRdNABEBAAEAB/oCD6EKLvjXgItlqdm/\n"
+        + "X+OWMYHDCtuRCMW7+2gEw/TxfLeGJaOHWxAouwUIArEEb/hjdaRfIg4wdJUxmyPX\n"
+        + "WyNqUdupkjdXNa7RNaesIi0ilrdZOn7NlHWJCCXwKt2R0jd2p8PDED6CWaE1+76I\n"
+        + "/IuwOHDTD8MABke3KvHDXMxjzdeuRbm670Aqz6zTVY+BZG1GH63Ef5JEyezMgAU5\n"
+        + "42+v+OgD0W0/jCxF7jt2ddP9QiOzu0q65mI4qlOuSebxjH8P7ye0LU9EuWVgAcwc\n"
+        + "YJh2lk3eH8bCWTwlIHj4+8MYgY5i510I5xfY3sWuylw/qtFP9vYjisrysadcUExc\n"
+        + "QUxFBADXQSCmvtgRoSLiGfQv2y2qInx67eJw8pUXFEIJKdOFOhX4vogT9qPWQAms\n"
+        + "/vSshcsAPgpZJZ8MNeGpMGLAGm8y4D2zWWd9YLNmVXsPu7EyrDpXlKHCFnsQfOGN\n"
+        + "c5j8u4CHBn1cS/Yk53S+6Yge2jvnOjVNFmxB0ocs0Y5zbdTJYwQA3b+hQebH7NNr\n"
+        + "FlPwthRZS0TiX5+qkE9tE/0mpRrUN3iS9bnF0IXRmHFp7Hz+EsVbA2Re2A5HIHnQ\n"
+        + "/BSpAsSHRhjU3MH4gzwfg9W43eZGVfofSY6IlUCIcd1bGjSAjJgmfhjU7ofS59i/\n"
+        + "DjzP1jBfXdjOEUQULTkXjHPqO7j4048D/jqMwZNY3AawTMjqKr9nGK49aWv/OVdy\n"
+        + "6xGn4dRJNk3gnnIvjAEFy5+HHbUCJ2lA3X2AssQ9tvbuyDnoSL5/G+zEYtyRuAC5\n"
+        + "9TLQQRmy4qjsYC5TwfoUwFbgqRsmGUcjj2wtE+gb1S8P/zudYrEqOD3K60Y5qXcn\n"
+        + "S3PHgJ++5TzFQba0HlRlc3R1c2VyIEkgPHRlc3RpQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTH5pMQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ0CLaOl6a\n"
+        + "7dAjNQf/fLmGeKgaesawP53UeioQ8hgDEFKPBddNQP248NpReZ1rg3h8Q21PQJVK\n"
+        + "rtDYn94WJi5NTqUtk9rtx9SiqKaEc3wzIpLcnIYrgGLWot5nq+5V1nY9t9QAiJJD\n"
+        + "rmm2/3tX+jTWW6CpuLih7IsD+gJmpZkY6PfMT+teKEeh5E1XBbu10fwDwMJta+04\n"
+        + "3/TiljInjER1f/b41EnSjI6YXFnxnyiLeDgDA1QIIzB/W2ccGqphzJriDETDJhKF\n"
+        + "ZIeqvjylZofgCLyMRSyZtsu+b4pfBK3hMpu5aaYylaM1BWOpAiqUmGUKqxN/o9EG\n"
+        + "x4wvsMxK6xgiZe5UdQPaoDcFCsEMg50DmARMfmkxAQgAxeT+bUBbADga+lYtkmtY\n"
+        + "VbuG7uWjwdg9TR6qWKD7n37mcu6OgNNlrPaHoClvOL20fcArZ8wT/FbjvDI6ZHn2\n"
+        + "2YA19OvAR+Eqmf3D7qTmebchnCu955PkX7AOOpKfX48qoYq8BoskZDnbFidm5YKf\n"
+        + "Iin3CNDdlQbd3na+ihGCuv0KoGzefuAHcITeYEUESh7HLzQ9/pMES9eCgdTEkwYD\n"
+        + "5NJjfkLnj2kZtDsSiNnENZ0TIlyKOBMnixgsARDjLrkqyTg79thWALiqVBXUKn2N\n"
+        + "BtMkK5xTDc/7q3nIw4InYMIrLtntSu1wpn1gXbdg1HFl5BgqEB9Fp0k02YvrSiiV\n"
+        + "swARAQABAAf/VXp4O5CUvh9956vZu2kKmt2Jhx9CALT6pZkdU3MVvOr/d517iEHH\n"
+        + "pVJHevLqy8OFdtvO4+LOryyI6f14I3ZbHc+3frdmMqYb1LA8NZScyO5FYkOyn5jO\n"
+        + "CFbvjnVOyeP5MhXO6bSoX3JuI7+ZPoGRYxxlTDWLwJdatoDsBI9TvJhVekyAchTH\n"
+        + "Tyt3NQIvLXqHvKU/8WAgclBKeL/y/idep1BrJ4cIJ+EFp0agEG0WpRRUAYjwfE3P\n"
+        + "aSEV0NOoB8rapPW3XuEjO+ZTht+NYvqgPIdTjwXZGFPYnwvEuz772Th4pO3o/PdF\n"
+        + "2cljvRn3qo+lSVnJ0Ki2pb+LukJSIdfHgQQA1DBdm29a/3dBla2y6wxlSXW/3WBp\n"
+        + "51Vpd8SBuwdVrNNQMwPmf1L93YskJnUKSTo7MwgrYZFWf7QzgfD/cHXr8QK2C1TP\n"
+        + "czUC0/uFCm8pPQoOt/osp3PjDAzGgUAMFXCgLtb04P2JqbFvtse5oTFWrKqmscTG\n"
+        + "KnEBkzfgy37U0iMEAO7BEgXCYvqyztHmQATqJfbpxgQGqk738UW6qWwG8mK6aT5V\n"
+        + "OidZvrWqJ3WeIKmEhoJlY2Ky1ZTuJfeQuVucqzNWlZy2yzDijs+t3v4pFGajv4nV\n"
+        + "ivGvlb/O/QoHBuF/9K36lIIqcZstfa2UIYRqkkdEz2JHWJsr81VvCw2Gb38xA/sG\n"
+        + "hqErrIgSBPRCJObM/gb9rJ6dbA5SNY5trc778EjS1myhyPhGOaOmYbdQMONUqLo2\n"
+        + "q1UZo1G7oaI1Um9v5MXN1yZNX/kvx1TMldZEEixrhCIob81eXSpEUfs+Mz2RqvqT\n"
+        + "YsYquYQNPrPXWZQwTJV6fpsBQUMeE/pmlisaSAijHkXPiQEfBBgBAgAJBQJMfmkx\n"
+        + "AhsMAAoJENAi2jpemu3QFPoH/1ynX1j1QWL8TfJFPoB3vXivwGURs3J7LsywHTRj\n"
+        + "pQVQvxQvKTzB1+woUxtEbdjKgMbvY/ShHSlEZKVV9l3ZihrNewHA1GMHrDtBGXcN\n"
+        + "RP9BRfJHTrDzjUxrEEwu4QIq71o4tS89NvQmlYYi7O4ThtVB4hYSwl436+vAT9yb\n"
+        + "IQkUOjCkYrKye6JHs1K4BnVuWcOVujQwW4H8QFbddcWF49uN6DSqrwDFsjFog6wL\n"
+        + "7u6VUL5upRBP/RZWA4HKJVF2tS0Ptr6xLTmf4Tp5n10CGFYkPcRp9biVyeVRJBW4\n"
+        + "uZf0EDsn9J5rNG0pWtgnhAEi6smoT4fADTOzpOovUiTSQhQ=\n"
+        + "=RcWw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * pub   2048R/C2E6A198 2010-09-01
+   *       Key fingerprint = 83AB CE4D 6845 D6DA F7FB  AA47 A139 3C46 C2E6 A198
+   * uid                  Testuser J &lt;testj@example.com&gt;
+   * sub   2048R/863E8ABF 2010-09-01
+   */
+  public static TestKey keyJ() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "mQENBEx+aUgBCADczNio9UWnUggUZkdqJye57497oD9vNo9rmtR+i1TkMpVeWaMH\n"
+        + "UWrm1twzIPV9D4lAWLJoG2cYF6nXG1JxsKc9mOIZ6O1WfsopMUU0p+EfU8H/cvdM\n"
+        + "/iccYS6OnNL4/xR1R7hlA4+b/jaOZfzdS3i5jwOf+TtCk7c5qOuFhraiVQ9H1G86\n"
+        + "+LsiWVeXEFc/FXxnESRmbaZFNJrAdJl23eKXRC6az0S5FwMVBvUhRpLwIGDbVT/0\n"
+        + "/QwaNUOq3bYwudPREFLg/1HtBuxNhRdV6mCrit+tsPan9o0/WtsHuq8n4/pqOKBc\n"
+        + "RRmOIQR9SEohE2TuVT3XVFpMXa4a4CBuNXjTABEBAAG0HlRlc3R1c2VyIEogPHRl\n"
+        + "c3RqQGV4YW1wbGUuY29tPokBOAQTAQIAIgUCTH5pSAIbAwYLCQgHAwIGFQgCCQoL\n"
+        + "BBYCAwECHgECF4AACgkQoTk8RsLmoZi0BggAlnbCwmwaLwcpU9YcOE9/8KF56dIs\n"
+        + "XhdxzqdP91UmhVT0df1OBhgTqFkKprBLCT+B9yBClsnyXMatkvuhQG6C7lw9toMO\n"
+        + "TITRPZoFJe3Ezi+HRRPqAPubIcSgeILuilvFhkoUOgoC1ubmVPgcGBLb8tdvI3bA\n"
+        + "svq+n2jaYUlgL5N6ZNRNakc07e8vH5SeKiD8ZntJlTU49fkxzlawtDaI3+GhyUiB\n"
+        + "0Ah8pl143DFNAq8CfvQCPKwX4WFPkEflh0LlgaEPJUZ/H6KxKXXF8SC9cD2VIii8\n"
+        + "Yrue8y9T+j5y699A0GCptb1IKrgxbfhgD//3g3l1eXsEwn2cwFNCt7pZFLkBDQRM\n"
+        + "fmlIAQgA3E2pM6oDJGgfxbqSfykuRtTbiAi7JEd1DNvEe6gJ7qkBLM4ipILBD0qR\n"
+        + "qCwL37E4/3nMsZjA7GIFLQj2DrFW3aEEKwR/zdh7R67lo9CunCY+FPWTuOkCG8Sh\n"
+        + "3RLpbAV6I61NG/wDznW30vmKNJDgPpkzYj8u0T4MtpywEgxTxCqWZKCufWDRfNAy\n"
+        + "IBLt+piG+bcYKfw9pS8PvXPQMNIi4U2pu3hb/BHC3Y1A8FVpEe4CFV7rWb/K2Ydx\n"
+        + "eBxwwxm9sBxF+vhlI+ZEeb9JxGH6jYlc6twD4e6p3KqusAKLKiLsS5uLQnpMGGZ8\n"
+        + "vcpTSfyHjG2QHc3qG9S/yDCZjhhe2QARAQABiQEfBBgBAgAJBQJMfmlIAhsMAAoJ\n"
+        + "EKE5PEbC5qGYClMIANTdZ+/g/FPl1Lm0tO1CSnHVHekeGNA9n3L6SGiSZQJjEDo0\n"
+        + "gsye5xgxh5JGKf7CqbEFfeLC9Jx5W5EN4dVFudncIlC/SutfRzdt5W8CLXMl0c41\n"
+        + "5FmtpWNStk3MglkcjE5PrRRrSiRc45S0e2kIPb8eiVKg98/rCToC9+Qn3pMi/fcM\n"
+        + "LVpYE+dhvB5EhOSwBWWgvWXzeLhv5CnBKxH0ItHhNwvt8qPOHgQAJKPc6dV888xn\n"
+        + "Sew62LFefHPnGTHLP8RRgVIvZBG5IoovxSz89QGHQZiC4xv00I7zNwmtr6eEcl+y\n"
+        + "BkUK9QWITEBHUDqR+cbVa2dRr3fUHwRP7G2G+ow=\n"
+        + "=ucAX\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "lQOYBEx+aUgBCADczNio9UWnUggUZkdqJye57497oD9vNo9rmtR+i1TkMpVeWaMH\n"
+        + "UWrm1twzIPV9D4lAWLJoG2cYF6nXG1JxsKc9mOIZ6O1WfsopMUU0p+EfU8H/cvdM\n"
+        + "/iccYS6OnNL4/xR1R7hlA4+b/jaOZfzdS3i5jwOf+TtCk7c5qOuFhraiVQ9H1G86\n"
+        + "+LsiWVeXEFc/FXxnESRmbaZFNJrAdJl23eKXRC6az0S5FwMVBvUhRpLwIGDbVT/0\n"
+        + "/QwaNUOq3bYwudPREFLg/1HtBuxNhRdV6mCrit+tsPan9o0/WtsHuq8n4/pqOKBc\n"
+        + "RRmOIQR9SEohE2TuVT3XVFpMXa4a4CBuNXjTABEBAAEAB/9sW1MQR53xKP6yFCeD\n"
+        + "3sdOJlSB1PiMeXgU1JznpTT58CEBdnfdRYVy14qkxM30m8U9gMm88YW8exBscgoZ\n"
+        + "pRnNztNW58phokNPx9AwsRp3p0ETPbZDYI6NDNwuPKQEchn2HEZPvFmjsjPP2hkn\n"
+        + "+Lu8RIUA4uzEFX3bnBxJIP1L2AztqyTgHDfXS4/nqerO/cheXhN7j1TUyRO4hinp\n"
+        + "C3WXaxm2kpQXFP2ktq2eu7YPFoW6I6HzHVDN2Z7fD/NzfmR2h4gcIaSDEjIs893N\n"
+        + "b3hsYiOTYwVFX9TBWLr9rSWyrjR4sWelFuMZpjQ53qq+rBm/+8knoNtoWgZFhbR0\n"
+        + "WJyRBADlBuX8kveqLl31QShgw+6TwTHXI40GiCA6DHwZiTstOO6d2KDNq2nHdtuo\n"
+        + "HBvSKYP4a2na39JKb7YfuSMg16QvxQNd7BQWz+NzbGLQEGuX455OD3TE74ZfVElo\n"
+        + "2H/i51hSjOdWihJVNBGlcDYPgb7oLLTbPdKXxptRM1+wrk2//QQA9s3pw2O3lSbV\n"
+        + "U8JyL/FhdyhDvRDuiNBPnB4O/Ynnzz8YSFwSdSE/u8FpguFWdh+UdSrdwE+Ux8kj\n"
+        + "W/miXaqTxUeKnpzOkiO5O2fLvAeriO3rU9KfBER03+NJo4weSorLXzeU4SWkw63N\n"
+        + "OiY3fc67Nj+l8qi1tmoEJyHUomuy7Q8EAOfBvMzGsQQJ12k+4gOSXN9DTWUa85P6\n"
+        + "IphFHC2cpTDy30IRR55sI6Mf3GpC+KzxEyw7WXjlTensEJAHMpyVVRhv6uF0eMaY\n"
+        + "+QGS+vyCgtUfGIwM5Teu6NjeqyShJDTC8qnM+75JgCNu6gZ2F2iTeY+tM3zE1auq\n"
+        + "po1pUACVm7qwR6u0HlRlc3R1c2VyIEogPHRlc3RqQGV4YW1wbGUuY29tPokBOAQT\n"
+        + "AQIAIgUCTH5pSAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQoTk8RsLm\n"
+        + "oZi0BggAlnbCwmwaLwcpU9YcOE9/8KF56dIsXhdxzqdP91UmhVT0df1OBhgTqFkK\n"
+        + "prBLCT+B9yBClsnyXMatkvuhQG6C7lw9toMOTITRPZoFJe3Ezi+HRRPqAPubIcSg\n"
+        + "eILuilvFhkoUOgoC1ubmVPgcGBLb8tdvI3bAsvq+n2jaYUlgL5N6ZNRNakc07e8v\n"
+        + "H5SeKiD8ZntJlTU49fkxzlawtDaI3+GhyUiB0Ah8pl143DFNAq8CfvQCPKwX4WFP\n"
+        + "kEflh0LlgaEPJUZ/H6KxKXXF8SC9cD2VIii8Yrue8y9T+j5y699A0GCptb1IKrgx\n"
+        + "bfhgD//3g3l1eXsEwn2cwFNCt7pZFJ0DmARMfmlIAQgA3E2pM6oDJGgfxbqSfyku\n"
+        + "RtTbiAi7JEd1DNvEe6gJ7qkBLM4ipILBD0qRqCwL37E4/3nMsZjA7GIFLQj2DrFW\n"
+        + "3aEEKwR/zdh7R67lo9CunCY+FPWTuOkCG8Sh3RLpbAV6I61NG/wDznW30vmKNJDg\n"
+        + "PpkzYj8u0T4MtpywEgxTxCqWZKCufWDRfNAyIBLt+piG+bcYKfw9pS8PvXPQMNIi\n"
+        + "4U2pu3hb/BHC3Y1A8FVpEe4CFV7rWb/K2YdxeBxwwxm9sBxF+vhlI+ZEeb9JxGH6\n"
+        + "jYlc6twD4e6p3KqusAKLKiLsS5uLQnpMGGZ8vcpTSfyHjG2QHc3qG9S/yDCZjhhe\n"
+        + "2QARAQABAAf7BUTPxk/u/vi935DpBXoXRKHZnLM3bFuIexCGQ74rQqR2qazUMH8o\n"
+        + "SFEsaBJpm2WyR47J5WqSHNi5SxPT2AUdNFeh/39hxY61Q6SuBFED+WMRbHrKbURR\n"
+        + "WjPiFuwus02eAkAYFWfBFY0n9/BcAhicQa90MTRj+RZb/EHa+GDdbgDatpwEK22z\n"
+        + "pPb3t/D2TC7ModizelngBN7bdp4Vqna/vMLhsiE+FqL+Ob0KiLkDxtcjZljc9xLK\n"
+        + "B7ZuGH/AZfhF08OAxUcsJdu5cF3viBT+HeSI4OUvdfxPFX98U/SFfuW4mPdHPEI9\n"
+        + "438pdjDUIpJFtcnROtZdS2o6C9ohHa5BUwQA52P8AKKRfg7LpaFMvtKkNORnscac\n"
+        + "1qvXLqAXaMeSsvyU5o1GNvSgbhFzDcXbAFJcXdOo2XgT7JzW/6v1uW9AuQPAkYhr\n"
+        + "ep0uE3mewlzWHZR41MQRaMGN4l80RN6ju4c/Ei+OMHYp2DUfZFDBXbxwWpN8tNoR\n"
+        + "S1X+rOL5RsQgkrcEAPO7zthR+GQnIgJC3c9Las9JkPywCxddjoWZoyt6yITVjIso\n"
+        + "IGD0SJppAkOS3Vdb+raydLuN7HmbpPFnvzyc+RdSt+YCGUObrHb/z9MfahzDNG3S\n"
+        + "VwUQEIl+L6glhwscQOCz80MCcYMFMk4TiankvChRFF5Wil//8QnaonH4bcrvA/46\n"
+        + "VB+ZaEdR+Z8IkYIf7oHLJNEwaH+kRTBQ2x5F9Gnwr9SL6AXAkNkvYD4in/+Bw35r\n"
+        + "o9zGirQQvNrvH3JlZ5PWp1/9rRl2Tefaaf8P2ij/Ky2poBLAhPwK56JXHLt5v+BZ\n"
+        + "mQwhY+teJnbfCwiiS0OeWtpVY/tDVU7wYOd2RIhVfkUziQEfBBgBAgAJBQJMfmlI\n"
+        + "AhsMAAoJEKE5PEbC5qGYClMIANTdZ+/g/FPl1Lm0tO1CSnHVHekeGNA9n3L6SGiS\n"
+        + "ZQJjEDo0gsye5xgxh5JGKf7CqbEFfeLC9Jx5W5EN4dVFudncIlC/SutfRzdt5W8C\n"
+        + "LXMl0c415FmtpWNStk3MglkcjE5PrRRrSiRc45S0e2kIPb8eiVKg98/rCToC9+Qn\n"
+        + "3pMi/fcMLVpYE+dhvB5EhOSwBWWgvWXzeLhv5CnBKxH0ItHhNwvt8qPOHgQAJKPc\n"
+        + "6dV888xnSew62LFefHPnGTHLP8RRgVIvZBG5IoovxSz89QGHQZiC4xv00I7zNwmt\n"
+        + "r6eEcl+yBkUK9QWITEBHUDqR+cbVa2dRr3fUHwRP7G2G+ow=\n"
+        + "=NiQI\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  private TestTrustKeys() {
+  }
+}
diff --git a/gerrit-gwtdebug/BUCK b/gerrit-gwtdebug/BUCK
index bf05af0..3670916 100644
--- a/gerrit-gwtdebug/BUCK
+++ b/gerrit-gwtdebug/BUCK
@@ -2,6 +2,7 @@
   name = 'gwtdebug',
   srcs = glob(['src/main/java/**/*.java']),
   deps = [
+    '//gerrit-pgm:daemon',
     '//gerrit-pgm:pgm',
     '//gerrit-pgm:util',
     '//gerrit-util-cli:cli',
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index f8b82d4..4b2cb03 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -76,7 +76,7 @@
   ]),
   deps = [
     ':SafeHtml',
-    '//lib:junit',
+    '//lib:truth',
     '//lib/gwt:user',
     '//lib/gwt:dev',
   ],
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
index af80b3c..753d25f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
@@ -77,19 +77,15 @@
 
   private String name(final TreeLogger logger, final PublicResource r)
       throws UnableToCompleteException {
-    final InputStream in = r.getContents(logger);
-    final ByteArrayOutputStream tmp = new ByteArrayOutputStream();
-    try {
-      try {
-        final byte[] buf = new byte[2048];
-        int n;
-        while ((n = in.read(buf)) >= 0) {
-          tmp.write(buf, 0, n);
-        }
-        tmp.close();
-      } finally {
-        in.close();
+    byte[] out;
+    try (ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+        InputStream in = r.getContents(logger)) {
+      final byte[] buf = new byte[2048];
+      int n;
+      while ((n = in.read(buf)) >= 0) {
+        tmp.write(buf, 0, n);
       }
+      out = tmp.toByteArray();
     } catch (IOException e) {
       final UnableToCompleteException ute = new UnableToCompleteException();
       ute.initCause(e);
@@ -103,7 +99,7 @@
     } else {
       base = "";
     }
-    return base + Util.computeStrongName(tmp.toByteArray()) + ".cache.css";
+    return base + Util.computeStrongName(out) + ".cache.css";
   }
 
   private static class CssPubRsrc extends PublicResource {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
index e744239..a85d704 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -114,7 +114,7 @@
   }
 
   private void populate(final Grid lists) {
-    int end[] = new int[5];
+    int[] end = new int[5];
     int column = 0;
     for (final KeyCommandSet set : combinedSetsByName()) {
       int row = end[column];
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
index eb87e7f..6c820a8 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
@@ -67,9 +67,6 @@
         if (v >= 8000) {
           return "ie8";
         }
-        if (v >= 6000) {
-          return "ie6";
-        }
       }
       return null;
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
index 69da38d..0e7f7eb 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -162,7 +162,7 @@
   }
 
   /**
-   * Open an element, appending "<tagName>" to the buffer.
+   * Open an element, appending "{@code <tagName>}" to the buffer.
    * <p>
    * After the element is open the attributes may be manipulated until the next
    * {@code append}, {@code openElement}, {@code closeSelf} or
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
index d44405f..950400a 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -35,7 +35,7 @@
     String cc = "no-cache, no-store, max-age=0, must-revalidate";
     res.setHeader("Cache-Control", cc);
     res.setHeader("Pragma", "no-cache");
-    res.setHeader("Expires", "Fri, 01 Jan 1990 00:00:00 GMT");
+    res.setHeader("Expires", "Mon, 01 Jan 1990 00:00:00 GMT");
     res.setDateHeader("Date", System.currentTimeMillis());
   }
 
@@ -128,6 +128,12 @@
     cache(res, "private", age, unit, mustRevalidate);
   }
 
+  public static boolean hasCacheHeader(HttpServletResponse res) {
+    return res.getHeader("Cache-Control") != null
+        || res.getHeader("Expires") != null
+        || "no-cache".equals(res.getHeader("Pragma"));
+  }
+
   private static void cache(HttpServletResponse res,
       String type, long age, TimeUnit unit, boolean revalidate) {
     res.setHeader("Cache-Control", String.format(
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
index 2ffa7c5d..2070200 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -81,6 +81,7 @@
       try {
         return Integer.parseInt(p);
       } catch (NumberFormatException nan) {
+        // Ignored
       }
     }
     return -1;
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
index 0d8336e..554315e 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
@@ -14,75 +14,73 @@
 
 package com.google.gwtexpui.safehtml.client;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gwtexpui.safehtml.client.LinkFindReplace.hasValidScheme;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 public class LinkFindReplaceTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Test
   public void testNoEscaping() {
     String find = "find";
     String link = "link";
     LinkFindReplace a = new LinkFindReplace(find, link);
-    assertEquals(find, a.pattern().getSource());
-    assertEquals("<a href=\"link\">find</a>", a.replace(find));
-    assertEquals("find = " + find + ", link = " + link, a.toString());
+    assertThat(a.pattern().getSource()).isEqualTo(find);
+    assertThat(a.replace(find)).isEqualTo("<a href=\"link\">find</a>");
+    assertThat(a.toString()).isEqualTo("find = " + find + ", link = " + link);
   }
 
   @Test
   public void testBackreference() {
-    assertEquals("<a href=\"/bug?id=123\">issue 123</a>",
-        new LinkFindReplace("(bug|issue)\\s*([0-9]+)", "/bug?id=$2")
-            .replace("issue 123"));
+    LinkFindReplace l = new LinkFindReplace(
+        "(bug|issue)\\s*([0-9]+)", "/bug?id=$2");
+    assertThat(l.replace("issue 123"))
+      .isEqualTo("<a href=\"/bug?id=123\">issue 123</a>");
   }
 
   @Test
   public void testHasValidScheme() {
-    assertTrue(hasValidScheme("/absolute/path"));
-    assertTrue(hasValidScheme("relative/path"));
-    assertTrue(hasValidScheme("http://url/"));
-    assertTrue(hasValidScheme("HTTP://url/"));
-    assertTrue(hasValidScheme("https://url/"));
-    assertTrue(hasValidScheme("mailto://url/"));
-    assertFalse(hasValidScheme("ftp://url/"));
-    assertFalse(hasValidScheme("data:evil"));
-    assertFalse(hasValidScheme("javascript:alert(1)"));
+    assertThat(hasValidScheme("/absolute/path")).isTrue();
+    assertThat(hasValidScheme("relative/path")).isTrue();
+    assertThat(hasValidScheme("http://url/")).isTrue();
+    assertThat(hasValidScheme("HTTP://url/")).isTrue();
+    assertThat(hasValidScheme("https://url/")).isTrue();
+    assertThat(hasValidScheme("mailto://url/")).isTrue();
+    assertThat(hasValidScheme("ftp://url/")).isFalse();
+    assertThat(hasValidScheme("data:evil")).isFalse();
+    assertThat(hasValidScheme("javascript:alert(1)")).isFalse();
   }
 
   @Test
   public void testInvalidSchemeInReplace() {
-    try {
-      new LinkFindReplace("find", "javascript:alert(1)").replace("find");
-      fail("Expected IllegalStateException");
-    } catch (IllegalArgumentException expected) {
-    }
+    exception.expect(IllegalArgumentException.class);
+    new LinkFindReplace("find", "javascript:alert(1)").replace("find");
   }
 
   @Test
   public void testInvalidSchemeWithBackreference() {
-    try {
-      new LinkFindReplace(".*(script:[^;]*)", "java$1")
-          .replace("Look at this script: alert(1);");
-      fail("Expected IllegalStateException");
-    } catch (IllegalArgumentException expected) {
-    }
+    exception.expect(IllegalArgumentException.class);
+    new LinkFindReplace(".*(script:[^;]*)", "java$1")
+        .replace("Look at this script: alert(1);");
   }
 
   @Test
   public void testReplaceEscaping() {
-    assertEquals("<a href=\"a&quot;&amp;&#39;&lt;&gt;b\">find</a>",
-        new LinkFindReplace("find", "a\"&'<>b").replace("find"));
+    assertThat(new LinkFindReplace("find", "a\"&'<>b").replace("find"))
+      .isEqualTo("<a href=\"a&quot;&amp;&#39;&lt;&gt;b\">find</a>");
   }
 
   @Test
   public void testHtmlInFind() {
     String rawFind = "<b>&quot;bold&quot;</b>";
     LinkFindReplace a = new LinkFindReplace(rawFind, "/bold");
-    assertEquals(rawFind, a.pattern().getSource());
-    assertEquals("<a href=\"/bold\">" + rawFind + "</a>", a.replace(rawFind));
+    assertThat(a.pattern().getSource()).isEqualTo(rawFind);
+    assertThat(a.replace(rawFind))
+      .isEqualTo("<a href=\"/bold\">" + rawFind + "</a>");
   }
 }
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
index 182eac3..0f124c0 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
@@ -14,7 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -24,8 +24,8 @@
     final String find = "find";
     final String replace = "replace";
     final RawFindReplace a = new RawFindReplace(find, replace);
-    assertEquals(find, a.pattern().getSource());
-    assertEquals(replace, a.replace(find));
-    assertEquals("find = " + find + ", replace = " + replace, a.toString());
+    assertThat(a.pattern().getSource()).isEqualTo(find);
+    assertThat(a.replace(find)).isEqualTo(replace);
+    assertThat(a.toString()).isEqualTo("find = " + find + ", replace = " + replace);
   }
 }
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
index 0163d7f..0862711 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
@@ -14,27 +14,26 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static com.google.common.truth.Truth.assertThat;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 public class SafeHtmlBuilderTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Test
   public void testEmpty() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertTrue(b.isEmpty());
-    assertFalse(b.hasContent());
-    assertEquals("", b.asString());
+    assertThat(b.isEmpty()).isTrue();
+    assertThat(b.hasContent()).isFalse();
+    assertThat(b.asString()).isEmpty();
 
     b.append("a");
-    assertTrue(b.hasContent());
-    assertEquals("a", b.asString());
+    assertThat(b.hasContent()).isTrue();
+    assertThat(b.asString()).isEqualTo("a");
   }
 
   @Test
@@ -43,249 +42,240 @@
     b.append(1);
 
     final SafeHtml h = b.toSafeHtml();
-    assertNotNull(h);
-    assertNotSame(h, b);
-    assertFalse(h instanceof SafeHtmlBuilder);
-    assertEquals("1", h.asString());
+    assertThat(h).isNotNull();
+    assertThat(h).isNotSameAs(b);
+    assertThat(h).isNotInstanceOf(SafeHtmlBuilder.class);
+    assertThat(h.asString()).isEqualTo("1");
   }
 
   @Test
   public void testAppend_boolean() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append(true));
-    assertSame(b, b.append(false));
-    assertEquals("truefalse", b.asString());
+    assertThat(b).isSameAs(b.append(true));
+    assertThat(b).isSameAs(b.append(false));
+    assertThat(b.asString()).isEqualTo("truefalse");
   }
 
   @Test
   public void testAppend_char() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append('a'));
-    assertSame(b, b.append('b'));
-    assertEquals("ab", b.asString());
+    assertThat(b).isSameAs(b.append('a'));
+    assertThat(b).isSameAs(b.append('b'));
+    assertThat(b.asString()).isEqualTo("ab");
   }
 
   @Test
   public void testAppend_int() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append(4));
-    assertSame(b, b.append(2));
-    assertSame(b, b.append(-100));
-    assertEquals("42-100", b.asString());
+    assertThat(b).isSameAs(b.append(4));
+    assertThat(b).isSameAs(b.append(2));
+    assertThat(b).isSameAs(b.append(-100));
+    assertThat(b.asString()).isEqualTo("42-100");
   }
 
   @Test
   public void testAppend_long() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append(4L));
-    assertSame(b, b.append(2L));
-    assertEquals("42", b.asString());
+    assertThat(b).isSameAs(b.append(4L));
+    assertThat(b).isSameAs(b.append(2L));
+    assertThat(b.asString()).isEqualTo("42");
   }
 
   @Test
   public void testAppend_float() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append(0.0f));
-    assertEquals("0.0", b.asString());
+    assertThat(b).isSameAs(b.append(0.0f));
+    assertThat(b.asString()).isEqualTo("0.0");
   }
 
   @Test
   public void testAppend_double() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append(0.0));
-    assertEquals("0.0", b.asString());
+    assertThat(b).isSameAs(b.append(0.0));
+    assertThat(b.asString()).isEqualTo("0.0");
   }
 
   @Test
   public void testAppend_String() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append((String) null));
-    assertEquals("", b.asString());
-    assertSame(b, b.append("foo"));
-    assertSame(b, b.append("bar"));
-    assertEquals("foobar", b.asString());
+    assertThat(b).isSameAs(b.append((String) null));
+    assertThat(b.asString()).isEmpty();
+    assertThat(b).isSameAs(b.append("foo"));
+    assertThat(b).isSameAs(b.append("bar"));
+    assertThat(b.asString()).isEqualTo("foobar");
   }
 
   @Test
   public void testAppend_StringBuilder() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append((StringBuilder) null));
-    assertEquals("", b.asString());
-    assertSame(b, b.append(new StringBuilder("foo")));
-    assertSame(b, b.append(new StringBuilder("bar")));
-    assertEquals("foobar", b.asString());
+    assertThat(b).isSameAs(b.append((StringBuilder) null));
+    assertThat(b.asString()).isEmpty();
+    assertThat(b).isSameAs(b.append(new StringBuilder("foo")));
+    assertThat(b).isSameAs(b.append(new StringBuilder("bar")));
+    assertThat(b.asString()).isEqualTo("foobar");
   }
 
   @Test
   public void testAppend_StringBuffer() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append((StringBuffer) null));
-    assertEquals("", b.asString());
-    assertSame(b, b.append(new StringBuffer("foo")));
-    assertSame(b, b.append(new StringBuffer("bar")));
-    assertEquals("foobar", b.asString());
+    assertThat(b).isSameAs(b.append((StringBuffer) null));
+    assertThat(b.asString()).isEmpty();
+    assertThat(b).isSameAs(b.append(new StringBuffer("foo")));
+    assertThat(b).isSameAs(b.append(new StringBuffer("bar")));
+    assertThat(b.asString()).isEqualTo("foobar");
   }
 
   @Test
   public void testAppend_Object() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append((Object) null));
-    assertEquals("", b.asString());
-    assertSame(b, b.append(new Object() {
+    assertThat(b).isSameAs(b.append((Object) null));
+    assertThat(b.asString()).isEmpty();
+    assertThat(b).isSameAs(b.append(new Object() {
       @Override
       public String toString() {
         return "foobar";
       }
     }));
-    assertEquals("foobar", b.asString());
+    assertThat(b.asString()).isEqualTo("foobar");
   }
 
   @Test
   public void testAppend_CharSequence() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append((CharSequence) null));
-    assertEquals("", b.asString());
-    assertSame(b, b.append((CharSequence) "foo"));
-    assertSame(b, b.append((CharSequence) "bar"));
-    assertEquals("foobar", b.asString());
+    assertThat(b).isSameAs(b.append((CharSequence) null));
+    assertThat(b.asString()).isEmpty();
+    assertThat(b).isSameAs(b.append((CharSequence) "foo"));
+    assertThat(b).isSameAs(b.append((CharSequence) "bar"));
+    assertThat(b.asString()).isEqualTo("foobar");
   }
 
   @Test
   public void testAppend_SafeHtml() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.append((SafeHtml) null));
-    assertEquals("", b.asString());
-    assertSame(b, b.append(new SafeHtmlString("foo")));
-    assertSame(b, b.append(new SafeHtmlBuilder().append("bar")));
-    assertEquals("foobar", b.asString());
+    assertThat(b).isSameAs(b.append((SafeHtml) null));
+    assertThat(b.asString()).isEmpty();
+    assertThat(b).isSameAs(b.append(new SafeHtmlString("foo")));
+    assertThat(b).isSameAs(b.append(new SafeHtmlBuilder().append("bar")));
+    assertThat(b.asString()).isEqualTo("foobar");
   }
 
   @Test
   public void testHtmlSpecialCharacters() {
-    assertEquals("&amp;", escape("&"));
-    assertEquals("&lt;", escape("<"));
-    assertEquals("&gt;", escape(">"));
-    assertEquals("&quot;", escape("\""));
-    assertEquals("&#39;", escape("'"));
+    assertThat(escape("&")).isEqualTo("&amp;");
+    assertThat(escape("<")).isEqualTo("&lt;");
+    assertThat(escape(">")).isEqualTo("&gt;");
+    assertThat(escape("\"")).isEqualTo("&quot;");
+    assertThat(escape("'")).isEqualTo("&#39;");
 
-    assertEquals("&amp;", escape('&'));
-    assertEquals("&lt;", escape('<'));
-    assertEquals("&gt;", escape('>'));
-    assertEquals("&quot;", escape('"'));
-    assertEquals("&#39;", escape('\''));
+    assertThat(escape('&')).isEqualTo("&amp;");
+    assertThat(escape('<')).isEqualTo("&lt;");
+    assertThat(escape('>')).isEqualTo("&gt;");
+    assertThat(escape('"')).isEqualTo("&quot;");
+    assertThat(escape('\'')).isEqualTo("&#39;");
 
-    assertEquals("&lt;b&gt;", escape("<b>"));
-    assertEquals("&amp;lt;b&amp;gt;", escape("&lt;b&gt;"));
+    assertThat(escape("<b>")).isEqualTo("&lt;b&gt;");
+    assertThat(escape("&lt;b&gt;")).isEqualTo("&amp;lt;b&amp;gt;");
   }
 
   @Test
   public void testEntityNbsp() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.nbsp());
-    assertEquals("&nbsp;", b.asString());
+    assertThat(b).isSameAs(b.nbsp());
+    assertThat(b.asString()).isEqualTo("&nbsp;");
   }
 
   @Test
   public void testTagBr() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.br());
-    assertEquals("<br />", b.asString());
+    assertThat(b).isSameAs(b.br());
+    assertThat(b.asString()).isEqualTo("<br />");
   }
 
   @Test
   public void testTagTableTrTd() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.openElement("table"));
-    assertSame(b, b.openTr());
-    assertSame(b, b.openTd());
-    assertSame(b, b.append("d<a>ta"));
-    assertSame(b, b.closeTd());
-    assertSame(b, b.closeTr());
-    assertSame(b, b.closeElement("table"));
-    assertEquals("<table><tr><td>d&lt;a&gt;ta</td></tr></table>", b.asString());
+    assertThat(b).isSameAs(b.openElement("table"));
+    assertThat(b).isSameAs(b.openTr());
+    assertThat(b).isSameAs(b.openTd());
+    assertThat(b).isSameAs(b.append("d<a>ta"));
+    assertThat(b).isSameAs(b.closeTd());
+    assertThat(b).isSameAs(b.closeTr());
+    assertThat(b).isSameAs(b.closeElement("table"));
+    assertThat(b.asString()).isEqualTo("<table><tr><td>d&lt;a&gt;ta</td></tr></table>");
   }
 
   @Test
   public void testTagDiv() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.openDiv());
-    assertSame(b, b.append("d<a>ta"));
-    assertSame(b, b.closeDiv());
-    assertEquals("<div>d&lt;a&gt;ta</div>", b.asString());
+    assertThat(b).isSameAs(b.openDiv());
+    assertThat(b).isSameAs(b.append("d<a>ta"));
+    assertThat(b).isSameAs(b.closeDiv());
+    assertThat(b.asString()).isEqualTo("<div>d&lt;a&gt;ta</div>");
   }
 
   @Test
   public void testTagAnchor() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.openAnchor());
+    assertThat(b).isSameAs(b.openAnchor());
 
-    assertEquals("", b.getAttribute("href"));
-    assertSame(b, b.setAttribute("href", "http://here"));
-    assertEquals("http://here", b.getAttribute("href"));
-    assertSame(b, b.setAttribute("href", "d<a>ta"));
-    assertEquals("d<a>ta", b.getAttribute("href"));
+    assertThat(b.getAttribute("href")).isEmpty();
+    assertThat(b).isSameAs(b.setAttribute("href", "http://here"));
+    assertThat(b.getAttribute("href")).isEqualTo("http://here");
+    assertThat(b).isSameAs(b.setAttribute("href", "d<a>ta"));
+    assertThat(b.getAttribute("href")).isEqualTo("d<a>ta");
 
-    assertEquals("", b.getAttribute("target"));
-    assertSame(b, b.setAttribute("target", null));
-    assertEquals("", b.getAttribute("target"));
+    assertThat(b.getAttribute("target")).isEmpty();
+    assertThat(b).isSameAs(b.setAttribute("target", null));
+    assertThat(b.getAttribute("target")).isEmpty();
 
-    assertSame(b, b.append("go"));
-    assertSame(b, b.closeAnchor());
-    assertEquals("<a href=\"d&lt;a&gt;ta\">go</a>", b.asString());
+    assertThat(b).isSameAs(b.append("go"));
+    assertThat(b).isSameAs(b.closeAnchor());
+    assertThat(b.asString()).isEqualTo("<a href=\"d&lt;a&gt;ta\">go</a>");
   }
 
   @Test
   public void testTagHeightWidth() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.openElement("img"));
-    assertSame(b, b.setHeight(100));
-    assertSame(b, b.setWidth(42));
-    assertSame(b, b.closeSelf());
-    assertEquals("<img height=\"100\" width=\"42\" />", b.asString());
+    assertThat(b).isSameAs(b.openElement("img"));
+    assertThat(b).isSameAs(b.setHeight(100));
+    assertThat(b).isSameAs(b.setWidth(42));
+    assertThat(b).isSameAs(b.closeSelf());
+    assertThat(b.asString()).isEqualTo("<img height=\"100\" width=\"42\" />");
   }
 
   @Test
   public void testStyleName() {
     final SafeHtmlBuilder b = new SafeHtmlBuilder();
-    assertSame(b, b.openSpan());
-    assertSame(b, b.setStyleName("foo"));
-    assertSame(b, b.addStyleName("bar"));
-    assertSame(b, b.append("d<a>ta"));
-    assertSame(b, b.closeSpan());
-    assertEquals("<span class=\"foo bar\">d&lt;a&gt;ta</span>", b.asString());
+    assertThat(b).isSameAs(b.openSpan());
+    assertThat(b).isSameAs(b.setStyleName("foo"));
+    assertThat(b).isSameAs(b.addStyleName("bar"));
+    assertThat(b).isSameAs(b.append("d<a>ta"));
+    assertThat(b).isSameAs(b.closeSpan());
+    assertThat(b.asString()).isEqualTo("<span class=\"foo bar\">d&lt;a&gt;ta</span>");
   }
 
   @Test
   public void testRejectJavaScript_AnchorHref() {
     final String href = "javascript:window.close();";
-    try {
-      new SafeHtmlBuilder().openAnchor().setAttribute("href", href);
-      fail("accepted javascript in a href");
-    } catch (RuntimeException e) {
-      assertEquals("javascript unsafe in href: " + href, e.getMessage());
-    }
+    exception.expect(RuntimeException.class);
+    exception.expectMessage("javascript unsafe in href: " + href);
+    new SafeHtmlBuilder().openAnchor().setAttribute("href", href);
   }
 
   @Test
   public void testRejectJavaScript_ImgSrc() {
     final String href = "javascript:window.close();";
-    try {
-      new SafeHtmlBuilder().openElement("img").setAttribute("src", href);
-      fail("accepted javascript in img src");
-    } catch (RuntimeException e) {
-      assertEquals("javascript unsafe in href: " + href, e.getMessage());
-    }
+    exception.expect(RuntimeException.class);
+    exception.expectMessage("javascript unsafe in href: " + href);
+    new SafeHtmlBuilder().openElement("img").setAttribute("src", href);
   }
 
   @Test
   public void testRejectJavaScript_FormAction() {
     final String href = "javascript:window.close();";
-    try {
-      new SafeHtmlBuilder().openElement("form").setAttribute("action", href);
-      fail("accepted javascript in form action");
-    } catch (RuntimeException e) {
-      assertEquals("javascript unsafe in href: " + href, e.getMessage());
-    }
+    exception.expect(RuntimeException.class);
+    exception.expectMessage("javascript unsafe in href: " + href);
+    new SafeHtmlBuilder().openElement("form").setAttribute("action", href);
   }
 
   private static String escape(final char c) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
index f89c62b..bf96d77 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -24,72 +23,81 @@
   public void testLinkify_SimpleHttp1() {
     final SafeHtml o = html("A http://go.here/ B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B");
   }
 
   @Test
   public void testLinkify_SimpleHttps2() {
     final SafeHtml o = html("A https://go.here/ B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B");
   }
 
   @Test
   public void testLinkify_Parens1() {
     final SafeHtml o = html("A (http://go.here/) B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B");
   }
 
   @Test
   public void testLinkify_Parens() {
     final SafeHtml o = html("A http://go.here/#m() B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B");
   }
 
   @Test
   public void testLinkify_AngleBrackets1() {
     final SafeHtml o = html("A <http://go.here/> B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B");
   }
 
   @Test
   public void testLinkify_TrailingPlainLetter() {
     final SafeHtml o = html("A http://go.here/foo B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"http://go.here/foo\" target=\"_blank\">http://go.here/foo</a> B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"http://go.here/foo\" target=\"_blank\">http://go.here/foo</a> B");
   }
 
   @Test
   public void testLinkify_TrailingDot() {
     final SafeHtml o = html("A http://go.here/. B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>. B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>. B");
   }
 
   @Test
   public void testLinkify_TrailingComma() {
     final SafeHtml o = html("A http://go.here/, B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>, B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>, B");
   }
 
   @Test
   public void testLinkify_TrailingDotDot() {
     final SafeHtml o = html("A http://go.here/.. B");
     final SafeHtml n = o.linkify();
-    assertNotSame(o, n);
-    assertEquals("A <a href=\"http://go.here/.\" target=\"_blank\">http://go.here/.</a>. B", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A <a href=\"http://go.here/.\" target=\"_blank\">http://go.here/.</a>. B");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
index 4fa6254..0401c9e 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -14,9 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -28,8 +26,8 @@
   @Test
   public void testReplaceEmpty() {
     SafeHtml o = html("A\nissue42\nB");
-    assertSame(o, o.replaceAll(null));
-    assertSame(o, o.replaceAll(Collections.<FindReplace> emptyList()));
+    assertThat(o.replaceAll(null)).isSameAs(o);
+    assertThat(o.replaceAll(Collections.<FindReplace> emptyList())).isSameAs(o);
   }
 
   @Test
@@ -37,8 +35,9 @@
     SafeHtml o = html("A\nissue 42\nB");
     SafeHtml n = o.replaceAll(repls(
         new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
-    assertNotSame(o, n);
-    assertEquals("A\n<a href=\"?42\">issue 42</a>\nB", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A\n<a href=\"?42\">issue 42</a>\nB");
   }
 
   @Test
@@ -46,8 +45,9 @@
     SafeHtml o = html("issue 42");
     SafeHtml n = o.replaceAll(repls(
         new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
-    assertNotSame(o, n);
-    assertEquals("<a href=\"?42\">issue 42</a>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<a href=\"?42\">issue 42</a>");
   }
 
   @Test
@@ -55,12 +55,12 @@
     SafeHtml o = html("A\nissue 42\nissue 9918\nB");
     SafeHtml n = o.replaceAll(repls(
         new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
-    assertNotSame(o, n);
-    assertEquals("A\n"
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A\n"
         + "<a href=\"?42\">issue 42</a>\n"
         + "<a href=\"?9918\">issue 9918</a>\n"
-        + "B"
-    , n.asString());
+        + "B");
   }
 
   @Test
@@ -71,12 +71,12 @@
             "<a href=\"gwtexpui-bug?$2\">$1</a>"),
         new RawFindReplace("(issue\\s+(\\d+))",
             "<a href=\"generic-bug?$2\">$1</a>")));
-    assertNotSame(o, n);
-    assertEquals("A\n"
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "A\n"
         + "<a href=\"generic-bug?42\">issue 42</a>\n"
         + "Really <a href=\"gwtexpui-bug?9918\">GWTEXPUI-9918</a> is better\n"
-        + "B"
-    , n.asString());
+        + "B");
   }
 
   @Test
@@ -86,9 +86,9 @@
     RawFindReplace bc = new RawFindReplace("bc", "23");
     RawFindReplace cd = new RawFindReplace("cd", "YZ");
 
-    assertEquals("ABcd", o.replaceAll(repls(ab, bc)).asString());
-    assertEquals("ABcd", o.replaceAll(repls(bc, ab)).asString());
-    assertEquals("ABYZ", o.replaceAll(repls(ab, bc, cd)).asString());
+    assertThat(o.replaceAll(repls(ab, bc)).asString()).isEqualTo("ABcd");
+    assertThat(o.replaceAll(repls(bc, ab)).asString()).isEqualTo("ABcd");
+    assertThat(o.replaceAll(repls(ab, bc, cd)).asString()).isEqualTo("ABYZ");
   }
 
   @Test
@@ -97,8 +97,8 @@
     RawFindReplace ab = new RawFindReplace("ab", "AB");
     RawFindReplace abc = new RawFindReplace("[^d][^d][^d]", "234");
 
-    assertEquals("ABcd", o.replaceAll(repls(ab, abc)).asString());
-    assertEquals("234d", o.replaceAll(repls(abc, ab)).asString());
+    assertThat(o.replaceAll(repls(ab, abc)).asString()).isEqualTo("ABcd");
+    assertThat(o.replaceAll(repls(abc, ab)).asString()).isEqualTo("234d");
   }
 
   @Test
@@ -107,8 +107,8 @@
     RawFindReplace ab1 = new RawFindReplace("ab", "AB");
     RawFindReplace ab2 = new RawFindReplace("[^cd][^cd]", "12");
 
-    assertEquals("ABcd", o.replaceAll(repls(ab1, ab2)).asString());
-    assertEquals("12cd", o.replaceAll(repls(ab2, ab1)).asString());
+    assertThat(o.replaceAll(repls(ab1, ab2)).asString()).isEqualTo("ABcd");
+    assertThat(o.replaceAll(repls(ab2, ab1)).asString()).isEqualTo("12cd");
   }
 
   @Test
@@ -116,10 +116,10 @@
     SafeHtml o = html("abcd");
     LinkFindReplace evil = new LinkFindReplace("(b)", "javascript:alert('$1')");
     LinkFindReplace ok = new LinkFindReplace("(b)", "/$1");
-    assertEquals("abcd", o.replaceAll(repls(evil)).asString());
+    assertThat(o.replaceAll(repls(evil)).asString()).isEqualTo("abcd");
     String linked = "a<a href=\"/b\">b</a>cd";
-    assertEquals(linked, o.replaceAll(repls(ok)).asString());
-    assertEquals(linked, o.replaceAll(repls(evil, ok)).asString());
+    assertThat(o.replaceAll(repls(ok)).asString()).isEqualTo(linked);
+    assertThat(o.replaceAll(repls(evil, ok)).asString()).isEqualTo(linked);
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
index ea91ee3..9a7108d 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -31,40 +30,40 @@
   public void testBulletList1() {
     final SafeHtml o = html("A\n\n* line 1\n* 2nd line");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>"//
-        + BEGIN_LIST //
-        + item("line 1") //
-        + item("2nd line") //
-        + END_LIST //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + BEGIN_LIST
+        + item("line 1")
+        + item("2nd line")
+        + END_LIST);
   }
 
   @Test
   public void testBulletList2() {
     final SafeHtml o = html("A\n\n* line 1\n* 2nd line\n\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>"//
-        + BEGIN_LIST //
-        + item("line 1") //
-        + item("2nd line") //
-        + END_LIST //
-        + "<p>B</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + BEGIN_LIST
+        + item("line 1")
+        + item("2nd line")
+        + END_LIST
+        + "<p>B</p>");
   }
 
   @Test
   public void testBulletList3() {
     final SafeHtml o = html("* line 1\n* 2nd line\n\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals(BEGIN_LIST //
-        + item("line 1") //
-        + item("2nd line") //
-        + END_LIST //
-        + "<p>B</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        BEGIN_LIST
+        + item("line 1")
+        + item("2nd line")
+        + END_LIST
+        + "<p>B</p>");
   }
 
   @Test
@@ -73,13 +72,13 @@
         + "* Be on IMAP or EAS (not on POP)\n"//
         + "* Be very unlucky\n");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>To see this bug, you have to:</p>" //
-        + BEGIN_LIST //
-        + item("Be on IMAP or EAS (not on POP)") //
-        + item("Be very unlucky") //
-        + END_LIST //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>To see this bug, you have to:</p>"
+        + BEGIN_LIST
+        + item("Be on IMAP or EAS (not on POP)")
+        + item("Be very unlucky")
+        + END_LIST);
   }
 
   @Test
@@ -89,53 +88,53 @@
         + "* Be on IMAP or EAS (not on POP)\n"//
         + "* Be very unlucky\n");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>To see this bug, you have to:</p>" //
-        + BEGIN_LIST //
-        + item("Be on IMAP or EAS (not on POP)") //
-        + item("Be very unlucky") //
-        + END_LIST //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>To see this bug, you have to:</p>"
+        + BEGIN_LIST
+        + item("Be on IMAP or EAS (not on POP)")
+        + item("Be very unlucky")
+        + END_LIST);
   }
 
   @Test
   public void testDashList1() {
     final SafeHtml o = html("A\n\n- line 1\n- 2nd line");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>"//
-        + BEGIN_LIST //
-        + item("line 1") //
-        + item("2nd line") //
-        + END_LIST //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + BEGIN_LIST
+        + item("line 1")
+        + item("2nd line")
+        + END_LIST);
   }
 
   @Test
   public void testDashList2() {
     final SafeHtml o = html("A\n\n- line 1\n- 2nd line\n\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>"//
-        + BEGIN_LIST //
-        + item("line 1") //
-        + item("2nd line") //
-        + END_LIST //
-        + "<p>B</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + BEGIN_LIST
+        + item("line 1")
+        + item("2nd line")
+        + END_LIST
+        + "<p>B</p>");
   }
 
   @Test
   public void testDashList3() {
     final SafeHtml o = html("- line 1\n- 2nd line\n\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals(BEGIN_LIST //
-        + item("line 1") //
-        + item("2nd line") //
-        + END_LIST //
-        + "<p>B</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        BEGIN_LIST
+        + item("line 1")
+        + item("2nd line")
+        + END_LIST
+        + "<p>B</p>");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
index 57399dc..8085cac 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -31,56 +30,56 @@
   public void testPreformat1() {
     final SafeHtml o = html("A\n\n  This is pre\n  formatted");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>"//
-        + "<p>" //
-        + pre("  This is pre") //
-        + pre("  formatted") //
-        + "</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + "<p>"
+        + pre("  This is pre")
+        + pre("  formatted")
+        + "</p>");
   }
 
   @Test
   public void testPreformat2() {
     final SafeHtml o = html("A\n\n  This is pre\n  formatted\n\nbut this is not");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>" //
-        + "<p>" //
-        + pre("  This is pre") //
-        + pre("  formatted") //
-        + "</p>" //
-        + "<p>but this is not</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + "<p>"
+        + pre("  This is pre")
+        + pre("  formatted")
+        + "</p>"
+        + "<p>but this is not</p>");
   }
 
   @Test
   public void testPreformat3() {
     final SafeHtml o = html("A\n\n  Q\n    <R>\n  S\n\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A</p>" //
-        + "<p>" //
-        + pre("  Q") //
-        + pre("    &lt;R&gt;") //
-        + pre("  S") //
-        + "</p>" //
-        + "<p>B</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A</p>"
+        + "<p>"
+        + pre("  Q")
+        + pre("    &lt;R&gt;")
+        + pre("  S")
+        + "</p>"
+        + "<p>B</p>");
   }
 
   @Test
   public void testPreformat4() {
     final SafeHtml o = html("  Q\n    <R>\n  S\n\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>" //
-        + pre("  Q") //
-        + pre("    &lt;R&gt;") //
-        + pre("  S") //
-        + "</p>" //
-        + "<p>B</p>" //
-    , n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>"
+        + pre("  Q")
+        + pre("    &lt;R&gt;")
+        + pre("  S")
+        + "</p>"
+        + "<p>B</p>");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
index f6b6b91..766760f 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -31,32 +30,28 @@
   public void testQuote1() {
     final SafeHtml o = html("> I'm happy\n > with quotes!\n\nSee above.");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals(
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
         quote("I&#39;m happy\nwith quotes!")
-        + "<p>See above.</p>",
-      n.asString());
+        + "<p>See above.</p>");
   }
 
   @Test
   public void testQuote2() {
     final SafeHtml o = html("See this said:\n\n > a quoted\n > string block\n\nOK?");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals(
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
         "<p>See this said:</p>"
         + quote("a quoted\nstring block")
-        + "<p>OK?</p>",
-      n.asString());
+        + "<p>OK?</p>");
   }
 
   @Test
   public void testNestedQuotes1() {
     final SafeHtml o = html(" > > prior\n > \n > next\n");
     final SafeHtml n = o.wikify();
-    assertEquals(
-      quote(quote("prior") + "next\n"),
-      n.asString());
+    assertThat(n.asString()).isEqualTo(quote(quote("prior") + "next\n"));
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
index 3c261d0..41d6f37 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -24,80 +23,85 @@
   public void testWikify_OneLine1() {
     final SafeHtml o = html("A  B");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A  B</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo("<p>A  B</p>");
   }
 
   @Test
   public void testWikify_OneLine2() {
     final SafeHtml o = html("A  B\n");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A  B\n</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo("<p>A  B\n</p>");
   }
 
   @Test
   public void testWikify_OneParagraph1() {
     final SafeHtml o = html("A\nB");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A\nB</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo("<p>A\nB</p>");
   }
 
   @Test
   public void testWikify_OneParagraph2() {
     final SafeHtml o = html("A\nB\n");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A\nB\n</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo("<p>A\nB\n</p>");
   }
 
   @Test
   public void testWikify_TwoParagraphs() {
     final SafeHtml o = html("A\nB\n\nC\nD");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A\nB</p><p>C\nD</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo("<p>A\nB</p><p>C\nD</p>");
   }
 
   @Test
   public void testLinkify_SimpleHttp1() {
     final SafeHtml o = html("A http://go.here/ B");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A <a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a> B</p>");
   }
 
   @Test
   public void testLinkify_SimpleHttps2() {
     final SafeHtml o = html("A https://go.here/ B");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A <a href=\"https://go.here/\" target=\"_blank\">https://go.here/</a> B</p>");
   }
 
   @Test
   public void testLinkify_Parens1() {
     final SafeHtml o = html("A (http://go.here/) B");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A (<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>) B</p>");
   }
 
   @Test
   public void testLinkify_Parens() {
     final SafeHtml o = html("A http://go.here/#m() B");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A <a href=\"http://go.here/#m()\" target=\"_blank\">http://go.here/#m()</a> B</p>");
   }
 
   @Test
   public void testLinkify_AngleBrackets1() {
     final SafeHtml o = html("A <http://go.here/> B");
     final SafeHtml n = o.wikify();
-    assertNotSame(o, n);
-    assertEquals("<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B</p>", n.asString());
+    assertThat(o).isNotSameAs(n);
+    assertThat(n.asString()).isEqualTo(
+        "<p>A &lt;<a href=\"http://go.here/\" target=\"_blank\">http://go.here/</a>&gt; B</p>");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index 2eb0684..436714a 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -1,3 +1,12 @@
+EXPORTED_DEPS = [
+  '//gerrit-common:client',
+  '//gerrit-gwtexpui:Clippy',
+  '//gerrit-gwtexpui:GlobalKey',
+  '//gerrit-gwtexpui:Progress',
+  '//gerrit-gwtexpui:SafeHtml',
+  '//gerrit-gwtexpui:UserAgent',
+]
+DEPS = ['//lib/gwt:user']
 SRC = 'src/main/java/com/google/gerrit/'
 DIFFY = glob(['src/main/resources/com/google/gerrit/client/diffy*.png'])
 
@@ -6,21 +15,17 @@
   srcs = glob([SRC + 'client/**/*.java']),
   gwt_xml = SRC + 'GerritGwtUICommon.gwt.xml',
   resources = glob(['src/main/**/*']),
-  deps = ['//lib/gwt:user'],
+  exported_deps = EXPORTED_DEPS,
+  provided_deps = DEPS,
   visibility = ['PUBLIC'],
 )
 
 java_library(
   name = 'client-lib',
-  exported_deps = [':client-lib2'],
-  visibility = ['PUBLIC'],
-)
-
-java_library(
-  name = 'client-lib2',
   srcs = glob(['src/main/**/*.java']),
   resources = glob(['src/main/**/*']),
-  provided_deps = ['//lib/gwt:user'],
+  exported_deps = EXPORTED_DEPS,
+  provided_deps = DEPS,
   visibility = ['PUBLIC'],
 )
 
@@ -44,7 +49,6 @@
 genrule(
   name = 'diffy_image_files_ln',
   cmd = 'ln -s $(location :diffy_image_files) $OUT',
-  deps = [':diffy_image_files'],
   out = 'diffy_images.jar',
 )
 
@@ -52,3 +56,17 @@
   name = 'diffy_image_files',
   resources = DIFFY,
 )
+
+java_test(
+  name = 'client_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':client',
+    '//lib:junit',
+    '//lib/gwt:user',
+    '//lib/jgit:jgit',
+  ],
+  source_under_test = [':client'],
+  vm_args = ['-Xmx512m'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml b/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
index eb551c4..c147195 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/GerritGwtUICommon.gwt.xml
@@ -14,5 +14,13 @@
  limitations under the License.
 -->
 <module>
+  <inherits name='org.eclipse.jgit.JGit'/>
+  <inherits name='com.google.gerrit.Common'/>
+  <inherits name='com.google.gerrit.extensions.Extensions'/>
+  <inherits name='com.google.gerrit.prettify.PrettyFormatter'/>
+  <inherits name='com.google.gwtexpui.clippy.Clippy'/>
+  <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
+  <inherits name='com.google.gwtexpui.progress.Progress'/>
+  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
   <source path='client' />
 </module>
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/AccountFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/AccountFormatter.java
new file mode 100644
index 0000000..1b41f62
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/AccountFormatter.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2015 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.client;
+
+import com.google.gerrit.client.info.AccountInfo;
+
+public class AccountFormatter {
+  private final String anonymousCowardName;
+
+  public AccountFormatter(String anonymousCowardName) {
+    this.anonymousCowardName = anonymousCowardName;
+  }
+
+  /**
+   * Formats an account as a name and an email address.
+   * <p>
+   * Example output:
+   * <ul>
+   * <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated</li>
+   * <li>{@code A U. Thor (12)}: missing email address</li>
+   * <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name</li>
+   * <li>{@code Anonymous Coward (12)}: missing name and email address</li>
+   * </ul>
+   */
+  public String nameEmail(AccountInfo info) {
+    String name = info.name();
+    if (name == null || name.trim().isEmpty()) {
+      name = anonymousCowardName;
+    }
+
+    StringBuilder b = new StringBuilder().append(name);
+    if (info.email() != null) {
+      b.append(" <").append(info.email()).append(">");
+    } else if (info._accountId() > 0) {
+      b.append(" (").append(info._accountId()).append(")");
+    }
+    return b.toString();
+  }
+
+  /**
+   * Formats an account name.
+   * <p>
+   * If the account has a full name, it returns only the full name. Otherwise it
+   * returns a longer form that includes the email address.
+   */
+  public String name(AccountInfo ai) {
+    if (ai.name() != null && !ai.name().trim().isEmpty()) {
+      return ai.name();
+    }
+    String email = ai.email();
+    if (email != null) {
+      int at = email.indexOf('@');
+      return 0 < at ? email.substring(0, at) : email;
+    }
+    return nameEmail(ai);
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
new file mode 100644
index 0000000..032c47c
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 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.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Constants;
+
+public interface CommonConstants extends Constants {
+  public static final CommonConstants C = GWT.create(CommonConstants.class);
+
+  String inTheFuture();
+  String month();
+  String months();
+  String year();
+  String years();
+
+  String oneSecondAgo();
+  String oneMinuteAgo();
+  String oneHourAgo();
+  String oneDayAgo();
+  String oneWeekAgo();
+  String oneMonthAgo();
+  String oneYearAgo();
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.properties b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.properties
new file mode 100644
index 0000000..3202bfc
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonConstants.properties
@@ -0,0 +1,13 @@
+inTheFuture = in the future
+month = month
+months = months
+years = years
+year = year
+
+oneSecondAgo = 1 second ago
+oneMinuteAgo = 1 minute ago
+oneHourAgo = 1 hour ago
+oneDayAgo = 1 day ago
+oneWeekAgo = 1 week ago
+oneMonthAgo = 1 month ago
+oneYearAgo = 1 year ago
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
new file mode 100644
index 0000000..aa5e3cf
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2015 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.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Messages;
+
+public interface CommonMessages extends Messages {
+  public static final CommonMessages M = GWT.create(CommonMessages.class);
+
+  String secondsAgo(long seconds);
+  String minutesAgo(long minutes);
+  String hoursAgo(long hours);
+  String daysAgo(long days);
+  String weeksAgo(long weeks);
+  String monthsAgo(long months);
+  String yearsAgo(long years);
+  String years0MonthsAgo(long years, String yearLabel);
+  String yearsMonthsAgo(long years, String yearLabel, long months,
+      String monthLabel);
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.properties b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.properties
new file mode 100644
index 0000000..738602e
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/CommonMessages.properties
@@ -0,0 +1,9 @@
+secondsAgo = {0} seconds ago
+minutesAgo = {0} minutes ago
+hoursAgo = {0} hours ago
+daysAgo = {0} days ago
+weeksAgo = {0} weeks ago
+monthsAgo = {0} months ago
+years0MonthsAgo = {0} {1} ago
+yearsMonthsAgo = {0} {1}, {2} {3} ago
+yearsAgo = {0} years ago
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
new file mode 100644
index 0000000..b6af366
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2015 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.client;
+
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gwt.i18n.client.DateTimeFormat;
+
+import java.util.Date;
+
+public class DateFormatter {
+  private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
+
+  private final DateTimeFormat sTime;
+  private final DateTimeFormat sDate;
+  private final DateTimeFormat sdtFmt;
+  private final DateTimeFormat mDate;
+  private final DateTimeFormat dtfmt;
+
+  public DateFormatter(AccountPreferencesInfo prefs) {
+    String fmt_sTime = prefs.timeFormat().getFormat();
+    String fmt_sDate = prefs.dateFormat().getShortFormat();
+    String fmt_mDate = prefs.dateFormat().getLongFormat();
+
+    sTime = DateTimeFormat.getFormat(fmt_sTime);
+    sDate = DateTimeFormat.getFormat(fmt_sDate);
+    sdtFmt = DateTimeFormat.getFormat(fmt_sDate + " " + fmt_sTime);
+    mDate = DateTimeFormat.getFormat(fmt_mDate);
+    dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
+  }
+
+  /** Format a date using a really short format. */
+  public String shortFormat(Date dt) {
+    if (dt == null) {
+      return "";
+    }
+
+    Date now = new Date();
+    dt = new Date(dt.getTime());
+    if (mDate.format(now).equals(mDate.format(dt))) {
+      // Same day as today, report only the time.
+      //
+      return sTime.format(dt);
+
+    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
+      // Within the last year, show a shorter date.
+      //
+      return sDate.format(dt);
+
+    } else {
+      // Report only date and year, its far away from now.
+      //
+      return mDate.format(dt);
+    }
+  }
+
+  /** Format a date using a really short format. */
+  public String shortFormatDayTime(Date dt) {
+    if (dt == null) {
+      return "";
+    }
+
+    Date now = new Date();
+    dt = new Date(dt.getTime());
+    if (mDate.format(now).equals(mDate.format(dt))) {
+      // Same day as today, report only the time.
+      //
+      return sTime.format(dt);
+
+    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
+      // Within the last year, show a shorter date.
+      //
+      return sdtFmt.format(dt);
+
+    } else {
+      // Report only date and year, its far away from now.
+      //
+      return mDate.format(dt);
+    }
+  }
+
+  /** Format a date using the locale's medium length format. */
+  public String mediumFormat(final Date dt) {
+    if (dt == null) {
+      return "";
+    }
+    return dtfmt.format(new Date(dt.getTime()));
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
new file mode 100644
index 0000000..61f73c0
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 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.client;
+
+public enum GerritUiExtensionPoint {
+  /* ChangeScreen */
+  CHANGE_SCREEN_HEADER,
+  CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
+  CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
+  CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+
+  /* MyPasswordScreen */
+  PASSWORD_SCREEN_BOTTOM,
+
+  /* MyPreferencesScreen */
+  PREFERENCES_SCREEN_BOTTOM,
+
+  /* MyProfileScreen */
+  PROFILE_SCREEN_BOTTOM,
+
+  /* ProjectInfoScreen */
+  PROJECT_INFO_SCREEN_TOP, PROJECT_INFO_SCREEN_BOTTOM;
+
+  public enum Key {
+    ACCOUNT_INFO, CHANGE_INFO, PROJECT_NAME
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
similarity index 77%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
index 5fc8cb3..eb4b0ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.client;
 
-import com.google.gerrit.client.changes.Util;
+import static com.google.gerrit.client.CommonConstants.C;
+import static com.google.gerrit.client.CommonMessages.M;
 
 import java.util.Date;
 
@@ -24,17 +25,11 @@
  */
 public class RelativeDateFormatter {
   static final long SECOND_IN_MILLIS = 1000;
-
   static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
-
   static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
-
   static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
-
   static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
-
   static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
-
   static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
 
   /**
@@ -47,15 +42,17 @@
     long ageMillis = (new Date()).getTime() - when.getTime();
 
     // shouldn't happen in a perfect world
-    if (ageMillis < 0) return Util.C.inTheFuture();
+    if (ageMillis < 0) {
+      return C.inTheFuture();
+    }
 
     // seconds
     if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) {
       long seconds = round(ageMillis, SECOND_IN_MILLIS);
       if (seconds == 1) {
-        return Util.C.oneSecondAgo();
+        return C.oneSecondAgo();
       } else {
-        return Util.M.secondsAgo(seconds);
+        return M.secondsAgo(seconds);
       }
     }
 
@@ -63,9 +60,9 @@
     if (ageMillis < upperLimit(HOUR_IN_MILLIS)) {
       long minutes = round(ageMillis, MINUTE_IN_MILLIS);
       if (minutes == 1) {
-        return Util.C.oneMinuteAgo();
+        return C.oneMinuteAgo();
       } else {
-        return Util.M.minutesAgo(minutes);
+        return M.minutesAgo(minutes);
       }
     }
 
@@ -73,9 +70,9 @@
     if (ageMillis < upperLimit(DAY_IN_MILLIS)) {
       long hours = round(ageMillis, HOUR_IN_MILLIS);
       if (hours == 1) {
-        return Util.C.oneHourAgo();
+        return C.oneHourAgo();
       } else {
-        return Util.M.hoursAgo(hours);
+        return M.hoursAgo(hours);
       }
     }
 
@@ -83,9 +80,9 @@
     if (ageMillis < 14 * DAY_IN_MILLIS) {
       long days = round(ageMillis, DAY_IN_MILLIS);
       if (days == 1) {
-        return Util.C.oneDayAgo();
+        return C.oneDayAgo();
       } else {
-        return Util.M.daysAgo(days);
+        return M.daysAgo(days);
       }
     }
 
@@ -93,9 +90,9 @@
     if (ageMillis < 10 * WEEK_IN_MILLIS) {
       long weeks = round(ageMillis, WEEK_IN_MILLIS);
       if (weeks == 1) {
-        return Util.C.oneWeekAgo();
+        return C.oneWeekAgo();
       } else {
-        return Util.M.weeksAgo(weeks);
+        return M.weeksAgo(weeks);
       }
     }
 
@@ -103,32 +100,31 @@
     if (ageMillis < YEAR_IN_MILLIS) {
       long months = round(ageMillis, MONTH_IN_MILLIS);
       if (months == 1) {
-        return Util.C.oneMonthAgo();
+        return C.oneMonthAgo();
       } else {
-        return Util.M.monthsAgo(months);
+        return M.monthsAgo(months);
       }
     }
 
     // up to 5 years use "year, months" rounded to months
     if (ageMillis < 5 * YEAR_IN_MILLIS) {
       long years = ageMillis / YEAR_IN_MILLIS;
-      String yearLabel = (years > 1) ? Util.C.years() : Util.C.year();
+      String yearLabel = (years > 1) ? C.years() : C.year();
       long months = round(ageMillis % YEAR_IN_MILLIS, MONTH_IN_MILLIS);
-      String monthLabel =
-          (months > 1) ? Util.C.months() : (months == 1 ? Util.C.month() : "");
+      String monthLabel = (months > 1) ? C.months() : (months == 1 ? C.month() : "");
       if (months == 0) {
-        return Util.M.years0MonthsAgo(years, yearLabel);
+        return M.years0MonthsAgo(years, yearLabel);
       } else {
-        return Util.M.yearsMonthsAgo(years, yearLabel, months, monthLabel);
+        return M.yearsMonthsAgo(years, yearLabel, months, monthLabel);
       }
     }
 
     // years
     long years = round(ageMillis, YEAR_IN_MILLIS);
     if (years == 1) {
-      return Util.C.oneYearAgo();
+      return C.oneYearAgo();
     } else {
-      return Util.M.yearsAgo(years);
+      return M.yearsAgo(years);
     }
   }
 
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
index c4e2167..a5a02cd 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -18,90 +18,93 @@
 import com.google.gwt.resources.client.ImageResource;
 
 public interface Resources extends ClientBundle {
+  @Source("addFileComment.png")
+  public ImageResource addFileComment();
+
+  @Source("arrowDown.png")
+  public ImageResource arrowDown();
+
   @Source("arrowRight.png")
   public ImageResource arrowRight();
 
   @Source("arrowUp.png")
   public ImageResource arrowUp();
 
-  @Source("arrowDown.png")
-  public ImageResource arrowDown();
-
-  @Source("editText.png")
-  public ImageResource edit();
-
-  @Source("mediaFloppy.png")
-  public ImageResource save();
-
-  @Source("starOpen.png")
-  public ImageResource starOpen();
-
-  @Source("starFilled.png")
-  public ImageResource starFilled();
-
-  @Source("greenCheck.png")
-  public ImageResource greenCheck();
-
-  @Source("redNot.png")
-  public ImageResource redNot();
-
-  @Source("editUndo.png")
-  public ImageResource editUndo();
-
-  @Source("downloadIcon.png")
-  public ImageResource downloadIcon();
-
-  @Source("queryIcon.png")
-  public ImageResource queryIcon();
-
-  @Source("addFileComment.png")
-  public ImageResource addFileComment();
-
-  @Source("diffy26.png")
-  public ImageResource gerritAvatar26();
-
-  @Source("draftComments.png")
-  public ImageResource draftComments();
-
-  @Source("readOnly.png")
-  public ImageResource readOnly();
-
-  @Source("gear.png")
-  public ImageResource gear();
-
-  @Source("info.png")
-  public ImageResource info();
-
-  @Source("warning.png")
-  public ImageResource warning();
-
-  @Source("listAdd.png")
-  public ImageResource listAdd();
-
-  @Source("merge.png")
-  public ImageResource merge();
+  @Source("deleteHover.png")
+  public ImageResource deleteHover();
 
   @Source("deleteNormal.png")
   public ImageResource deleteNormal();
 
-  @Source("deleteHover.png")
-  public ImageResource deleteHover();
+  @Source("diffy26.png")
+  public ImageResource gerritAvatar26();
 
-  @Source("undoNormal.png")
-  public ImageResource undoNormal();
+  @Source("downloadIcon.png")
+  public ImageResource downloadIcon();
 
-  @Source("goPrev.png")
-  public ImageResource goPrev();
+  @Source("draftComments.png")
+  public ImageResource draftComments();
+
+  @Source("editText.png")
+  public ImageResource edit();
+
+  @Source("editUndo.png")
+  public ImageResource editUndo();
+
+  @Source("gear.png")
+  public ImageResource gear();
 
   @Source("goNext.png")
   public ImageResource goNext();
 
+  @Source("goPrev.png")
+  public ImageResource goPrev();
+
   @Source("goUp.png")
   public ImageResource goUp();
 
+  @Source("greenCheck.png")
+  public ImageResource greenCheck();
+
+  @Source("info.png")
+  public ImageResource info();
+
+  @Source("listAdd.png")
+  public ImageResource listAdd();
+
+  @Source("mediaFloppy.png")
+  public ImageResource save();
+
+  @Source("merge.png")
+  public ImageResource merge();
+
+  @Source("queryIcon.png")
+  public ImageResource queryIcon();
+
+  @Source("readOnly.png")
+  public ImageResource readOnly();
+
+  @Source("redNot.png")
+  public ImageResource redNot();
+
   @Source("sideBySideDiff.png")
   public ImageResource sideBySideDiff();
 
+  @Source("starFilled.png")
+  public ImageResource starFilled();
+
+  @Source("starOpen.png")
+  public ImageResource starOpen();
+
+  @Source("undoNormal.png")
+  public ImageResource undoNormal();
+
   @Source("unifiedDiff.png")
   public ImageResource unifiedDiff();
+
+  @Source("warning.png")
+  public ImageResource warning();
+
+  @Source("question.png")
+  public ImageResource question();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
similarity index 67%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
index 1127374..6ac0404 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
@@ -12,24 +12,45 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.account;
+package com.google.gerrit.client.info;
 
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
 
 public class AccountInfo extends JavaScriptObject {
-  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
+  public final Account.Id getId() {
+    return new Account.Id(_accountId());
+  }
+
+  public final native int _accountId() /*-{ return this._account_id || 0; }-*/;
   public final native String name() /*-{ return this.name; }-*/;
   public final native String email() /*-{ return this.email; }-*/;
   public final native String username() /*-{ return this.username; }-*/;
 
+  public final Timestamp registeredOn() {
+    Timestamp ts = _getRegisteredOn();
+    if (ts == null) {
+      ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(registeredOnRaw());
+      _setRegisteredOn(ts);
+    }
+    return ts;
+  }
+
+  private final native String registeredOnRaw() /*-{ return this.registered_on; }-*/;
+  private final native Timestamp _getRegisteredOn() /*-{ return this._cts; }-*/;
+  private final native void _setRegisteredOn(Timestamp ts) /*-{ this._cts = ts; }-*/;
+
   /**
    * @return true if the server supplied avatar information about this account.
    *         The information may be an empty list, indicating no avatars are
    *         available, such as when no plugin is installed. This method returns
    *         false if the server did not check on avatars for the account.
    */
-  public final native boolean has_avatar_info()
+  public final native boolean hasAvatarInfo()
   /*-{ return this.hasOwnProperty('avatars') }-*/;
 
   public final AvatarInfo avatar(int sz) {
@@ -46,6 +67,10 @@
   private final native JsArray<AvatarInfo> avatars()
   /*-{ return this.avatars }-*/;
 
+  public final native void name(String n) /*-{ this.name = n }-*/;
+  public final native void email(String e) /*-{ this.email = e }-*/;
+  public final native void username(String n) /*-{ this.username = n }-*/;
+
   public static native AccountInfo create(int id, String name,
       String email, String username) /*-{
     return {'_account_id': id, 'name': name, 'email': email,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
similarity index 67%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
index e9bd5f1..11a1b6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
@@ -12,47 +12,56 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.account;
+package com.google.gerrit.client.info;
 
-import com.google.gerrit.client.extensions.TopMenuItem;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
-public class Preferences extends JavaScriptObject {
-  public static Preferences create(AccountGeneralPreferences in, List<TopMenuItem> myMenus) {
-    Preferences p = createObject().cast();
-    if (in == null) {
-      in = AccountGeneralPreferences.createDefault();
-    }
-    p.changesPerPage(in.getMaximumPageSize());
-    p.showSiteHeader(in.isShowSiteHeader());
-    p.useFlashClipboard(in.isUseFlashClipboard());
-    p.downloadScheme(in.getDownloadUrl());
-    p.downloadCommand(in.getDownloadCommand());
-    p.copySelfOnEmail(in.isCopySelfOnEmails());
-    p.dateFormat(in.getDateFormat());
-    p.timeFormat(in.getTimeFormat());
-    p.relativeDateInChangeTable(in.isRelativeDateInChangeTable());
-    p.sizeBarInChangeTable(in.isSizeBarInChangeTable());
-    p.legacycidInChangeTable(in.isLegacycidInChangeTable());
-    p.muteCommonPathPrefixes(in.isMuteCommonPathPrefixes());
-    p.reviewCategoryStrategy(in.getReviewCategoryStrategy());
-    p.diffView(in.getDiffView());
-    p.setMyMenus(myMenus);
+public class AccountPreferencesInfo extends JavaScriptObject {
+  public static AccountPreferencesInfo create() {
+    return createObject().cast();
+  }
+
+  public static AccountPreferencesInfo createDefault() {
+    AccountGeneralPreferences defaultPrefs =
+        AccountGeneralPreferences.createDefault();
+    AccountPreferencesInfo p = createObject().cast();
+    p.changesPerPage(defaultPrefs.getMaximumPageSize());
+    p.showSiteHeader(defaultPrefs.isShowSiteHeader());
+    p.useFlashClipboard(defaultPrefs.isUseFlashClipboard());
+    p.downloadScheme(defaultPrefs.getDownloadUrl());
+    p.downloadCommand(defaultPrefs.getDownloadCommand());
+    p.copySelfOnEmail(defaultPrefs.isCopySelfOnEmails());
+    p.dateFormat(defaultPrefs.getDateFormat());
+    p.timeFormat(defaultPrefs.getTimeFormat());
+    p.relativeDateInChangeTable(defaultPrefs.isRelativeDateInChangeTable());
+    p.sizeBarInChangeTable(defaultPrefs.isSizeBarInChangeTable());
+    p.legacycidInChangeTable(defaultPrefs.isLegacycidInChangeTable());
+    p.muteCommonPathPrefixes(defaultPrefs.isMuteCommonPathPrefixes());
+    p.reviewCategoryStrategy(defaultPrefs.getReviewCategoryStrategy());
+    p.diffView(defaultPrefs.getDiffView());
     return p;
   }
 
   public final short changesPerPage() {
-    return get("changes_per_page", AccountGeneralPreferences.DEFAULT_PAGESIZE);
+    short changesPerPage =
+        get("changes_per_page", AccountGeneralPreferences.DEFAULT_PAGESIZE);
+    return 0 < changesPerPage
+        ? changesPerPage
+        : AccountGeneralPreferences.DEFAULT_PAGESIZE;
   }
   private final native short get(String n, int d)
   /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
@@ -63,11 +72,7 @@
   public final native boolean useFlashClipboard()
   /*-{ return this.use_flash_clipboard || false }-*/;
 
-  public final DownloadScheme downloadScheme() {
-    String s = downloadSchemeRaw();
-    return s != null ? DownloadScheme.valueOf(s) : null;
-  }
-  private final native String downloadSchemeRaw()
+  public final native String downloadScheme()
   /*-{ return this.download_scheme }-*/;
 
   public final DownloadCommand downloadCommand() {
@@ -132,10 +137,7 @@
   public final native void useFlashClipboard(boolean u)
   /*-{ this.use_flash_clipboard = u }-*/;
 
-  public final void downloadScheme(DownloadScheme d) {
-    downloadSchemeRaw(d != null ? d.toString() : null);
-  }
-  private final native void downloadSchemeRaw(String d)
+  public final native void downloadScheme(String d)
   /*-{ this.download_scheme = d }-*/;
 
   public final void downloadCommand(DownloadCommand d) {
@@ -183,7 +185,7 @@
   private final native void diffViewRaw(String d)
   /*-{ this.diff_view = d }-*/;
 
-  final void setMyMenus(List<TopMenuItem> myMenus) {
+  public final void setMyMenus(List<TopMenuItem> myMenus) {
     initMy();
     for (TopMenuItem n : myMenus) {
       addMy(n);
@@ -192,6 +194,26 @@
   final native void initMy() /*-{ this.my = []; }-*/;
   final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
 
-  protected Preferences() {
+  public final Map<String, String> urlAliases() {
+    Map<String, String> urlAliases = new HashMap<>();
+    for (String k : Natives.keys(_urlAliases())) {
+      urlAliases.put(k, urlAliasToken(k));
+    }
+    return urlAliases;
+  }
+
+  private final native String urlAliasToken(String m) /*-{ return this.url_aliases[m]; }-*/;
+  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+
+  public final void setUrlAliases(Map<String, String> urlAliases) {
+    initUrlAliases();
+    for (Map.Entry<String, String> e : urlAliases.entrySet()) {
+      putUrlAlias(e.getKey(), e.getValue());
+    }
+  }
+  private final native void putUrlAlias(String m, String t) /*-{ this.url_aliases[m] = t; }-*/;
+  private final native void initUrlAliases() /*-{ this.url_aliases = {}; }-*/;
+
+  protected AccountPreferencesInfo() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ActionInfo.java
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ActionInfo.java
index ef0c4b5..3b283ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ActionInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.actions;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
 
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
new file mode 100644
index 0000000..15d1c6c
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2015 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.client.info;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuthInfo extends JavaScriptObject {
+  public final AuthType authType() {
+    return AuthType.valueOf(authTypeRaw());
+  }
+
+  public final boolean isLdap() {
+    return authType() == AuthType.LDAP || authType() == AuthType.LDAP_BIND;
+  }
+  public final boolean isOpenId() {
+    return authType() == AuthType.OPENID;
+  }
+
+  public final boolean isOAuth() {
+    return authType() == AuthType.OAUTH;
+  }
+
+  public final boolean isDev() {
+    return authType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
+  }
+
+  public final boolean isClientSslCertLdap() {
+    return authType() == AuthType.CLIENT_SSL_CERT_LDAP;
+  }
+
+  public final boolean isCustomExtension() {
+    return authType() == AuthType.CUSTOM_EXTENSION;
+  }
+
+  public final boolean canEdit(Account.FieldName f) {
+    return editableAccountFields().contains(f);
+  }
+
+  public final List<Account.FieldName> editableAccountFields() {
+    List<Account.FieldName> fields = new ArrayList<>();
+    for (String f : Natives.asList(_editableAccountFields())) {
+      fields.add(Account.FieldName.valueOf(f));
+    }
+    return fields;
+  }
+
+  public final boolean siteHasUsernames() {
+    if (isCustomExtension()
+        && httpPasswordUrl() != null
+        && !canEdit(FieldName.USER_NAME)) {
+      return false;
+    }
+    return true;
+  }
+
+  public final boolean isHttpPasswordSettingsEnabled() {
+    if (isLdap() && isGitBasicAuth()) {
+      return false;
+    }
+    return true;
+  }
+
+  public final native boolean useContributorAgreements()
+  /*-{ return this.use_contributor_agreements || false; }-*/;
+  public final native String loginUrl() /*-{ return this.login_url; }-*/;
+  public final native String loginText() /*-{ return this.login_text; }-*/;
+  public final native String switchAccountUrl() /*-{ return this.switch_account_url; }-*/;
+  public final native String registerUrl() /*-{ return this.register_url; }-*/;
+  public final native String registerText() /*-{ return this.register_text; }-*/;
+  public final native String editFullNameUrl() /*-{ return this.edit_full_name_url; }-*/;
+  public final native String httpPasswordUrl() /*-{ return this.http_password_url; }-*/;
+  public final native boolean isGitBasicAuth() /*-{ return this.is_git_basic_auth || false; }-*/;
+  private final native String authTypeRaw() /*-{ return this.auth_type; }-*/;
+  private final native JsArrayString _editableAccountFields()
+  /*-{ return this.editable_account_fields; }-*/;
+
+  protected AuthInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
similarity index 68%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index f5d5ffe..cace7ad 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -12,12 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.changes;
+package com.google.gerrit.client.info;
 
-import com.google.gerrit.client.WebLinkInfo;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.actions.ActionInfo;
-import com.google.gerrit.client.diff.FileInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
@@ -34,30 +30,31 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.List;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
 public class ChangeInfo extends JavaScriptObject {
   public final void init() {
-    if (all_labels() != null) {
-      all_labels().copyKeysIntoChildren("_name");
+    if (allLabels() != null) {
+      allLabels().copyKeysIntoChildren("_name");
     }
   }
 
-  public final Project.NameKey project_name_key() {
+  public final Project.NameKey projectNameKey() {
     return new Project.NameKey(project());
   }
 
-  public final Change.Id legacy_id() {
+  public final Change.Id legacyId() {
     return new Change.Id(_number());
   }
 
   public final Timestamp created() {
-    Timestamp ts = _get_cts();
+    Timestamp ts = _getCts();
     if (ts == null) {
       ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(createdRaw());
-      _set_cts(ts);
+      _setCts(ts);
     }
     return ts;
   }
@@ -65,18 +62,18 @@
   public final boolean hasEditBasedOnCurrentPatchSet() {
     JsArray<RevisionInfo> revList = revisions().values();
     RevisionInfo.sortRevisionInfoByNumber(revList);
-    return revList.get(revList.length() - 1).is_edit();
+    return revList.get(revList.length() - 1).isEdit();
   }
 
-  private final native Timestamp _get_cts() /*-{ return this._cts; }-*/;
-  private final native void _set_cts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+  private final native Timestamp _getCts() /*-{ return this._cts; }-*/;
+  private final native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
   public final Timestamp updated() {
     return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
   }
 
-  public final String id_abbreviated() {
-    return new Change.Key(change_id()).abbreviate();
+  public final String idAbbreviated() {
+    return new Change.Key(changeId()).abbreviate();
   }
 
   public final Change.Status status() {
@@ -84,15 +81,15 @@
   }
 
   public final Set<String> labels() {
-    return all_labels().keySet();
+    return allLabels().keySet();
   }
 
   public final native String id() /*-{ return this.id; }-*/;
   public final native String project() /*-{ return this.project; }-*/;
   public final native String branch() /*-{ return this.branch; }-*/;
   public final native String topic() /*-{ return this.topic; }-*/;
-  public final native String change_id() /*-{ return this.change_id; }-*/;
-  public final native boolean mergeable() /*-{ return this.mergeable || false; }-*/;
+  public final native String changeId() /*-{ return this.change_id; }-*/;
+  public final native boolean mergeable() /*-{ return this.mergeable ? true : false; }-*/;
   public final native int insertions() /*-{ return this.insertions; }-*/;
   public final native int deletions() /*-{ return this.deletions; }-*/;
   private final native String statusRaw() /*-{ return this.status; }-*/;
@@ -102,35 +99,85 @@
   private final native String updatedRaw() /*-{ return this.updated; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
-  public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
+  public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
-  public final native String current_revision() /*-{ return this.current_revision; }-*/;
-  public final native void set_current_revision(String r) /*-{ this.current_revision = r; }-*/;
+  public final native String currentRevision() /*-{ return this.current_revision; }-*/;
+  public final native void setCurrentRevision(String r) /*-{ this.current_revision = r; }-*/;
   public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
   public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
   public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
-  public final native void set_edit(EditInfo edit) /*-{ this.edit = edit; }-*/;
+  public final native void setEdit(EditInfo edit) /*-{ this.edit = edit; }-*/;
   public final native EditInfo edit() /*-{ return this.edit; }-*/;
-  public final native boolean has_edit() /*-{ return this.hasOwnProperty('edit') }-*/;
+  public final native boolean hasEdit() /*-{ return this.hasOwnProperty('edit') }-*/;
   public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
 
-  public final native boolean has_permitted_labels()
+  public final native boolean hasPermittedLabels()
   /*-{ return this.hasOwnProperty('permitted_labels') }-*/;
-  public final native NativeMap<JsArrayString> permitted_labels()
+  public final native NativeMap<JsArrayString> permittedLabels()
   /*-{ return this.permitted_labels; }-*/;
-  public final native JsArrayString permitted_values(String n)
+  public final native JsArrayString permittedValues(String n)
   /*-{ return this.permitted_labels[n]; }-*/;
 
-  public final native JsArray<AccountInfo> removable_reviewers()
+  public final native JsArray<AccountInfo> removableReviewers()
   /*-{ return this.removable_reviewers; }-*/;
 
-  public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+  public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
-  final native int _number() /*-{ return this._number; }-*/;
-  final native boolean _more_changes()
+  public final native int _number() /*-{ return this._number; }-*/;
+  public final native boolean _more_changes()
   /*-{ return this._more_changes ? true : false; }-*/;
 
+  public final boolean submittable() {
+    init();
+    return _submittable();
+  }
+
+  private final native boolean _submittable()
+  /*-{ return this.submittable ? true : false; }-*/;
+
+  /**
+   * @return the index of the missing label or -1
+   *         if no label is missing, or if more than one label is missing.
+   */
+  public final int getMissingLabelIndex() {
+    int i = -1;
+    int ret = -1;
+    List<LabelInfo> labels = Natives.asList(allLabels().values());
+    for (LabelInfo label : labels) {
+      i++;
+      if (!permittedLabels().containsKey(label.name())) {
+        continue;
+      }
+
+      JsArrayString values = permittedValues(label.name());
+      if (values.length() == 0) {
+        continue;
+      }
+
+      switch (label.status()) {
+        case NEED: // Label is required for submit.
+          if (ret != -1) {
+            // more than one label is missing, so it's unclear which to quick
+            // approve, return -1
+            return -1;
+          } else {
+            ret = i;
+          }
+          continue;
+
+        case OK: // Label already applied.
+        case MAY: // Label is not required.
+          continue;
+
+        case REJECT: // Submit cannot happen, do not quick approve.
+        case IMPOSSIBLE:
+          return -1;
+      }
+    }
+    return ret;
+  }
+
   protected ChangeInfo() {
   }
 
@@ -155,10 +202,10 @@
     public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
 
     public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
-    public final ApprovalInfo for_user(int user) {
+    public final ApprovalInfo forUser(int user) {
       JsArray<ApprovalInfo> all = all();
       for (int i = 0; all != null && i < all.length(); i++) {
-        if (all.get(i)._account_id() == user) {
+        if (all.get(i)._accountId() == user) {
           return all.get(i);
         }
       }
@@ -169,12 +216,12 @@
     public final Set<String> values() {
       return Natives.keys(_values());
     }
-    public final native String value_text(String n) /*-{ return this.values[n]; }-*/;
+    public final native String valueText(String n) /*-{ return this.values[n]; }-*/;
 
     public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
     public final native boolean blocking() /*-{ return this.blocking ? true : false; }-*/;
     public final native short defaultValue() /*-{ return this.default_value; }-*/;
-    final native short _value()
+    public final native short _value()
     /*-{
       if (this.value) return this.value;
       if (this.disliked) return -1;
@@ -182,11 +229,11 @@
       return 0;
     }-*/;
 
-    public final String max_value() {
-      return LabelValue.formatValue(value_set().last());
+    public final String maxValue() {
+      return LabelValue.formatValue(valueSet().last());
     }
 
-    public final SortedSet<Short> value_set() {
+    public final SortedSet<Short> valueSet() {
       SortedSet<Short> values = new TreeSet<>();
       for (String v : values()) {
         values.add(parseValue(v));
@@ -208,7 +255,7 @@
   }
 
   public static class ApprovalInfo extends AccountInfo {
-    public final native boolean has_value() /*-{ return this.hasOwnProperty('value'); }-*/;
+    public final native boolean hasValue() /*-{ return this.hasOwnProperty('value'); }-*/;
     public final native short value() /*-{ return this.value || 0; }-*/;
 
     protected ApprovalInfo() {
@@ -217,17 +264,17 @@
 
   public static class EditInfo extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
-    public final native String set_name(String n) /*-{ this.name = n; }-*/;
-    public final native String base_revision() /*-{ return this.base_revision; }-*/;
+    public final native String setName(String n) /*-{ this.name = n; }-*/;
+    public final native String baseRevision() /*-{ return this.base_revision; }-*/;
     public final native CommitInfo commit() /*-{ return this.commit; }-*/;
 
-    public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+    public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
     public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
-    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
 
-    public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+    public final native boolean hasFiles() /*-{ return this.hasOwnProperty('files') }-*/;
     public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
 
     protected EditInfo() {
@@ -249,21 +296,24 @@
     public final native int _number() /*-{ return this._number; }-*/;
     public final native String name() /*-{ return this.name; }-*/;
     public final native boolean draft() /*-{ return this.draft || false; }-*/;
-    public final native boolean has_draft_comments() /*-{ return this.has_draft_comments || false; }-*/;
-    public final native boolean is_edit() /*-{ return this._number == 0; }-*/;
+    public final native AccountInfo uploader() /*-{ return this.uploader; }-*/;
+    public final native boolean isEdit() /*-{ return this._number == 0; }-*/;
     public final native CommitInfo commit() /*-{ return this.commit; }-*/;
-    public final native void set_commit(CommitInfo c) /*-{ this.commit = c; }-*/;
-    public final native String edit_base() /*-{ return this.edit_base; }-*/;
+    public final native void setCommit(CommitInfo c) /*-{ this.commit = c; }-*/;
+    public final native String editBase() /*-{ return this.edit_base; }-*/;
 
-    public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+    public final native boolean hasFiles() /*-{ return this.hasOwnProperty('files') }-*/;
     public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
 
-    public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+    public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
     public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
-    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
 
+    public final native boolean hasPushCertificate() /*-{ return this.hasOwnProperty('push_certificate'); }-*/;
+    public final native PushCertificateInfo pushCertificate() /*-{ return this.push_certificate; }-*/;
+
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
       final int editParent = findEditParent(list);
       Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
@@ -273,7 +323,7 @@
         }
 
         private int num(RevisionInfo r) {
-          return !r.is_edit() ? 2 * (r._number() - 1) + 1 : 2 * editParent;
+          return !r.isEdit() ? 2 * (r._number() - 1) + 1 : 2 * editParent;
         }
       });
     }
@@ -288,8 +338,8 @@
       for (int i = 0; i < list.length(); i++) {
         // edit under revisions?
         RevisionInfo editInfo = list.get(i);
-        if (editInfo.is_edit()) {
-          String parentRevision = editInfo.edit_base();
+        if (editInfo.isEdit()) {
+          String parentRevision = editInfo.editBase();
           // find parent
           for (int j = 0; j < list.length(); j++) {
             RevisionInfo parentInfo = list.get(j);
@@ -329,7 +379,7 @@
     public final native GitPerson committer() /*-{ return this.committer; }-*/;
     public final native String subject() /*-{ return this.subject; }-*/;
     public final native String message() /*-{ return this.message; }-*/;
-    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+    public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
     protected CommitInfo() {
     }
@@ -363,7 +413,7 @@
   }
 
   public static class MergeableInfo extends JavaScriptObject {
-    public final native String submit_type() /*-{ return this.submit_type }-*/;
+    public final native String submitType() /*-{ return this.submit_type }-*/;
     public final native boolean mergeable() /*-{ return this.mergeable }-*/;
 
     protected MergeableInfo() {
@@ -371,8 +421,14 @@
   }
 
   public static class IncludedInInfo extends JavaScriptObject {
+    public final Set<String> externalNames() {
+      return Natives.keys(external());
+    }
+
     public final native JsArrayString branches() /*-{ return this.branches; }-*/;
     public final native JsArrayString tags() /*-{ return this.tags; }-*/;
+    public final native JsArrayString external(String n) /*-{ return this.external[n]; }-*/;
+    private final native NativeMap<JsArrayString> external() /*-{ return this.external; }-*/;
 
     protected IncludedInInfo() {
     }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
new file mode 100644
index 0000000..eee2847
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2015 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.client.info;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class DownloadInfo extends JavaScriptObject {
+  public final List<String> schemes() {
+    return _schemes().sortedKeys();
+  }
+
+  public final List<String> archives() {
+    List<String> archives = new ArrayList<>();
+    for (String f : Natives.asList(_archives())) {
+      archives.add(f);
+    }
+    return archives;
+  }
+
+  public final native DownloadSchemeInfo scheme(String n) /*-{ return this.schemes[n]; }-*/;
+  private final native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
+  private final native JsArrayString _archives() /*-{ return this.archives; }-*/;
+
+  protected DownloadInfo() {
+  }
+
+  public static class DownloadSchemeInfo extends JavaScriptObject {
+    public final List<String> commandNames() {
+      return _commands().sortedKeys();
+    }
+
+    public final Set<DownloadCommandInfo> commands(String project) {
+      Set<DownloadCommandInfo> commands = new HashSet<>();
+      for (String commandName : commandNames()) {
+        commands.add(new DownloadCommandInfo(commandName, command(commandName,
+            project)));
+      }
+      return commands;
+    }
+
+    public final String command(String commandName, String project) {
+      return command(commandName).replaceAll("\\$\\{project\\}", project);
+    }
+
+    private static String projectBaseName(String project) {
+      return project.substring(project.lastIndexOf('/') + 1);
+    }
+
+    public final List<String> cloneCommandNames() {
+      return _cloneCommands().sortedKeys();
+    }
+
+    public final List<DownloadCommandInfo> cloneCommands(String project) {
+      List<String> commandNames = cloneCommandNames();
+      List<DownloadCommandInfo> commands = new ArrayList<>(commandNames.size());
+      for (String commandName : commandNames) {
+        commands.add(new DownloadCommandInfo(commandName, cloneCommand(
+            commandName, project)));
+      }
+      return commands;
+    }
+
+    public final String cloneCommand(String commandName, String project) {
+      return cloneCommand(commandName).replaceAll("\\$\\{project\\}", project)
+          .replaceAll("\\$\\{project-base-name\\}", projectBaseName(project));
+    }
+
+    public final String getUrl(String project) {
+      return url().replaceAll("\\$\\{project\\}", project);
+    }
+
+    public final native String name() /*-{ return this.name; }-*/;
+    public final native String url() /*-{ return this.url; }-*/;
+    public final native boolean isAuthRequired() /*-{ return this.is_auth_required || false; }-*/;
+    public final native boolean isAuthSupported() /*-{ return this.is_auth_supported || false; }-*/;
+    public final native String command(String n) /*-{ return this.commands[n]; }-*/;
+    public final native String cloneCommand(String n) /*-{ return this.clone_commands[n]; }-*/;
+    private final native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
+    private final native NativeMap<NativeString> _cloneCommands() /*-{ return this.clone_commands; }-*/;
+
+    protected DownloadSchemeInfo() {
+    }
+  }
+
+  public static class DownloadCommandInfo {
+    private final String name;
+    private final String command;
+
+    DownloadCommandInfo(String name, String command) {
+      this.name = name;
+      this.command = command;
+    }
+
+    public String name() {
+      return name;
+    }
+
+    public String command() {
+      return command;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
similarity index 78%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
index c4459b6..d95f9ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.diff;
+package com.google.gerrit.client.info;
 
-import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -25,12 +24,21 @@
 
 public class FileInfo extends JavaScriptObject {
   public final native String path() /*-{ return this.path; }-*/;
-  public final native String old_path() /*-{ return this.old_path; }-*/;
-  public final native int lines_inserted() /*-{ return this.lines_inserted || 0; }-*/;
-  public final native int lines_deleted() /*-{ return this.lines_deleted || 0; }-*/;
+  public final native String oldPath() /*-{ return this.old_path; }-*/;
+  public final native int linesInserted() /*-{ return this.lines_inserted || 0; }-*/;
+  public final native int linesDeleted() /*-{ return this.lines_deleted || 0; }-*/;
   public final native boolean binary() /*-{ return this.binary || false; }-*/;
   public final native String status() /*-{ return this.status; }-*/;
 
+
+  // JSNI methods cannot have 'long' as a parameter type or a return type and
+  // it's suggested to use double in this case:
+  // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html#important
+  public final long sizeDelta() {
+    return (long)_sizeDelta();
+  }
+  private final native double _sizeDelta() /*-{ return this.size_delta || 0; }-*/;
+
   public final native int _row() /*-{ return this._row }-*/;
   public final native void _row(int r) /*-{ this._row = r }-*/;
 
@@ -64,7 +72,7 @@
 
   public static String getFileName(String path) {
     String fileName = Patch.COMMIT_MSG.equals(path)
-        ? Util.C.commitMessage()
+        ? "Commit Message"
         : path;
     int s = fileName.lastIndexOf('/');
     return s >= 0 ? fileName.substring(s + 1) : fileName;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
new file mode 100644
index 0000000..55ef892
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 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.client.info;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class GerritInfo extends JavaScriptObject {
+  public final Project.NameKey allProjectsNameKey() {
+    return new Project.NameKey(allProjects());
+  }
+
+  public final boolean isAllProjects(Project.NameKey p) {
+    return allProjectsNameKey().equals(p);
+  }
+
+  public final Project.NameKey allUsersNameKey() {
+    return new Project.NameKey(allUsers());
+  }
+
+  public final boolean isAllUsers(Project.NameKey p) {
+    return allUsersNameKey().equals(p);
+  }
+
+  public final native String allProjects() /*-{ return this.all_projects; }-*/;
+  public final native String allUsers() /*-{ return this.all_users; }-*/;
+  public final native String docUrl() /*-{ return this.doc_url; }-*/;
+  public final native boolean editGpgKeys() /*-{ return this.edit_gpg_keys || false; }-*/;
+  public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
+  public final native String reportBugText() /*-{ return this.report_bug_text; }-*/;
+
+  protected GerritInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
new file mode 100644
index 0000000..eb5a697
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebInfo.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2008 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.client.info;
+
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.URL;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GitwebInfo extends JavaScriptObject {
+  public final native String url() /*-{ return this.url; }-*/;
+  public final native GitwebTypeInfo type() /*-{ return this.type; }-*/;
+
+  /**
+   * Checks whether the given patch set can be linked.
+   *
+   * Draft patch sets can only be linked if linking of drafts was enabled by
+   * configuration.
+   *
+   * @param ps patch set to check whether it can be linked
+   * @return true if the patch set can be linked, otherwise false
+   */
+  public final boolean canLink(PatchSet ps) {
+    return !ps.isDraft() || type().linkDrafts();
+  }
+
+  /**
+   * Checks whether the given revision can be linked.
+   *
+   * Draft revisions can only be linked if linking of drafts was enabled by
+   * configuration.
+   *
+   * @param revision revision to check whether it can be linked
+   * @return true if the revision can be linked, otherwise false
+   */
+  public final boolean canLink(RevisionInfo revision) {
+    return revision.draft() || type().linkDrafts();
+  }
+
+  /**
+   * Returns the name for gitweb links.
+   *
+   * @return the name for gitweb links
+   */
+  public final String getLinkName() {
+    return "(" + type().name() + ")";
+  }
+
+  /**
+   * Returns the gitweb link to a revision.
+   *
+   * @param project the name of the project
+   * @param commit the commit ID
+   * @return gitweb link to a revision
+   */
+  public final String toRevision(String  project, String commit) {
+    ParameterizedString pattern = new ParameterizedString(type().revision());
+    Map<String, String> p = new HashMap<>();
+    p.put("project", encode(project));
+    p.put("commit", encode(commit));
+    return url() + pattern.replace(p);
+  }
+
+  /**
+   * Returns the gitweb link to a revision.
+   *
+   * @param project the name of the project
+   * @param ps the patch set
+   * @return gitweb link to a revision
+   */
+  public final String toRevision(Project.NameKey project, PatchSet ps) {
+    return toRevision(project.get(), ps.getRevision().get());
+  }
+
+  /**
+   * Returns the gitweb link to a project.
+   *
+   * @param project the project name key
+   * @return gitweb link to a project
+   */
+  public final String toProject(Project.NameKey project) {
+    ParameterizedString pattern = new ParameterizedString(type().project());
+
+    Map<String, String> p = new HashMap<>();
+    p.put("project", encode(project.get()));
+    return url() + pattern.replace(p);
+  }
+
+  /**
+   * Returns the gitweb link to a branch.
+   *
+   * @param branch the branch name key
+   * @return gitweb link to a branch
+   */
+  public final String toBranch(Branch.NameKey branch) {
+    ParameterizedString pattern = new ParameterizedString(type().branch());
+
+    Map<String, String> p = new HashMap<>();
+    p.put("project", encode(branch.getParentKey().get()));
+    p.put("branch", encode(branch.get()));
+    return url() + pattern.replace(p);
+  }
+
+  /**
+   * Returns the gitweb link to a file.
+   *
+   * @param project the branch name key
+   * @param commit the commit ID
+   * @param file the path of the file
+   * @return gitweb link to a file
+   */
+  public final String toFile(String  project, String commit, String file) {
+    Map<String, String> p = new HashMap<>();
+    p.put("project", encode(project));
+    p.put("commit", encode(commit));
+    p.put("file", encode(file));
+
+    ParameterizedString pattern = (file == null || file.isEmpty())
+        ? new ParameterizedString(type().rootTree())
+        : new ParameterizedString(type().file());
+    return url() + pattern.replace(p);
+  }
+
+  /**
+   * Returns the gitweb link to a file history.
+   *
+   * @param branch the branch name key
+   * @param file the path of the file
+   * @return gitweb link to a file history
+   */
+  public final String toFileHistory(Branch.NameKey branch, String file) {
+    ParameterizedString pattern = new ParameterizedString(type().fileHistory());
+
+    Map<String, String> p = new HashMap<>();
+    p.put("project", encode(branch.getParentKey().get()));
+    p.put("branch", encode(branch.get()));
+    p.put("file", encode(file));
+    return url() + pattern.replace(p);
+  }
+
+  private final String encode(String segment) {
+    if (type().urlEncode()) {
+      return URL.encodeQueryString(type().replacePathSeparator(segment));
+    } else {
+      return segment;
+    }
+  }
+
+  protected GitwebInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebTypeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebTypeInfo.java
new file mode 100644
index 0000000..6726719
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GitwebTypeInfo.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2015 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.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class GitwebTypeInfo extends JavaScriptObject {
+  /**
+   * Replace the standard path separator ('/') in a branch name or project
+   * name with a custom path separator configured by the property
+   * gitweb.pathSeparator.
+   * @param urlSegment The branch or project to replace the path separator in
+   * @return the urlSegment with the standard path separator replaced by the
+   * custom path separator
+   */
+  public final String replacePathSeparator(String urlSegment) {
+    if (!"/".equals(pathSeparator())) {
+      return urlSegment.replace("/", pathSeparator());
+    }
+    return urlSegment;
+  }
+
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String revision() /*-{ return this.revision; }-*/;
+  public final native String project() /*-{ return this.project; }-*/;
+  public final native String branch() /*-{ return this.branch; }-*/;
+  public final native String rootTree() /*-{ return this.root_tree; }-*/;
+  public final native String file() /*-{ return this.file; }-*/;
+  public final native String fileHistory() /*-{ return this.file_history; }-*/;
+  public final native String pathSeparator() /*-{ return this.path_separator; }-*/;
+  public final native boolean linkDrafts() /*-{ return this.link_drafts || false; }-*/;
+  public final native boolean urlEncode() /*-{ return this.url_encode || false; }-*/;
+
+  protected GitwebTypeInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
new file mode 100644
index 0000000..f7477a1
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GpgKeyInfo.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 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.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+public class GpgKeyInfo extends JavaScriptObject {
+  public enum Status {
+    BAD, OK, TRUSTED;
+  }
+
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String fingerprint() /*-{ return this.fingerprint; }-*/;
+  public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
+  public final native String key() /*-{ return this.key; }-*/;
+
+  private final native String statusRaw() /*-{ return this.status; }-*/;
+  public final Status status() {
+    String s = statusRaw();
+    if (s == null) {
+      return null;
+    }
+    return Status.valueOf(s);
+  }
+
+  public final native boolean hasProblems()
+  /*-{ return this.hasOwnProperty('problems'); }-*/;
+  public final native JsArrayString problems() /*-{ return this.problems; }-*/;
+
+  protected GpgKeyInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
similarity index 62%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java
copy to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
index e78c5ce..ebfec1a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/PushCertificateInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,17 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
 
-public class TopMenu extends JavaScriptObject {
+public class PushCertificateInfo extends JavaScriptObject {
+  public final native String certificate() /*-{ return this.certificate; }-*/;
+  public final native GpgKeyInfo key() /*-{ return this.key; }-*/;
 
-  protected TopMenu() {
+  protected PushCertificateInfo() {
   }
-
-  public final native String getName() /*-{ return this.name; }-*/;
-
-  public final native JsArray<TopMenuItem> getItems() /*-{ return this.items; }-*/;
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
new file mode 100644
index 0000000..b0e52fa
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2015 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.client.info;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ServerInfo extends JavaScriptObject {
+  public final native AuthInfo auth() /*-{ return this.auth; }-*/;
+  public final native ChangeConfigInfo change() /*-{ return this.change; }-*/;
+  public final native DownloadInfo download() /*-{ return this.download; }-*/;
+  public final native GerritInfo gerrit() /*-{ return this.gerrit; }-*/;
+  public final native GitwebInfo gitweb() /*-{ return this.gitweb; }-*/;
+  public final native PluginConfigInfo plugin() /*-{ return this.plugin; }-*/;
+  public final native SshdInfo sshd() /*-{ return this.sshd; }-*/;
+  public final native SuggestInfo suggest() /*-{ return this.suggest; }-*/;
+  public final native UserConfigInfo user() /*-{ return this.user; }-*/;
+  public final native ReceiveInfo receive() /*-{ return this.receive; }-*/;
+
+  public final Map<String, String> urlAliases() {
+    Map<String, String> urlAliases = new HashMap<>();
+    for (String k : Natives.keys(_urlAliases())) {
+      urlAliases.put(k, urlAliasToken(k));
+    }
+    return urlAliases;
+  }
+
+  public final native String urlAliasToken(String n) /*-{ return this.url_aliases[n]; }-*/;
+  private final native NativeMap<NativeString> _urlAliases() /*-{ return this.url_aliases; }-*/;
+
+
+  public final boolean hasSshd() {
+    return sshd() != null;
+  }
+
+  protected ServerInfo() {
+  }
+
+  public static class ChangeConfigInfo extends JavaScriptObject {
+    public final native boolean allowDrafts() /*-{ return this.allow_drafts || false; }-*/;
+    public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
+    public final native String replyLabel() /*-{ return this.reply_label; }-*/;
+    public final native String replyTooltip() /*-{ return this.reply_tooltip; }-*/;
+    public final native int updateDelay() /*-{ return this.update_delay || 0; }-*/;
+    public final native boolean isSubmitWholeTopicEnabled() /*-{
+        return this.submit_whole_topic; }-*/;
+
+    protected ChangeConfigInfo() {
+    }
+  }
+
+  public static class PluginConfigInfo extends JavaScriptObject {
+    public final native boolean hasAvatars() /*-{ return this.has_avatars || false; }-*/;
+
+    protected PluginConfigInfo() {
+    }
+  }
+
+  public static class SshdInfo extends JavaScriptObject {
+    protected SshdInfo() {
+    }
+  }
+
+  public static class SuggestInfo extends JavaScriptObject {
+    public final native int from() /*-{ return this.from || 0; }-*/;
+
+    protected SuggestInfo() {
+    }
+  }
+
+  public static class UserConfigInfo extends JavaScriptObject {
+    public final native String anonymousCowardName() /*-{ return this.anonymous_coward_name; }-*/;
+
+    protected UserConfigInfo() {
+    }
+  }
+
+  public static class ReceiveInfo extends JavaScriptObject {
+    public final native boolean enableSignedPush()
+    /*-{ return this.enable_signed_push || false; }-*/;
+
+    protected ReceiveInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenu.java
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenu.java
index e78c5ce..bad0475 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenu.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenu.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuItem.java
similarity index 96%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuItem.java
index ba43068..54cde9c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuItem.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuItem.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuList.java
similarity index 93%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuList.java
index 4413603..d51e778 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/TopMenuList.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JsArray;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/WebLinkInfo.java
similarity index 97%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/WebLinkInfo.java
index 45731f7..367486b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/WebLinkInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client;
+package com.google.gerrit.client.info;
 
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.ui.Anchor;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
index 2e407d4..1f003de 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -18,6 +18,9 @@
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 /** A map of native JSON objects, keyed by a string. */
@@ -58,6 +61,13 @@
     return Natives.keys(this);
   }
 
+  public final List<String> sortedKeys() {
+    Set<String> keys = keySet();
+    List<String> sorted = new ArrayList<>(keys);
+    Collections.sort(sorted);
+    return sorted;
+  }
+
   public final native JsArray<T> values()
   /*-{
     var s = this;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
index 564e95a..dcd96da 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -91,32 +91,18 @@
     return arr;
   }
 
-  @SuppressWarnings("unchecked")
-  public static <T extends JavaScriptObject> T parseJSON(String json) {
-    if (json.startsWith("\"")) {
-      return (T) NativeString.wrap(parseString(parser, json));
+  public static JsArrayString arrayOf(Iterable<String> elements) {
+    JsArrayString arr = JavaScriptObject.createArray().cast();
+    for (String elem : elements) {
+      arr.push(elem);
     }
-    return Natives.<T> parseObject(parser, json); // javac generics bug
+    return arr;
   }
 
-  private static native <T extends JavaScriptObject>
-  T parseObject(JavaScriptObject p, String s)
-  /*-{ return p(s); }-*/;
-
-  private static native
-  String parseString(JavaScriptObject p, String s)
-  /*-{ return p(s); }-*/;
-
-  private static JavaScriptObject parser;
-  private static native JavaScriptObject bestJsonParser()
-  /*-{
-    if ($wnd.JSON && typeof $wnd.JSON.parse === 'function')
-      return $wnd.JSON.parse;
-    return function(s) { return eval('(' + s + ')'); };
-  }-*/;
-
-  static {
-    parser = bestJsonParser();
+  public static JsArrayString arrayOf(String element) {
+    JsArrayString arr = JavaScriptObject.createArray().cast();
+    arr.push(element);
+    return arr;
   }
 
   private Natives() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java
new file mode 100644
index 0000000..10e20bf
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/HighlightSuggestion.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.client.ui;
+
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+
+/** A {@code Suggestion} with highlights. */
+public class HighlightSuggestion implements Suggestion {
+
+  private final String keyword;
+  private final String value;
+
+  public HighlightSuggestion(String keyword, String value) {
+    this.keyword = keyword;
+    this.value = value;
+  }
+
+  @Override
+  public String getDisplayString() {
+    int start = 0;
+    int keyLen = keyword.length();
+    SafeHtmlBuilder builder = new SafeHtmlBuilder();
+    for (;;) {
+      int index = value.indexOf(keyword, start);
+      if (index == -1) {
+        builder.appendEscaped(value.substring(start));
+        break;
+      }
+      builder.appendEscaped(value.substring(start, index));
+      builder.appendHtmlConstant("<strong>");
+      start = index + keyLen;
+      builder.appendEscaped(value.substring(index, start));
+      builder.appendHtmlConstant("</strong>");
+    }
+    return builder.toSafeHtml().asString();
+  }
+
+  @Override
+  public String getReplacementString() {
+    return value;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
similarity index 97%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
index 9554ac5..cf7e1d8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
@@ -44,9 +44,11 @@
   public void requestSuggestions(Request req, Callback cb) {
     Query q = new Query(req, cb);
     if (query == null) {
+      query = q;
       q.start();
+    } else {
+      query = q;
     }
-    query = q;
   }
 
   @Override
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
new file mode 100644
index 0000000..f25fc3f
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/question.png
Binary files differ
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
similarity index 100%
rename from gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
rename to gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
diff --git a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java
new file mode 100644
index 0000000..44ed50b
--- /dev/null
+++ b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/ui/HighlightSuggestionTest.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2015 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.client.ui;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class HighlightSuggestionTest {
+
+  @Test
+  public void singleHighlight() throws Exception {
+    String keyword = "key";
+    String value = "somethingkeysomething";
+    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
+    assertEquals(
+        "something<strong>key</strong>something",
+        suggestion.getDisplayString());
+    assertEquals(value, suggestion.getReplacementString());
+  }
+
+  @Test
+  public void noHighlight() throws Exception {
+    String keyword = "key";
+    String value = "something";
+    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
+    assertEquals(value, suggestion.getDisplayString());
+    assertEquals(value, suggestion.getReplacementString());
+  }
+
+  @Test
+  public void doubleHighlight() throws Exception {
+    String keyword = "key";
+    String value = "somethingkeysomethingkeysomething";
+    HighlightSuggestion suggestion = new HighlightSuggestion(keyword, value);
+    assertEquals(
+        "something<strong>key</strong>something<strong>key</strong>something",
+        suggestion.getDisplayString());
+    assertEquals(value, suggestion.getReplacementString());
+  }
+}
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index 9b7ec49..ead19f4 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -1,66 +1,14 @@
 include_defs('//gerrit-gwtui/gwt.defs')
 include_defs('//tools/gwt-constants.defs')
-from multiprocessing import cpu_count
 
-DEPS = [
+DEPS = GWT_TRANSITIVE_DEPS + [
   '//gerrit-gwtexpui:CSS',
   '//lib:gwtjsonrpc',
   '//lib/gwt:dev',
 ]
 
-genrule(
-  name = 'ui_optdbg',
-  cmd = 'cd $TMP;' +
-    'unzip -q $(location :ui_dbg);' +
-    'mv' +
-    ' gerrit_ui/gerrit_ui.nocache.js' +
-    ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
-    'unzip -qo $(location :ui_opt);' +
-    'mkdir -p \$(dirname $OUT);' +
-    'zip -qr $OUT .',
-  deps = [
-    ':ui_dbg',
-    ':ui_opt',
-  ],
-  out = 'ui_optdbg.zip',
-  visibility = ['PUBLIC'],
-)
-
-gwt_binary(
-  name = 'ui_opt',
-  modules = [MODULE],
-  module_deps = [':ui_module'],
-  deps = DEPS + [':ui_dbg'],
-  local_workers = cpu_count(),
-  strict = True,
-  experimental_args = GWT_COMPILER_ARGS,
-  vm_args = GWT_JVM_ARGS,
-)
-
-gwt_binary(
-  name = 'ui_soyc',
-  modules = [MODULE],
-  module_deps = [':ui_module'],
-  deps = DEPS + [':ui_dbg'],
-  local_workers = cpu_count(),
-  strict = True,
-  experimental_args = GWT_COMPILER_ARGS + ['-compileReport'],
-  vm_args = GWT_JVM_ARGS,
-)
-
-gwt_binary(
-  name = 'ui_dbg',
-  modules = [MODULE],
-  style = 'PRETTY',
-  optimize = 0,
-  module_deps = [':ui_module'],
-  deps = DEPS,
-  local_workers = cpu_count(),
-  strict = True,
-  experimental_args = GWT_COMPILER_ARGS,
-  vm_args = GWT_JVM_ARGS,
-  visibility = ['//:eclipse'],
-)
+gwt_genrule(MODULE, DEPS)
+gwt_genrule(MODULE, DEPS, '_r')
 
 gwt_user_agent_permutations(
   name = 'ui',
@@ -79,28 +27,15 @@
   deps = [
     ':freebie_application_icon_set',
     '//gerrit-gwtui-common:diffy_logo',
-    '//gerrit-gwtexpui:Clippy',
-    '//gerrit-gwtexpui:GlobalKey',
-    '//gerrit-gwtexpui:Progress',
-    '//gerrit-gwtexpui:SafeHtml',
-    '//gerrit-gwtexpui:UserAgent',
     '//gerrit-gwtui-common:client',
-    '//gerrit-common:client',
-    '//gerrit-extension-api:client',
-    '//gerrit-patch-jgit:client',
-    '//gerrit-prettify:client',
-    '//gerrit-reviewdb:client',
     '//gerrit-gwtexpui:CSS',
-    '//lib:gwtjsonrpc',
-    '//lib:gwtjsonrpc_src',
-    '//lib:gwtorm',
     '//lib/codemirror:codemirror',
     '//lib/gwt:user',
-    '//lib/jgit:jgit',
   ],
   visibility = [
     '//tools/eclipse:classpath',
     '//Documentation:licenses.txt',
+    '//Documentation:js_licenses.txt',
   ],
 )
 
@@ -126,7 +61,6 @@
     '//lib/gwt:dev',
     '//lib/gwt:user',
     '//lib/gwt:gwt-test-utils',
-    '//lib/jgit:jgit',
   ],
   source_under_test = [':ui_module'],
   vm_args = ['-Xmx512m'],
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
index cd206c0..783c343 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -11,13 +11,14 @@
 # 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.
+from multiprocessing import cpu_count
 
 BROWSERS = [
   'chrome',
   'firefox',
   'gecko1_8',
   'safari',
-  'msie', 'ie6', 'ie8', 'ie9',
+  'msie', 'ie8', 'ie9',
 ]
 ALIASES = {
   'chrome': 'safari',
@@ -25,6 +26,63 @@
   'msie': 'ie9',
 }
 MODULE = 'com.google.gerrit.GerritGwtUI'
+CPU_COUNT = cpu_count()
+
+def gwt_genrule(module, deps, suffix = ""):
+  dbg = 'ui_dbg' + suffix
+  opt = 'ui_opt' + suffix
+  soyc = 'ui_soyc' + suffix
+  args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
+
+  genrule(
+    name = 'ui_optdbg' + suffix,
+    cmd = 'cd $TMP;' +
+      'unzip -q $(location :%s);' % dbg +
+      'mv' +
+      ' gerrit_ui/gerrit_ui.nocache.js' +
+      ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
+      'unzip -qo $(location :%s);' % opt +
+      'mkdir -p \$(dirname $OUT);' +
+      'zip -qr $OUT .',
+    out = 'ui_optdbg' + suffix + '.zip',
+    visibility = ['PUBLIC'],
+  )
+
+  gwt_binary(
+    name = opt,
+    modules = [module],
+    module_deps = [':ui_module'],
+    deps = deps + ([':' + dbg] if CPU_COUNT < 8 else []),
+    local_workers = CPU_COUNT,
+    strict = True,
+    experimental_args = args,
+    vm_args = GWT_JVM_ARGS,
+  )
+
+  gwt_binary(
+    name = dbg,
+    modules = [module],
+    style = 'PRETTY',
+    optimize = 0,
+    module_deps = [':ui_module'],
+    deps = deps,
+    local_workers = CPU_COUNT,
+    strict = True,
+    experimental_args = args,
+    vm_args = GWT_JVM_ARGS,
+    visibility = ['//:eclipse'],
+  )
+
+  gwt_binary(
+    name = soyc,
+    modules = [module],
+    module_deps = [':ui_module'],
+    deps = deps + [':' + dbg],
+    local_workers = CPU_COUNT,
+    strict = True,
+    experimental_args = args + ['-compileReport'],
+    vm_args = GWT_JVM_ARGS,
+   )
 
 def gwt_user_agent_permutations(
     name,
@@ -37,7 +95,6 @@
     deps = [],
     browsers = BROWSERS,
     visibility = []):
-  from multiprocessing import cpu_count
   for ua in browsers:
     impl = ua
     if ua in ALIASES:
@@ -74,7 +131,7 @@
       draft_compile = draft_compile,
       module_deps = module_deps + [':%s_gwtxml_lib' % gwt_name],
       deps = deps,
-      local_workers = cpu_count(),
+      local_workers = CPU_COUNT,
       strict = True,
       experimental_args = GWT_COMPILER_ARGS,
       vm_args = GWT_JVM_ARGS,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
index fd717ee..9ac919b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
@@ -18,17 +18,9 @@
   <inherits name='com.google.gwt.user.User'/>
   <inherits name='com.google.gwt.resources.Resources'/>
   <inherits name='com.google.gwt.user.theme.chrome.Chrome'/>
-  <inherits name='com.google.gwtexpui.clippy.Clippy'/>
   <inherits name='com.google.gwtexpui.css.CSS'/>
-  <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
-  <inherits name='com.google.gwtexpui.progress.Progress'/>
-  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
-  <inherits name='com.google.gerrit.extensions.Extensions'/>
-  <inherits name='com.google.gerrit.prettify.PrettyFormatter'/>
-  <inherits name='com.google.gerrit.Common'/>
   <inherits name='com.google.gerrit.GerritGwtUICommon'/>
   <inherits name='com.google.gerrit.UserAgent'/>
-  <inherits name='org.eclipse.jgit.JGit'/>
   <inherits name='net.codemirror.CodeMirror'/>
 
   <extend-property name='locale' values='en'/>
@@ -40,5 +32,8 @@
 
   <set-property name='gwt.logging.logLevel' value='SEVERE'/>
 
+  <!-- Disable GSS -->
+  <set-configuration-property name='CssResource.enableGss' value='false'/>
+
   <entry-point class='com.google.gerrit.client.Gerrit'/>
 </module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
index e6f3acb..c02518b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -21,10 +21,9 @@
     <when-property-is name="user.agent" value="gecko1_8" />
   </replace-with>
 
-  <replace-with class="com.google.gerrit.client.ui.FancyFlexTableImplIE6">
+  <replace-with class="com.google.gerrit.client.ui.FancyFlexTableImplIE8">
     <when-type-is class="com.google.gerrit.client.ui.FancyFlexTableImpl" />
     <any>
-      <when-property-is name="user.agent" value="ie6"/>
       <when-property-is name="user.agent" value="ie8"/>
     </any>
   </replace-with>
@@ -32,7 +31,6 @@
   <replace-with class="com.google.gerrit.client.Themer.ThemerIE">
     <when-type-is class="com.google.gerrit.client.Themer" />
     <any>
-      <when-property-is name="user.agent" value="ie6"/>
       <when-property-is name="user.agent" value="ie8"/>
       <when-property-is name="user.agent" value="ie9"/>
       <when-property-is name="user.agent" value="ie10"/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 053999a..bec89cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.client;
 
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.event.dom.client.LoadEvent;
 import com.google.gwt.event.dom.client.LoadHandler;
@@ -72,7 +72,7 @@
     } else if (isGerritServer(account)) {
       setVisible(true);
       setResource(Gerrit.RESOURCES.gerritAvatar26());
-    } else if (account.has_avatar_info()) {
+    } else if (account.hasAvatarInfo()) {
       setVisible(false);
       AvatarInfo info = account.avatar(size);
       if (info != null) {
@@ -91,10 +91,15 @@
   }
 
   private void loadAvatar(AccountInfo account, int size, boolean addPopup) {
+    if (!Gerrit.info().plugin().hasAvatars()) {
+      setVisible(false);
+      return;
+    }
+
      // TODO Kill /accounts/*/avatar URL.
     String u = account.email();
     if (Gerrit.isSignedIn()
-        && u.equals(Gerrit.getUserAccount().getPreferredEmail())) {
+        && u.equals(Gerrit.getUserAccount().email())) {
       u = "self";
     }
     RestApi api = new RestApi("/accounts/").id(u).view("avatar");
@@ -121,7 +126,7 @@
   }
 
   private static boolean isGerritServer(AccountInfo account) {
-    return account._account_id() == 0
+    return account._accountId() == 0
         && Util.C.messageNoAuthor().equals(account.name());
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
index bcf4256..d52838d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.info.WebLinkInfo;
+
 public class DiffWebLinkInfo extends WebLinkInfo {
   public final native boolean showOnSideBySideDiffView()
   /*-{ return this.show_on_side_by_side_diff_view || false; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index e2bf142..ba3cc4c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -28,6 +28,10 @@
 import static com.google.gerrit.common.PageLinks.SETTINGS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_CONTACT;
+import static com.google.gerrit.common.PageLinks.SETTINGS_DIFF_PREFERENCES;
+import static com.google.gerrit.common.PageLinks.SETTINGS_EDIT_PREFERENCES;
+import static com.google.gerrit.common.PageLinks.SETTINGS_EXTENSION;
+import static com.google.gerrit.common.PageLinks.SETTINGS_GPGKEYS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
 import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT;
@@ -35,11 +39,13 @@
 import static com.google.gerrit.common.PageLinks.SETTINGS_PROJECTS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_SSHKEYS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_WEBIDENT;
-import static com.google.gerrit.common.PageLinks.op;
 import static com.google.gerrit.common.PageLinks.toChangeQuery;
 
 import com.google.gerrit.client.account.MyAgreementsScreen;
 import com.google.gerrit.client.account.MyContactInformationScreen;
+import com.google.gerrit.client.account.MyDiffPreferencesScreen;
+import com.google.gerrit.client.account.MyEditPreferencesScreen;
+import com.google.gerrit.client.account.MyGpgKeysScreen;
 import com.google.gerrit.client.account.MyGroupsScreen;
 import com.google.gerrit.client.account.MyIdentitiesScreen;
 import com.google.gerrit.client.account.MyPasswordScreen;
@@ -50,6 +56,7 @@
 import com.google.gerrit.client.account.NewAgreementScreen;
 import com.google.gerrit.client.account.RegisterScreen;
 import com.google.gerrit.client.account.ValidateEmailScreen;
+import com.google.gerrit.client.admin.AccountGroupAuditLogScreen;
 import com.google.gerrit.client.admin.AccountGroupInfoScreen;
 import com.google.gerrit.client.admin.AccountGroupMembersScreen;
 import com.google.gerrit.client.admin.AccountGroupScreen;
@@ -63,7 +70,9 @@
 import com.google.gerrit.client.admin.ProjectInfoScreen;
 import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.admin.ProjectTagsScreen;
 import com.google.gerrit.client.api.ExtensionScreen;
+import com.google.gerrit.client.api.ExtensionSettingsScreen;
 import com.google.gerrit.client.change.ChangeScreen;
 import com.google.gerrit.client.change.FileTable;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
@@ -93,7 +102,6 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.RunAsyncCallback;
 import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.Window;
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
@@ -201,7 +209,9 @@
     }
   }
 
-  private static void select(final String token) {
+  private static void select(String token) {
+    token = Gerrit.getUrlAliasMatcher().replace(token);
+
     if (matchPrefix(QUERY, token)) {
       query(token);
 
@@ -241,156 +251,11 @@
     } else if (matchPrefix("/admin/", token)) {
       admin(token);
 
-    } else if (/* DEPRECATED URL */matchPrefix("/c2/", token)) {
-      change(token);
-    } else if (/* LEGACY URL */matchPrefix("all,", token)) {
-      redirectFromLegacyToken(token, legacyAll(token));
-    } else if (/* LEGACY URL */matchPrefix("mine,", token)
-        || matchExact("mine", token)) {
-      redirectFromLegacyToken(token, legacyMine(token));
-    } else if (/* LEGACY URL */matchPrefix("project,", token)) {
-      redirectFromLegacyToken(token, legacyProject(token));
-    } else if (/* LEGACY URL */matchPrefix("change,", token)) {
-      redirectFromLegacyToken(token, legacyChange(token));
-    } else if (/* LEGACY URL */matchPrefix("patch,", token)) {
-      redirectFromLegacyToken(token, legacyPatch(token));
-    } else if (/* LEGACY URL */matchPrefix("admin,", token)) {
-      redirectFromLegacyToken(token, legacyAdmin(token));
-    } else if (/* LEGACY URL */matchPrefix("settings,", token)
-        || matchPrefix("register,", token)
-        || matchPrefix("q,", token)) {
-      redirectFromLegacyToken(token, legacySettings(token));
-
     } else {
       Gerrit.display(token, new NotFoundScreen());
     }
   }
 
-  private static void redirectFromLegacyToken(String oldToken, String newToken) {
-    if (newToken != null) {
-      Window.Location.replace(Window.Location.getPath() + "#" + newToken);
-    } else {
-      Gerrit.display(oldToken, new NotFoundScreen());
-    }
-  }
-
-  private static String legacyMine(final String token) {
-    if (matchExact("mine", token)) {
-      return MINE;
-    }
-
-    if (matchExact("mine,starred", token)) {
-      return toChangeQuery("is:starred");
-    }
-
-    if (matchExact("mine,drafts", token)) {
-      return toChangeQuery("owner:self is:draft");
-    }
-
-    if (matchExact("mine,comments", token)) {
-      return toChangeQuery("has:draft");
-    }
-
-    if (matchPrefix("mine,watched,", token)) {
-      return toChangeQuery("is:watched status:open");
-    }
-
-    return null;
-  }
-
-  private static String legacyAll(final String token) {
-    if (matchPrefix("all,abandoned,", token)) {
-      return toChangeQuery("status:abandoned");
-    }
-
-    if (matchPrefix("all,merged,", token)) {
-      return toChangeQuery("status:merged");
-    }
-
-    if (matchPrefix("all,open,", token)) {
-      return toChangeQuery("status:open");
-    }
-
-    return null;
-  }
-
-  private static String legacyProject(final String token) {
-    if (matchPrefix("project,open,", token)) {
-      final String s = skip(token);
-      final int c = s.indexOf(',');
-      Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return toChangeQuery("status:open " + op("project", proj.get()));
-    }
-
-    if (matchPrefix("project,merged,", token)) {
-      final String s = skip(token);
-      final int c = s.indexOf(',');
-      Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return toChangeQuery("status:merged " + op("project", proj.get()));
-    }
-
-    if (matchPrefix("project,abandoned,", token)) {
-      final String s = skip(token);
-      final int c = s.indexOf(',');
-      Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return toChangeQuery("status:abandoned " + op("project", proj.get()));
-    }
-
-    return null;
-  }
-
-  private static String legacyChange(final String token) {
-    final String s = skip(token);
-    final String t[] = s.split(",", 2);
-    if (t.length > 1 && matchPrefix("patchset=", t[1])) {
-      return PageLinks.toChange(PatchSet.Id.parse(t[0] + "," + skip(t[1])));
-    }
-    return PageLinks.toChange(Change.Id.parse(t[0]));
-  }
-
-  private static String legacyPatch(String token) {
-    if (/* LEGACY URL */matchPrefix("patch,sidebyside,", token)) {
-      return toPatch("", null, Patch.Key.parse(skip(token)));
-    }
-
-    if (/* LEGACY URL */matchPrefix("patch,unified,", token)) {
-      return toPatch("unified", null, Patch.Key.parse(skip(token)));
-    }
-
-    return null;
-  }
-
-  private static String legacyAdmin(String token) {
-    if (matchPrefix("admin,group,", token)) {
-      return ADMIN_GROUPS + skip(token);
-    }
-
-    if (matchPrefix("admin,project,", token)) {
-      String rest = skip(token);
-      int c = rest.indexOf(',');
-      String panel;
-      Project.NameKey k;
-      if (0 < c) {
-        panel = rest.substring(c + 1);
-        k = Project.NameKey.parse(rest.substring(0, c));
-      } else {
-        panel = ProjectScreen.INFO;
-        k = Project.NameKey.parse(rest);
-      }
-      return toProjectAdmin(k, panel);
-    }
-
-    return null;
-  }
-
-  private static String legacySettings(String token) {
-    int c = token.indexOf(',');
-    if (0 < c) {
-      return "/" + token.substring(0, c) + "/" + token.substring(c + 1);
-    }
-    return null;
-  }
-
   private static void query(String token) {
     String s = skip(token);
     int c = s.indexOf(',');
@@ -622,10 +487,7 @@
   }
 
   private static boolean preferUnified() {
-    return Gerrit.isSignedIn()
-        && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
-            .getGeneralPreferences()
-            .getDiffView());
+    return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView());
   }
 
   private static void unified(final String token,
@@ -669,6 +531,14 @@
           return new MyPreferencesScreen();
         }
 
+        if (matchExact(SETTINGS_DIFF_PREFERENCES, token)) {
+          return new MyDiffPreferencesScreen();
+        }
+
+        if (matchExact(SETTINGS_EDIT_PREFERENCES, token)) {
+          return new MyEditPreferencesScreen();
+        }
+
         if (matchExact(SETTINGS_PROJECTS, token)) {
           return new MyWatchedProjectsScreen();
         }
@@ -681,6 +551,11 @@
           return new MySshKeysScreen();
         }
 
+        if (matchExact(SETTINGS_GPGKEYS, token)
+            && Gerrit.info().gerrit().editGpgKeys()) {
+          return new MyGpgKeysScreen();
+        }
+
         if (matchExact(SETTINGS_WEBIDENT, token)) {
           return new MyIdentitiesScreen();
         }
@@ -695,7 +570,7 @@
         }
 
         if (matchExact(SETTINGS_AGREEMENTS, token)
-            && Gerrit.getConfig().isUseContributorAgreements()) {
+            && Gerrit.info().auth().useContributorAgreements()) {
           return new MyAgreementsScreen();
         }
 
@@ -707,16 +582,28 @@
           return new RegisterScreen("/" + skip(token));
         }
 
-        if (matchPrefix("/VE/", token) || matchPrefix("VE,", token))
+        if (matchPrefix("/VE/", token) || matchPrefix("VE,", token)) {
           return new ValidateEmailScreen(skip(token));
+        }
 
-        if (matchExact(SETTINGS_NEW_AGREEMENT, token))
+        if (matchExact(SETTINGS_NEW_AGREEMENT, token)) {
           return new NewAgreementScreen();
+        }
 
         if (matchPrefix(SETTINGS_NEW_AGREEMENT + "/", token)) {
           return new NewAgreementScreen(skip(token));
         }
 
+        if (matchPrefix(SETTINGS_EXTENSION, token)) {
+          ExtensionSettingsScreen view =
+              new ExtensionSettingsScreen(skip(token));
+          if (view.isFound()) {
+            return view;
+          } else {
+            return new NotFoundScreen();
+          }
+        }
+
         return new NotFoundScreen();
       }
     });
@@ -829,6 +716,8 @@
               Gerrit.display(token, new AccountGroupInfoScreen(group, token));
             } else if (AccountGroupScreen.MEMBERS.equals(panel)) {
               Gerrit.display(token, new AccountGroupMembersScreen(group, token));
+            } else if (AccountGroupScreen.AUDIT_LOG.equals(panel)) {
+              Gerrit.display(token, new AccountGroupAuditLogScreen(group, token));
             } else {
               Gerrit.display(token, new NotFoundScreen());
             }
@@ -858,11 +747,16 @@
             return new ProjectInfoScreen(k);
           }
 
-          if (ProjectScreen.BRANCH.equals(panel)
-              || matchPrefix(ProjectScreen.BRANCH, panel)) {
+          if (ProjectScreen.BRANCHES.equals(panel)
+              || matchPrefix(ProjectScreen.BRANCHES, panel)) {
             return new ProjectBranchesScreen(k);
           }
 
+          if (ProjectScreen.TAGS.equals(panel)
+              || matchPrefix(ProjectScreen.TAGS, panel)) {
+            return new ProjectTagsScreen(k);
+          }
+
           if (ProjectScreen.ACCESS.equals(panel)) {
             return new ProjectAccessScreen(k);
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 5b51b48..c77b71f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -14,108 +14,42 @@
 
 package com.google.gerrit.client;
 
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.NumberFormat;
 
 import java.util.Date;
 
 /** Misc. formatting functions. */
 public class FormatUtil {
-  private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
+  private static DateFormatter dateFormatter;
 
-  private static DateTimeFormat sTime;
-  private static DateTimeFormat sDate;
-  private static DateTimeFormat sdtFmt;
-  private static DateTimeFormat mDate;
-  private static DateTimeFormat dtfmt;
-
-  public static void setPreferences(AccountGeneralPreferences pref) {
-    if (pref == null) {
-      if (Gerrit.isSignedIn()) {
-        pref = Gerrit.getUserAccount().getGeneralPreferences();
-      } else {
-        pref = new AccountGeneralPreferences();
-        pref.resetToDefaults();
-      }
-    }
-
-    String fmt_sTime = pref.getTimeFormat().getFormat();
-    String fmt_sDate = pref.getDateFormat().getShortFormat();
-    String fmt_mDate = pref.getDateFormat().getLongFormat();
-
-    sTime = DateTimeFormat.getFormat(fmt_sTime);
-    sDate = DateTimeFormat.getFormat(fmt_sDate);
-    sdtFmt = DateTimeFormat.getFormat(fmt_sDate + " " + fmt_sTime);
-    mDate = DateTimeFormat.getFormat(fmt_mDate);
-    dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
+  public static void setPreferences(AccountPreferencesInfo prefs) {
+    dateFormatter = new DateFormatter(prefs);
   }
 
   /** Format a date using a really short format. */
   public static String shortFormat(Date dt) {
-    if (dt == null) {
-      return "";
-    }
-
     ensureInited();
-    final Date now = new Date();
-    dt = new Date(dt.getTime());
-    if (mDate.format(now).equals(mDate.format(dt))) {
-      // Same day as today, report only the time.
-      //
-      return sTime.format(dt);
-
-    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
-      // Within the last year, show a shorter date.
-      //
-      return sDate.format(dt);
-
-    } else {
-      // Report only date and year, its far away from now.
-      //
-      return mDate.format(dt);
-    }
+    return dateFormatter.shortFormat(dt);
   }
 
   /** Format a date using a really short format. */
   public static String shortFormatDayTime(Date dt) {
-    if (dt == null) {
-      return "";
-    }
-
     ensureInited();
-    final Date now = new Date();
-    dt = new Date(dt.getTime());
-    if (mDate.format(now).equals(mDate.format(dt))) {
-      // Same day as today, report only the time.
-      //
-      return sTime.format(dt);
-
-    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
-      // Within the last year, show a shorter date.
-      //
-      return sdtFmt.format(dt);
-
-    } else {
-      // Report only date and year, its far away from now.
-      //
-      return mDate.format(dt);
-    }
+    return dateFormatter.shortFormatDayTime(dt);
   }
 
   /** Format a date using the locale's medium length format. */
-  public static String mediumFormat(final Date dt) {
-    if (dt == null) {
-      return "";
-    }
+  public static String mediumFormat(Date dt) {
     ensureInited();
-    return dtfmt.format(new Date(dt.getTime()));
+    return dateFormatter.mediumFormat(dt);
   }
 
   private static void ensureInited() {
-    if (dtfmt == null) {
-      setPreferences(null);
+    if (dateFormatter == null) {
+      setPreferences(Gerrit.getUserPreferences());
     }
   }
 
@@ -136,18 +70,7 @@
    * </ul>
    */
   public static String nameEmail(AccountInfo info) {
-    String name = info.name();
-    if (name == null || name.trim().isEmpty()) {
-      name = Gerrit.getConfig().getAnonymousCowardName();
-    }
-
-    StringBuilder b = new StringBuilder().append(name);
-    if (info.email() != null) {
-      b.append(" <").append(info.email()).append(">");
-    } else if (info._account_id() > 0) {
-      b.append(" (").append(info._account_id()).append(")");
-    }
-    return b.toString();
+    return createAccountFormatter().nameEmail(info);
   }
 
   /**
@@ -156,26 +79,8 @@
    * If the account has a full name, it returns only the full name. Otherwise it
    * returns a longer form that includes the email address.
    */
-  public static String name(Account acct) {
-    return name(asInfo(acct));
-  }
-
-  /**
-   * Formats an account name.
-   * <p>
-   * If the account has a full name, it returns only the full name. Otherwise it
-   * returns a longer form that includes the email address.
-   */
-  public static String name(AccountInfo ai) {
-    if (ai.name() != null && !ai.name().trim().isEmpty()) {
-      return ai.name();
-    }
-    String email = ai.email();
-    if (email != null) {
-      int at = email.indexOf('@');
-      return 0 < at ? email.substring(0, at) : email;
-    }
-    return nameEmail(ai);
+  public static String name(AccountInfo info) {
+    return createAccountFormatter().name(info);
   }
 
   public static AccountInfo asInfo(Account acct) {
@@ -199,4 +104,34 @@
         acct.getPreferredEmail(),
         acct.getUsername());
   }
+
+  private static AccountFormatter createAccountFormatter() {
+    return new AccountFormatter(Gerrit.info().user().anonymousCowardName());
+  }
+
+  /** The returned format string doesn't contain any +/- sign. */
+  public static String formatAbsBytes(long bytes) {
+    return formatBytes(bytes, true);
+  }
+
+  public static String formatBytes(long bytes) {
+    return formatBytes(bytes, false);
+  }
+
+  private static String formatBytes(long bytes, boolean abs) {
+    bytes = abs ? Math.abs(bytes) : bytes;
+
+    if (bytes == 0) {
+      return abs ? "0 B" : "+/- 0 B";
+    }
+
+    if (Math.abs(bytes) < 1024) {
+      return (bytes > 0 && !abs ? "+" : "") + bytes + " B";
+    }
+
+    int exp = (int) (Math.log(Math.abs(bytes)) / Math.log(1024));
+    return (bytes > 0 && !abs ? "+" : "")
+        + NumberFormat.getFormat("#.0").format(bytes / Math.pow(1024, exp))
+        + " " + "KMGTPE".charAt(exp - 1) + "iB";
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 296f93f..560fa9e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -17,20 +17,25 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS;
+import static com.google.gerrit.common.data.HostPageData.XSRF_COOKIE_NAME;
 
 import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.account.AccountCapabilities;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.account.Preferences;
+import com.google.gerrit.client.account.EditPreferences;
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.api.ApiGlue;
 import com.google.gerrit.client.api.PluginLoader;
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
 import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.extensions.TopMenu;
-import com.google.gerrit.client.extensions.TopMenuItem;
-import com.google.gerrit.client.extensions.TopMenuList;
+import com.google.gerrit.client.documentation.DocInfo;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.AuthInfo;
+import com.google.gerrit.client.info.ServerInfo;
+import com.google.gerrit.client.info.TopMenu;
+import com.google.gerrit.client.info.TopMenuItem;
+import com.google.gerrit.client.info.TopMenuList;
 import com.google.gerrit.client.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -41,15 +46,11 @@
 import com.google.gerrit.client.ui.ProjectLinkMenuItem;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.GerritConfig;
-import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GerritTopMenu;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
@@ -65,6 +66,11 @@
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
 import com.google.gwt.http.client.URL;
 import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.user.client.Command;
@@ -101,15 +107,21 @@
       GWT.create(GerritResources.class);
   public static final SystemInfoService SYSTEM_SVC;
   public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
-  public static Themer THEMER = GWT.create(Themer.class);
+  public static final Themer THEMER = GWT.create(Themer.class);
   public static final String PROJECT_NAME_MENU_VAR = "${projectName}";
+  public static final String INDEX = "Documentation/index.html";
 
   private static String myHost;
-  private static GerritConfig myConfig;
+  private static ServerInfo myServerInfo;
+  private static AccountInfo myAccount;
+  private static AccountPreferencesInfo myPrefs;
+  private static UrlAliasMatcher urlAliasMatcher;
+  private static boolean hasDocumentation;
+  private static String docUrl;
   private static HostPageData.Theme myTheme;
-  private static Account myAccount;
   private static String defaultScreenToken;
-  private static AccountDiffPreference myAccountDiffPref;
+  private static DiffPreferencesInfo myAccountDiffPref;
+  private static EditPreferencesInfo editPrefs;
   private static String xGerritAuth;
   private static boolean isNoteDbEnabled;
 
@@ -266,9 +278,7 @@
 
   public static void setHeaderVisible(boolean visible) {
     topMenu.setVisible(visible);
-    siteHeader.setVisible(visible && (myAccount != null
-        ? myAccount.getGeneralPreferences().isShowSiteHeader()
-        : true));
+    siteHeader.setVisible(visible && getUserPreferences().showSiteHeader());
   }
 
   public static boolean isHeaderVisible() {
@@ -284,13 +294,12 @@
   }
 
   /** Get the public configuration data used by this Gerrit instance. */
-  public static GerritConfig getConfig() {
-    return myConfig;
+  public static ServerInfo info() {
+    return myServerInfo;
   }
 
-  public static GitwebLink getGitwebLink() {
-    GitwebConfig gw = getConfig().getGitwebLink();
-    return gw != null && gw.type != null ? new GitwebLink(gw) : null;
+  public static UrlAliasMatcher getUrlAliasMatcher() {
+    return urlAliasMatcher;
   }
 
   /** Site theme information (site specific colors)/ */
@@ -298,14 +307,9 @@
     return myTheme;
   }
 
-  /** @return the currently signed in user's account data; null if no account */
-  public static Account getUserAccount() {
-    return myAccount;
-  }
-
   /** @return the currently signed in user's account data; empty account data if no account */
-  public static AccountInfo getUserAccountInfo() {
-    return FormatUtil.asInfo(myAccount);
+  public static AccountInfo getUserAccount() {
+    return myAccount;
   }
 
   /** @return access token to prove user identity during REST API calls. */
@@ -313,18 +317,32 @@
     return xGerritAuth;
   }
 
-  /** @return the currently signed in users's diff preferences; null if no diff preferences defined for the account */
-  public static AccountDiffPreference getAccountDiffPreference() {
+  /** @return the preferences of the currently signed in user, the default preferences if not signed in */
+  public static AccountPreferencesInfo getUserPreferences() {
+    return myPrefs;
+  }
+
+  /** @return the currently signed in users's diff preferences, or default values */
+  public static DiffPreferencesInfo getDiffPreferences() {
     return myAccountDiffPref;
   }
 
-  public static void setAccountDiffPreference(AccountDiffPreference accountDiffPref) {
+  public static void setDiffPreferences(DiffPreferencesInfo accountDiffPref) {
     myAccountDiffPref = accountDiffPref;
   }
 
+  /** @return the edit preferences of the current user, null if not signed-in */
+  public static EditPreferencesInfo getEditPreferences() {
+    return editPrefs;
+  }
+
+  public static void setEditPreferences(EditPreferencesInfo p) {
+    editPrefs = p;
+  }
+
   /** @return true if the user is currently authenticated */
   public static boolean isSignedIn() {
-    return getUserAccount() != null;
+    return xGerritAuth != null;
   }
 
   /** Sign the user into the application. */
@@ -383,8 +401,11 @@
   }
 
   static void deleteSessionCookie() {
-    myAccount = null;
+    myAccount = AccountInfo.create(0, null, null, null);
     myAccountDiffPref = null;
+    editPrefs = null;
+    myPrefs = AccountPreferencesInfo.createDefault();
+    urlAliasMatcher.clearUserAliases();
     xGerritAuth = null;
     refreshMenuBar();
 
@@ -394,9 +415,26 @@
     Cookies.removeCookie("GerritAccount");
   }
 
+  private void setXsrfToken() {
+    xGerritAuth = Cookies.getCookie(XSRF_COOKIE_NAME);
+    Cookies.removeCookie(XSRF_COOKIE_NAME);
+    JsonUtil.setDefaultXsrfManager(new XsrfManager() {
+      @Override
+      public String getToken(JsonDefTarget proxy) {
+        return xGerritAuth;
+      }
+
+      @Override
+      public void setToken(JsonDefTarget proxy, String token) {
+        // Ignore the request, we always rely upon the cookie.
+      }
+    });
+  }
+
   @Override
   public void onModuleLoad() {
     UserAgent.assertNotInIFrame();
+    setXsrfToken();
 
     KeyUtil.setEncoderImpl(new KeyUtil.Encoder() {
       @Override
@@ -426,25 +464,74 @@
     initHostname();
     Window.setTitle(M.windowTitle1(myHost));
 
-    final HostPageDataService hpd = GWT.create(HostPageDataService.class);
-    hpd.load(new GerritCallback<HostPageData>() {
+    RpcStatus.INSTANCE = new RpcStatus();
+    CallbackGroup cbg = new CallbackGroup();
+    getDocIndex(cbg.add(new GerritCallback<DocInfo>() {
+      @Override
+      public void onSuccess(DocInfo indexInfo) {
+        hasDocumentation = indexInfo != null;
+        docUrl = selfRedirect("/Documentation/");
+      }
+    }));
+    ConfigServerApi.serverInfo(cbg.add(new GerritCallback<ServerInfo>() {
+      @Override
+      public void onSuccess(ServerInfo info) {
+        myServerInfo = info;
+        urlAliasMatcher = new UrlAliasMatcher(info.urlAliases());
+        String du = info.gerrit().docUrl();
+        if (du != null && !du.isEmpty()) {
+          hasDocumentation = true;
+          docUrl = du;
+        }
+      }
+    }));
+    HostPageDataService hpd = GWT.create(HostPageDataService.class);
+    hpd.load(cbg.addFinal(new GerritCallback<HostPageData>() {
       @Override
       public void onSuccess(final HostPageData result) {
         Document.get().getElementById("gerrit_hostpagedata").removeFromParent();
-        myConfig = result.config;
         myTheme = result.theme;
         isNoteDbEnabled = result.isNoteDbEnabled;
-        if (result.account != null) {
-          myAccount = result.account;
-          xGerritAuth = result.xGerritAuth;
-        }
         if (result.accountDiffPref != null) {
           myAccountDiffPref = result.accountDiffPref;
-          applyUserPreferences();
         }
-        onModuleLoad2(result);
+        if (result.accountDiffPref != null) {
+          // TODO: Support options on the GetDetail REST endpoint so that it can
+          // also return the preferences. Then we can fetch everything with a
+          // single request and we don't need the callback group anymore.
+          CallbackGroup cbg = new CallbackGroup();
+          AccountApi.self().view("detail")
+              .get(cbg.add(new GerritCallback<AccountInfo>() {
+                @Override
+                public void onSuccess(AccountInfo result) {
+                  myAccount = result;
+                }
+          }));
+          AccountApi.self().view("preferences")
+              .get(cbg.add(new GerritCallback<AccountPreferencesInfo>() {
+            @Override
+            public void onSuccess(AccountPreferencesInfo prefs) {
+              myPrefs = prefs;
+              onModuleLoad2(result);
+            }
+          }));
+          AccountApi.getEditPreferences(
+              cbg.addFinal(new GerritCallback<EditPreferences>() {
+            @Override
+            public void onSuccess(EditPreferences prefs) {
+              EditPreferencesInfo prefsInfo = new EditPreferencesInfo();
+              prefs.copyTo(prefsInfo);
+              editPrefs = prefsInfo;
+            }
+          }));
+        } else {
+          myAccount = AccountInfo.create(0, null, null, null);
+          myPrefs = AccountPreferencesInfo.createDefault();
+          editPrefs = null;
+          onModuleLoad2(result);
+        }
       }
-    });
+    }));
   }
 
   private static void initHostname() {
@@ -471,9 +558,9 @@
 
     btmmenu.add(new InlineHTML(M.poweredBy(vs)));
 
-    String reportBugUrl = getConfig().getReportBugUrl();
+    String reportBugUrl = info().gerrit().reportBugUrl();
     if (reportBugUrl != null) {
-      String reportBugText = getConfig().getReportBugText();
+      String reportBugText = info().gerrit().reportBugText();
       Anchor a = new Anchor(
           reportBugText == null ? C.reportBug() : reportBugText,
           reportBugUrl);
@@ -538,20 +625,8 @@
     };
     gBody.add(body);
 
-    RpcStatus.INSTANCE = new RpcStatus();
     JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
     JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
-    JsonUtil.setDefaultXsrfManager(new XsrfManager() {
-      @Override
-      public String getToken(JsonDefTarget proxy) {
-        return xGerritAuth;
-      }
-
-      @Override
-      public void setToken(JsonDefTarget proxy, String token) {
-        // Ignore the request, we always rely upon the cookie.
-      }
-    });
 
     gStarting.getElement().getParentElement().removeChild(
         gStarting.getElement());
@@ -560,7 +635,7 @@
 
     applyUserPreferences();
     populateBottomMenu(bottomMenu, hpd);
-    refreshMenuBar(false);
+    refreshMenuBar();
 
     History.addValueChangeHandler(new ValueChangeHandler<String>() {
       @Override
@@ -574,12 +649,9 @@
     if (hpd.messages != null) {
       new MessageOfTheDayBar(hpd.messages).show();
     }
-    CallbackGroup cbg = new CallbackGroup();
-    if (isSignedIn()) {
-      AccountApi.self().view("preferences").get(cbg.add(createMyMenuBarCallback()));
-    }
     PluginLoader.load(hpd.plugins,
-        cbg.addFinal(new GerritCallback<VoidResult>() {
+        hpd.pluginsLoadTimeout,
+        new GerritCallback<VoidResult>() {
           @Override
           public void onSuccess(VoidResult result) {
             String token = History.getToken();
@@ -590,7 +662,7 @@
             }
             display(token);
           }
-        }));
+        });
   }
 
   private void saveDefaultTheme() {
@@ -600,17 +672,13 @@
   }
 
   public static void refreshMenuBar() {
-    refreshMenuBar(true);
-  }
-
-  private static void refreshMenuBar(boolean populateMyMenu) {
     menuLeft.clear();
     menuRight.clear();
 
     menuBars = new HashMap<>();
 
-    final boolean signedIn = isSignedIn();
-    final GerritConfig cfg = getConfig();
+    boolean signedIn = isSignedIn();
+    AuthInfo authInfo = info().auth();
     LinkMenuBar m;
 
     m = new LinkMenuBar();
@@ -623,9 +691,22 @@
     if (signedIn) {
       LinkMenuBar myBar = new LinkMenuBar();
       menuBars.put(GerritTopMenu.MY.menuName, myBar);
-      if (populateMyMenu) {
-        AccountApi.self().view("preferences").get(createMyMenuBarCallback());
+
+      if (myPrefs.my() != null) {
+        myBar.clear();
+        String url = null;
+        List<TopMenuItem> myMenuItems = Natives.asList(myPrefs.my());
+        if (!myMenuItems.isEmpty()) {
+          if (myMenuItems.get(0).getUrl().startsWith("#")) {
+            url = myMenuItems.get(0).getUrl().substring(1);
+          }
+          for (TopMenuItem item : myMenuItems) {
+            addExtensionLink(myBar, item);
+          }
+        }
+        defaultScreenToken = url;
       }
+
       menuLeft.add(myBar, C.menuMine());
       menuLeft.selectTab(1);
     } else {
@@ -645,7 +726,8 @@
     menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsInfo(), ProjectScreen.INFO));
-    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCH));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsBranches(), ProjectScreen.BRANCHES));
+    projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsTags(), ProjectScreen.TAGS));
     projectsBar.addItem(new ProjectLinkMenuItem(C.menuProjectsAccess(), ProjectScreen.ACCESS));
     final LinkMenuItem dashboardsMenuItem =
         new ProjectLinkMenuItem(C.menuProjectsDashboards(),
@@ -691,7 +773,7 @@
       }, CREATE_PROJECT, CREATE_GROUP, VIEW_PLUGINS);
     }
 
-    if (getConfig().isDocumentationAvailable()) {
+    if (hasDocumentation) {
       m = new LinkMenuBar();
       menuBars.put(GerritTopMenu.DOCUMENTATION.menuName, m);
       addDocLink(m, C.menuDocumentationTOC(), "index.html");
@@ -704,9 +786,9 @@
     }
 
     if (signedIn) {
-      whoAmI(cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP);
+      whoAmI(!authInfo.isClientSslCertLdap());
     } else {
-      switch (cfg.getAuthType()) {
+      switch (authInfo.authType()) {
         case CLIENT_SSL_CERT_LDAP:
           break;
 
@@ -749,18 +831,22 @@
 
         case HTTP:
         case HTTP_LDAP:
-          if (cfg.getLoginUrl() != null) {
-            final String signinText = cfg.getLoginText() == null ? C.menuSignIn() : cfg.getLoginText();
-            menuRight.add(anchor(signinText, cfg.getLoginUrl()));
+          if (authInfo.loginUrl() != null) {
+            String signinText = authInfo.loginText() == null
+                ? C.menuSignIn()
+                : authInfo.loginText();
+            menuRight.add(anchor(signinText, authInfo.loginUrl()));
           }
           break;
 
         case LDAP:
         case LDAP_BIND:
         case CUSTOM_EXTENSION:
-          if (cfg.getRegisterUrl() != null) {
-            final String registerText = cfg.getRegisterText() == null ? C.menuRegister() : cfg.getRegisterText();
-            menuRight.add(anchor(registerText, cfg.getRegisterUrl()));
+          if (authInfo.registerUrl() != null) {
+            String registerText = authInfo.registerText() == null
+                ? C.menuRegister()
+                : authInfo.registerText();
+            menuRight.add(anchor(registerText, authInfo.registerUrl()));
           }
           menuRight.addItem(C.menuSignIn(), new Command() {
             @Override
@@ -796,43 +882,71 @@
     });
   }
 
-  private static AsyncCallback<Preferences> createMyMenuBarCallback() {
-    return new GerritCallback<Preferences>() {
-      @Override
-      public void onSuccess(Preferences prefs) {
-        LinkMenuBar myBar = menuBars.get(GerritTopMenu.MY.menuName);
-        myBar.clear();
-        List<TopMenuItem> myMenuItems = Natives.asList(prefs.my());
-        String url = null;
-        if (!myMenuItems.isEmpty()) {
-          if (myMenuItems.get(0).getUrl().startsWith("#")) {
-            url = myMenuItems.get(0).getUrl().substring(1);
-          }
-          for (TopMenuItem item : myMenuItems) {
-            addExtensionLink(myBar, item);
-          }
-        }
-        defaultScreenToken = url;
-      }
-    };
+  public static void refreshUserPreferences() {
+    if (isSignedIn()) {
+      AccountApi.self().view("preferences")
+          .get(new GerritCallback<AccountPreferencesInfo>() {
+            @Override
+            public void onSuccess(AccountPreferencesInfo prefs) {
+              setUserPreferences(prefs);
+            }
+          });
+    } else {
+      setUserPreferences(AccountPreferencesInfo.createDefault());
+    }
   }
 
-  public static void applyUserPreferences() {
-    if (myAccount != null) {
-      final AccountGeneralPreferences p = myAccount.getGeneralPreferences();
-      CopyableLabel.setFlashEnabled(p.isUseFlashClipboard());
-      if (siteHeader != null) {
-        siteHeader.setVisible(p.isShowSiteHeader());
+  public static void setUserPreferences(AccountPreferencesInfo prefs) {
+    myPrefs = prefs;
+    applyUserPreferences();
+    refreshMenuBar();
+  }
+
+  private static void applyUserPreferences() {
+    CopyableLabel.setFlashEnabled(myPrefs.useFlashClipboard());
+    if (siteHeader != null) {
+      siteHeader.setVisible(myPrefs.showSiteHeader());
+    }
+    if (siteFooter != null) {
+      siteFooter.setVisible(myPrefs.showSiteHeader());
+    }
+    FormatUtil.setPreferences(myPrefs);
+    urlAliasMatcher.updateUserAliases(myPrefs.urlAliases());
+  }
+
+  private static void getDocIndex(final AsyncCallback<DocInfo> cb) {
+    RequestBuilder req =
+        new RequestBuilder(RequestBuilder.HEAD, GWT.getHostPageBaseURL()
+            + INDEX);
+    req.setCallback(new RequestCallback() {
+      @Override
+      public void onResponseReceived(Request req, Response resp) {
+        switch (resp.getStatusCode()) {
+          case Response.SC_OK:
+          case Response.SC_MOVED_PERMANENTLY:
+          case Response.SC_MOVED_TEMPORARILY:
+            cb.onSuccess(DocInfo.create());
+            break;
+          default:
+            cb.onSuccess(null);
+            break;
+        }
       }
-      if (siteFooter != null) {
-        siteFooter.setVisible(p.isShowSiteHeader());
+
+      @Override
+      public void onError(Request request, Throwable e) {
+        cb.onFailure(e);
       }
-      FormatUtil.setPreferences(myAccount.getGeneralPreferences());
+    });
+    try {
+      req.send();
+    } catch (RequestException e) {
+      cb.onFailure(e);
     }
   }
 
   private static void whoAmI(boolean canLogOut) {
-    AccountInfo account = getUserAccountInfo();
+    AccountInfo account = getUserAccount();
     final UserPopupPanel userPopup =
         new UserPopupPanel(account, canLogOut, true);
     final FlowPanel userSummaryPanel = new FlowPanel();
@@ -951,7 +1065,7 @@
 
   private static void addDocLink(final LinkMenuBar m, final String text,
       final String href) {
-    final Anchor atag = anchor(text, selfRedirect("/Documentation/" + href));
+    final Anchor atag = anchor(text, docUrl + href);
     atag.setTarget("_blank");
     m.add(atag);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 6bbc8f1..269999c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -77,6 +77,7 @@
   String menuProjectsList();
   String menuProjectsInfo();
   String menuProjectsBranches();
+  String menuProjectsTags();
   String menuProjectsAccess();
   String menuProjectsDashboards();
   String menuProjectsCreate();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 05de983..fb74506 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -60,6 +60,7 @@
 menuProjectsList = List
 menuProjectsInfo = General
 menuProjectsBranches = Branches
+menuProjectsTags = Tags
 menuProjectsAccess = Access
 menuProjectsDashboards = Dashboards
 menuProjectsCreate = Create New Project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 2844b5e..7735e1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -17,8 +17,6 @@
 import com.google.gwt.resources.client.CssResource;
 
 public interface GerritCss extends CssResource {
-  String accountContactOnFile();
-  String accountContactPrivacyDetails();
   String accountDashboard();
   String accountInfoBlock();
   String accountLinkPanel();
@@ -31,6 +29,7 @@
   String addWatchPanel();
   String avatarInfoPanel();
   String bottomheader();
+  String branchTableDeleteButton();
   String branchTablePrevNextLinks();
   String cAPPROVAL();
   String cLastUpdate();
@@ -98,6 +97,7 @@
   String errorDialogErrorType();
   String errorDialogGlass();
   String errorDialogTitle();
+  String extensionPanel();
   String loadingPluginsDialog();
   String fileColumnHeader();
   String fileCommentBorder();
@@ -142,6 +142,8 @@
   String needsReview();
   String negscore();
   String noborder();
+  String nowrap();
+  String pagingLink();
   String patchBrowserPopup();
   String patchBrowserPopupBody();
   String patchCellReverseDiff();
@@ -183,6 +185,7 @@
   String sshHostKeyPanelKnownHostEntry();
   String sshKeyPanelEncodedKey();
   String sshKeyPanelInvalid();
+  String sshKeyTable();
   String stringListPanelButtons();
   String topMostCell();
   String topmenu();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index 4a97c81..2832d41 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -1,6 +1,6 @@
 windowTitle1 = {0} Code Review
 windowTitle2 = {0} | {1} Code Review
-poweredBy = Powered by <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ({0})
+poweredBy = Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a> ({0})
 
 noSuchAccountMessage = {0} is not a registered user.
 noSuchGroupMessage = Group {0} does not exist or is not visible to you.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
deleted file mode 100644
index e32a602..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) 2008 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.client;
-
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
-import com.google.gerrit.common.data.GitWebType;
-import com.google.gerrit.common.data.ParameterizedString;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.http.client.URL;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** Link to an external gitweb server. */
-public class GitwebLink {
-  protected String baseUrl;
-
-  protected GitWebType type;
-
-  public GitwebLink(com.google.gerrit.common.data.GitwebConfig link) {
-    baseUrl = link.baseUrl;
-    type = link.type;
-  }
-
-  /**
-   * Can we link to a patch set if it's a draft
-   *
-   * @param ps Patch set to check draft status
-   * @return true if it's not a draft, or we can link to drafts
-   */
-  public boolean canLink(final PatchSet ps) {
-    return !ps.isDraft() || type.getLinkDrafts();
-  }
-
-  public boolean canLink(RevisionInfo revision) {
-    return revision.draft() || type.getLinkDrafts();
-  }
-
-  public String getLinkName() {
-    return "(" + type.getLinkName() + ")";
-  }
-
-  public String toRevision(String  project, String commit) {
-    ParameterizedString pattern = new ParameterizedString(type.getRevision());
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(project));
-    p.put("commit", encode(commit));
-    return baseUrl + pattern.replace(p);
-  }
-
-  public String toRevision(final Project.NameKey project, final PatchSet ps) {
-    return toRevision(project.get(), ps.getRevision().get());
-  }
-
-  public String toProject(final Project.NameKey project) {
-    ParameterizedString pattern = new ParameterizedString(type.getProject());
-
-    final Map<String, String> p = new HashMap<>();
-    p.put("project", encode(project.get()));
-    return baseUrl + pattern.replace(p);
-  }
-
-  public String toBranch(final Branch.NameKey branch) {
-    ParameterizedString pattern = new ParameterizedString(type.getBranch());
-
-    final Map<String, String> p = new HashMap<>();
-    p.put("project", encode(branch.getParentKey().get()));
-    p.put("branch", encode(branch.get()));
-    return baseUrl + pattern.replace(p);
-  }
-
-  public String toFile(String  project, String commit, String file) {
-    Map<String, String> p = new HashMap<>();
-    p.put("project", encode(project));
-    p.put("commit", encode(commit));
-    p.put("file", encode(file));
-
-    ParameterizedString pattern = (file == null || file.isEmpty())
-        ? new ParameterizedString(type.getRootTree())
-        : new ParameterizedString(type.getFile());
-    return baseUrl + pattern.replace(p);
-  }
-
-  public String toFileHistory(final Branch.NameKey branch, final String file) {
-    ParameterizedString pattern = new ParameterizedString(type.getFileHistory());
-
-    final Map<String, String> p = new HashMap<>();
-    p.put("project", encode(branch.getParentKey().get()));
-    p.put("branch", encode(branch.get()));
-    p.put("file", encode(file));
-    return baseUrl + pattern.replace(p);
-  }
-
-  private String encode(String segment) {
-    if (type.isUrlEncode()) {
-      return URL.encodeQueryString(type.replacePathSeparator(segment));
-    } else {
-      return segment;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
index ff50ec6..36d08b1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     .popup {
       position: fixed;
       top: 5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index f86e1fe..45b1d52 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -56,11 +56,12 @@
 
     final SuggestBox suggestBox =
         new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
-    searchBox.setStyleName("gwt-TextBox");
+    searchBox.setStyleName("searchTextBox");
     searchBox.setVisibleLength(70);
     searchBox.setHintText(Gerrit.C.searchHint());
 
     final Button searchButton = new Button(Gerrit.C.searchButton());
+    searchButton.setStyleName("searchButton");
     searchButton.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 0e1c375..afbaf85 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -72,27 +72,35 @@
     suggestions.add("owner:");
     suggestions.add("owner:self");
     suggestions.add("ownerin:");
+    suggestions.add("author:");
+    suggestions.add("committer:");
 
     suggestions.add("reviewer:");
     suggestions.add("reviewer:self");
     suggestions.add("reviewerin:");
+    suggestions.add("reviewedby:");
 
     suggestions.add("commit:");
     suggestions.add("comment:");
+    suggestions.add("message:");
+    suggestions.add("commentby:");
+    suggestions.add("from:");
+    suggestions.add("file:");
     suggestions.add("conflicts:");
     suggestions.add("project:");
     suggestions.add("projects:");
     suggestions.add("parentproject:");
     suggestions.add("branch:");
     suggestions.add("topic:");
+    suggestions.add("intopic:");
     suggestions.add("ref:");
     suggestions.add("tr:");
     suggestions.add("bug:");
     suggestions.add("label:");
-    suggestions.add("message:");
-    suggestions.add("file:");
+    suggestions.add("query:");
     suggestions.add("has:");
     suggestions.add("has:draft");
+    suggestions.add("has:edit");
     suggestions.add("has:star");
 
     suggestions.add("is:");
@@ -105,7 +113,6 @@
     suggestions.add("is:pending");
     suggestions.add("is:draft");
     suggestions.add("is:closed");
-    suggestions.add("is:submitted");
     suggestions.add("is:merged");
     suggestions.add("is:abandoned");
     suggestions.add("is:mergeable");
@@ -114,7 +121,6 @@
     suggestions.add("status:open");
     suggestions.add("status:pending");
     suggestions.add("status:reviewed");
-    suggestions.add("status:submitted");
     suggestions.add("status:closed");
     suggestions.add("status:merged");
     suggestions.add("status:abandoned");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
new file mode 100644
index 0000000..f4ba870
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2015 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.client;
+
+import com.google.gwt.regexp.shared.RegExp;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class UrlAliasMatcher {
+  private final Map<RegExp, String> userUrlAliases;
+  private final Map<RegExp, String> globalUrlAliases;
+
+  UrlAliasMatcher(Map<String, String> globalUrlAliases) {
+    this.globalUrlAliases = compile(globalUrlAliases);
+    this.userUrlAliases = new HashMap<>();
+  }
+
+  private static Map<RegExp, String> compile(Map<String, String> urlAliases) {
+    Map<RegExp, String> compiledUrlAliases = new HashMap<>();
+    if (urlAliases != null) {
+      for (Map.Entry<String, String> e : urlAliases.entrySet()) {
+        compiledUrlAliases.put(RegExp.compile(e.getKey()), e.getValue());
+      }
+    }
+    return compiledUrlAliases;
+  }
+
+  void clearUserAliases() {
+    this.userUrlAliases.clear();
+  }
+
+  void updateUserAliases(Map<String, String> userUrlAliases) {
+    clearUserAliases();
+    this.userUrlAliases.putAll(compile(userUrlAliases));
+  }
+
+  public String replace(String token) {
+    for (Map.Entry<RegExp, String> e : userUrlAliases.entrySet()) {
+      RegExp pat = e.getKey();
+      if (pat.exec(token) != null) {
+        return pat.replace(token, e.getValue());
+      }
+    }
+
+    for (Map.Entry<RegExp, String> e : globalUrlAliases.entrySet()) {
+      RegExp pat = e.getKey();
+      if (pat.exec(token) != null) {
+        return pat.replace(token, e.getValue());
+      }
+    }
+    return token;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 1857043..0924796 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.client;
 
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.AnchorElement;
 import com.google.gwt.dom.client.Element;
@@ -51,10 +50,10 @@
       userEmail.setText(account.email());
     }
     if (showSettingsLink) {
-      if (Gerrit.getConfig().getSwitchAccountUrl() != null) {
-        switchAccount.setHref(Gerrit.getConfig().getSwitchAccountUrl());
-      } else if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT
-          || Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+      if (Gerrit.info().auth().switchAccountUrl() != null) {
+        switchAccount.setHref(Gerrit.info().auth().switchAccountUrl());
+      } else if (Gerrit.info().auth().isDev()
+          || Gerrit.info().auth().isOpenId()) {
         switchAccount.setHref(Gerrit.selfRedirect("/login"));
       } else {
         switchAccount.removeFromParent();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
index cd51485..8f5073d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
@@ -19,7 +19,7 @@
   xmlns:g='urn:import:com.google.gwt.user.client.ui'
   xmlns:gerrit='urn:import:com.google.gerrit.client'
   xmlns:u='urn:import:com.google.gerrit.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     .panel {
       padding: 8px;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 59d65f6..a1bcfe8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -15,11 +15,16 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import java.util.Set;
@@ -33,6 +38,17 @@
     return new RestApi("/accounts/").view("self");
   }
 
+  /** Retrieve the account edit preferences */
+  public static void getEditPreferences(AsyncCallback<EditPreferences> cb) {
+    self().view("preferences.edit").get(cb);
+  }
+
+  /** Put the account edit preferences */
+  public static void putEditPreferences(EditPreferences in,
+      AsyncCallback<VoidResult> cb) {
+    self().view("preferences.edit").put(in, cb);
+  }
+
   public static void suggest(String query, int limit,
       AsyncCallback<JsArray<AccountInfo>> cb) {
     new RestApi("/accounts/")
@@ -52,6 +68,14 @@
     new RestApi("/accounts/").id(account).view("username").get(cb);
   }
 
+  /** Set the username */
+  public static void setUsername(String account, String username,
+      AsyncCallback<NativeString> cb) {
+    UsernameInput input = UsernameInput.create();
+    input.username(username);
+    new RestApi("/accounts/").id(account).view("username").put(input, cb);
+  }
+
   /** Retrieve email addresses */
   public static void getEmails(String account,
       AsyncCallback<JsArray<EmailInfo>> cb) {
@@ -127,4 +151,53 @@
     protected HttpPasswordInput() {
     }
   }
+
+  private static class UsernameInput extends JavaScriptObject {
+    final native void username(String u) /*-{ if(u)this.username=u; }-*/;
+
+    static UsernameInput create() {
+      return createObject().cast();
+    }
+
+    protected UsernameInput() {
+    }
+  }
+
+  public static void addGpgKey(String account, String armored,
+      AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
+    new RestApi("/accounts/")
+      .id(account)
+      .view("gpgkeys")
+      .post(GpgKeysInput.add(armored), cb);
+  }
+
+  public static void deleteGpgKeys(String account,
+      Iterable<String> fingerprints, AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
+    new RestApi("/accounts/")
+      .id(account)
+      .view("gpgkeys")
+      .post(GpgKeysInput.delete(fingerprints), cb);
+  }
+
+  private static class GpgKeysInput extends JavaScriptObject {
+    static GpgKeysInput add(String key) {
+      return createWithAdd(Natives.arrayOf(key));
+    }
+
+    static GpgKeysInput delete(Iterable<String> fingerprints) {
+      return createWithDelete(Natives.arrayOf(fingerprints));
+    }
+
+    private static native GpgKeysInput createWithAdd(JsArrayString keys) /*-{
+      return {'add': keys};
+    }-*/;
+
+    private static native GpgKeysInput createWithDelete(
+        JsArrayString fingerprints) /*-{
+      return {'delete': fingerprints};
+    }-*/;
+
+    protected GpgKeysInput() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 4c3cc29..94884fa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -50,14 +50,17 @@
   String myMenuReset();
 
   String tabAccountSummary();
-  String tabPreferences();
-  String tabWatchedProjects();
-  String tabContactInformation();
-  String tabSshKeys();
-  String tabHttpAccess();
-  String tabWebIdentities();
-  String tabMyGroups();
   String tabAgreements();
+  String tabContactInformation();
+  String tabDiffPreferences();
+  String tabEditPreferences();
+  String tabGpgKeys();
+  String tabHttpAccess();
+  String tabMyGroups();
+  String tabPreferences();
+  String tabSshKeys();
+  String tabWatchedProjects();
+  String tabWebIdentities();
 
   String buttonShowAddSshKey();
   String buttonCloseAddSshKey();
@@ -94,6 +97,10 @@
   String sshHostKeyFingerprint();
   String sshHostKeyKnownHostEntry();
 
+  String gpgKeyId();
+  String gpgKeyFingerprint();
+  String gpgKeyUserIds();
+
   String webIdStatus();
   String webIdEmail();
   String webIdIdentity();
@@ -119,11 +126,6 @@
 
   String contactFieldFullName();
   String contactFieldEmail();
-  String contactPrivacyDetailsHtml();
-  String contactFieldAddress();
-  String contactFieldCountry();
-  String contactFieldPhone();
-  String contactFieldFax();
   String buttonOpenRegisterNewEmail();
   String buttonSendRegisterNewEmail();
   String buttonCancel();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index fe09af5..0944448 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -33,14 +33,17 @@
 myMenuReset = Reset
 
 tabAccountSummary = Profile
-tabPreferences = Preferences
-tabWatchedProjects = Watched Projects
-tabContactInformation = Contact Information
-tabSshKeys = SSH Public Keys
-tabHttpAccess = HTTP Password
-tabWebIdentities = Identities
-tabMyGroups = Groups
 tabAgreements = Agreements
+tabContactInformation = Contact Information
+tabDiffPreferences = Diff Preferences
+tabEditPreferences = Edit Preferences
+tabGpgKeys = GPG Public Keys
+tabHttpAccess = HTTP Password
+tabMyGroups = Groups
+tabPreferences = Preferences
+tabSshKeys = SSH Public Keys
+tabWatchedProjects = Watched Projects
+tabWebIdentities = Identities
 
 buttonShowAddSshKey = Add Key ...
 buttonCloseAddSshKey = Close
@@ -70,6 +73,10 @@
 sshHostKeyFingerprint = Fingerprint:
 sshHostKeyKnownHostEntry = Entry for <code>~/.ssh/known_hosts</code>:
 
+gpgKeyId = ID
+gpgKeyFingerprint = Fingerprint
+gpgKeyUserIds = User IDs
+
 webIdStatus = Status
 webIdEmail = Email Address
 webIdIdentity = Identity
@@ -120,18 +127,6 @@
 
 contactFieldFullName = Full Name
 contactFieldEmail = Preferred Email
-contactPrivacyDetailsHtml = \
-  <b>The following offline contact information is stored encrypted.</b><br />\
-  <br />\
-  Contact information will only be made available to administrators if it is \
-  necessary to reach you through non-email based communication.  Received data \
-  is stored encrypted with a strong public/private key pair algorithm, and \
-  this site does not have the private key.  Once saved, you will be unable to \
-  retrieve previously stored contact details.
-contactFieldAddress = Mailing Address
-contactFieldCountry = Country
-contactFieldPhone = Phone Number
-contactFieldFax = Fax Number
 buttonOpenRegisterNewEmail = Register New Email ...
 buttonSendRegisterNewEmail = Register
 buttonCancel = Cancel
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
index e55be79..68a99e0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
@@ -16,12 +16,9 @@
 
 import com.google.gwt.i18n.client.Messages;
 
-import java.util.Date;
-
 public interface AccountMessages extends Messages {
   String lines(short cnt);
   String rowsPerPage(short cnt);
   String changeScreenServerDefault(String d);
   String enterIAGREE(String iagree);
-  String contactOnFile(Date lastDate);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
index 994c236..a8d61cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
@@ -1,7 +1,4 @@
 lines = {0} lines
 rowsPerPage = {0} rows per page
-
 changeScreenServerDefault = Server Default ({0})
-
 enterIAGREE = (enter {0} in the box to the left)
-contactOnFile = Contact information last updated on {0,date,medium} at {0,time,short}.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
deleted file mode 100644
index 4b8f0e2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright (C) 2008 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.client.account;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-import java.sql.Timestamp;
-import java.util.Date;
-
-class ContactPanelFull extends ContactPanelShort {
-  private Label hasContact;
-  private NpTextArea addressTxt;
-  private NpTextBox countryTxt;
-  private NpTextBox phoneTxt;
-  private NpTextBox faxTxt;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    addressTxt = new NpTextArea();
-    addressTxt.setVisibleLines(4);
-    addressTxt.setCharacterWidth(60);
-
-    countryTxt = new NpTextBox();
-    countryTxt.setVisibleLength(40);
-    countryTxt.setMaxLength(40);
-
-    phoneTxt = new NpTextBox();
-    phoneTxt.setVisibleLength(30);
-    phoneTxt.setMaxLength(30);
-
-    faxTxt = new NpTextBox();
-    faxTxt.setVisibleLength(30);
-    faxTxt.setMaxLength(30);
-
-    final Grid infoSecure = new Grid(4, 2);
-    infoSecure.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    infoSecure.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
-
-    final HTML privhtml = new HTML(Util.C.contactPrivacyDetailsHtml());
-    privhtml.setStyleName(Gerrit.RESOURCES.css().accountContactPrivacyDetails());
-
-    hasContact = new Label();
-    hasContact.setStyleName(Gerrit.RESOURCES.css().accountContactOnFile());
-    hasContact.setVisible(false);
-
-    if (Gerrit.getConfig().isUseContactInfo()) {
-      body.add(privhtml);
-      body.add(hasContact);
-      body.add(infoSecure);
-    }
-
-    row(infoSecure, 0, Util.C.contactFieldAddress(), addressTxt);
-    row(infoSecure, 1, Util.C.contactFieldCountry(), countryTxt);
-    row(infoSecure, 2, Util.C.contactFieldPhone(), phoneTxt);
-    row(infoSecure, 3, Util.C.contactFieldFax(), faxTxt);
-
-    infoSecure.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    infoSecure.getCellFormatter().addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    infoSecure.getCellFormatter().addStyleName(3, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    final OnEditEnabler sbl = new OnEditEnabler(save);
-    sbl.listenTo(addressTxt);
-    sbl.listenTo(countryTxt);
-    sbl.listenTo(phoneTxt);
-    sbl.listenTo(faxTxt);
-  }
-
-  @Override
-  protected void display(final Account userAccount) {
-    super.display(userAccount);
-    displayHasContact(userAccount);
-    addressTxt.setText("");
-    countryTxt.setText("");
-    phoneTxt.setText("");
-    faxTxt.setText("");
-  }
-
-  private void displayHasContact(final Account userAccount) {
-    if (userAccount.isContactFiled()) {
-      final Timestamp dt = userAccount.getContactFiledOn();
-      hasContact.setText(Util.M.contactOnFile(new Date(dt.getTime())));
-      hasContact.setVisible(true);
-    } else {
-      hasContact.setVisible(false);
-    }
-  }
-
-  @Override
-  void onSaveSuccess(final Account userAccount) {
-    super.onSaveSuccess(userAccount);
-    displayHasContact(userAccount);
-  }
-
-  @Override
-  ContactInformation toContactInformation() {
-    final ContactInformation info;
-    if (Gerrit.getConfig().isUseContactInfo()) {
-      info = new ContactInformation();
-      info.setAddress(addressTxt.getText());
-      info.setCountry(countryTxt.getText());
-      info.setPhoneNumber(phoneTxt.getText());
-      info.setFaxNumber(faxTxt.getText());
-    } else {
-      info = null;
-    }
-    return info;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index b5fe70f..901fe96 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.ErrorDialog;
+import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.OnEditEnabler;
@@ -23,8 +25,6 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
@@ -48,7 +48,8 @@
 
 class ContactPanelShort extends Composite {
   protected final FlowPanel body;
-  protected int labelIdx, fieldIdx;
+  protected int labelIdx;
+  protected int fieldIdx;
   protected Button save;
 
   private String currentEmail;
@@ -100,8 +101,8 @@
     }
 
     int row = 0;
-    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
-        && Gerrit.getConfig().siteHasUsernames()) {
+    if (!Gerrit.info().auth().canEdit(FieldName.USER_NAME)
+        && Gerrit.info().auth().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
     }
@@ -109,12 +110,12 @@
     if (!canEditFullName()) {
       FlowPanel nameLine = new FlowPanel();
       nameLine.add(nameTxt);
-      if (Gerrit.getConfig().getEditFullNameUrl() != null) {
+      if (Gerrit.info().auth().editFullNameUrl() != null) {
         Button edit = new Button(Util.C.linkEditFullName());
         edit.addClickHandler(new ClickHandler() {
           @Override
           public void onClick(ClickEvent event) {
-            Window.open(Gerrit.getConfig().getEditFullNameUrl(), "_blank", null);
+            Window.open(Gerrit.info().auth().editFullNameUrl(), "_blank", null);
           }
         });
         nameLine.add(edit);
@@ -167,11 +168,11 @@
   }
 
   private boolean canEditFullName() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.FULL_NAME);
+    return Gerrit.info().auth().canEdit(Account.FieldName.FULL_NAME);
   }
 
   private boolean canRegisterNewEmail() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.REGISTER_NEW_EMAIL);
+    return Gerrit.info().auth().canEdit(Account.FieldName.REGISTER_NEW_EMAIL);
   }
 
   void hideSaveButton() {
@@ -195,11 +196,11 @@
 
     Util.ACCOUNT_SVC.myAccount(new GerritCallback<Account>() {
       @Override
-      public void onSuccess(final Account result) {
+      public void onSuccess(Account result) {
         if (!isAttached()) {
           return;
         }
-        display(result);
+        display(FormatUtil.asInfo(result));
         haveAccount = true;
         postLoad();
       }
@@ -237,9 +238,9 @@
     info.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header());
   }
 
-  protected void display(final Account userAccount) {
-    currentEmail = userAccount.getPreferredEmail();
-    nameTxt.setText(userAccount.getFullName());
+  protected void display(AccountInfo account) {
+    currentEmail = account.email();
+    nameTxt.setText(account.name());
     save.setEnabled(false);
     new OnEditEnabler(save, nameTxt);
   }
@@ -274,11 +275,11 @@
           @Override
           public void onSuccess(EmailInfo result) {
             box.hide();
-            if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+            if (Gerrit.info().auth().isDev()) {
               currentEmail = addr;
               if (emailPick.getItemCount() == 0) {
-                final Account me = Gerrit.getUserAccount();
-                me.setPreferredEmail(addr);
+                AccountInfo me = Gerrit.getUserAccount();
+                me.email(addr);
                 onSaveSuccess(me);
               } else {
                 save.setEnabled(true);
@@ -324,7 +325,7 @@
     buttons.add(register);
     buttons.add(cancel);
 
-    if (Gerrit.getConfig().getAuthType() != AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+    if (!Gerrit.info().auth().isDev()) {
       body.add(new HTML(Util.C.descRegisterNewEmail()));
     }
     body.add(inEmail);
@@ -354,16 +355,15 @@
       newEmail = currentEmail;
     }
 
-    final ContactInformation info = toContactInformation();
     save.setEnabled(false);
     registerNewEmail.setEnabled(false);
 
-    Util.ACCOUNT_SEC.updateContact(newName, newEmail, info,
+    Util.ACCOUNT_SEC.updateContact(newName, newEmail,
         new GerritCallback<Account>() {
           @Override
-          public void onSuccess(final Account result) {
+          public void onSuccess(Account result) {
             registerNewEmail.setEnabled(true);
-            onSaveSuccess(result);
+            onSaveSuccess(FormatUtil.asInfo(result));
             if (onSave != null) {
               onSave.onSuccess(result);
             }
@@ -378,18 +378,14 @@
         });
   }
 
-  void onSaveSuccess(final Account result) {
-    final Account me = Gerrit.getUserAccount();
-    me.setFullName(result.getFullName());
-    me.setPreferredEmail(result.getPreferredEmail());
+  void onSaveSuccess(AccountInfo result) {
+    AccountInfo me = Gerrit.getUserAccount();
+    me.name(result.name());
+    me.email(result.email());
     Gerrit.refreshMenuBar();
     display(me);
   }
 
-  ContactInformation toContactInformation() {
-    return null;
-  }
-
   private int emailListIndexOf(String value) {
     for (int i = 0; i < emailPick.getItemCount(); i++) {
       if (value.equalsIgnoreCase(emailPick.getValue(i))) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
index 292e0b9..daf1181 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
@@ -14,70 +14,119 @@
 
 package com.google.gerrit.client.account;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gwt.core.client.JavaScriptObject;
 
 public class DiffPreferences extends JavaScriptObject {
-  public static DiffPreferences create(AccountDiffPreference in) {
-    DiffPreferences p = createObject().cast();
+  public static DiffPreferences create(DiffPreferencesInfo in) {
     if (in == null) {
-      in = AccountDiffPreference.createDefault(null);
+      in = DiffPreferencesInfo.defaults();
     }
-    p.ignoreWhitespace(in.getIgnoreWhitespace());
-    p.tabSize(in.getTabSize());
-    p.lineLength(in.getLineLength());
-    p.context(in.getContext());
-    p.intralineDifference(in.isIntralineDifference());
-    p.showLineEndings(in.isShowLineEndings());
-    p.showTabs(in.isShowTabs());
-    p.showWhitespaceErrors(in.isShowWhitespaceErrors());
-    p.syntaxHighlighting(in.isSyntaxHighlighting());
-    p.hideTopMenu(in.isHideTopMenu());
-    p.autoHideDiffTableHeader(in.isAutoHideDiffTableHeader());
-    p.hideLineNumbers(in.isHideLineNumbers());
-    p.expandAllComments(in.isExpandAllComments());
-    p.manualReview(in.isManualReview());
-    p.renderEntireFile(in.isRenderEntireFile());
-    p.theme(in.getTheme());
-    p.hideEmptyPane(in.isHideEmptyPane());
+    DiffPreferences p = createObject().cast();
+    p.ignoreWhitespace(in.ignoreWhitespace);
+    p.tabSize(in.tabSize);
+    p.lineLength(in.lineLength);
+    p.cursorBlinkRate(in.cursorBlinkRate);
+    p.context(in.context);
+    p.intralineDifference(in.intralineDifference);
+    p.showLineEndings(in.showLineEndings);
+    p.showTabs(in.showTabs);
+    p.showWhitespaceErrors(in.showWhitespaceErrors);
+    p.syntaxHighlighting(in.syntaxHighlighting);
+    p.hideTopMenu(in.hideTopMenu);
+    p.autoHideDiffTableHeader(in.autoHideDiffTableHeader);
+    p.hideLineNumbers(in.hideLineNumbers);
+    p.expandAllComments(in.expandAllComments);
+    p.manualReview(in.manualReview);
+    p.renderEntireFile(in.renderEntireFile);
+    p.theme(in.theme);
+    p.hideEmptyPane(in.hideEmptyPane);
+    p.retainHeader(in.retainHeader);
+    p.skipUncommented(in.skipUncommented);
+    p.skipDeleted(in.skipDeleted);
+    p.matchBrackets(in.matchBrackets);
+    p.lineWrapping(in.lineWrapping);
     return p;
   }
 
-  public final void copyTo(AccountDiffPreference p) {
-    p.setIgnoreWhitespace(ignoreWhitespace());
-    p.setTabSize(tabSize());
-    p.setLineLength(lineLength());
-    p.setContext((short)context());
-    p.setIntralineDifference(intralineDifference());
-    p.setShowLineEndings(showLineEndings());
-    p.setShowTabs(showTabs());
-    p.setShowWhitespaceErrors(showWhitespaceErrors());
-    p.setSyntaxHighlighting(syntaxHighlighting());
-    p.setHideTopMenu(hideTopMenu());
-    p.setAutoHideDiffTableHeader(autoHideDiffTableHeader());
-    p.setHideLineNumbers(hideLineNumbers());
-    p.setExpandAllComments(expandAllComments());
-    p.setManualReview(manualReview());
-    p.setRenderEntireFile(renderEntireFile());
-    p.setTheme(theme());
-    p.setHideEmptyPane(hideEmptyPane());
+  public final void copyTo(DiffPreferencesInfo p) {
+    p.context = context();
+    p.tabSize = tabSize();
+    p.lineLength = lineLength();
+    p.cursorBlinkRate = cursorBlinkRate();
+    p.expandAllComments = expandAllComments();
+    p.intralineDifference = intralineDifference();
+    p.manualReview = manualReview();
+    p.retainHeader = retainHeader();
+    p.showLineEndings = showLineEndings();
+    p.showTabs = showTabs();
+    p.showWhitespaceErrors = showWhitespaceErrors();
+    p.skipDeleted = skipDeleted();
+    p.skipUncommented = skipUncommented();
+    p.syntaxHighlighting = syntaxHighlighting();
+    p.hideTopMenu = hideTopMenu();
+    p.autoHideDiffTableHeader = autoHideDiffTableHeader();
+    p.hideLineNumbers = hideLineNumbers();
+    p.renderEntireFile = renderEntireFile();
+    p.hideEmptyPane = hideEmptyPane();
+    p.matchBrackets = matchBrackets();
+    p.lineWrapping = lineWrapping();
+    p.theme = theme();
+    p.ignoreWhitespace = ignoreWhitespace();
   }
 
   public final void ignoreWhitespace(Whitespace i) {
     setIgnoreWhitespaceRaw(i.toString());
   }
-  private final native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
 
   public final void theme(Theme i) {
     setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
   }
-  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+
+  public final void showLineNumbers(boolean s) {
+    hideLineNumbers(!s);
+  }
+
+  public final Whitespace ignoreWhitespace() {
+    String s = ignoreWhitespaceRaw();
+    return s != null ? Whitespace.valueOf(s) : Whitespace.IGNORE_NONE;
+  }
+
+  public final Theme theme() {
+    String s = themeRaw();
+    return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
+  }
+
+  public final int tabSize() {
+    return get("tab_size", 8);
+  }
+
+  public final int context() {
+    return get("context", 10);
+  }
+
+  public final int lineLength() {
+    return get("line_length", 100);
+  }
+
+  public final int cursorBlinkRate() {
+    return get("cursor_blink_rate", 0);
+  }
+
+  public final boolean showLineNumbers() {
+    return !hideLineNumbers();
+  }
+
+  public final boolean autoReview() {
+    return !manualReview();
+  }
 
   public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
   public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
   public final native void context(int c) /*-{ this.context = c }-*/;
+  public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
   public final native void intralineDifference(boolean i) /*-{ this.intraline_difference = i }-*/;
   public final native void showLineEndings(boolean s) /*-{ this.show_line_endings = s }-*/;
   public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
@@ -89,24 +138,12 @@
   public final native void expandAllComments(boolean e) /*-{ this.expand_all_comments = e }-*/;
   public final native void manualReview(boolean r) /*-{ this.manual_review = r }-*/;
   public final native void renderEntireFile(boolean r) /*-{ this.render_entire_file = r }-*/;
+  public final native void retainHeader(boolean r) /*-{ this.retain_header = r }-*/;
   public final native void hideEmptyPane(boolean s) /*-{ this.hide_empty_pane = s }-*/;
-  public final void showLineNumbers(boolean s) { hideLineNumbers(!s); }
-
-  public final Whitespace ignoreWhitespace() {
-    String s = ignoreWhitespaceRaw();
-    return s != null ? Whitespace.valueOf(s) : Whitespace.IGNORE_NONE;
-  }
-  private final native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
-
-  public final Theme theme() {
-    String s = themeRaw();
-    return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
-  }
-  private final native String themeRaw() /*-{ return this.theme }-*/;
-
-  public final int tabSize() {return get("tab_size", 8); }
-  public final int context() {return get("context", 10); }
-  public final int lineLength() {return get("line_length", 100); }
+  public final native void skipUncommented(boolean s) /*-{ this.skip_uncommented = s }-*/;
+  public final native void skipDeleted(boolean s) /*-{ this.skip_deleted = s }-*/;
+  public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
+  public final native void lineWrapping(boolean w) /*-{ this.line_wrapping = w }-*/;
   public final native boolean intralineDifference() /*-{ return this.intraline_difference || false }-*/;
   public final native boolean showLineEndings() /*-{ return this.show_line_endings || false }-*/;
   public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
@@ -119,11 +156,17 @@
   public final native boolean manualReview() /*-{ return this.manual_review || false }-*/;
   public final native boolean renderEntireFile() /*-{ return this.render_entire_file || false }-*/;
   public final native boolean hideEmptyPane() /*-{ return this.hide_empty_pane || false }-*/;
-  public final boolean showLineNumbers() { return !hideLineNumbers(); }
-  public final boolean autoReview() { return !manualReview(); }
+  public final native boolean retainHeader() /*-{ return this.retain_header || false }-*/;
+  public final native boolean skipUncommented() /*-{ return this.skip_uncommented || false }-*/;
+  public final native boolean skipDeleted() /*-{ return this.skip_deleted || false }-*/;
+  public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
+  public final native boolean lineWrapping() /*-{ return this.line_wrapping || false }-*/;
 
-  private final native int get(String n, int d)
-  /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+  private final native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
+  private final native String ignoreWhitespaceRaw() /*-{ return this.ignore_whitespace }-*/;
+  private final native String themeRaw() /*-{ return this.theme }-*/;
+  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
 
   protected DiffPreferences() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
new file mode 100644
index 0000000..39af4d4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EditPreferences.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2014 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.client.account;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class EditPreferences extends JavaScriptObject {
+  public static EditPreferences create(EditPreferencesInfo in) {
+    EditPreferences p = createObject().cast();
+    p.tabSize(in.tabSize);
+    p.lineLength(in.lineLength);
+    p.cursorBlinkRate(in.cursorBlinkRate);
+    p.hideTopMenu(in.hideTopMenu);
+    p.showTabs(in.showTabs);
+    p.showWhitespaceErrors(in.showWhitespaceErrors);
+    p.syntaxHighlighting(in.syntaxHighlighting);
+    p.hideLineNumbers(in.hideLineNumbers);
+    p.matchBrackets(in.matchBrackets);
+    p.lineWrapping(in.lineWrapping);
+    p.autoCloseBrackets(in.autoCloseBrackets);
+    p.theme(in.theme);
+    p.keyMapType(in.keyMapType);
+    return p;
+  }
+
+  public final void copyTo(EditPreferencesInfo p) {
+    p.tabSize = tabSize();
+    p.lineLength = lineLength();
+    p.cursorBlinkRate = cursorBlinkRate();
+    p.hideTopMenu = hideTopMenu();
+    p.showTabs = showTabs();
+    p.showWhitespaceErrors = showWhitespaceErrors();
+    p.syntaxHighlighting = syntaxHighlighting();
+    p.hideLineNumbers = hideLineNumbers();
+    p.matchBrackets = matchBrackets();
+    p.lineWrapping = lineWrapping();
+    p.autoCloseBrackets = autoCloseBrackets();
+    p.theme = theme();
+    p.keyMapType = keyMapType();
+  }
+
+  public final void theme(Theme i) {
+    setThemeRaw(i != null ? i.toString() : Theme.DEFAULT.toString());
+  }
+  private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
+
+  public final void keyMapType(KeyMapType i) {
+    setkeyMapTypeRaw(i != null ? i.toString() : KeyMapType.DEFAULT.toString());
+  }
+  private final native void setkeyMapTypeRaw(String i) /*-{ this.key_map_type = i }-*/;
+
+  public final native void tabSize(int t) /*-{ this.tab_size = t }-*/;
+  public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
+  public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
+  public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
+  public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
+  public final native void showWhitespaceErrors(boolean s) /*-{ this.show_whitespace_errors = s }-*/;
+  public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
+  public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
+  public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
+  public final native void lineWrapping(boolean w) /*-{ this.line_wrapping = w }-*/;
+  public final native void autoCloseBrackets(boolean c) /*-{ this.auto_close_brackets = c }-*/;
+
+  public final Theme theme() {
+    String s = themeRaw();
+    return s != null ? Theme.valueOf(s) : Theme.DEFAULT;
+  }
+  private final native String themeRaw() /*-{ return this.theme }-*/;
+
+  public final KeyMapType keyMapType() {
+    String s = keyMapTypeRaw();
+    return s != null ? KeyMapType.valueOf(s) : KeyMapType.DEFAULT;
+  }
+  private final native String keyMapTypeRaw() /*-{ return this.key_map_type }-*/;
+
+  public final int tabSize() {
+    return get("tab_size", 8);
+  }
+
+  public final int lineLength() {
+    return get("line_length", 100);
+  }
+
+  public final int cursorBlinkRate() {
+    return get("cursor_blink_rate", 0);
+  }
+
+  public final native boolean hideTopMenu() /*-{ return this.hide_top_menu || false }-*/;
+  public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
+  public final native boolean showWhitespaceErrors() /*-{ return this.show_whitespace_errors || false }-*/;
+  public final native boolean syntaxHighlighting() /*-{ return this.syntax_highlighting || false }-*/;
+  public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
+  public final native boolean matchBrackets() /*-{ return this.match_brackets || false }-*/;
+  public final native boolean lineWrapping() /*-{ return this.line_wrapping || false }-*/;
+  public final native boolean autoCloseBrackets() /*-{ return this.auto_close_brackets || false }-*/;
+  private final native int get(String n, int d) /*-{ return this.hasOwnProperty(n) ? this[n] : d }-*/;
+
+  protected EditPreferences() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 0908f6b..308cf30 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -47,7 +47,7 @@
     });
   }
 
-  private class AgreementTable extends FancyFlexTable<ContributorAgreement> {
+  private static class AgreementTable extends FancyFlexTable<ContributorAgreement> {
     AgreementTable() {
       table.setWidth("");
       table.setText(0, 1, Util.C.agreementStatus());
@@ -61,8 +61,9 @@
     }
 
     void display(final AgreementInfo result) {
-      while (1 < table.getRowCount())
+      while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
+      }
 
       for (final String k : result.accepted) {
         addOne(result, k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
index c542511..4c1016a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyContactInformationScreen.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.client.account;
 
 public class MyContactInformationScreen extends SettingsScreen {
-  private ContactPanelFull panel;
+  private ContactPanelShort panel;
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    panel = new ContactPanelFull() {
+    panel = new ContactPanelShort() {
       @Override
       void display() {
         MyContactInformationScreen.this.display();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
new file mode 100644
index 0000000..a721441
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 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.client.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.diff.PreferencesBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class MyDiffPreferencesScreen extends SettingsScreen {
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    PreferencesBox pb = new PreferencesBox(null);
+    pb.set(DiffPreferences.create(Gerrit.getDiffPreferences()));
+    FlowPanel p = new FlowPanel();
+    p.setStyleName(pb.getStyle().dialog());
+    p.add(pb);
+    add(p);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    display();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java
new file mode 100644
index 0000000..424b5d5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyEditPreferencesScreen.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 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.client.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.editor.EditPreferencesBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class MyEditPreferencesScreen extends SettingsScreen {
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    EditPreferencesBox pb = new EditPreferencesBox(null);
+    pb.set(EditPreferences.create(Gerrit.getEditPreferences()));
+    FlowPanel p = new FlowPanel();
+    p.setStyleName(pb.getStyle().dialog());
+    p.add(pb);
+    add(p);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    display();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
new file mode 100644
index 0000000..99d791b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
@@ -0,0 +1,284 @@
+// Copyright (C) 2015 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.client.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.GpgKeyInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class MyGpgKeysScreen extends SettingsScreen {
+  interface Binder extends UiBinder<HTMLPanel, MyGpgKeysScreen> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true) GpgKeyTable keys;
+  @UiField Button deleteKey;
+  @UiField Button addKey;
+
+  @UiField VerticalPanel addKeyBlock;
+  @UiField NpTextArea keyText;
+
+  @UiField VerticalPanel errorPanel;
+  @UiField Label errorText;
+
+  @UiField Button clearButton;
+  @UiField Button addButton;
+  @UiField Button closeButton;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    keys = new GpgKeyTable();
+    add(uiBinder.createAndBindUi(this));
+    keys.updateDeleteButton();
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    refreshKeys();
+  }
+
+  @UiHandler("deleteKey")
+  void onDeleteKey(@SuppressWarnings("unused") ClickEvent e) {
+    keys.deleteChecked();
+  }
+
+  @UiHandler("addKey")
+  void onAddKey(@SuppressWarnings("unused") ClickEvent e) {
+    showAddKeyBlock(true);
+  }
+
+  @UiHandler("clearButton")
+  void onClearButton(@SuppressWarnings("unused") ClickEvent e) {
+    keyText.setText("");
+    keyText.setFocus(true);
+    errorPanel.setVisible(false);
+  }
+
+  @UiHandler("closeButton")
+  void onCloseButton(@SuppressWarnings("unused") ClickEvent e) {
+    showAddKeyBlock(false);
+  }
+
+  @UiHandler("addButton")
+  void onAddButton(@SuppressWarnings("unused") ClickEvent e) {
+    doAddKey();
+  }
+
+  private void refreshKeys() {
+    AccountApi.self().view("gpgkeys").get(NativeMap.copyKeysIntoChildren("id",
+        new GerritCallback<NativeMap<GpgKeyInfo>>() {
+          @Override
+          public void onSuccess(NativeMap<GpgKeyInfo> result) {
+            List<GpgKeyInfo> list = Natives.asList(result.values());
+            // TODO(dborowitz): Sort on something more meaningful, like
+            // created date?
+            Collections.sort(list, new Comparator<GpgKeyInfo>() {
+              @Override
+              public int compare(GpgKeyInfo a, GpgKeyInfo b) {
+                return a.id().compareTo(b.id());
+              }
+            });
+            keys.clear();
+            keyText.setText("");
+            errorPanel.setVisible(false);
+            addButton.setEnabled(true);
+            if (!list.isEmpty()) {
+              keys.setVisible(true);
+              for (GpgKeyInfo k : list) {
+                keys.addOneKey(k);
+              }
+              showKeyTable(true);
+              showAddKeyBlock(false);
+            } else {
+              keys.setVisible(false);
+              showAddKeyBlock(true);
+              showKeyTable(false);
+            }
+
+            display();
+          }
+        }));
+  }
+
+  private void showAddKeyBlock(boolean show) {
+    addKey.setVisible(!show);
+    addKeyBlock.setVisible(show);
+  }
+
+  private void showKeyTable(boolean show) {
+    keys.setVisible(show);
+    deleteKey.setVisible(show);
+    addKey.setVisible(show);
+  }
+
+  private void doAddKey() {
+    if (keyText.getText().isEmpty()) {
+      return;
+    }
+    addButton.setEnabled(false);
+    keyText.setEnabled(false);
+    AccountApi.addGpgKey("self", keyText.getText(),
+        new AsyncCallback<NativeMap<GpgKeyInfo>>() {
+          @Override
+          public void onSuccess(NativeMap<GpgKeyInfo> result) {
+            keyText.setEnabled(true);
+            refreshKeys();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            keyText.setEnabled(true);
+            addButton.setEnabled(true);
+            if (caught instanceof StatusCodeException) {
+              StatusCodeException sce = (StatusCodeException) caught;
+              if (sce.getStatusCode() == Response.SC_CONFLICT
+                  || sce.getStatusCode() == Response.SC_BAD_REQUEST) {
+                errorText.setText(sce.getEncodedResponse());
+              } else {
+                errorText.setText(sce.getMessage());
+              }
+            } else {
+              errorText.setText(
+                  "Unexpected error saving key: " + caught.getMessage());
+            }
+            errorPanel.setVisible(true);
+          }
+        });
+  }
+
+  private class GpgKeyTable extends FancyFlexTable<GpgKeyInfo> {
+    private final ValueChangeHandler<Boolean> updateDeleteHandler;
+
+    GpgKeyTable() {
+      table.setWidth("");
+      table.setText(0, 1, Util.C.gpgKeyId());
+      table.setText(0, 2, Util.C.gpgKeyFingerprint());
+      table.setText(0, 3, Util.C.gpgKeyUserIds());
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().iconHeader());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+
+      updateDeleteHandler = new ValueChangeHandler<Boolean>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<Boolean> event) {
+          updateDeleteButton();
+        }
+      };
+    }
+
+    private void addOneKey(GpgKeyInfo k) {
+      int row = table.getRowCount();
+      table.insertRow(row);
+      applyDataRowStyle(row);
+
+      CheckBox sel = new CheckBox();
+      sel.addValueChangeHandler(updateDeleteHandler);
+      table.setWidget(row, 0, sel);
+      table.setWidget(row, 1, new CopyableLabel(k.id()));
+      table.setText(row, 2, k.fingerprint());
+
+      VerticalPanel userIds = new VerticalPanel();
+      for (int i = 0; i < k.userIds().length(); i++) {
+        userIds.add(new InlineLabel(k.userIds().get(i)));
+      }
+      table.setWidget(row, 3, userIds);
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell());
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, k);
+    }
+
+    private void updateDeleteButton() {
+      for (int row = 1; row < table.getRowCount(); row++) {
+        if (isChecked(row)) {
+          deleteKey.setEnabled(true);
+          return;
+        }
+      }
+      deleteKey.setEnabled(false);
+    }
+
+    private void deleteChecked() {
+      deleteKey.setEnabled(false);
+      List<String> toDelete = new ArrayList<>(table.getRowCount());
+      for (int row = 1; row < table.getRowCount(); row++) {
+        if (isChecked(row)) {
+          toDelete.add(getRowItem(row).fingerprint());
+        }
+      }
+      AccountApi.deleteGpgKeys("self", toDelete,
+          new GerritCallback<NativeMap<GpgKeyInfo>>() {
+            @Override
+            public void onSuccess(NativeMap<GpgKeyInfo> result) {
+              refreshKeys();
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              deleteKey.setEnabled(true);
+              super.onFailure(caught);
+            }
+          });
+    }
+
+    private boolean isChecked(int row) {
+      return ((CheckBox) table.getWidget(row, 0)).getValue();
+    }
+
+    private void clear() {
+      while (table.getRowCount() > 1) {
+        table.removeRow(1);
+      }
+      for (int i = table.getRowCount() - 1; i >= 1; i++) {
+        table.removeRow(i);
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml
new file mode 100644
index 0000000..dc73736
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2015 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:expui='urn:import:com.google.gwtexpui.globalkey.client'>
+  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+
+  <ui:style gss='false'>
+    .errorHeader {
+      font-weight: bold;
+    }
+    .errorText {
+      white-space: pre-wrap;
+      padding-bottom: 6px;
+    }
+  </ui:style>
+
+  <g:HTMLPanel>
+    <g:Widget ui:field='keys' addStyleNames='{res.css.sshKeyTable}'/>
+    <g:FlowPanel>
+      <g:Button ui:field='deleteKey'>
+        <div><ui:msg>Delete</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='addKey'>
+        <div><ui:msg>Add Key ...</ui:msg></div>
+      </g:Button>
+    </g:FlowPanel>
+    <g:VerticalPanel ui:field='addKeyBlock'
+        styleName='{res.css.addSshKeyPanel}'
+        visible='false'>
+      <g:Label>Add GPG Public Key</g:Label>
+      <g:DisclosurePanel>
+        <g:header>How to generate a GPG key</g:header>
+        <g:HTMLPanel>
+          <ol>
+            <li>
+              From the Terminal or Git Bash, run <em>gpg --gen-key</em> and
+              follow the prompts to create the key.
+            </li>
+            <li>
+              Use the default kind. Use the default (or higher) keysize. Choose
+              any value for your expiration.
+            </li>
+            <li>
+              The user ID should contain one of your registered email addresses.
+            </li>
+            <li>Setting a passphrase is strongly recommended.</li>
+            <li>Note the ID of your new key.</li>
+            <li>
+              To export your key, run the following and paste the full output
+              into the text box:
+              <br/>
+              <code>gpg --export -a &lt;key ID&gt;</code>
+            </li>
+          </ol>
+        </g:HTMLPanel>
+      </g:DisclosurePanel>
+      <expui:NpTextArea
+          visibleLines='12'
+          characterWidth='80'
+          spellCheck='false'
+          ui:field='keyText'/>
+      <g:VerticalPanel ui:field='errorPanel' visible='false'>
+        <g:Label styleName='{style.errorHeader}'>Error adding GPG key:</g:Label>
+        <g:Label styleName='{style.errorText}' ui:field='errorText'/>
+      </g:VerticalPanel>
+      <g:FlowPanel>
+        <g:Button ui:field='clearButton'>
+          <div><ui:msg>Clear</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='addButton'>
+          <div><ui:msg>Add</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='closeButton'>
+          <div><ui:msg>Close</ui:msg></div>
+        </g:Button>
+      </g:FlowPanel>
+    </g:VerticalPanel>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index f83e9ec..f6ac36a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -36,6 +36,7 @@
       protected void preDisplay(GroupList result) {
         groups.display(result);
         groups.finishDisplay();
-      }});
+      }
+    });
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index 1191696..b638575 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
@@ -59,8 +58,8 @@
     });
     add(deleteIdentity);
 
-    if (Gerrit.getConfig().getAuthType() == AuthType.OPENID
-        || Gerrit.getConfig().getAuthType() == AuthType.OAUTH) {
+    if (Gerrit.info().auth().isOpenId()
+        || Gerrit.info().auth().isOAuth()) {
       Button linkIdentity = new Button(Util.C.buttonLinkIdentity());
       linkIdentity.addClickHandler(new ClickHandler() {
         @Override
@@ -180,8 +179,9 @@
         }
       });
 
-      while (1 < table.getRowCount())
+      while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
+      }
 
       for (final AccountExternalId k : result) {
         addOneId(k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
index 72ea795..01772f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GerritUiExtensionPoint;
 import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.api.ExtensionPanel;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
@@ -40,7 +42,7 @@
   protected void onInitUI() {
     super.onInitUI();
 
-    String url = Gerrit.getConfig().getHttpPasswordUrl();
+    String url = Gerrit.info().auth().httpPasswordUrl();
     if (url != null) {
       Anchor link = new Anchor();
       link.setText(Util.C.linkObtainPassword());
@@ -91,6 +93,10 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    ExtensionPanel extensionPanel =
+        createExtensionPoint(GerritUiExtensionPoint.PASSWORD_SCREEN_BOTTOM);
+    extensionPanel.addStyleName(Gerrit.RESOURCES.css().extensionPanel());
+    add(extensionPanel);
 
     if (password == null) {
       display();
@@ -101,14 +107,14 @@
     AccountApi.getUsername("self", new GerritCallback<NativeString>() {
       @Override
       public void onSuccess(NativeString user) {
-        Gerrit.getUserAccount().setUserName(user.asString());
+        Gerrit.getUserAccount().username(user.asString());
         refreshHttpPassword();
       }
 
       @Override
       public void onFailure(final Throwable caught) {
         if (RestApi.isNotFound(caught)) {
-          Gerrit.getUserAccount().setUserName(null);
+          Gerrit.getUserAccount().username(null);
           display();
         } else {
           super.onFailure(caught);
@@ -158,7 +164,7 @@
   }
 
   private void doGeneratePassword() {
-    if (Gerrit.getUserAccount().getUserName() != null) {
+    if (Gerrit.getUserAccount().username() != null) {
       enableUI(false);
       AccountApi.generateHttpPassword("self",
           new GerritCallback<NativeString>() {
@@ -176,7 +182,7 @@
   }
 
   private void doClearPassword() {
-    if (Gerrit.getUserAccount().getUserName() != null) {
+    if (Gerrit.getUserAccount().username() != null) {
       enableUI(false);
       AccountApi.clearHttpPassword("self",
           new GerritCallback<VoidResult>() {
@@ -194,7 +200,7 @@
   }
 
   private void enableUI(boolean on) {
-    on &= Gerrit.getUserAccount().getUserName() != null;
+    on &= Gerrit.getUserAccount().username() != null;
 
     generatePassword.setEnabled(on);
     clearPassword.setVisible(on && !"".equals(password.getText()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index a64ba57..2b20ad6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -18,9 +18,12 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.PAGESIZE_CHOICES;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GerritUiExtensionPoint;
 import com.google.gerrit.client.StringListPanel;
+import com.google.gerrit.client.api.ExtensionPanel;
 import com.google.gerrit.client.config.ConfigServerApi;
-import com.google.gerrit.client.extensions.TopMenuItem;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.TopMenuItem;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
@@ -118,7 +121,8 @@
 
     FlowPanel dateTimePanel = new FlowPanel();
 
-    final int labelIdx, fieldIdx;
+    final int labelIdx;
+    final int fieldIdx;
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
       fieldIdx = 0;
@@ -219,10 +223,15 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    ExtensionPanel extensionPanel =
+        createExtensionPoint(GerritUiExtensionPoint.PREFERENCES_SCREEN_BOTTOM);
+    extensionPanel.addStyleName(Gerrit.RESOURCES.css().extensionPanel());
+    add(extensionPanel);
+
     AccountApi.self().view("preferences")
-        .get(new ScreenLoadCallback<Preferences>(this) {
+        .get(new ScreenLoadCallback<AccountPreferencesInfo>(this) {
       @Override
-      public void preDisplay(Preferences prefs) {
+      public void preDisplay(AccountPreferencesInfo prefs) {
         display(prefs);
       }
     });
@@ -243,7 +252,7 @@
     diffView.setEnabled(on);
   }
 
-  private void display(Preferences p) {
+  private void display(AccountPreferencesInfo p) {
     showSiteHeader.setValue(p.showSiteHeader());
     useFlashClipboard.setValue(p.useFlashClipboard());
     copySelfOnEmails.setValue(p.copySelfOnEmail());
@@ -325,45 +334,44 @@
   }
 
   private void doSave() {
-    final AccountGeneralPreferences p = new AccountGeneralPreferences();
-    p.setShowSiteHeader(showSiteHeader.getValue());
-    p.setUseFlashClipboard(useFlashClipboard.getValue());
-    p.setCopySelfOnEmails(copySelfOnEmails.getValue());
-    p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
-    p.setDateFormat(getListBox(dateFormat,
+    AccountPreferencesInfo p = AccountPreferencesInfo.create();
+    p.showSiteHeader(showSiteHeader.getValue());
+    p.useFlashClipboard(useFlashClipboard.getValue());
+    p.copySelfOnEmail(copySelfOnEmails.getValue());
+    p.changesPerPage(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
+    p.dateFormat(getListBox(dateFormat,
         AccountGeneralPreferences.DateFormat.STD,
         AccountGeneralPreferences.DateFormat.values()));
-    p.setTimeFormat(getListBox(timeFormat,
+    p.timeFormat(getListBox(timeFormat,
         AccountGeneralPreferences.TimeFormat.HHMM_12,
         AccountGeneralPreferences.TimeFormat.values()));
-    p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
-    p.setSizeBarInChangeTable(sizeBarInChangeTable.getValue());
-    p.setLegacycidInChangeTable(legacycidInChangeTable.getValue());
-    p.setMuteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
-    p.setReviewCategoryStrategy(getListBox(reviewCategoryStrategy,
+    p.relativeDateInChangeTable(relativeDateInChangeTable.getValue());
+    p.sizeBarInChangeTable(sizeBarInChangeTable.getValue());
+    p.legacycidInChangeTable(legacycidInChangeTable.getValue());
+    p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
+    p.reviewCategoryStrategy(getListBox(reviewCategoryStrategy,
         ReviewCategoryStrategy.NONE,
         ReviewCategoryStrategy.values()));
-    p.setDiffView(getListBox(diffView,
+    p.diffView(getListBox(diffView,
         AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
         AccountGeneralPreferences.DiffView.values()));
 
-    enable(false);
-    save.setEnabled(false);
-
     List<TopMenuItem> items = new ArrayList<>();
     for (List<String> v : myMenus.getValues()) {
       items.add(TopMenuItem.create(v.get(0), v.get(1)));
     }
+    p.setMyMenus(items);
+
+    enable(false);
+    save.setEnabled(false);
 
     AccountApi.self().view("preferences")
-        .put(Preferences.create(p, items), new GerritCallback<Preferences>() {
+        .put(p, new GerritCallback<AccountPreferencesInfo>() {
           @Override
-          public void onSuccess(Preferences prefs) {
-            Gerrit.getUserAccount().setGeneralPreferences(p);
-            Gerrit.applyUserPreferences();
+          public void onSuccess(AccountPreferencesInfo prefs) {
+            Gerrit.setUserPreferences(prefs);
             enable(true);
             display(prefs);
-            Gerrit.refreshMenuBar();
           }
 
           @Override
@@ -386,13 +394,14 @@
       resetButton.addClickHandler(new ClickHandler() {
         @Override
         public void onClick(ClickEvent event) {
-          ConfigServerApi.defaultPreferences(new GerritCallback<Preferences>() {
-            @Override
-            public void onSuccess(Preferences p) {
-              MyPreferencesScreen.this.display(p.my());
-              widget.setEnabled(true);
-            }
-          });
+          ConfigServerApi.defaultPreferences(
+              new GerritCallback<AccountPreferencesInfo>() {
+                @Override
+                public void onSuccess(AccountPreferencesInfo p) {
+                  MyPreferencesScreen.this.display(p.my());
+                  widget.setEnabled(true);
+                }
+              });
         }
       });
       buttonPanel.add(resetButton);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index 52246b2..0dfea4f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -17,11 +17,11 @@
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
 import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GerritUiExtensionPoint;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
@@ -33,7 +33,8 @@
 public class MyProfileScreen extends SettingsScreen {
   private AvatarImage avatar;
   private Anchor changeAvatar;
-  private int labelIdx, fieldIdx;
+  private int labelIdx;
+  private int fieldIdx;
   private Grid info;
 
   @Override
@@ -43,14 +44,16 @@
     HorizontalPanel h = new HorizontalPanel();
     add(h);
 
-    VerticalPanel v = new VerticalPanel();
-    v.addStyleName(Gerrit.RESOURCES.css().avatarInfoPanel());
-    h.add(v);
-    avatar = new AvatarImage();
-    v.add(avatar);
-    changeAvatar = new Anchor(Util.C.changeAvatar(), "", "_blank");
-    changeAvatar.setVisible(false);
-    v.add(changeAvatar);
+    if (Gerrit.info().plugin().hasAvatars()) {
+      VerticalPanel v = new VerticalPanel();
+      v.addStyleName(Gerrit.RESOURCES.css().avatarInfoPanel());
+      h.add(v);
+      avatar = new AvatarImage();
+      v.add(avatar);
+      changeAvatar = new Anchor(Util.C.changeAvatar(), "", "_blank");
+      changeAvatar.setVisible(false);
+      v.add(changeAvatar);
+    }
 
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
@@ -60,13 +63,13 @@
       fieldIdx = 1;
     }
 
-    info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
+    info = new Grid((Gerrit.info().auth().siteHasUsernames() ? 1 : 0) + 4, 2);
     info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
     h.add(info);
 
     int row = 0;
-    if (Gerrit.getConfig().siteHasUsernames()) {
+    if (Gerrit.info().auth().siteHasUsernames()) {
       infoRow(row++, Util.C.userName());
     }
     infoRow(row++, Util.C.fullName());
@@ -83,6 +86,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    add(createExtensionPoint(GerritUiExtensionPoint.PROFILE_SCREEN_BOTTOM));
     display(Gerrit.getUserAccount());
     display();
   }
@@ -93,28 +97,30 @@
         Gerrit.RESOURCES.css().header());
   }
 
-  void display(final Account account) {
-    avatar.setAccount(FormatUtil.asInfo(account), 93, false);
-    new RestApi("/accounts/").id("self").view("avatar.change.url")
-        .get(new AsyncCallback<NativeString>() {
-          @Override
-          public void onSuccess(NativeString changeUrl) {
-            changeAvatar.setHref(changeUrl.asString());
-            changeAvatar.setVisible(true);
-          }
+  void display(AccountInfo account) {
+    if (Gerrit.info().plugin().hasAvatars()) {
+      avatar.setAccount(account, 93, false);
+      new RestApi("/accounts/").id("self").view("avatar.change.url")
+          .get(new AsyncCallback<NativeString>() {
+            @Override
+            public void onSuccess(NativeString changeUrl) {
+              changeAvatar.setHref(changeUrl.asString());
+              changeAvatar.setVisible(true);
+            }
 
-          @Override
-          public void onFailure(Throwable caught) {
-          }
-        });
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          });
+    }
 
     int row = 0;
-    if (Gerrit.getConfig().siteHasUsernames()) {
+    if (Gerrit.info().auth().siteHasUsernames()) {
       info.setWidget(row++, fieldIdx, new UsernameField());
     }
-    info.setText(row++, fieldIdx, account.getFullName());
-    info.setText(row++, fieldIdx, account.getPreferredEmail());
-    info.setText(row++, fieldIdx, mediumFormat(account.getRegisteredOn()));
+    info.setText(row++, fieldIdx, account.name());
+    info.setText(row++, fieldIdx, account.email());
+    info.setText(row++, fieldIdx, mediumFormat(account.registeredOn()));
     info.setText(row, fieldIdx, account.getId().toString());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index 67f5b4a..08effdc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -113,8 +113,9 @@
   }
 
   public void display(final List<AccountProjectWatchInfo> result) {
-    while (2 < table.getRowCount())
+    while (2 < table.getRowCount()) {
       table.removeRow(table.getRowCount() - 1);
+    }
 
     for (final AccountProjectWatchInfo k : result) {
       final int row = table.getRowCount();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 95ea317..14f8e2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -42,7 +41,6 @@
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
@@ -60,9 +58,6 @@
   private Panel agreementGroup;
   private HTML agreementHtml;
 
-  private Panel contactGroup;
-  private ContactPanelFull contactPanel;
-
   private Panel finalGroup;
   private NpTextBox yesIAgreeBox;
   private Button submit;
@@ -117,11 +112,6 @@
     agreementGroup.add(agreementHtml);
     formBody.add(agreementGroup);
 
-    contactGroup = new FlowPanel();
-    contactGroup
-        .add(new SmallHeading(Util.C.newAgreementReviewContactHeading()));
-    formBody.add(contactGroup);
-
     finalGroup = new VerticalPanel();
     finalGroup.add(new SmallHeading(Util.C.newAgreementCompleteHeading()));
     final FlowPanel fp = new FlowPanel();
@@ -157,7 +147,6 @@
   private void renderSelf() {
     current = null;
     agreementGroup.setVisible(false);
-    contactGroup.setVisible(false);
     finalGroup.setVisible(false);
     radios.clear();
 
@@ -206,21 +195,7 @@
       yesIAgreeBox.setFocus(true);
       return;
     }
-
-    if (contactGroup.isVisible()) {
-      contactPanel.doSave(new AsyncCallback<Account>() {
-        @Override
-        public void onSuccess(Account result) {
-          doEnterAgreement();
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      });
-    } else {
-      doEnterAgreement();
-    }
+    doEnterAgreement();
   }
 
   private void doEnterAgreement() {
@@ -275,13 +250,6 @@
       agreementGroup.setVisible(false);
     }
 
-    if (contactPanel == null && cla.isRequireContactInformation()) {
-      contactPanel = new ContactPanelFull();
-      contactGroup.add(contactPanel);
-      contactPanel.hideSaveButton();
-    }
-    contactGroup.setVisible(
-        cla.isRequireContactInformation() && cla.getAutoVerify() != null);
     finalGroup.setVisible(cla.getAutoVerify() != null);
     yesIAgreeBox.setText("");
     submit.setEnabled(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index 2810931..c32a846 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -56,8 +56,8 @@
     contactGroup.add(whereFrom);
     contactGroup.add(new ContactPanelShort() {
       @Override
-      protected void display(final Account userAccount) {
-        super.display(userAccount);
+      protected void display(AccountInfo account) {
+        super.display(account);
 
         if ("".equals(nameTxt.getText())) {
           // No name? Encourage the user to provide us something.
@@ -69,8 +69,8 @@
     });
     formBody.add(contactGroup);
 
-    if (Gerrit.getUserAccount().getUserName() == null
-        && Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+    if (Gerrit.getUserAccount().username() == null
+        && Gerrit.info().auth().canEdit(FieldName.USER_NAME)) {
       final FlowPanel fp = new FlowPanel();
       fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
@@ -99,7 +99,7 @@
       formBody.add(fp);
     }
 
-    if (Gerrit.getConfig().getSshdAddress() != null) {
+    if (Gerrit.info().hasSshd()) {
       final FlowPanel sshKeyGroup = new FlowPanel();
       sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
@@ -116,7 +116,7 @@
 
     final FlowPanel choices = new FlowPanel();
     choices.setStyleName(Gerrit.RESOURCES.css().registerScreenNextLinks());
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
+    if (Gerrit.info().auth().useContributorAgreements()) {
       final FlowPanel agreementGroup = new FlowPanel();
       agreementGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       agreementGroup.add(new SmallHeading(Util.C.welcomeAgreementHeading()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index c689b49..e8c58ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -15,28 +15,75 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GerritUiExtensionPoint;
+import com.google.gerrit.client.api.ExtensionPanel;
+import com.google.gerrit.client.api.ExtensionSettingsScreen;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.MenuScreen;
 import com.google.gerrit.common.PageLinks;
 
+import java.util.HashSet;
+import java.util.Set;
+
 public abstract class SettingsScreen extends MenuScreen {
+  private final Set<String> allMenuNames;
+  private final Set<String> ambiguousMenuNames;
+
   public SettingsScreen() {
     setRequiresSignIn(true);
 
-    link(Util.C.tabAccountSummary(), PageLinks.SETTINGS);
-    link(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
-    link(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
-    link(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
-    if (Gerrit.getConfig().getSshdAddress() != null) {
-      link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
+    allMenuNames = new HashSet<>();
+    ambiguousMenuNames = new HashSet<>();
+
+    linkByGerrit(Util.C.tabAccountSummary(), PageLinks.SETTINGS);
+    linkByGerrit(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
+    linkByGerrit(Util.C.tabDiffPreferences(), PageLinks.SETTINGS_DIFF_PREFERENCES);
+    linkByGerrit(Util.C.tabEditPreferences(), PageLinks.SETTINGS_EDIT_PREFERENCES);
+    linkByGerrit(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
+    linkByGerrit(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
+    if (Gerrit.info().hasSshd()) {
+      linkByGerrit(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
     }
-    if (Gerrit.getConfig().isHttpPasswordSettingsEnabled()) {
-      link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
+    if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) {
+      linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     }
-    link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
-    link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
-      link(Util.C.tabAgreements(), PageLinks.SETTINGS_AGREEMENTS);
+    if (Gerrit.info().gerrit().editGpgKeys()) {
+      linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS);
     }
+    linkByGerrit(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
+    linkByGerrit(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
+    if (Gerrit.info().auth().useContributorAgreements()) {
+      linkByGerrit(Util.C.tabAgreements(), PageLinks.SETTINGS_AGREEMENTS);
+    }
+
+    for (String pluginName : ExtensionSettingsScreen.Definition.plugins()) {
+      for (ExtensionSettingsScreen.Definition def :
+          Natives.asList(ExtensionSettingsScreen.Definition.get(pluginName))) {
+        if (!allMenuNames.add(def.getMenu())) {
+          ambiguousMenuNames.add(def.getMenu());
+        }
+      }
+    }
+
+    for (String pluginName : ExtensionSettingsScreen.Definition.plugins()) {
+      for (ExtensionSettingsScreen.Definition def :
+          Natives.asList(ExtensionSettingsScreen.Definition.get(pluginName))) {
+        linkByPlugin(pluginName, def.getMenu(),
+            PageLinks.toSettings(pluginName, def.getPath()));
+      }
+    }
+  }
+
+  private void linkByGerrit(String text, String target) {
+    allMenuNames.add(text);
+    link(text, target);
+  }
+
+  private void linkByPlugin(String pluginName, String text, String target) {
+    if (ambiguousMenuNames.contains(text)) {
+      text += " ("+ pluginName + ")";
+    }
+    link(text, target);
   }
 
   @Override
@@ -44,4 +91,12 @@
     super.onInitUI();
     setPageTitle(Util.C.settingsHeading());
   }
+
+  protected ExtensionPanel createExtensionPoint(
+      GerritUiExtensionPoint extensionPoint) {
+    ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint);
+    extensionPanel.putObject(GerritUiExtensionPoint.Key.ACCOUNT_INFO,
+        Gerrit.getUserAccount());
+    return extensionPanel;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
index 0cfc0984..37ad764 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -306,8 +306,9 @@
         setKeyTableVisible(false);
         showAddKeyBlock(true);
       } else {
-        while (1 < table.getRowCount())
+        while (1 < table.getRowCount()) {
           table.removeRow(table.getRowCount() - 1);
+        }
         for (final SshKeyInfo k : result) {
           addOneKey(k);
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index 9975887..f388436 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -19,8 +19,9 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -34,7 +35,6 @@
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 class UsernameField extends Composite {
   private CopyableLabel userNameLbl;
@@ -42,7 +42,7 @@
   private Button setUserName;
 
   UsernameField() {
-    String user = Gerrit.getUserAccount().getUserName();
+    String user = Gerrit.getUserAccount().username();
     userNameLbl = new CopyableLabel(user != null ? user : "");
     userNameLbl.setStyleName(Gerrit.RESOURCES.css().accountUsername());
 
@@ -86,7 +86,7 @@
   }
 
   private boolean canEditUserName() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.USER_NAME);
+    return Gerrit.info().auth().canEdit(Account.FieldName.USER_NAME);
   }
 
   private void confirmSetUserName() {
@@ -114,27 +114,27 @@
     }
     final String newUserName = newName;
 
-    Util.ACCOUNT_SEC.changeUserName(newUserName,
-        new GerritCallback<VoidResult>() {
-          @Override
-          public void onSuccess(final VoidResult result) {
-            Gerrit.getUserAccount().setUserName(newUserName);
-            userNameLbl.setText(newUserName);
-            userNameLbl.setVisible(true);
-            userNameTxt.setVisible(false);
-            setUserName.setVisible(false);
-          }
+    AccountApi.setUsername("self", newUserName,
+        new GerritCallback<NativeString>() {
+      @Override
+      public void onSuccess(NativeString result) {
+        Gerrit.getUserAccount().username(newUserName);
+        userNameLbl.setText(newUserName);
+        userNameLbl.setVisible(true);
+        userNameTxt.setVisible(false);
+        setUserName.setVisible(false);
+      }
 
-          @Override
-          public void onFailure(final Throwable caught) {
-            enableUI(true);
-            if (caught instanceof InvalidUserNameException) {
-              new ErrorDialog(Util.C.invalidUserName()).center();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
+      @Override
+      public void onFailure(Throwable caught) {
+        enableUI(true);
+        if (RestApi.isExpected(422 /* Unprocessable Entity */)) {
+          new ErrorDialog(Util.C.invalidUserName()).center();
+        } else {
+          super.onFailure(caught);
+        }
+      }
+    });
   }
 
   private void enableUI(final boolean on) {
@@ -142,7 +142,7 @@
     setUserName.setEnabled(on);
   }
 
-  private final class UserNameValidator implements KeyPressHandler {
+  private static final class UserNameValidator implements KeyPressHandler {
     @Override
     public void onKeyPress(final KeyPressEvent event) {
       final char code = event.getCharCode();
@@ -178,10 +178,11 @@
         default:
           final TextBox box = (TextBox) event.getSource();
           final String re;
-          if (box.getCursorPos() == 0)
+          if (box.getCursorPos() == 0) {
             re = Account.USER_NAME_PATTERN_FIRST;
-          else
+          } else {
             re = Account.USER_NAME_PATTERN_REST;
+          }
           if (!String.valueOf(code).matches("^" + re + "$")) {
             event.preventDefault();
             event.stopPropagation();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
index 1164efc..b79723b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.config.ConfigServerApi;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 public class ValidateEmailScreen extends AccountScreen {
   private final String magicToken;
@@ -36,7 +37,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SEC.validateEmail(magicToken,
+    ConfigServerApi.confirmEmail(magicToken,
         new ScreenLoadCallback<VoidResult>(this) {
           @Override
           protected void preDisplay(final VoidResult result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
index 2e2d314..8f5d7a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
@@ -19,9 +19,10 @@
 import com.google.gerrit.client.api.EditGlue;
 import com.google.gerrit.client.api.ProjectGlue;
 import com.google.gerrit.client.api.RevisionGlue;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ClickEvent;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 157748f..af0b1f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -163,7 +163,8 @@
       @Override
       public void execute() {
         name.setFocus(true);
-      }});
+      }
+    });
   }
 
   void enableEditing() {
@@ -250,8 +251,7 @@
     if (value.getPermission(permissionName) != null) {
       return;
     }
-    if (Gerrit.getConfig().getWildProject()
-        .equals(projectAccess.getProjectName())
+    if (Gerrit.info().gerrit().isAllProjects(projectAccess.getProjectName())
         && !Permission.canBeOnAllProjects(value.getName(), permissionName)) {
       return;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
index b31e02e..52f3588 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
@@ -24,7 +24,7 @@
   ui:generateLocales='default,en'
   >
 <ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style>
+<ui:style gss='false'>
   @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
   @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
new file mode 100644
index 0000000..254d3e6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2015 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.client.admin;
+
+import static com.google.gerrit.client.FormatUtil.mediumFormat;
+import static com.google.gerrit.client.FormatUtil.name;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.groups.GroupApi;
+import com.google.gerrit.client.groups.GroupAuditEventInfo;
+import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+
+import java.util.List;
+
+public class AccountGroupAuditLogScreen extends AccountGroupScreen {
+  private AuditEventTable auditEventTable;
+
+  public AccountGroupAuditLogScreen(GroupInfo toShow, String token) {
+    super(toShow, token);
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    add(new SmallHeading(Util.C.headingAuditLog()));
+    auditEventTable = new AuditEventTable();
+    add(auditEventTable);
+  }
+
+  @Override
+  protected void display(GroupInfo group, boolean canModify) {
+    GroupApi.getAuditLog(group.getGroupUUID(),
+        new GerritCallback<JsArray<GroupAuditEventInfo>>() {
+          @Override
+          public void onSuccess(JsArray<GroupAuditEventInfo> result) {
+            auditEventTable.display(Natives.asList(result));
+          }
+        });
+  }
+
+  private class AuditEventTable extends FancyFlexTable<GroupAuditEventInfo> {
+    AuditEventTable() {
+      table.setText(0, 1, Util.C.columnDate());
+      table.setText(0, 2, Util.C.columnType());
+      table.setText(0, 3, Util.C.columnMember());
+      table.setText(0, 4, Util.C.columnByUser());
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void display(List<GroupAuditEventInfo> auditEvents) {
+      while (1 < table.getRowCount()) {
+        table.removeRow(table.getRowCount() - 1);
+      }
+
+      for (GroupAuditEventInfo auditEvent : auditEvents) {
+        int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, auditEvent);
+      }
+    }
+
+    void populate(int row, GroupAuditEventInfo auditEvent) {
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      table.setText(row, 1, mediumFormat(auditEvent.date()));
+
+      switch (auditEvent.type()) {
+        case ADD_USER:
+        case ADD_GROUP:
+          table.setText(row, 2, Util.C.typeAdded());
+          break;
+        case REMOVE_USER:
+        case REMOVE_GROUP:
+          table.setText(row, 2, Util.C.typeRemoved());
+          break;
+      }
+
+      switch (auditEvent.type()) {
+        case ADD_USER:
+        case REMOVE_USER:
+          table.setText(row, 3, formatAccount(auditEvent.memberAsUser()));
+          break;
+        case ADD_GROUP:
+        case REMOVE_GROUP:
+          GroupInfo member = auditEvent.memberAsGroup();
+          if (AccountGroup.isInternalGroup(member.getGroupUUID())) {
+            table.setWidget(row, 3,
+                new Hyperlink(member.name(),
+                    Dispatcher.toGroup(member.getGroupUUID())));
+            fmt.getElement(row, 3).setTitle(null);
+          } else if (member.url() != null) {
+            Anchor a = new Anchor();
+            a.setText(member.name());
+            a.setHref(member.url());
+            a.setTitle("UUID " + member.getGroupUUID().get());
+            table.setWidget(row, 3, a);
+            fmt.getElement(row, 3).setTitle(null);
+          } else {
+            table.setText(row, 3, member.name());
+            fmt.getElement(row, 3).setTitle(
+                "UUID " + member.getGroupUUID().get());
+          }
+          break;
+      }
+
+      table.setText(row, 4, formatAccount(auditEvent.user()));
+
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, auditEvent);
+    }
+  }
+
+  private static String formatAccount(AccountInfo account) {
+    StringBuilder b = new StringBuilder();
+    b.append(name(account));
+    b.append(" (");
+    b.append(account._accountId());
+    b.append(")");
+    return b.toString();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index cf3e940..7c0c8f6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.groups.GroupApi;
 import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
@@ -248,7 +248,7 @@
       for (int row = 1; row < table.getRowCount(); row++) {
         final AccountInfo i = getRowItem(row);
         if (i != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(i._account_id());
+          ids.add(i._accountId());
         }
       }
       if (!ids.isEmpty()) {
@@ -258,7 +258,7 @@
               public void onSuccess(final VoidResult result) {
                 for (int row = 1; row < table.getRowCount();) {
                   final AccountInfo i = getRowItem(row);
-                  if (i != null && ids.contains(i._account_id())) {
+                  if (i != null && ids.contains(i._accountId())) {
                     table.removeRow(row);
                   } else {
                     row++;
@@ -270,8 +270,9 @@
     }
 
     void display(final List<AccountInfo> result) {
-      while (1 < table.getRowCount())
+      while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
+      }
 
       for (final AccountInfo i : result) {
         final int row = table.getRowCount();
@@ -295,7 +296,7 @@
             return cmp;
           }
 
-          return a._account_id() - b._account_id();
+          return a._accountId() - b._accountId();
         }
 
         public String nullToEmpty(String str) {
@@ -376,8 +377,9 @@
     }
 
     void display(List<GroupInfo> list) {
-      while (1 < table.getRowCount())
+      while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
+      }
 
       for (final GroupInfo i : list) {
         final int row = table.getRowCount();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index 8893549..ad3b3f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -25,15 +25,20 @@
 public abstract class AccountGroupScreen extends MenuScreen {
   public static final String INFO = "info";
   public static final String MEMBERS = "members";
+  public static final String AUDIT_LOG = "audit-log";
 
   private final GroupInfo group;
+  private final String token;
   private final String membersTabToken;
+  private final String auditLogTabToken;
 
   public AccountGroupScreen(final GroupInfo toShow, final String token) {
     setRequiresSignIn(true);
 
     this.group = toShow;
+    this.token = token;
     this.membersTabToken = getTabToken(token, MEMBERS);
+    this.auditLogTabToken = getTabToken(token, AUDIT_LOG);
 
     link(Util.C.groupTabGeneral(), getTabToken(token, INFO));
     link(Util.C.groupTabMembers(), membersTabToken,
@@ -56,6 +61,11 @@
     GroupApi.isGroupOwner(group.name(), new GerritCallback<Boolean>() {
       @Override
       public void onSuccess(Boolean result) {
+        if (result) {
+          link(Util.C.groupTabAuditLog(), auditLogTabToken,
+              AccountGroup.isInternalGroup(group.getGroupUUID()));
+          setToken(token);
+        }
         display(group, result);
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 86f543a..511be5f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -42,6 +42,8 @@
   String useContributorAgreements();
   String useSignedOffBy();
   String createNewChangeForAllNotInTarget();
+  String enableSignedPush();
+  String requireSignedPush();
   String requireChangeID();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
@@ -67,6 +69,7 @@
   String headingParentProjectName();
   String columnProjectName();
   String headingAgreements();
+  String headingAuditLog();
 
   String headingProjectSubmitType();
   String projectSubmitType_FAST_FORWARD_ONLY();
@@ -88,6 +91,13 @@
   String columnGroupNotifications();
   String columnGroupVisibleToAll();
 
+  String columnDate();
+  String columnType();
+  String columnByUser();
+
+  String typeAdded();
+  String typeRemoved();
+
   String columnBranchName();
   String columnBranchRevision();
   String initialRevision();
@@ -95,6 +105,7 @@
   String buttonDeleteBranch();
   String saveHeadButton();
   String cancelHeadButton();
+  String columnTagName();
 
   String groupItemHelp();
 
@@ -103,6 +114,7 @@
   String createGroupTitle();
   String groupTabGeneral();
   String groupTabMembers();
+  String groupTabAuditLog();
   String projectListTitle();
   String projectFilter();
   String createProjectTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 4446354..7a8888c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -20,10 +20,12 @@
 buttonBrowseProjects = Browse
 projects = All projects
 projectRepoBrowser = Repository Browser
-useContentMerge = Automatically resolve conflicts
+useContentMerge = Allow content merges
 useContributorAgreements = Require a valid contributor agreement to upload
 useSignedOffBy = Require <code>Signed-off-by</code> in commit message
 createNewChangeForAllNotInTarget = Create a new change for every commit not in the target branch
+enableSignedPush = Enable signed push
+requireSignedPush = Require signed push
 requireChangeID = Require <code>Change-Id</code> in commit message
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
@@ -46,6 +48,7 @@
 headingExternalGroup = Selected External Group
 headingCreateGroup = Create New Group
 headingAgreements = Contributor Agreements
+headingAuditLog = Audit Log
 
 headingProjectSubmitType = Submit Type
 projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
@@ -67,6 +70,13 @@
 columnGroupNotifications = Email Only Authors
 columnGroupVisibleToAll = Visible To All
 
+columnDate = Date
+columnType = Type
+columnByUser = By User
+
+typeAdded = Added
+typeRemoved = Removed
+
 columnBranchName = Branch Name
 columnBranchRevision = Revision
 initialRevision = Initial Revision
@@ -74,6 +84,7 @@
 buttonDeleteBranch = Delete
 saveHeadButton = Save
 cancelHeadButton = Cancel
+columnTagName = Tag Name
 
 groupItemHelp = group
 
@@ -82,6 +93,7 @@
 createGroupTitle = Create Group
 groupTabGeneral = General
 groupTabMembers = Members
+groupTabAuditLog = Audit Log
 projectListTitle = Projects
 projectFilter = Filter
 createProjectTitle = Create Project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
index 1ffd6f0..ad8a595 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CreateChangeDialog;
 import com.google.gerrit.common.PageLinks;
@@ -38,13 +38,14 @@
       @Override
       public void onSend() {
         ChangeApi.createChange(project, getDestinationBranch(),
+          getDestinationTopic(),
           message.getText(), null,
           new GerritCallback<ChangeInfo>() {
             @Override
             public void onSuccess(ChangeInfo result) {
               sent = true;
               hide();
-              Gerrit.display(PageLinks.toChange(result.legacy_id()));
+              Gerrit.display(PageLinks.toChange(result.legacyId()));
             }
 
             @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
index 86a31ee..3e6086b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -27,12 +27,12 @@
   static void call(final Button b, final String project) {
     b.setEnabled(false);
 
-    ChangeApi.createChange(project, RefNames.REFS_CONFIG,
+    ChangeApi.createChange(project, RefNames.REFS_CONFIG, null,
         Util.C.editConfigMessage(), null, new GerritCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
             Gerrit.display(Dispatcher.toEditScreen(
-                new PatchSet.Id(result.legacy_id(), 1), "project.config"));
+                new PatchSet.Id(result.legacyId(), 1), "project.config"));
           }
 
           @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index c5f757d..bfe7787 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -20,9 +20,9 @@
 import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyUpEvent;
 import com.google.gwt.event.dom.client.KeyUpHandler;
@@ -44,7 +44,7 @@
 
   public GroupListScreen() {
     setRequiresSignIn(true);
-    configurePageSize();
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
   public GroupListScreen(String params) {
@@ -65,17 +65,6 @@
     }
   }
 
-  private void configurePageSize() {
-    if (Gerrit.isSignedIn()) {
-      final AccountGeneralPreferences p =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      final short m = p.getMaximumPageSize();
-      pageSize = 0 < m ? m : AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    } else {
-      pageSize = AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    }
-  }
-
   @Override
   protected void onLoad() {
     super.onLoad();
@@ -109,10 +98,10 @@
     setPageTitle(Util.C.groupListTitle());
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next = PagingHyperlink.createNext();
     next.setVisible(false);
 
     groups = new GroupTable(PageLinks.ADMIN_GROUPS);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 1b420b4..aed1dc2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -103,8 +103,9 @@
   }
 
   public void displaySubset(List<GroupInfo> list, String toHighlight, int fromIndex, int toIndex) {
-    while (1 < table.getRowCount())
+    while (1 < table.getRowCount()) {
       table.removeRow(table.getRowCount() - 1);
+    }
 
     Collections.sort(list, new Comparator<GroupInfo>() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
new file mode 100644
index 0000000..6349803
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2015 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.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.http.client.URL;
+
+abstract class PaginatedProjectScreen extends ProjectScreen {
+  protected int pageSize;
+  protected String match;
+  protected int start;
+
+  PaginatedProjectScreen(Project.NameKey toShow) {
+    super(toShow);
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
+  }
+
+  protected void parseToken(String token) {
+    for (String kvPair : token.split("[,;&/?]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("filter".equals(kv[0])) {
+        match = URL.decodeQueryString(kv[1]);
+      }
+
+      if ("skip".equals(kv[0])
+          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  protected void parseToken() {
+    parseToken(getToken());
+  }
+
+  protected String getTokenForScreen(String filter, int skip) {
+    String token = getScreenToken();
+    if (filter != null && !filter.isEmpty()) {
+      token += "?filter=" + URL.encodeQueryString(filter);
+    }
+    if (skip > 0) {
+      if (token.contains("?filter=")) {
+        token += ",";
+      } else {
+        token += "?";
+      }
+      token += "skip=" + skip;
+    }
+    return token;
+  }
+
+  protected abstract String getScreenToken();
+
+  protected void setupNavigationLink(Hyperlink link, String filter, int skip) {
+    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
+    link.setVisible(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index b9baccc..7678097 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.admin;
 
+import com.google.gerrit.client.ErrorDialog;
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.SuggestUtil;
 import com.google.gerrit.common.data.AccessSection;
@@ -219,7 +221,7 @@
     addStage2.getStyle().setDisplay(Display.NONE);
   }
 
-  private void addGroup(GroupReference ref) {
+  private void addGroup(final GroupReference ref) {
     if (ref.getUUID() != null) {
       if (value.getRule(ref) == null) {
         PermissionRule newRule = value.getRule(ref, true);
@@ -251,6 +253,8 @@
                 addGroup(result.get(0));
               } else {
                 groupToAdd.setFocus(true);
+                new ErrorDialog(Gerrit.M.noSuchGroupMessage(ref.getName()))
+                    .center();
               }
             }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
index ada070d..00c41dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
@@ -24,7 +24,7 @@
   ui:generateLocales='default,en'
   >
 <ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style>
+<ui:style gss='false'>
   @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
   @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
index 4d322c0..644fef4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
@@ -25,7 +25,7 @@
   ui:generateLocales='default,en'
   >
 <ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style>
+<ui:style gss='false'>
   @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
 
   .panel {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
index e1c73fa..4dcb52f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -60,7 +60,7 @@
     add(pluginPanel);
   }
 
-  private class PluginTable extends FancyFlexTable<PluginInfo> {
+  private static class PluginTable extends FancyFlexTable<PluginInfo> {
     PluginTable() {
       table.setText(0, 1, Util.C.columnPluginName());
       table.setText(0, 2, Util.C.columnPluginSettings());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index 55bc655..177faff0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -16,11 +16,12 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.ParentProjectBox;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -40,6 +41,7 @@
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -65,7 +67,7 @@
   DivElement history;
 
   @UiField
-  Anchor gitweb;
+  FlowPanel webLinkPanel;
 
   @UiField
   FlowPanel localContainer;
@@ -120,16 +122,7 @@
     } else {
       inheritsFrom.getStyle().setDisplay(Display.NONE);
     }
-
-    final GitwebLink c = Gerrit.getGitwebLink();
-    if (value.isConfigVisible() && c != null) {
-      history.getStyle().setDisplay(Display.BLOCK);
-      gitweb.setText(c.getLinkName());
-      gitweb.setHref(c.toFileHistory(new Branch.NameKey(value.getProjectName(),
-          RefNames.REFS_CONFIG), "project.config"));
-    } else {
-      history.getStyle().setDisplay(Display.NONE);
-    }
+    setUpWebLinks();
 
     addSection.setVisible(editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
   }
@@ -162,6 +155,53 @@
     addSection.setVisible(editing);
   }
 
+  private void setUpWebLinks() {
+    if (!value.isConfigVisible()) {
+      history.getStyle().setDisplay(Display.NONE);
+    } else {
+      GitwebInfo c = Gerrit.info().gitweb();
+      List<WebLinkInfoCommon> links = value.getFileHistoryLinks();
+      if (c == null && links == null) {
+        history.getStyle().setDisplay(Display.NONE);
+      }
+      if (c != null) {
+        webLinkPanel.add(toAnchor(c.toFileHistory(new Branch.NameKey(value.getProjectName(),
+            RefNames.REFS_CONFIG), "project.config"), c.getLinkName()));
+      }
+
+      if (links != null) {
+        for (WebLinkInfoCommon link : links) {
+          webLinkPanel.add(toAnchor(link));
+        }
+      }
+    }
+  }
+
+  private Anchor toAnchor(String href, String name) {
+    Anchor a = new Anchor();
+    a.setHref(href);
+    a.setText(name);
+    return a;
+  }
+
+  private static Anchor toAnchor(WebLinkInfoCommon info) {
+    Anchor a = new Anchor();
+    a.setHref(info.url);
+    if (info.target != null && !info.target.isEmpty()) {
+      a.setTarget(info.target);
+    }
+    if (info.imageUrl != null && !info.imageUrl.isEmpty()) {
+      Image img = new Image();
+      img.setAltText(info.name);
+      img.setUrl(info.imageUrl);
+      img.setTitle(info.name);
+      a.getElement().appendChild(img.getElement());
+    } else {
+      a.setText("(" + info.name + ")");
+    }
+    return a;
+  }
+
   private class Source extends EditorSource<AccessSectionEditor> {
     private final FlowPanel container;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
index 120824b..ebe6caf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -22,7 +22,7 @@
   ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
   ui:generateLocales='default,en'
   >
-<ui:style>
+<ui:style gss='false'>
   .inheritsFrom {
     margin-bottom: 0.5em;
   }
@@ -39,9 +39,12 @@
   .historyTitle {
     font-weight: bold;
   }
-  .gitwebLink {
+  .webLinkPanel a {
     display: inline;
   }
+  .webLinkPanel>a {
+    margin-left:2px;
+  }
 
   .addContainer {
     margin-top: 5px;
@@ -62,7 +65,9 @@
   </div>
   <div ui:field='history' class='{style.history}'>
     <span class='{style.historyTitle}'><ui:msg>History:</ui:msg></span>
-    <g:Anchor ui:field='gitweb' styleName='{style.gitwebLink}'></g:Anchor>
+    <td>
+      <g:FlowPanel ui:field="webLinkPanel" styleName='{style.webLinkPanel}'/>
+    </td>
   </div>
 
   <g:FlowPanel ui:field='localContainer'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
index ab26ba8..724c7a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -23,7 +23,7 @@
   ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
   ui:generateLocales='default,en'
   >
-<ui:style>
+<ui:style gss='false'>
   @external .gwt-TextArea;
 
   .commitMessage {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 3b3a6fc..606a2ea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -20,13 +20,13 @@
 import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.access.AccessMap;
 import com.google.gerrit.client.access.ProjectAccessInfo;
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.GitwebInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -37,8 +37,8 @@
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -54,7 +54,6 @@
 import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
@@ -75,7 +74,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class ProjectBranchesScreen extends ProjectScreen {
+public class ProjectBranchesScreen extends PaginatedProjectScreen {
   private Hyperlink prev;
   private Hyperlink next;
   private BranchesTable branchTable;
@@ -84,67 +83,16 @@
   private HintTextBox nameTxtBox;
   private HintTextBox irevTxtBox;
   private FlowPanel addPanel;
-  private int pageSize;
-  private int start;
   private NpTextBox filterTxt;
-  private String match;
   private Query query;
 
   public ProjectBranchesScreen(final Project.NameKey toShow) {
     super(toShow);
-    configurePageSize();
   }
 
-  private void configurePageSize() {
-    if (Gerrit.isSignedIn()) {
-      AccountGeneralPreferences p =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      short m = p.getMaximumPageSize();
-      pageSize = 0 < m ? m : AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    } else {
-      pageSize = AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    }
-  }
-
-  private void parseToken() {
-    String token = getToken();
-
-    for (String kvPair : token.split("[,;&/?]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0])
-          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
-  }
-
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = PageLinks.toProjectBranches(getProjectKey());
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
+  @Override
+  public String getScreenToken() {
+    return PageLinks.toProjectBranches(getProjectKey());
   }
 
   @Override
@@ -159,7 +107,7 @@
           }
         });
     query = new Query(match).start(start).run();
-    savedPanel = BRANCH;
+    savedPanel = BRANCHES;
   }
 
   private void updateForm() {
@@ -174,10 +122,10 @@
     super.onInitUI();
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next = PagingHyperlink.createNext();
     next.setVisible(false);
 
     addPanel = new FlowPanel();
@@ -227,6 +175,7 @@
     branchTable = new BranchesTable();
 
     delBranch = new Button(Util.C.buttonDeleteBranch());
+    delBranch.setStyleName(Gerrit.RESOURCES.css().branchTableDeleteButton());
     delBranch.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
@@ -446,8 +395,9 @@
     void displaySubset(List<BranchInfo> branches, int fromIndex, int toIndex) {
       canDelete = false;
 
-      while (1 < table.getRowCount())
+      while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
+      }
 
       for (BranchInfo k : branches.subList(fromIndex, toIndex)) {
         final int row = table.getRowCount();
@@ -458,7 +408,7 @@
     }
 
     void populate(int row, BranchInfo k) {
-      final GitwebLink c = Gerrit.getGitwebLink();
+      GitwebInfo c = Gerrit.info().gitweb();
 
       if (k.canDelete()) {
         CheckBox sel = new CheckBox();
@@ -486,8 +436,8 @@
         actionsPanel.add(new Anchor(c.getLinkName(), false,
             c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
       }
-      if (k.web_links() != null) {
-        for (WebLinkInfo webLink : Natives.asList(k.web_links())) {
+      if (k.webLinks() != null) {
+        for (WebLinkInfo webLink : Natives.asList(k.webLinks())) {
           actionsPanel.add(webLink.toAnchor());
         }
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 6cb9295..c6bd1d1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -15,13 +15,17 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GerritUiExtensionPoint;
 import com.google.gerrit.client.StringListPanel;
 import com.google.gerrit.client.access.AccessMap;
 import com.google.gerrit.client.access.ProjectAccessInfo;
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.api.ExtensionPanel;
 import com.google.gerrit.client.change.Resources;
 import com.google.gerrit.client.download.DownloadPanel;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.DownloadInfo.DownloadCommandInfo;
+import com.google.gerrit.client.info.DownloadInfo.DownloadSchemeInfo;
 import com.google.gerrit.client.projects.ConfigInfo;
 import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterInfo;
 import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
@@ -38,7 +42,6 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
@@ -80,6 +83,8 @@
   private ListBox state;
   private ListBox contentMerge;
   private ListBox newChangeForAllNotInTarget;
+  private ListBox enableSignedPush;
+  private ListBox requireSignedPush;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
   private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -111,6 +116,12 @@
       }
     });
 
+    ExtensionPanel extensionPanelTop =
+        new ExtensionPanel(GerritUiExtensionPoint.PROJECT_INFO_SCREEN_TOP);
+    extensionPanelTop.put(GerritUiExtensionPoint.Key.PROJECT_NAME,
+        getProjectKey().get());
+    add(extensionPanelTop);
+
     add(new ProjectDownloadPanel(getProjectKey().get(), true));
 
     initDescription();
@@ -123,6 +134,12 @@
     add(pluginOptionsPanel);
     add(saveProject);
     add(actionsGrid);
+
+    ExtensionPanel extensionPanelBottom =
+        new ExtensionPanel(GerritUiExtensionPoint.PROJECT_INFO_SCREEN_BOTTOM);
+    extensionPanelBottom.put(GerritUiExtensionPoint.Key.PROJECT_NAME,
+        getProjectKey().get());
+    add(extensionPanelBottom);
   }
 
   @Override
@@ -160,6 +177,9 @@
     submitType.setEnabled(isOwner);
     setEnabledForUseContentMerge();
     newChangeForAllNotInTarget.setEnabled(isOwner);
+    if (enableSignedPush != null) {
+      enableSignedPush.setEnabled(isOwner);
+    }
     descTxt.setEnabled(isOwner);
     contributorAgreements.setEnabled(isOwner);
     signedOffBy.setEnabled(isOwner);
@@ -224,6 +244,15 @@
     saveEnabler.listenTo(requireChangeID);
     grid.addHtml(Util.C.requireChangeID(), requireChangeID);
 
+    if (Gerrit.info().receive().enableSignedPush()) {
+      enableSignedPush = newInheritedBooleanBox();
+      saveEnabler.listenTo(enableSignedPush);
+      grid.add(Util.C.enableSignedPush(), enableSignedPush);
+      requireSignedPush = newInheritedBooleanBox();
+      saveEnabler.listenTo(requireSignedPush);
+      grid.add(Util.C.requireSignedPush(), requireSignedPush);
+    }
+
     maxObjectSizeLimit = new NpTextBox();
     saveEnabler.listenTo(maxObjectSizeLimit);
     effectiveMaxObjectSizeLimit = new Label();
@@ -265,7 +294,7 @@
     grid.addHeader(new SmallHeading(Util.C.headingAgreements()));
 
     contributorAgreements = newInheritedBooleanBox();
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
+    if (Gerrit.info().auth().useContributorAgreements()) {
       saveEnabler.listenTo(contributorAgreements);
       grid.add(Util.C.useContributorAgreements(), contributorAgreements);
     }
@@ -301,17 +330,20 @@
   }
 
   private void setBool(ListBox box, InheritedBooleanInfo inheritedBoolean) {
+    if (box == null) {
+      return;
+    }
     int inheritedIndex = -1;
     for (int i = 0; i < box.getItemCount(); i++) {
       if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
         inheritedIndex = i;
       }
-      if (box.getValue(i).startsWith(inheritedBoolean.configured_value().name())) {
+      if (box.getValue(i).startsWith(inheritedBoolean.configuredValue().name())) {
         box.setSelectedIndex(i);
       }
     }
     if (inheritedIndex >= 0) {
-      if (getProjectKey().equals(Gerrit.getConfig().getWildProject())) {
+      if (Gerrit.info().gerrit().isAllProjects(getProjectKey())) {
         if (box.getSelectedIndex() == inheritedIndex) {
           for (int i = 0; i < box.getItemCount(); i++) {
             if (box.getValue(i).equals(InheritableBoolean.FALSE.name())) {
@@ -323,7 +355,7 @@
         box.removeItem(inheritedIndex);
       } else {
         box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
-            + inheritedBoolean.inherited_value() + ")");
+            + inheritedBoolean.inheritedValue() + ")");
       }
     }
   }
@@ -342,20 +374,24 @@
 
   void display(ConfigInfo result) {
     descTxt.setText(result.description());
-    setBool(contributorAgreements, result.use_contributor_agreements());
-    setBool(signedOffBy, result.use_signed_off_by());
-    setBool(contentMerge, result.use_content_merge());
-    setBool(newChangeForAllNotInTarget, result.create_new_change_for_all_not_in_target());
-    setBool(requireChangeID, result.require_change_id());
-    setSubmitType(result.submit_type());
+    setBool(contributorAgreements, result.useContributorAgreements());
+    setBool(signedOffBy, result.useSignedOffBy());
+    setBool(contentMerge, result.useContentMerge());
+    setBool(newChangeForAllNotInTarget, result.createNewChangeForAllNotInTarget());
+    setBool(requireChangeID, result.requireChangeId());
+    if (Gerrit.info().receive().enableSignedPush()) {
+      setBool(enableSignedPush, result.enableSignedPush());
+      setBool(requireSignedPush, result.requireSignedPush());
+    }
+    setSubmitType(result.submitType());
     setState(result.state());
-    maxObjectSizeLimit.setText(result.max_object_size_limit().configured_value());
-    if (result.max_object_size_limit().inherited_value() != null) {
+    maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
+    if (result.maxObjectSizeLimit().inheritedValue() != null) {
       effectiveMaxObjectSizeLimit.setVisible(true);
       effectiveMaxObjectSizeLimit.setText(
-          Util.M.effectiveMaxObjectSizeLimit(result.max_object_size_limit().value()));
+          Util.M.effectiveMaxObjectSizeLimit(result.maxObjectSizeLimit().value()));
       effectiveMaxObjectSizeLimit.setTitle(
-          Util.M.globalMaxObjectSizeLimit(result.max_object_size_limit().inherited_value()));
+          Util.M.globalMaxObjectSizeLimit(result.maxObjectSizeLimit().inheritedValue()));
     } else {
       effectiveMaxObjectSizeLimit.setVisible(false);
     }
@@ -616,9 +652,14 @@
   private void doSave() {
     enableForm(false);
     saveProject.setEnabled(false);
+    InheritableBoolean esp = enableSignedPush != null
+        ? getBool(enableSignedPush) : null;
+    InheritableBoolean rsp = requireSignedPush != null
+        ? getBool(requireSignedPush) : null;
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
+        esp, rsp,
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
@@ -672,33 +713,18 @@
     return pluginConfigValues;
   }
 
-  public class ProjectDownloadPanel extends DownloadPanel {
+  public static class ProjectDownloadPanel extends DownloadPanel {
     public ProjectDownloadPanel(String project, boolean isAllowsAnonymous) {
-      super(project, null, isAllowsAnonymous);
+      super(project, isAllowsAnonymous);
     }
 
     @Override
-    public void populateDownloadCommandLinks() {
-      if (!urls.isEmpty()) {
-        if (allowedCommands.contains(DownloadCommand.CHECKOUT)
-            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-          commands.add(cmdLinkfactory.new CloneCommandLink());
-          if (Gerrit.getConfig().getSshdAddress() != null && hasUserName()) {
-            commands.add(
-                cmdLinkfactory.new CloneWithCommitMsgHookCommandLink(getProjectKey()));
-          }
-        }
-      }
+    protected List<DownloadCommandInfo> getCommands(DownloadSchemeInfo schemeInfo) {
+      return schemeInfo.cloneCommands(project);
     }
   }
 
-  private static boolean hasUserName() {
-    return Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0;
-  }
-
-  private class LabeledWidgetsGrid extends FlexTable {
+  private static class LabeledWidgetsGrid extends FlexTable {
     private String labelSuffix;
 
     public LabeledWidgetsGrid() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 828352c..4503265 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -18,23 +18,21 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
-import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.info.GitwebInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.PagingHyperlink;
 import com.google.gerrit.client.ui.ProjectSearchLink;
 import com.google.gerrit.client.ui.ProjectsTable;
-import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyUpEvent;
 import com.google.gwt.event.dom.client.KeyUpHandler;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -45,48 +43,21 @@
 
 import java.util.List;
 
-public class ProjectListScreen extends Screen {
+public class ProjectListScreen extends PaginatedProjectScreen {
   private Hyperlink prev;
   private Hyperlink next;
   private ProjectsTable projects;
   private NpTextBox filterTxt;
-  private int pageSize;
 
-  private String match = "";
-  private int start;
   private Query query;
 
   public ProjectListScreen() {
-    configurePageSize();
+    super(null);
   }
 
   public ProjectListScreen(String params) {
-    for (String kvPair : params.split("[,;&]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("filter".equals(kv[0])) {
-        match = URL.decodeQueryString(kv[1]);
-      }
-
-      if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
-      }
-    }
-    configurePageSize();
-  }
-
-  private void configurePageSize() {
-    if (Gerrit.isSignedIn()) {
-      final AccountGeneralPreferences p =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      final short m = p.getMaximumPageSize();
-      pageSize = 0 < m ? m : AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    } else {
-      pageSize = AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    }
+    this();
+    parseToken(params);
   }
 
   @Override
@@ -95,25 +66,9 @@
     query = new Query(match).start(start).run();
   }
 
-  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
-    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
-    link.setVisible(true);
-  }
-
-  private String getTokenForScreen(String filter, int skip) {
-    String token = ADMIN_PROJECTS;
-    if (filter != null && !filter.isEmpty()) {
-      token += "?filter=" + URL.encodeQueryString(filter);
-    }
-    if (skip > 0) {
-      if (token.contains("?filter=")) {
-        token += ",";
-      } else {
-        token += "?";
-      }
-      token += "skip=" + skip;
-    }
-    return token;
+  @Override
+  public String getScreenToken() {
+    return ADMIN_PROJECTS;
   }
 
   @Override
@@ -122,10 +77,10 @@
     setPageTitle(Util.C.projectListTitle());
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev = PagingHyperlink.createPrev();
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next = PagingHyperlink.createNext();
     next.setVisible(false);
 
     projects = new ProjectsTable() {
@@ -185,16 +140,16 @@
       }
 
       private void addWebLinks(int row, ProjectInfo k) {
-        GitwebLink gitWebLink = Gerrit.getGitwebLink();
-        List<WebLinkInfo> webLinks = Natives.asList(k.web_links());
-        if (gitWebLink != null || (webLinks != null && !webLinks.isEmpty())) {
+        GitwebInfo gitwebLink = Gerrit.info().gitweb();
+        List<WebLinkInfo> webLinks = Natives.asList(k.webLinks());
+        if (gitwebLink != null || (webLinks != null && !webLinks.isEmpty())) {
           FlowPanel p = new FlowPanel();
           table.setWidget(row, ProjectsTable.C_REPO_BROWSER, p);
 
-          if (gitWebLink != null) {
+          if (gitwebLink != null) {
             Anchor a = new Anchor();
-            a.setText(gitWebLink.getLinkName());
-            a.setHref(gitWebLink.toProject(k.name_key()));
+            a.setText(gitwebLink.getLinkName());
+            a.setHref(gitwebLink.toProject(k.name_key()));
             p.add(a);
           }
           if (webLinks != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
index fdf3ab8..a63dae4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -19,9 +19,10 @@
 
 public abstract class ProjectScreen extends Screen {
   public static final String INFO = "info";
-  public static final String BRANCH = "branches";
+  public static final String BRANCHES = "branches";
   public static final String ACCESS = "access";
   public static final String DASHBOARDS = "dashboards";
+  public static final String TAGS = "tags";
 
   protected static String savedPanel;
   protected static Project.NameKey savedKey;
@@ -47,7 +48,9 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    setPageTitle(Util.M.project(name.get()));
+    if (name != null) {
+      setPageTitle(Util.M.project(name.get()));
+    }
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
new file mode 100644
index 0000000..2db0eff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2015 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.client.admin;
+
+import static com.google.gerrit.client.ui.Util.highlight;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.projects.TagInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.PagingHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+import java.util.List;
+
+public class ProjectTagsScreen extends PaginatedProjectScreen {
+  private NpTextBox filterTxt;
+  private Query query;
+  private Hyperlink prev;
+  private Hyperlink next;
+  private TagsTable tagsTable;
+
+  public ProjectTagsScreen(Project.NameKey toShow) {
+    super(toShow);
+  }
+
+  @Override
+  public String getScreenToken() {
+    return PageLinks.toProjectTags(getProjectKey());
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initPageHeader();
+    prev = PagingHyperlink.createPrev();
+    prev.setVisible(false);
+
+    next = PagingHyperlink.createNext();
+    next.setVisible(false);
+
+    tagsTable = new TagsTable();
+
+    HorizontalPanel buttons = new HorizontalPanel();
+    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
+    buttons.add(prev);
+    buttons.add(next);
+    add(tagsTable);
+    add(buttons);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    query = new Query(match).start(start).run();
+    savedPanel = TAGS;
+  }
+
+  private void initPageHeader() {
+    parseToken();
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(match);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        Query q = new Query(filterTxt.getValue());
+        if (match.equals(q.qMatch)) {
+          q.start(start);
+        } else if (query == null) {
+          q.run();
+          query = q;
+        }
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
+  private class TagsTable extends NavigationTable<TagInfo> {
+
+    TagsTable() {
+      table.setWidth("");
+      table.setText(0, 1, Util.C.columnTagName());
+      table.setText(0, 2, Util.C.columnBranchRevision());
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void display(List<TagInfo> tags) {
+      displaySubset(tags, 0, tags.size());
+    }
+
+    void displaySubset(List<TagInfo> tags, int fromIndex, int toIndex) {
+      while (1 < table.getRowCount()) {
+        table.removeRow(table.getRowCount() - 1);
+      }
+
+      for (TagInfo k : tags.subList(fromIndex, toIndex)) {
+        int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, k);
+      }
+    }
+
+    void populate(int row, TagInfo k) {
+      table.setWidget(row, 1, new InlineHTML(highlight(k.getShortName(), match)));
+
+      if (k.revision() != null) {
+        table.setText(row, 2, k.revision());
+      } else {
+        table.setText(row, 2, "");
+      }
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
+      fmt.addStyleName(row, 1, dataCellStyle);
+      fmt.addStyleName(row, 2, dataCellStyle);
+
+      setRowItem(row, k);
+    }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (row > 0) {
+        movePointerTo(row);
+      }
+    }
+
+    @Override
+    protected Object getRowItemKey(TagInfo item) {
+      return item.ref();
+    }
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (match != null) {
+      filterTxt.setCursorPos(match.length());
+    }
+    filterTxt.setFocus(true);
+  }
+
+  private class Query {
+    private String qMatch;
+    private int qStart;
+
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query run() {
+      // Retrieve one more tag than page size to determine if there are more
+      // tags to display
+      ProjectApi.getTags(getProjectKey(), pageSize + 1, qStart, qMatch,
+          new ScreenLoadCallback<JsArray<TagInfo>>(ProjectTagsScreen.this) {
+            @Override
+            public void preDisplay(JsArray<TagInfo> result) {
+              if (!isAttached()) {
+                // View has been disposed.
+              } else if (query == Query.this) {
+                query = null;
+                showList(result);
+              } else {
+                query.run();
+              }
+            }
+          });
+      return this;
+    }
+
+    void showList(JsArray<TagInfo> result) {
+      setToken(getTokenForScreen(qMatch, qStart));
+      ProjectTagsScreen.this.match = qMatch;
+      ProjectTagsScreen.this.start = qStart;
+
+      if (result.length() <= pageSize) {
+        tagsTable.display(Natives.asList(result));
+        next.setVisible(false);
+      } else {
+        tagsTable.displaySubset(Natives.asList(result), 0,
+            result.length() - 1);
+        setupNavigationLink(next, qMatch, qStart + pageSize);
+      }
+      if (qStart > 0) {
+        setupNavigationLink(prev, qMatch, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
index e5f6649..137ad2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
@@ -19,7 +19,7 @@
   xmlns:g='urn:import:com.google.gwt.user.client.ui'
   >
 <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-<ui:style>
+<ui:style gss='false'>
   .panel {
     position: relative;
     white-space: nowrap;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
index 5533313..a4fbb85 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.api;
 
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeString;
@@ -114,7 +114,9 @@
         return e.options[e.selectedIndex].text;
       },
 
-      popup: function(e){this._p=@com.google.gerrit.client.api.PopupHelper::popup(Lcom/google/gerrit/client/api/ActionContext;Lcom/google/gwt/dom/client/Element;)(this,e)},
+      popup: function(e){
+        this._p=@com.google.gerrit.client.api.PopupHelper::popup(
+          Lcom/google/gerrit/client/api/ActionContext;Lcom/google/gwt/dom/client/Element;)(this,e)},
       hide: function() {
         this._p.@com.google.gerrit.client.api.PopupHelper::hide()();
         delete this['_p'];
@@ -125,11 +127,18 @@
         if (m == 'get' || m == 'delete' || i==null) this[m](b);
         else this[m](i,b);
       },
-      get: function(b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
-      post: function(i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,i,b)},
-      put: function(i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,i,b)},
-      'delete': function(b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
-      del: function(b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+      get: function(b){@com.google.gerrit.client.api.ActionContext::get(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+      post: function(i,b){@com.google.gerrit.client.api.ActionContext::post(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
+        this._u,i,b)},
+      put: function(i,b){@com.google.gerrit.client.api.ActionContext::put(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
+        this._u,i,b)},
+      'delete': function(b){@com.google.gerrit.client.api.ActionContext::delete(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+      del: function(b){@com.google.gerrit.client.api.ActionContext::delete(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
     };
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index e4a5446..18fb833 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -16,7 +16,9 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.ServerInfo;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.History;
@@ -30,17 +32,20 @@
     ActionContext.init();
     HtmlTemplate.init();
     Plugin.init();
-    addHistoryHook();
   }
 
   private static native void init0() /*-{
     var serverUrl = @com.google.gwt.core.client.GWT::getHostPageBaseURL()();
     var ScreenDefinition = @com.google.gerrit.client.api.ExtensionScreen.Definition::TYPE;
+    var SettingsScreenDefinition = @com.google.gerrit.client.api.ExtensionSettingsScreen.Definition::TYPE;
+    var PanelDefinition = @com.google.gerrit.client.api.ExtensionPanel.Definition::TYPE;
     $wnd.Gerrit = {
       JsonString: @com.google.gerrit.client.rpc.NativeString::TYPE,
       events: {},
       plugins: {},
       screens: {},
+      settingsScreens: {},
+      panels: {},
       change_actions: {},
       edit_actions: {},
       revision_actions: {},
@@ -68,7 +73,10 @@
       refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
       isSignedIn: @com.google.gerrit.client.api.ApiGlue::isSignedIn(),
       showError: @com.google.gerrit.client.api.ApiGlue::showError(Ljava/lang/String;),
+      getServerInfo: @com.google.gerrit.client.api.ApiGlue::getServerInfo(),
       getCurrentUser: @com.google.gerrit.client.api.ApiGlue::getCurrentUser(),
+      getUserPreferences: @com.google.gerrit.client.api.ApiGlue::getUserPreferences(),
+      refreshUserPreferences: @com.google.gerrit.client.api.ApiGlue::refreshUserPreferences(),
 
       on: function (e,f){(this.events[e] || (this.events[e]=[])).push(f)},
       onAction: function (t,n,c){this._onAction(this.getPluginName(),t,n,c)},
@@ -86,6 +94,16 @@
         var s = new ScreenDefinition(r,c);
         (this.screens[p] || (this.screens[p]=[])).push(s);
       },
+      settingsScreen: function(p,m,c){this._settingsScreen(this.getPluginName(),p,m,c)},
+      _settingsScreen: function(n,p,m,c){
+        var s = new SettingsScreenDefinition(p,m,c);
+        (this.settingsScreens[n] || (this.settingsScreens[n]=[])).push(s);
+      },
+      panel: function(i,c){this._panel(this.getPluginName(),i,c)},
+      _panel: function(n,i,c){
+        var p = new PanelDefinition(n,c);
+        (this.panels[i] || (this.panels[i]=[])).push(p);
+      },
 
       url: function (d) {
         if (d && d.length > 0)
@@ -203,14 +221,6 @@
     };
   }-*/;
 
-  /** Install deprecated {@code gerrit_addHistoryHook()} function. */
-  private static native void addHistoryHook() /*-{
-    $wnd.gerrit_addHistoryHook = function(h) {
-      var p = @com.google.gwt.user.client.Window.Location::getPath()();
-      $wnd.Gerrit.on('history', function(t) { h(p + "#" + t) })
-     };
-  }-*/;
-
   private static void install(JavaScriptObject cb, Plugin p) throws Exception {
     try {
       pluginName = p.name();
@@ -246,8 +256,20 @@
     Gerrit.display(History.getToken());
   }
 
+  private static final ServerInfo getServerInfo() {
+    return Gerrit.info();
+  }
+
   private static final AccountInfo getCurrentUser() {
-    return Gerrit.getUserAccountInfo();
+    return Gerrit.getUserAccount();
+  }
+
+  private static final AccountPreferencesInfo getUserPreferences() {
+    return Gerrit.getUserPreferences();
+  }
+
+  private static final void refreshUserPreferences() {
+    Gerrit.refreshUserPreferences();
   }
 
   private static final void refreshMenuBar() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
index a5243ae..82b6810 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.api;
 
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
@@ -42,7 +42,7 @@
       ChangeInfo change,
       ActionInfo action,
       ActionButton button) {
-    RestApi api = ChangeApi.change(change.legacy_id().get()).view(action.id());
+    RestApi api = ChangeApi.change(change.legacyId().get()).view(action.id());
     JavaScriptObject f = get(action.id());
     if (f != null) {
       ActionContext c = ActionContext.create(api);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
index 0e4048d..55c40f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.client.api;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.actions.ActionInfo;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
@@ -29,7 +29,7 @@
 
 class DefaultActions {
   static void invoke(ChangeInfo change, ActionInfo action, RestApi api) {
-    invoke(action, api, callback(PageLinks.toChange(change.legacy_id())));
+    invoke(action, api, callback(PageLinks.toChange(change.legacyId())));
   }
 
   static void invoke(Project.NameKey project, ActionInfo action, RestApi api) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
index ebcafb8..49b150a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.api;
 
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.core.client.JavaScriptObject;
 
@@ -29,7 +29,7 @@
       ActionInfo action,
       ActionButton button) {
     RestApi api = ChangeApi.edit(
-          change.legacy_id().get())
+          change.legacyId().get())
       .view(action.id());
 
     JavaScriptObject f = get(action.id());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
new file mode 100644
index 0000000..0702cba
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2015 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.client.api;
+
+import com.google.gerrit.client.GerritUiExtensionPoint;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ExtensionPanel extends FlowPanel {
+  private static final Logger logger =
+      Logger.getLogger(ExtensionPanel.class.getName());
+  private final GerritUiExtensionPoint extensionPoint;
+  private final List<Context> contexts;
+
+  public ExtensionPanel(GerritUiExtensionPoint extensionPoint) {
+    this.extensionPoint = extensionPoint;
+    this.contexts = create();
+  }
+
+  private List<Context> create() {
+    List<Context> contexts = new ArrayList<>();
+    for (Definition def : Natives.asList(Definition.get(extensionPoint.name()))) {
+      SimplePanel p = new SimplePanel();
+      add(p);
+      contexts.add(Context.create(def, p));
+    }
+    return contexts;
+  }
+
+  public void put(GerritUiExtensionPoint.Key key, String value) {
+    for (Context ctx : contexts) {
+      ctx.put(key.name(), value);
+    }
+  }
+
+  public void putInt(GerritUiExtensionPoint.Key key, int value) {
+    for (Context ctx : contexts) {
+      ctx.putInt(key.name(), value);
+    }
+  }
+
+  public void putBoolean(GerritUiExtensionPoint.Key key, boolean value) {
+    for (Context ctx : contexts) {
+      ctx.putBoolean(key.name(), value);
+    }
+  }
+
+  public void putObject(GerritUiExtensionPoint.Key key, JavaScriptObject value) {
+    for (Context ctx : contexts) {
+      ctx.putObject(key.name(), value);
+    }
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    for (Context ctx : contexts) {
+      try {
+        ctx.onLoad();
+      } catch (RuntimeException e) {
+        logger.log(Level.SEVERE,
+            "Failed to load extension panel for extension point "
+                + extensionPoint.name() + " from plugin " + ctx.getPluginName()
+                + ": " + e.getMessage());
+      }
+    }
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    for (Context ctx : contexts) {
+      for (JavaScriptObject u : Natives.asList(ctx.unload())) {
+        ApiGlue.invoke(u);
+      }
+    }
+  }
+
+  static class Definition extends JavaScriptObject {
+    static final JavaScriptObject TYPE = init();
+    private static native JavaScriptObject init() /*-{
+      function PanelDefinition(n, c) {
+        this.pluginName = n;
+        this.onLoad = c;
+      };
+      return PanelDefinition;
+    }-*/;
+
+    static native JsArray<Definition> get(String i)
+    /*-{ return $wnd.Gerrit.panels[i] || [] }-*/;
+
+    protected Definition() {
+    }
+  }
+
+  static class Context extends JavaScriptObject {
+    static final Context create(
+        Definition def,
+        SimplePanel panel) {
+      return create(TYPE, def, panel.getElement());
+    }
+
+    final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
+    final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
+    final native String getPluginName() /*-{ return this._d.pluginName; }-*/;
+
+    final native void put(String k, String v) /*-{ this.p[k] = v; }-*/;
+    final native void putInt(String k, int v) /*-{ this.p[k] = v; }-*/;
+    final native void putBoolean(String k, boolean v) /*-{ this.p[k] = v; }-*/;
+    final native void putObject(String k, JavaScriptObject v) /*-{ this.p[k] = v; }-*/;
+
+    private static final native Context create(
+        JavaScriptObject T,
+        Definition d,
+        Element e)
+    /*-{ return new T(d,e) }-*/;
+
+    private static final JavaScriptObject TYPE = init();
+    private static final native JavaScriptObject init() /*-{
+      var T = function(d,e) {
+        this._d = d;
+        this._u = [];
+        this.body = e;
+        this.p = {};
+      };
+      T.prototype = {
+        onUnload: function(f){this._u.push(f)},
+      };
+      return T;
+    }-*/;
+
+    protected Context() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
new file mode 100644
index 0000000..c351bbf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionSettingsScreen.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2015 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.client.api;
+
+import com.google.gerrit.client.account.SettingsScreen;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.Element;
+
+import java.util.Set;
+
+/** SettingsScreen contributed by a plugin. */
+public class ExtensionSettingsScreen extends SettingsScreen {
+  private Context ctx;
+
+  public ExtensionSettingsScreen(String token) {
+    if (token.contains("?")) {
+      token = token.substring(0, token.indexOf('?'));
+    }
+    String name;
+    String rest;
+    int s = token.indexOf('/');
+    if (0 < s) {
+      name = token.substring(0, s);
+      rest = token.substring(s + 1);
+    } else {
+      name = token;
+      rest = "";
+    }
+    ctx = create(name, rest);
+  }
+
+  private Context create(String name, String rest) {
+    for (Definition def : Natives.asList(Definition.get(name))) {
+      if (def.matches(rest)) {
+        return Context.create(def, this);
+      }
+    }
+    return null;
+  }
+
+  public boolean isFound() {
+    return ctx != null;
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    setHeaderVisible(false);
+    ctx.onLoad();
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    for (JavaScriptObject u : Natives.asList(ctx.unload())) {
+      ApiGlue.invoke(u);
+    }
+  }
+
+  public static class Definition extends JavaScriptObject {
+    static final JavaScriptObject TYPE = init();
+    private static native JavaScriptObject init() /*-{
+      function SettingsScreenDefinition(p, m, c) {
+        this.path = p;
+        this.menu = m;
+        this.onLoad = c;
+      };
+      return SettingsScreenDefinition;
+    }-*/;
+
+    public static native JsArray<Definition> get(String n)
+    /*-{ return $wnd.Gerrit.settingsScreens[n] || [] }-*/;
+
+    public static final Set<String> plugins() {
+      return Natives.keys(settingsScreens());
+    }
+
+    private static final native NativeMap<NativeString> settingsScreens()
+    /*-{ return $wnd.Gerrit.settingsScreens; }-*/;
+
+    public final native String getPath() /*-{ return this.path; }-*/;
+    public final native String getMenu() /*-{ return this.menu; }-*/;
+
+    final native boolean matches(String t)
+    /*-{ return this.path == t; }-*/;
+
+    protected Definition() {
+    }
+  }
+
+  static class Context extends JavaScriptObject {
+    static final Context create(
+        Definition def,
+        ExtensionSettingsScreen view) {
+      return create(TYPE, def, view, view.getBody().getElement());
+    }
+
+    final native void onLoad() /*-{ this._d.onLoad(this) }-*/;
+    final native JsArray<JavaScriptObject> unload() /*-{ return this._u }-*/;
+
+    private static final native Context create(
+        JavaScriptObject T,
+        Definition d,
+        ExtensionSettingsScreen s,
+        Element e)
+    /*-{ return new T(d,s,e) }-*/;
+
+    private static final JavaScriptObject TYPE = init();
+    private static final native JavaScriptObject init() /*-{
+      var T = function(d,s,e) {
+        this._d = d;
+        this._s = s;
+        this._u = [];
+        this.body = e;
+      };
+      T.prototype = {
+        setTitle: function(t){this._s.@com.google.gerrit.client.ui.Screen::setPageTitle(Ljava/lang/String;)(t)},
+        setWindowTitle: function(t){this._s.@com.google.gerrit.client.ui.Screen::setWindowTitle(Ljava/lang/String;)(t)},
+        show: function(){$entry(this._s.@com.google.gwtexpui.user.client.View::display()())},
+        onUnload: function(f){this._u.push(f)},
+      };
+      return T;
+    }-*/;
+
+    protected Context() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
index 931a04d..04d4ce5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
@@ -50,7 +50,10 @@
     var G = $wnd.Gerrit;
     @com.google.gerrit.client.api.Plugin::TYPE.prototype = {
       getPluginName: function(){return this.name},
+      getServerInfo: @com.google.gerrit.client.api.ApiGlue::getServerInfo(),
       getCurrentUser: @com.google.gerrit.client.api.ApiGlue::getCurrentUser(),
+      getUserPreferences: @com.google.gerrit.client.api.ApiGlue::getUserPreferences(),
+      refreshUserPreferences: @com.google.gerrit.client.api.ApiGlue::refreshUserPreferences(),
       go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
       refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
       refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
@@ -59,13 +62,22 @@
       on: function(e,f){G.on(e,f)},
       onAction: function(t,n,c){G._onAction(this.name,t,n,c)},
       screen: function(p,c){G._screen(this.name,p,c)},
+      settingsScreen: function(p,m,c){G._settingsScreen(this.name,p,m,c)},
+      panel: function(i,c){G._panel(this.name,i,c)},
 
       url: function (u){return G.url(this._url(u))},
-      get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
-      post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
-      put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
-      'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
-      del: function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+      get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+      post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
+        this._api(u),i,b)},
+      put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(
+        this._api(u),i,b)},
+      'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+      del: function(u,b){@com.google.gerrit.client.api.ActionContext::delete(
+        Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
 
       _loadedGwt: function(){@com.google.gerrit.client.api.PluginLoader::loaded()()},
       _api: function(u){return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(this._url(u))},
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
index f0fb436..ceb0eee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
@@ -32,15 +32,14 @@
 
 /** Loads JavaScript plugins with a progress meter visible. */
 public class PluginLoader extends DialogBox {
-  private static final int MAX_LOAD_TIME_MILLIS = 5000;
   private static PluginLoader self;
 
   public static void load(List<String> plugins,
-      AsyncCallback<VoidResult> callback) {
+      int loadTimeout, AsyncCallback<VoidResult> callback) {
     if (plugins == null || plugins.isEmpty()) {
       callback.onSuccess(VoidResult.create());
     } else {
-      self = new PluginLoader(callback);
+      self = new PluginLoader(loadTimeout, callback);
       self.load(plugins);
       self.startTimers();
       self.center();
@@ -51,6 +50,7 @@
     self.loadedOne();
   }
 
+  private final int loadTimeout;
   private final AsyncCallback<VoidResult> callback;
   private ProgressBar progress;
   private Timer show;
@@ -58,9 +58,10 @@
   private Timer timeout;
   private boolean visible;
 
-  private PluginLoader(AsyncCallback<VoidResult> cb) {
+  private PluginLoader(int loadTimeout, AsyncCallback<VoidResult> cb) {
     super(/* auto hide */false, /* modal */true);
     callback = cb;
+    this.loadTimeout = loadTimeout;
     progress = new ProgressBar(Gerrit.C.loadingPlugins());
 
     setStyleName(Gerrit.RESOURCES.css().errorDialog());
@@ -98,7 +99,7 @@
 
       @Override
       public void run() {
-        progress.setValue(100 * ++cycle * 250 / MAX_LOAD_TIME_MILLIS);
+        progress.setValue(100 * ++cycle * 250 / loadTimeout);
       }
     };
     update.scheduleRepeating(250);
@@ -109,7 +110,7 @@
         finish();
       }
     };
-    timeout.schedule(MAX_LOAD_TIME_MILLIS);
+    timeout.schedule(loadTimeout);
   }
 
   private void loadedOne() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
index 67b8a98..a560711 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
@@ -25,7 +25,7 @@
  * JavaScript call stack and identifying the URL of the script file calling
  * {@code Gerrit.install()}. The simple approach applied here is looking at
  * the source URLs and extracting the name out of the string, e.g.:
- * {@code "http://localhost:8080/plugins/{name}/static/foo.js"}.
+ * {@code "http://localhost:8080/plugins/[name]/static/foo.js"}.
  */
 class PluginName {
   private static final String UNKNOWN = "<unknown>";
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
index 3f2e794..69887e7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ProjectGlue.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.api;
 
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.info.ActionInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.RestApi;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
index fb489cc..914ef85 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.api;
 
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.core.client.JavaScriptObject;
 
@@ -29,7 +29,7 @@
       ActionInfo action,
       ActionButton button) {
     RestApi api = ChangeApi.revision(
-          change.legacy_id().get(),
+          change.legacyId().get(),
           revision.name())
       .view(action.id());
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
index 54b83f1..e93bcd9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
index d639150..a9e1bb3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style type='com.google.gerrit.client.change.ActionMessageBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.ActionMessageBox.Style'>
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
     .popup { background-color: trimColor; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index edf1105..033a3a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -16,10 +16,10 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.actions.ActionButton;
-import com.google.gerrit.client.actions.ActionInfo;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
@@ -36,9 +36,8 @@
 
 class Actions extends Composite {
   private static final String[] CORE = {
-    "abandon", "restore", "revert", "topic",
-    "cherrypick", "submit", "rebase", "message",
-    "publish", "followup", "/"};
+    "abandon", "cherrypick", "followup", "hashtags", "publish",
+    "rebase", "restore", "revert", "submit", "topic", "/"};
 
   interface Binder extends UiBinder<FlowPanel, Actions> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
@@ -61,11 +60,13 @@
   private ChangeInfo changeInfo;
   private String revision;
   private String project;
+  private String topic;
   private String subject;
   private String message;
   private String branch;
   private String key;
-  private boolean canSubmit;
+
+  private boolean rebaseParentNotCurrent = true;
 
   Actions() {
     initWidget(uiBinder.createAndBindUi(this));
@@ -78,20 +79,26 @@
     boolean hasUser = Gerrit.isSignedIn();
     RevisionInfo revInfo = info.revision(revision);
     CommitInfo commit = revInfo.commit();
-    changeId = info.legacy_id();
+    changeId = info.legacyId();
     project = info.project();
+    topic = info.topic();
     subject = commit.subject();
     message = commit.message();
     branch = info.branch();
-    key = info.change_id();
+    key = info.changeId();
     changeInfo = info;
 
     initChangeActions(info, hasUser);
-    initRevisionActions(info, revInfo, hasUser);
+
+    NativeMap<ActionInfo> actionMap = revInfo.hasActions()
+        ? revInfo.actions()
+        : NativeMap.<ActionInfo> create();
+    actionMap.copyKeysIntoChildren("id");
+    reloadRevisionActions(actionMap);
   }
 
   private void initChangeActions(ChangeInfo info, boolean hasUser) {
-    NativeMap<ActionInfo> actions = info.has_actions()
+    NativeMap<ActionInfo> actions = info.hasActions()
         ? info.actions()
         : NativeMap.<ActionInfo> create();
     actions.copyKeysIntoChildren("id");
@@ -107,34 +114,35 @@
     }
   }
 
-  private void initRevisionActions(ChangeInfo info, RevisionInfo revInfo,
-      boolean hasUser) {
-    NativeMap<ActionInfo> actions = revInfo.has_actions()
-        ? revInfo.actions()
-        : NativeMap.<ActionInfo> create();
-    actions.copyKeysIntoChildren("id");
+  void reloadRevisionActions(NativeMap<ActionInfo> actions) {
+    if (!Gerrit.isSignedIn()) {
+      return;
+    }
+    boolean canSubmit = actions.containsKey("submit");
+    if (canSubmit) {
+      ActionInfo action = actions.get("submit");
+      submit.setTitle(action.title());
+      submit.setEnabled(action.enabled());
+      submit.setHTML(new SafeHtmlBuilder()
+          .openDiv()
+          .append(action.label())
+          .closeDiv());
+      submit.setEnabled(action.enabled());
+    }
+    submit.setVisible(canSubmit);
 
-    canSubmit = false;
-    if (hasUser) {
-      canSubmit = actions.containsKey("submit");
-      if (canSubmit) {
-        ActionInfo action = actions.get("submit");
-        submit.setTitle(action.title());
-        submit.setEnabled(action.enabled());
-        submit.setHTML(new SafeHtmlBuilder()
-            .openDiv()
-            .append(action.label())
-            .closeDiv());
-      }
-      a2b(actions, "cherrypick", cherrypick);
-      a2b(actions, "rebase", rebase);
-      if (rebase.isVisible()) {
-        // it is the rebase button in RebaseDialog that the server wants to disable
-        rebase.setEnabled(true);
-      }
-      for (String id : filterNonCore(actions)) {
-        add(new ActionButton(info, revInfo, actions.get(id)));
-      }
+    a2b(actions, "cherrypick", cherrypick);
+    a2b(actions, "rebase", rebase);
+
+    // The rebase button on change screen is always enabled.
+    // It is the "Rebase" button in the RebaseDialog that might be disabled.
+    rebaseParentNotCurrent = rebase.isEnabled();
+    if (rebase.isVisible()) {
+      rebase.setEnabled(true);
+    }
+    RevisionInfo revInfo = changeInfo.revision(revision);
+    for (String id : filterNonCore(actions)) {
+      add(new ActionButton(changeInfo, revInfo, actions.get(id)));
     }
   }
 
@@ -150,15 +158,11 @@
     return ids;
   }
 
-  void setSubmitEnabled() {
-    submit.setVisible(canSubmit);
-  }
-
   @UiHandler("followUp")
   void onFollowUp(@SuppressWarnings("unused") ClickEvent e) {
     if (followUpAction == null) {
       followUpAction = new FollowUpAction(followUp, project,
-          branch, key);
+          branch, topic, key);
     }
     followUpAction.show();
   }
@@ -181,16 +185,8 @@
 
   @UiHandler("rebase")
   void onRebase(@SuppressWarnings("unused") ClickEvent e) {
-    boolean enabled = true;
-    RevisionInfo revInfo = changeInfo.revision(revision);
-    if (revInfo.has_actions()) {
-        NativeMap<ActionInfo> actions = revInfo.actions();
-        if (actions.containsKey("rebase")) {
-          enabled = actions.get("rebase").enabled();
-        }
-    }
     RebaseAction.call(rebase, project, changeInfo.branch(), changeId, revision,
-        enabled);
+        rebaseParentNotCurrent);
   }
 
   @UiHandler("submit")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index 40d732a..bc5a321 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     @def BUTTON_HEIGHT 14px;
 
     #change_actions {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
index 0b6c5cd..195623a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
@@ -1,20 +1,20 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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
+// 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
+// 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.
+// 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.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java
index 7245e47..f747e0d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java
@@ -1,22 +1,22 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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
+// 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
+// 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.
+// 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.client.change;
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
index d8236e6..c3539bc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:u='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
index be6879e..f5b26d1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
@@ -21,7 +21,6 @@
   String nextChange();
   String openChange();
   String reviewedFileTitle();
-  String editMessage();
   String editFileInline();
   String removeFileInline();
   String restoreFileInline();
@@ -34,7 +33,6 @@
   String date();
   String author();
   String draft();
-  String draftCommentsTooltip();
 
   String notAvailable();
   String relatedChanges();
@@ -45,6 +43,8 @@
   String cherryPicksTooltip();
   String sameTopic();
   String sameTopicTooltip();
+  String submittedTogether();
+  String submittedTogetherTooltip();
   String noChanges();
   String indirectAncestor();
   String merged();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
index 682cd18..5b4f18f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
@@ -2,7 +2,6 @@
 nextChange = Next related change
 openChange = Open related change
 reviewedFileTitle = Mark file as reviewed (Shortcut: r)
-editMessage = Edit commit message
 editFileInline = Edit file inline
 removeFileInline = Remove file inline
 restoreFileInline = Restore file inline
@@ -15,7 +14,6 @@
 date = Date
 author = Author / Committer
 draft = (DRAFT)
-draftCommentsTooltip = Draft comment(s) inside
 
 notAvailable = N/A
 relatedChanges = Related Changes
@@ -26,6 +24,8 @@
 cherryPicksTooltip = Changes with the same Change-Id
 sameTopic = Same Topic
 sameTopicTooltip = Changes with the same topic
+submittedTogether = Submitted Together
+submittedTogetherTooltip = Changes submitted together with this change
 noChanges = No Changes
 indirectAncestor = Indirect ancestor
 merged = Merged
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
index 8d5b72c..62c3636 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
@@ -27,5 +27,7 @@
   String cherryPicks(String count);
   String sameTopic(int count);
   String sameTopic(String count);
+  String submittedTogether(int count);
+  String submittedTogether(String count);
   String editPatchSet(int patchSet);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
index 6e095fb..6461899 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
@@ -4,4 +4,5 @@
 conflictingChanges = Conflicts With ({0})
 cherryPicks = Cherry-Picks ({0})
 sameTopic = Same Topic ({0})
+submittedTogether = Submitted Together ({0})
 editPatchSet = edit:{0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 83514dc..2673f49 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -18,23 +18,28 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
-import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.GerritUiExtensionPoint;
 import com.google.gerrit.client.api.ChangeGlue;
+import com.google.gerrit.client.api.ExtensionPanel;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.RevisionInfoCache;
 import com.google.gerrit.client.changes.StarredChanges;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.diff.DiffApi;
-import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.FileInfo;
+import com.google.gerrit.client.info.GpgKeyInfo;
+import com.google.gerrit.client.info.PushCertificateInfo;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -64,6 +69,7 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.SelectElement;
+import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -84,7 +90,10 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.user.client.ui.ToggleButton;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
@@ -96,6 +105,7 @@
 
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 
@@ -104,16 +114,18 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface Style extends CssResource {
-    String labelName();
     String avatar();
-    String label_user();
-    String label_ok();
-    String label_reject();
+    String hashtagName();
+    String highlight();
+    String labelName();
     String label_may();
     String label_need();
+    String label_ok();
+    String label_reject();
+    String label_user();
+    String pushCertStatus();
     String replyBox();
     String selected();
-    String hashtagName();
   }
 
   static ChangeScreen get(NativeEvent in) {
@@ -131,11 +143,10 @@
   private String base;
   private String revision;
   private ChangeInfo changeInfo;
+  private boolean hasDraftComments;
   private CommentLinkProcessor commentLinkProcessor;
   private EditInfo edit;
 
-  private KeyCommandSet keysNavigation;
-  private KeyCommandSet keysAction;
   private List<HandlerRegistration> handlers = new ArrayList<>(4);
   private UpdateCheckTimer updateCheck;
   private Timestamp lastDisplayedUpdate;
@@ -145,6 +156,9 @@
   private FileTable.Mode fileTableMode;
 
   @UiField HTMLPanel headerLine;
+  @UiField SimplePanel headerExtension;
+  @UiField SimplePanel headerExtensionMiddle;
+  @UiField SimplePanel headerExtensionRight;
   @UiField Style style;
   @UiField ToggleButton star;
   @UiField Anchor permalink;
@@ -153,8 +167,14 @@
   @UiField Reviewers reviewers;
   @UiField Hashtags hashtags;
   @UiField Element hashtagTableRow;
+
   @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
+
+  @UiField Element uploaderRow;
+  @UiField FlowPanel uploaderPanel;
+  @UiField InlineLabel uploaderName;
+
   @UiField Element statusText;
   @UiField Image projectSettings;
   @UiField AnchorElement projectSettingsLink;
@@ -166,6 +186,7 @@
   @UiField Topic topic;
   @UiField Element actionText;
   @UiField Element actionDate;
+  @UiField SimplePanel changeExtension;
 
   @UiField Actions actions;
   @UiField Labels labels;
@@ -223,6 +244,18 @@
     super.onLoad();
     CallbackGroup group = new CallbackGroup();
     if (Gerrit.isSignedIn()) {
+      ChangeList.query("change:" + changeId.get() + " has:draft",
+          Collections.<ListChangesOption> emptySet(),
+          group.add(new AsyncCallback<ChangeList>() {
+            @Override
+            public void onSuccess(ChangeList result) {
+              hasDraftComments = result.length() > 0;
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          }));
       ChangeApi.editWithFiles(changeId.get(), group.add(
           new AsyncCallback<EditInfo>() {
             @Override
@@ -240,22 +273,62 @@
           @Override
           public void onSuccess(ChangeInfo info) {
             info.init();
+            addExtensionPoints(info);
             loadConfigInfo(info, base);
           }
         }));
   }
 
+  private void addExtensionPoints(ChangeInfo change) {
+    addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER,
+        headerExtension, change);
+    addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
+        headerExtensionMiddle, change);
+    addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
+        headerExtensionRight, change);
+    addExtensionPoint(
+        GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+        changeExtension, change);
+  }
+
+  private void addExtensionPoint(GerritUiExtensionPoint extensionPoint,
+      Panel p, ChangeInfo change) {
+    ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint);
+    extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change);
+    p.add(extensionPanel);
+  }
+
+  private boolean enableSignedPush() {
+    return Gerrit.info().receive().enableSignedPush();
+  }
+
   void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
     RestApi call = ChangeApi.detail(changeId.get());
-    ChangeList.addOptions(call, EnumSet.of(
-      ListChangesOption.CURRENT_ACTIONS,
-      ListChangesOption.ALL_REVISIONS));
+    EnumSet<ListChangesOption> opts = EnumSet.of(
+      ListChangesOption.ALL_REVISIONS,
+      ListChangesOption.CHANGE_ACTIONS);
+    if (enableSignedPush()) {
+      opts.add(ListChangesOption.PUSH_CERTIFICATES);
+    }
+    ChangeList.addOptions(call, opts);
     if (!fg) {
       call.background();
     }
     call.get(cb);
   }
 
+  void loadRevisionInfo() {
+    RestApi call = ChangeApi.actions(changeId.get(), revision);
+    call.background();
+    call.get(new GerritCallback<NativeMap<ActionInfo>>() {
+      @Override
+      public void onSuccess(NativeMap<ActionInfo> actionMap) {
+        actionMap.copyKeysIntoChildren("id");
+        renderRevisionInfo(changeInfo, actionMap);
+      }
+    });
+  }
+
   @Override
   protected void onUnload() {
     if (replyAction != null) {
@@ -281,84 +354,24 @@
     labels.init(style);
     reviewers.init(style, ccText);
     hashtags.init(style);
-
-    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        Gerrit.displayLastChangeList();
-      }
-    });
-    keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        Gerrit.display(PageLinks.toChange(changeId));
-      }
-    });
-    keysNavigation.add(new KeyCommand(0, 'n', Util.C.keyNextPatchSet()) {
-        @Override
-        public void onKeyPress(final KeyPressEvent event) {
-          gotoSibling(1);
-        }
-      }, new KeyCommand(0, 'p', Util.C.keyPreviousPatchSet()) {
-        @Override
-        public void onKeyPress(final KeyPressEvent event) {
-          gotoSibling(-1);
-        }
-      });
-
-    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-    keysAction.add(new KeyCommand(0, 'a', Util.C.keyPublishComments()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (Gerrit.isSignedIn()) {
-          onReply(null);
-        } else {
-          Gerrit.doSignIn(getToken());
-        }
-      }
-    });
-    keysAction.add(new KeyCommand(0, 'x', Util.C.keyExpandAllMessages()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        onExpandAll(null);
-      }
-    });
-    keysAction.add(new KeyCommand(0, 'z', Util.C.keyCollapseAllMessages()) {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        onCollapseAll(null);
-      }
-    });
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new KeyCommand(0, 's', Util.C.changeTableStar()) {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
-          star.setValue(!star.getValue(), true);
-        }
-      });
-      keysAction.add(new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
-          reviewers.onOpenForm();
-        }
-      });
-    }
   }
 
   private void initReplyButton(ChangeInfo info, String revision) {
-    if (!info.revision(revision).is_edit()) {
-      reply.setTitle(Gerrit.getConfig().getReplyTitle());
+    if (!info.revision(revision).isEdit()) {
+      reply.setTitle(Gerrit.info().change().replyLabel());
       reply.setHTML(new SafeHtmlBuilder()
         .openDiv()
-        .append(Gerrit.getConfig().getReplyLabel())
+        .append(Gerrit.info().change().replyLabel())
         .closeDiv());
+      if (hasDraftComments) {
+        reply.setStyleName(style.highlight());
+      }
       reply.setVisible(true);
     }
   }
 
   private void gotoSibling(final int offset) {
-    if (offset > 0 && changeInfo.current_revision().equals(revision)) {
+    if (offset > 0 && changeInfo.currentRevision().equals(revision)) {
       return;
     }
 
@@ -372,7 +385,7 @@
       if (revision.equals(revisions.get(i).name())) {
         if (0 <= i + offset && i + offset < revisions.length()) {
           Gerrit.display(PageLinks.toChange(
-              new PatchSet.Id(changeInfo.legacy_id(),
+              new PatchSet.Id(changeInfo.legacyId(),
               revisions.get(i + offset)._number())));
           return;
         }
@@ -384,7 +397,7 @@
   private void initIncludedInAction(ChangeInfo info) {
     if (info.status() == Status.MERGED) {
       includedInAction = new IncludedInAction(
-          info.legacy_id(),
+          info.legacyId(),
           style, headerLine, includedIn);
       includedIn.setVisible(true);
     }
@@ -392,7 +405,7 @@
 
   private void initChangeAction(ChangeInfo info) {
     if (info.status() == Status.DRAFT) {
-      NativeMap<ActionInfo> actions = info.has_actions()
+      NativeMap<ActionInfo> actions = info.hasActions()
           ? info.actions()
           : NativeMap.<ActionInfo> create();
       actions.copyKeysIntoChildren("id");
@@ -403,11 +416,12 @@
     }
   }
 
-  private void initRevisionsAction(ChangeInfo info, String revision) {
+  private void initRevisionsAction(ChangeInfo info, String revision,
+      NativeMap<ActionInfo> actions) {
     int currentPatchSet;
-    if (info.current_revision() != null
-        && info.revisions().containsKey(info.current_revision())) {
-      currentPatchSet = info.revision(info.current_revision())._number();
+    if (info.currentRevision() != null
+        && info.revisions().containsKey(info.currentRevision())) {
+      currentPatchSet = info.revision(info.currentRevision())._number();
     } else {
       JsArray<RevisionInfo> revList = info.revisions().values();
       RevisionInfo.sortRevisionInfoByNumber(revList);
@@ -426,16 +440,11 @@
     patchSetsText.setInnerText(Resources.M.patchSets(
         currentlyViewedPatchSet, currentPatchSet));
     patchSetsAction = new PatchSetsAction(
-        info.legacy_id(), revision, edit,
+        info.legacyId(), revision, edit,
         style, headerLine, patchSets);
 
     RevisionInfo revInfo = info.revision(revision);
     if (revInfo.draft()) {
-      NativeMap<ActionInfo> actions = revInfo.has_actions()
-          ? revInfo.actions()
-          : NativeMap.<ActionInfo> create();
-      actions.copyKeysIntoChildren("id");
-
       if (actions.containsKey("publish")) {
         publish.setVisible(true);
         publish.setTitle(actions.get("publish").title());
@@ -454,20 +463,20 @@
 
   private void initProjectLinks(final ChangeInfo info) {
     projectSettingsLink.setHref(
-        "#" + PageLinks.toProject(info.project_name_key()));
+        "#" + PageLinks.toProject(info.projectNameKey()));
     projectSettings.addDomHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
         if (Hyperlink.impl.handleAsClick((Event) event.getNativeEvent())) {
           event.stopPropagation();
           event.preventDefault();
-          Gerrit.display(PageLinks.toProject(info.project_name_key()));
+          Gerrit.display(PageLinks.toProject(info.projectNameKey()));
         }
       }
     }, ClickEvent.getType());
     projectDashboard.setText(info.project());
     projectDashboard.setTargetHistoryToken(
-        PageLinks.toProjectDefaultDashboard(info.project_name_key()));
+        PageLinks.toProjectDefaultDashboard(info.projectNameKey()));
   }
 
   private void initBranchLink(ChangeInfo info) {
@@ -475,7 +484,7 @@
     branchLink.setTargetHistoryToken(
         PageLinks.toChangeQuery(
             BranchLink.query(
-                info.project_name_key(),
+                info.projectNameKey(),
                 info.status(),
                 info.branch(),
                 null)));
@@ -505,7 +514,7 @@
         reviewMode.setVisible(false);
       }
 
-      if (rev.is_edit()) {
+      if (rev.isEdit()) {
         if (info.hasEditBasedOnCurrentPatchSet()) {
           publishEdit.setVisible(true);
         } else {
@@ -517,11 +526,11 @@
   }
 
   private boolean isEditModeEnabled(ChangeInfo info, RevisionInfo rev) {
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       return true;
     }
     if (edit == null) {
-      return revision.equals(info.current_revision());
+      return revision.equals(info.currentRevision());
     }
     return rev._number() == RevisionInfo.findEditParent(
         info.revisions().values());
@@ -566,7 +575,94 @@
   @Override
   public void registerKeys() {
     super.registerKeys();
+
+    KeyCommandSet keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
+    keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
+      @Override
+      public void onKeyPress(final KeyPressEvent event) {
+        Gerrit.displayLastChangeList();
+      }
+    });
+    keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
+      @Override
+      public void onKeyPress(final KeyPressEvent event) {
+        Gerrit.display(PageLinks.toChange(changeId));
+      }
+    });
+    keysNavigation.add(new KeyCommand(0, 'n', Util.C.keyNextPatchSet()) {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          gotoSibling(1);
+        }
+      }, new KeyCommand(0, 'p', Util.C.keyPreviousPatchSet()) {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
+          gotoSibling(-1);
+        }
+      });
     handlers.add(GlobalKey.add(this, keysNavigation));
+
+    KeyCommandSet keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+    keysAction.add(new KeyCommand(0, 'a', Util.C.keyPublishComments()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (Gerrit.isSignedIn()) {
+          onReply(null);
+        } else {
+          Gerrit.doSignIn(getToken());
+        }
+      }
+    });
+    keysAction.add(new KeyCommand(0, 'x', Util.C.keyExpandAllMessages()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        onExpandAll(null);
+      }
+    });
+    keysAction.add(new KeyCommand(0, 'z', Util.C.keyCollapseAllMessages()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        onCollapseAll(null);
+      }
+    });
+    keysAction.add(new KeyCommand(0, 's', Util.C.changeTableStar()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (Gerrit.isSignedIn()) {
+          star.setValue(!star.getValue(), true);
+        } else {
+          Gerrit.doSignIn(getToken());
+        }
+      }
+    });
+    keysAction.add(new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (Gerrit.isSignedIn()) {
+          reviewers.onOpenForm();
+        } else {
+          Gerrit.doSignIn(getToken());
+        }
+      }
+    });
+    keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (Gerrit.isSignedIn()) {
+          // In Firefox this event is mistakenly called when F5 is pressed so
+          // differentiate F5 from 't' by checking the charCode(F5=0, t=116).
+          if (event.getNativeEvent().getCharCode() == 0) {
+            Window.Location.reload();
+            return;
+          }
+          if (topic.canEdit()) {
+            topic.onEdit();
+          }
+        } else {
+          Gerrit.doSignIn(getToken());
+        }
+      }
+    });
     handlers.add(GlobalKey.add(this, keysAction));
     files.registerKeys();
   }
@@ -746,9 +842,9 @@
   private void loadConfigInfo(final ChangeInfo info, final String base) {
     info.revisions().copyKeysIntoChildren("name");
     if (edit != null) {
-      edit.set_name(edit.commit().commit());
-      info.set_edit(edit);
-      if (edit.has_files()) {
+      edit.setName(edit.commit().commit());
+      info.setEdit(edit);
+      if (edit.hasFiles()) {
         edit.files().copyKeysIntoChildren("path");
       }
       info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
@@ -766,14 +862,14 @@
       if (revision == null) {
         RevisionInfo.sortRevisionInfoByNumber(list);
         RevisionInfo rev = list.get(list.length() - 1);
-        if (rev.is_edit()) {
-          info.set_current_revision(rev.name());
+        if (rev.isEdit()) {
+          info.setCurrentRevision(rev.name());
         }
       } else if (revision.equals("edit") || revision.equals("0")) {
         for (int i = 0; i < list.length(); i++) {
           RevisionInfo r = list.get(i);
-          if (r.is_edit()) {
-            info.set_current_revision(r.name());
+          if (r.isEdit()) {
+            info.setCurrentRevision(r.name());
             break;
           }
         }
@@ -784,7 +880,7 @@
 
     CallbackGroup group = new CallbackGroup();
     Timestamp lastReply = myLastReply(info);
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       // Comments are filtered for the current revision. Use parent
       // patch set for edits, as edits themself can never have comments.
       RevisionInfo p = RevisionInfo.findEditParentRevision(
@@ -803,7 +899,7 @@
 
     RevisionInfoCache.add(changeId, rev);
     ConfigInfoCache.add(info);
-    ConfigInfoCache.get(info.project_name_key(),
+    ConfigInfoCache.get(info.projectNameKey(),
       group.addFinal(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
         @Override
         protected void preDisplay(Entry result) {
@@ -811,16 +907,17 @@
           commentLinkProcessor = result.getCommentLinkProcessor();
           setTheme(result.getTheme());
           renderChangeInfo(info);
+          loadRevisionInfo();
         }
       }));
   }
 
   static Timestamp myLastReply(ChangeInfo info) {
     if (Gerrit.isSignedIn() && info.messages() != null) {
-      int self = Gerrit.getUserAccountInfo()._account_id();
+      int self = Gerrit.getUserAccount()._accountId();
       for (int i = info.messages().length() - 1; i >= 0; i--) {
         MessageInfo m = info.messages().get(i);
-        if (m.author() != null && m.author()._account_id() == self) {
+        if (m.author() != null && m.author()._accountId() == self) {
           return m.date();
         }
       }
@@ -877,16 +974,19 @@
   }
 
   private List<NativeMap<JsArray<CommentInfo>>> loadComments(
-      RevisionInfo rev, CallbackGroup group) {
-    final int id = rev._number();
+      final RevisionInfo rev, CallbackGroup group) {
     final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
-    ChangeApi.revision(changeId.get(), rev.name())
-      .view("comments")
+    // TODO(dborowitz): Could eliminate this call by adding an option to include
+    // inline comments in the change detail.
+    ChangeApi.comments(changeId.get())
       .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
         @Override
         public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-          r.add(result);
-          history.addComments(id, result);
+          // Return value is used for populating the file table, so only count
+          // comments for the current revision. Still include all comments in
+          // the history table.
+          r.add(filterForRevision(result, rev._number()));
+          history.addComments(result);
         }
 
         @Override
@@ -896,6 +996,23 @@
     return r;
   }
 
+  private static NativeMap<JsArray<CommentInfo>> filterForRevision(
+      NativeMap<JsArray<CommentInfo>> comments, int id) {
+    NativeMap<JsArray<CommentInfo>> filtered = NativeMap.create();
+    for (String k : comments.keySet()) {
+      JsArray<CommentInfo> allRevisions = comments.get(k);
+      JsArray<CommentInfo> thisRevision = JsArray.createArray().cast();
+      for (int i = 0; i < allRevisions.length(); i++) {
+        CommentInfo c = allRevisions.get(i);
+        if (c.patchSet() == id) {
+          thisRevision.push(c);
+        }
+      }
+      filtered.put(k, thisRevision);
+    }
+    return filtered;
+  }
+
   private List<NativeMap<JsArray<CommentInfo>>> loadDrafts(
       RevisionInfo rev, CallbackGroup group) {
     final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
@@ -919,7 +1036,7 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       return;
     }
 
@@ -927,7 +1044,7 @@
         group.add(new AsyncCallback<CommitInfo>() {
           @Override
           public void onSuccess(CommitInfo info) {
-            rev.set_commit(info);
+            rev.setCommit(info);
           }
 
           @Override
@@ -938,7 +1055,6 @@
 
   private void loadSubmitType(final Change.Status status, final boolean canSubmit) {
     if (canSubmit) {
-      actions.setSubmitEnabled();
       if (status == Change.Status.NEW) {
         statusText.setInnerText(Util.C.readyToSubmit());
       }
@@ -968,7 +1084,7 @@
 
   private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
     RevisionInfo rev = resolveRevisionOrPatchSetId(info, revision,
-        info.current_revision());
+        info.currentRevision());
     if (rev != null) {
       revision = rev.name();
       return rev;
@@ -985,7 +1101,7 @@
       return rev;
     } else {
       new ErrorDialog(
-          Resources.M.changeWithNoRevisions(info.legacy_id().get())).center();
+          Resources.M.changeWithNoRevisions(info.legacyId().get())).center();
       throw new IllegalStateException("no revision, cannot proceed");
     }
   }
@@ -1023,7 +1139,7 @@
   private boolean isSubmittable(ChangeInfo info) {
     boolean canSubmit =
         info.status().isOpen() &&
-        revision.equals(info.current_revision()) &&
+        revision.equals(info.currentRevision()) &&
         !info.revision(revision).draft();
     if (canSubmit && info.status() == Change.Status.NEW) {
       for (String name : info.labels()) {
@@ -1049,30 +1165,19 @@
   }
 
   private void renderChangeInfo(ChangeInfo info) {
+    RevisionInfo revisionInfo = info.revision(revision);
     changeInfo = info;
     lastDisplayedUpdate = info.updated();
-    RevisionInfo revisionInfo = info.revision(revision);
-    boolean current = revision.equals(info.current_revision())
-        && !revisionInfo.is_edit();
 
-    if (revisionInfo.is_edit()) {
-      statusText.setInnerText(Util.C.changeEdit());
-    } else if (!current) {
-      statusText.setInnerText(Util.C.notCurrent());
-      labels.setVisible(false);
-    } else {
-      Status s = info.revision(revision).draft() ? Status.DRAFT : info.status();
-      statusText.setInnerText(Util.toLongString(s));
-    }
     labels.set(info);
 
     renderOwner(info);
+    renderUploader(info, revisionInfo);
     renderActionTextDate(info);
     renderDiffBaseListBox(info);
     initReplyButton(info, revision);
     initIncludedInAction(info);
     initChangeAction(info);
-    initRevisionsAction(info, revision);
     initDownloadAction(info, revision);
     initProjectLinks(info);
     initBranchLink(info);
@@ -1081,7 +1186,7 @@
 
     star.setValue(info.starred());
     permalink.setHref(ChangeLink.permalink(changeId));
-    permalink.setText(String.valueOf(info.legacy_id()));
+    permalink.setText(String.valueOf(info.legacyId()));
     topic.set(info, revision);
     commit.set(commentLinkProcessor, info, revision);
     related.set(info, revision);
@@ -1092,23 +1197,45 @@
       setVisible(hashtagTableRow, false);
     }
 
+    StringBuilder sb = new StringBuilder();
+    sb.append(Util.M.changeScreenTitleId(info.idAbbreviated()));
+    if (info.subject() != null) {
+      sb.append(": ");
+      sb.append(info.subject());
+    }
+    setWindowTitle(sb.toString());
+
+    // Although this is related to the revision, we can process it early to
+    // render it faster.
+    if (!info.status().isOpen()
+        || !revision.equals(info.currentRevision())
+        || revisionInfo.isEdit()) {
+      setVisible(strategy, false);
+    }
+
+    // Properly render revision actions initially while waiting for
+    // the callback to populate them correctly.
+    NativeMap<ActionInfo> emptyMap = NativeMap.<ActionInfo> create();
+    initRevisionsAction(info, revision, emptyMap);
+    quickApprove.setVisible(false);
+    actions.reloadRevisionActions(emptyMap);
+
+    boolean current = revision.equals(info.currentRevision())
+        && !revisionInfo.isEdit();
+
+    if (revisionInfo.isEdit()) {
+      statusText.setInnerText(Util.C.changeEdit());
+    } else if (!current) {
+      statusText.setInnerText(Util.C.notCurrent());
+      labels.setVisible(false);
+    } else {
+      Status s = info.revision(revision).draft() ? Status.DRAFT : info.status();
+      statusText.setInnerText(Util.toLongString(s));
+    }
+
     if (Gerrit.isSignedIn()) {
-      replyAction = new ReplyAction(info, revision,
+      replyAction = new ReplyAction(info, revision, hasDraftComments,
           style, commentLinkProcessor, reply, quickApprove);
-      if (topic.canEdit()) {
-        keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            // In Firefox this event is mistakenly called when F5 is pressed so
-            // differentiate F5 from 't' by checking the charCode(F5=0, t=116).
-            if (event.getNativeEvent().getCharCode() == 0) {
-              Window.Location.reload();
-              return;
-            }
-            topic.onEdit();
-          }
-        });
-      }
     }
     history.set(commentLinkProcessor, replyAction, changeId, info);
 
@@ -1117,37 +1244,106 @@
       loadSubmitType(info.status(), isSubmittable(info));
     } else {
       quickApprove.setVisible(false);
-      setVisible(strategy, false);
     }
+  }
 
-    StringBuilder sb = new StringBuilder();
-    sb.append(Util.M.changeScreenTitleId(info.id_abbreviated()));
-    if (info.subject() != null) {
-      sb.append(": ");
-      sb.append(info.subject());
-    }
-    setWindowTitle(sb.toString());
+  private void renderRevisionInfo(ChangeInfo info,
+      NativeMap<ActionInfo> actionMap) {
+    initRevisionsAction(info, revision, actionMap);
+    commit.setParentNotCurrent(actionMap.containsKey("rebase")
+        && actionMap.get("rebase").enabled());
+    actions.reloadRevisionActions(actionMap);
   }
 
   private void renderOwner(ChangeInfo info) {
     // TODO info card hover
-    String name = info.owner().name() != null
-        ? info.owner().name()
-        : Gerrit.getConfig().getAnonymousCowardName();
-
+    String name = name(info.owner());
     if (info.owner().avatar(AvatarInfo.DEFAULT_SIZE) != null) {
       ownerPanel.insert(new AvatarImage(info.owner()), 0);
     }
     ownerLink.setText(name);
-    ownerLink.setTitle(info.owner().email() != null
-        ? info.owner().email()
-        : name);
+    ownerLink.setTitle(email(info.owner(), name));
     ownerLink.setTargetHistoryToken(PageLinks.toAccountQuery(
         info.owner().name() != null
         ? info.owner().name()
         : info.owner().email() != null
         ? info.owner().email()
-        : String.valueOf(info.owner()._account_id()), Change.Status.NEW));
+        : String.valueOf(info.owner()._accountId()), Change.Status.NEW));
+  }
+
+  private void renderUploader(ChangeInfo changeInfo, RevisionInfo revInfo) {
+    AccountInfo uploader = revInfo.uploader();
+    boolean isOwner = uploader == null
+        || uploader._accountId() == changeInfo.owner()._accountId();
+    renderPushCertificate(revInfo, isOwner ? ownerPanel : uploaderPanel);
+    if (isOwner) {
+      uploaderRow.getStyle().setDisplay(Display.NONE);
+      return;
+    }
+    uploaderRow.getStyle().setDisplay(Display.TABLE_ROW);
+
+    if (uploader.avatar(AvatarInfo.DEFAULT_SIZE) != null) {
+      uploaderPanel.insert(new AvatarImage(uploader), 0);
+    }
+    String name = name(uploader);
+    uploaderName.setText(name);
+    uploaderName.setTitle(email(uploader, name));
+  }
+
+  private void renderPushCertificate(RevisionInfo revInfo, FlowPanel panel) {
+    if (!enableSignedPush()) {
+      return;
+    }
+    Image status = new Image();
+    panel.add(status);
+    status.setStyleName(style.pushCertStatus());
+    if (!revInfo.hasPushCertificate()
+        || revInfo.pushCertificate().key() == null) {
+      status.setResource(Gerrit.RESOURCES.question());
+      status.setTitle(Util.C.pushCertMissing());
+      return;
+    }
+    PushCertificateInfo certInfo = revInfo.pushCertificate();
+    GpgKeyInfo.Status s = certInfo.key().status();
+    switch (s) {
+      case BAD:
+        status.setResource(Gerrit.RESOURCES.redNot());
+        status.setTitle(problems(Util.C.pushCertBad(), certInfo));
+        break;
+      case OK:
+        status.setResource(Gerrit.RESOURCES.warning());
+        status.setTitle(problems(Util.C.pushCertOk(), certInfo));
+        break;
+      case TRUSTED:
+        status.setResource(Gerrit.RESOURCES.greenCheck());
+        status.setTitle(Util.C.pushCertTrusted());
+        break;
+    }
+  }
+
+  private static String name(AccountInfo info) {
+    return info.name() != null
+        ? info.name()
+        : Gerrit.info().user().anonymousCowardName();
+  }
+
+  private static String email(AccountInfo info, String name) {
+    return info.email() != null ? info.email() : name;
+  }
+
+  private static String problems(String msg, PushCertificateInfo info) {
+    if (info.key() == null
+        || !info.key().hasProblems()
+        || info.key().problems().length() == 0) {
+      return msg;
+    }
+
+    StringBuilder sb = new StringBuilder();
+    sb.append(msg).append(':');
+    for (String problem : Natives.asList(info.key().problems())) {
+      sb.append('\n').append(problem);
+    }
+    return sb.toString();
   }
 
   private void renderSubmitType(String action) {
@@ -1235,7 +1431,7 @@
   }
 
   private void startPoller() {
-    if (Gerrit.isSignedIn() && 0 < Gerrit.getConfig().getChangeUpdateDelay()) {
+    if (Gerrit.isSignedIn() && 0 < Gerrit.info().change().updateDelay()) {
       updateCheck = new UpdateCheckTimer(this);
       updateCheck.schedule();
       handlers.add(UserActivityMonitor.addValueChangeHandler(updateCheck));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 6f8d3aa..c643072 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -21,7 +21,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style type='com.google.gerrit.client.change.ChangeScreen.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.ChangeScreen.Style'>
     @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
@@ -81,7 +81,6 @@
       display: inline-block;
       position: relative;
       height: HEADER_HEIGHT;
-      width: 300px;
     }
     .star {
       position: absolute;
@@ -316,12 +315,29 @@
       background-color: trimColor;
     }
 
-    .ownerPanel img {
+    .ownerPanel img, .uploaderPanel img {
       margin: 0 2px 0 0;
       width: 16px;
       height: 16px !important;
       vertical-align: bottom;
     }
+
+    .headerExtension {
+      display: inline-block;
+      float: right;
+    }
+
+    .headerExtension>div>div {
+      float: left;
+    }
+
+    .changeExtension {
+      padding-top: 5px;
+    }
+
+    .pushCertStatus {
+      padding-left: 5px;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.cs2}'>
@@ -333,6 +349,7 @@
               <ui:attribute name='title'/>
             </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/></ui:msg>
           </span>
+          <g:SimplePanel ui:field='headerExtension' styleName='{style.headerExtension}'/>
         </div>
       </div>
 
@@ -370,6 +387,7 @@
           <g:Button ui:field='deleteRevision' styleName='' visible='false'>
             <div><ui:msg>Delete Revision</ui:msg></div>
           </g:Button>
+          <g:SimplePanel ui:field='headerExtensionMiddle' styleName='{style.headerExtension}'/>
         </div>
       </div>
 
@@ -384,6 +402,7 @@
           <g:Button ui:field='download' styleName=''>
             <div><ui:msg>Download</ui:msg></div>
           </g:Button>
+          <g:SimplePanel ui:field='headerExtensionRight' styleName='{style.headerExtension}'/>
         </g:FlowPanel>
         <c:StarIcon ui:field='star' styleName='{style.star}' title='Star the change (Shortcut: s)'>
           <ui:attribute name='title'/>
@@ -406,6 +425,14 @@
                 </g:FlowPanel>
               </td>
             </tr>
+            <tr ui:field='uploaderRow'>
+              <th><ui:msg>Uploader</ui:msg></th>
+              <td>
+                <g:FlowPanel ui:field='uploaderPanel' styleName='{style.uploaderPanel}'>
+                  <g:InlineLabel ui:field='uploaderName'/>
+                </g:FlowPanel>
+              </td>
+            </tr>
             <tr>
               <th><ui:msg>Reviewers</ui:msg></th>
               <td>
@@ -472,6 +499,7 @@
           </table>
           <hr/>
           <c:Labels ui:field='labels' styleName='{style.labels}'/>
+          <g:SimplePanel ui:field='changeExtension' styleName='{style.changeExtension}'/>
           <div id='change_plugins'/>
         </td>
         <td class='{style.relatedColumn}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
index bcd8f6b..3b72581 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CherryPickDialog;
 import com.google.gerrit.common.PageLinks;
@@ -47,7 +47,7 @@
 
       @Override
       public void onSend() {
-        ChangeApi.cherrypick(info.legacy_id().get(), revision,
+        ChangeApi.cherrypick(info.legacyId().get(), revision,
             getDestinationBranch(),
             getMessageText(),
             new GerritCallback<ChangeInfo>() {
@@ -55,7 +55,7 @@
               public void onSuccess(ChangeInfo result) {
                 sent = true;
                 hide();
-                Gerrit.display(PageLinks.toChange(result.legacy_id()));
+                Gerrit.display(PageLinks.toChange(result.legacyId()));
               }
 
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 93db1a7..f8ddef4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -17,15 +17,13 @@
 import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
-import com.google.gerrit.client.WebLinkInfo;
-import com.google.gerrit.client.actions.ActionInfo;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.ChangeInfo.GitPerson;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.GitwebInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -111,8 +109,8 @@
     CommitInfo commit = revInfo.commit();
 
     commitName.setText(revision);
-    idText.setText("Change-Id: " + change.change_id());
-    idText.setPreviewText(change.change_id());
+    idText.setText("Change-Id: " + change.changeId());
+    idText.setPreviewText(change.changeId());
 
     formatLink(commit.author(), authorPanel, authorNameEmail, authorDate,
         change);
@@ -127,28 +125,23 @@
     }
 
     setParents(change.project(), revInfo.commit().parents());
+  }
 
+  void setParentNotCurrent(boolean parentNotCurrent) {
     // display the orange ball if parent has moved on (not current)
-    boolean parentNotCurrent = false;
-    if (revInfo.has_actions()) {
-      NativeMap<ActionInfo> actions = revInfo.actions();
-      if (actions.containsKey("rebase")) {
-        parentNotCurrent = actions.get("rebase").enabled();
-      }
-    }
     UIObject.setVisible(parentNotCurrentText, parentNotCurrent);
     parentNotCurrentText.setInnerText(parentNotCurrent ? "\u25CF" : "");
   }
 
   private void setWebLinks(ChangeInfo change, String revision,
       RevisionInfo revInfo) {
-    GitwebLink gw = Gerrit.getGitwebLink();
+    GitwebInfo gw = Gerrit.info().gitweb();
     if (gw != null && gw.canLink(revInfo)) {
       toAnchor(gw.toRevision(change.project(), revision),
           gw.getLinkName());
     }
 
-    JsArray<WebLinkInfo> links = revInfo.commit().web_links();
+    JsArray<WebLinkInfo> links = revInfo.commit().webLinks();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
         webLinkPanel.add(link.toAnchor());
@@ -191,14 +184,14 @@
   }
 
   private void addLinks(String project, CommitInfo c, FlowPanel panel) {
-    GitwebLink gw = Gerrit.getGitwebLink();
+    GitwebInfo gw = Gerrit.info().gitweb();
     if (gw != null) {
       Anchor a =
           new Anchor(gw.getLinkName(), gw.toRevision(project, c.commit()));
       a.setStyleName(style.parentWebLink());
       panel.add(a);
     }
-    JsArray<WebLinkInfo> links = c.web_links();
+    JsArray<WebLinkInfo> links = c.webLinks();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
         panel.add(link.toAnchor());
@@ -219,7 +212,7 @@
     // only try to fetch the avatar image for author and committer if an avatar
     // plugin is installed, if the change owner has no avatar info assume that
     // no avatar plugin is installed
-    if (change.owner().has_avatar_info()) {
+    if (change.owner().hasAvatarInfo()) {
       AvatarImage avatar;
       if (change.owner().email().equals(person.email())) {
         avatar = new AvatarImage(change.owner());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
index 93312fa..5f476be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -21,7 +21,7 @@
     xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:image field="toggle" src="moreLess.png"/>
-  <ui:style type='com.google.gerrit.client.change.CommitBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.CommitBox.Style'>
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
     .collapsed .scroll { height: 250px }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
index 49e08aa..3a7977a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
@@ -1,20 +1,20 @@
-//Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2015 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
+// 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
+// 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.
+// 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.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
index e55b7ed..3d889d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
@@ -1,23 +1,23 @@
-//Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2015 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
+// 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
+// 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.
+// 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.client.change;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
index 4e7b2ba..9e79f752 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:u='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
index b1bb4e0..9698aef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
@@ -30,7 +30,7 @@
       Widget downloadButton) {
     super(style, relativeTo, downloadButton);
     this.downloadBox = new DownloadBox(info, revision,
-        new PatchSet.Id(info.legacy_id(),
+        new PatchSet.Id(info.legacyId(),
             info.revision(revision)._number()));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
index 49389f3..c8326cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -17,16 +17,14 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
 import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.FetchInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -80,7 +78,7 @@
   protected void onLoad() {
     if (fetch == null) {
       if (psId.get() == 0) {
-        ChangeApi.editWithCommands(change.legacy_id().get()).get(
+        ChangeApi.editWithCommands(change.legacyId().get()).get(
             new AsyncCallback<EditInfo>() {
           @Override
           public void onSuccess(EditInfo result) {
@@ -93,9 +91,9 @@
           }
         });
       } else {
-        RestApi call = ChangeApi.detail(change.legacy_id().get());
+        RestApi call = ChangeApi.detail(change.legacyId().get());
         ChangeList.addOptions(call, EnumSet.of(
-            revision.equals(change.current_revision())
+            revision.equals(change.currentRevision())
                ? ListChangesOption.CURRENT_REVISION
                : ListChangesOption.ALL_REVISIONS,
             ListChangesOption.DOWNLOAD_COMMANDS));
@@ -120,7 +118,7 @@
     if (scheme.getItemCount() > 0) {
       FetchInfo fetchInfo =
           fetch.get(scheme.getValue(scheme.getSelectedIndex()));
-      for (String commandName : Natives.keys(fetchInfo.commands())) {
+      for (String commandName : fetchInfo.commands().sortedKeys()) {
         CopyableLabel copyLabel =
             new CopyableLabel(fetchInfo.command(commandName));
         copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadBoxCopyLabel());
@@ -164,7 +162,7 @@
   }
 
   private void insertArchive() {
-    List<String> activated = Gerrit.getConfig().getArchiveFormats();
+    List<String> activated = Gerrit.info().download().archives();
     if (activated.isEmpty()) {
       return;
     }
@@ -210,7 +208,7 @@
   }
 
   private void renderScheme() {
-    for (String id : fetch.keySet()) {
+    for (String id : fetch.sortedKeys()) {
       scheme.addItem(id);
     }
     if (scheme.getItemCount() == 0) {
@@ -221,7 +219,7 @@
         scheme.setVisible(false);
       } else {
         int select = 0;
-        String find = getUserPreference();
+        String find = Gerrit.getUserPreferences().downloadScheme();
         if (find != null) {
           for (int i = 0; i < scheme.getItemCount(); i++) {
             if (find.equals(scheme.getValue(i))) {
@@ -236,39 +234,13 @@
     renderCommands();
   }
 
-  private static String getUserPreference() {
-    if (Gerrit.isSignedIn()) {
-      DownloadScheme pref =
-          Gerrit.getUserAccount().getGeneralPreferences().getDownloadUrl();
-      if (pref != null) {
-        switch (pref) {
-          case ANON_GIT:
-            return "git";
-          case ANON_HTTP:
-            return "anonymous http";
-          case HTTP:
-            return "http";
-          case SSH:
-            return "ssh";
-          case REPO_DOWNLOAD:
-            return "repo";
-          default:
-            return null;
-        }
-      }
-    }
-    return null;
-  }
-
   private void saveScheme() {
-    DownloadScheme scheme = getSelectedScheme();
-    AccountGeneralPreferences pref =
-        Gerrit.getUserAccount().getGeneralPreferences();
-
-    if (scheme != null && scheme != pref.getDownloadUrl()) {
-      pref.setDownloadUrl(scheme);
-      PreferenceInput in = PreferenceInput.create();
-      in.download_scheme(scheme);
+    String schemeStr = scheme.getValue(scheme.getSelectedIndex());
+    AccountPreferencesInfo prefs = Gerrit.getUserPreferences();
+    if (Gerrit.isSignedIn() && !schemeStr.equals(prefs.downloadScheme())) {
+      prefs.downloadScheme(schemeStr);
+      AccountPreferencesInfo in = AccountPreferencesInfo.create();
+      in.downloadScheme(schemeStr);
       AccountApi.self().view("preferences")
           .put(in, new AsyncCallback<JavaScriptObject>() {
             @Override
@@ -281,37 +253,4 @@
           });
     }
   }
-
-  private DownloadScheme getSelectedScheme() {
-    String id = scheme.getValue(scheme.getSelectedIndex());
-    if ("git".equals(id)) {
-      return DownloadScheme.ANON_GIT;
-    } else if ("anonymous http".equals(id)) {
-      return DownloadScheme.ANON_HTTP;
-    } else if ("http".equals(id)) {
-      return DownloadScheme.HTTP;
-    } else if ("ssh".equals(id)) {
-      return DownloadScheme.SSH;
-    } else if ("repo".equals(id)) {
-      return DownloadScheme.REPO_DOWNLOAD;
-    }
-    return null;
-  }
-
-  private static class PreferenceInput extends JavaScriptObject {
-    static PreferenceInput create() {
-      return createObject().cast();
-    }
-
-    final void download_scheme(DownloadScheme s) {
-      download_scheme0(s.name());
-    }
-
-    private final native void download_scheme0(String n) /*-{
-      this.download_scheme = n;
-    }-*/;
-
-    protected PreferenceInput() {
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
index 7172011..e73c70a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
@@ -26,8 +26,6 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 class FileComments extends Composite {
@@ -38,22 +36,15 @@
   @UiField FlowPanel comments;
 
   FileComments(CommentLinkProcessor clp,
-      PatchSet.Id ps,
+      PatchSet.Id defaultPs,
       String title,
       List<CommentInfo> list) {
     initWidget(uiBinder.createAndBindUi(this));
 
-    path.setTargetHistoryToken(url(ps, list.get(0)));
+    path.setTargetHistoryToken(url(defaultPs, list.get(0)));
     path.setText(title);
-
-    Collections.sort(list, new Comparator<CommentInfo>() {
-      @Override
-      public int compare(CommentInfo a, CommentInfo b) {
-        return a.line() - b.line();
-      }
-    });
     for (CommentInfo c : list) {
-      comments.add(new LineComment(clp, ps, c));
+      comments.add(new LineComment(clp, defaultPs, c));
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
index 5de04cc..e463e95 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     .box {
     }
     .path {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 2947be8..d1ca517 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.client.change;
 
+import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
+import static com.google.gerrit.client.FormatUtil.formatBytes;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
@@ -22,7 +25,7 @@
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.ReviewInfo;
 import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
@@ -87,7 +90,7 @@
     String deltaColumn2();
     String inserted();
     String deleted();
-    String removeButton();
+    String restoreDelete();
   }
 
   public static enum Mode {
@@ -457,8 +460,12 @@
     private ProgressBar meter;
     private String lastPath = "";
 
+    private boolean hasBinaryFile;
+    private boolean hasNonBinaryFile;
     private int inserted;
     private int deleted;
+    private long bytesInserted;
+    private long bytesDeleted;
 
     private DisplayCommand(NativeMap<FileInfo> map,
         JsArray<FileInfo> list,
@@ -471,8 +478,8 @@
       this.comments = comments;
       this.drafts = drafts;
       this.hasUser = Gerrit.isSignedIn();
-      this.showChangeSizeBars = !hasUser
-          || Gerrit.getUserAccount().getGeneralPreferences().isSizeBarInChangeTable();
+      this.showChangeSizeBars =
+          Gerrit.getUserPreferences().sizeBarInChangeTable();
       myTable.addStyleName(R.css().table());
     }
 
@@ -513,11 +520,23 @@
     private void computeInsertedDeleted() {
       inserted = 0;
       deleted = 0;
+      bytesInserted = 0;
+      bytesDeleted = 0;
       for (int i = 0; i < list.length(); i++) {
         FileInfo info = list.get(i);
-        if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
-          inserted += info.lines_inserted();
-          deleted += info.lines_deleted();
+        if (!Patch.COMMIT_MSG.equals(info.path())) {
+          if (!info.binary()) {
+            hasNonBinaryFile = true;
+            inserted += info.linesInserted();
+            deleted += info.linesDeleted();
+          } else {
+            hasBinaryFile = true;
+            if (info.sizeDelta() >= 0) {
+              bytesInserted += info.sizeDelta();
+            } else {
+              bytesDeleted += info.sizeDelta();
+            }
+          }
         }
       }
     }
@@ -548,7 +567,7 @@
       if (mode == Mode.REVIEW) {
         sb.openTh().setStyleName(R.css().reviewed()).closeTh();
       } else {
-        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+        sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
       }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
@@ -592,20 +611,26 @@
     }
 
     private void columnDeleteRestore(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().removeButton());
+      sb.openTd().setStyleName(R.css().restoreDelete());
       if (hasUser) {
         if (!Patch.COMMIT_MSG.equals(info.path())) {
           boolean editable = isEditable(info);
-          sb.openElement("button")
-            .setAttribute("title", editable
-                ? Resources.C.removeFileInline()
-                : Resources.C.restoreFileInline())
-            .setAttribute("onclick", (editable ? DELETE : RESTORE)
-                + "(event," + info._row() + ")")
-            .append(new ImageResourceRenderer().render(editable
-                ? Gerrit.RESOURCES.redNot()
-                : Gerrit.RESOURCES.editUndo()))
+          sb.openDiv()
+            .openElement("button")
+            .setAttribute("title", Resources.C.restoreFileInline())
+            .setAttribute("onclick", RESTORE + "(event," + info._row() + ")")
+            .append(new ImageResourceRenderer().render(
+                Gerrit.RESOURCES.editUndo()))
             .closeElement("button");
+          if (editable) {
+            sb.openElement("button")
+              .setAttribute("title", Resources.C.removeFileInline())
+              .setAttribute("onclick", DELETE + "(event," + info._row() + ")")
+              .append(new ImageResourceRenderer().render(
+                  Gerrit.RESOURCES.redNot()))
+              .closeElement("button");
+          }
+          sb.closeDiv();
         }
       }
       sb.closeTd();
@@ -642,8 +667,7 @@
 
       if (Patch.COMMIT_MSG.equals(path)) {
         sb.append(Util.C.commitMessage());
-      } else if (!hasUser || Gerrit.getUserAccount().getGeneralPreferences()
-          .isMuteCommonPathPrefixes()) {
+      } else if (Gerrit.getUserPreferences().muteCommonPathPrefixes()) {
         int commonPrefixLen = commonPrefix(path);
         if (commonPrefixLen > 0) {
           sb.openSpan().setStyleName(R.css().commonPrefix())
@@ -657,10 +681,10 @@
       }
 
       sb.closeAnchor();
-      if (info.old_path() != null) {
+      if (info.oldPath() != null) {
         sb.br();
         sb.openSpan().setStyleName(R.css().renameCopySource())
-          .append(info.old_path())
+          .append(info.oldPath())
           .closeSpan();
       }
       sb.closeTd();
@@ -733,18 +757,20 @@
       sb.openTd().setStyleName(R.css().deltaColumn1());
       if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
         if (showChangeSizeBars) {
-          sb.append(info.lines_inserted() + info.lines_deleted());
+          sb.append(info.linesInserted() + info.linesDeleted());
         } else if (!ChangeType.DELETED.matches(info.status())) {
           if (ChangeType.ADDED.matches(info.status())) {
-            sb.append(info.lines_inserted())
+            sb.append(info.linesInserted())
               .append(" lines");
           } else {
             sb.append("+")
-              .append(info.lines_inserted())
+              .append(info.linesInserted())
               .append(", -")
-              .append(info.lines_deleted());
+              .append(info.linesDeleted());
           }
         }
+      } else if (info.binary()) {
+        sb.append(formatBytes(info.sizeDelta()));
       }
       sb.closeTd();
     }
@@ -753,24 +779,24 @@
       sb.openTd().setStyleName(R.css().deltaColumn2());
       if (showChangeSizeBars
           && !Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
-          && (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
+          && (info.linesInserted() != 0 || info.linesDeleted() != 0)) {
         int w = 80;
         int t = inserted + deleted;
-        int i = Math.max(5, (int) (((double) w) * info.lines_inserted() / t));
-        int d = Math.max(5, (int) (((double) w) * info.lines_deleted() / t));
+        int i = Math.max(5, (int) (((double) w) * info.linesInserted() / t));
+        int d = Math.max(5, (int) (((double) w) * info.linesDeleted() / t));
 
         sb.setAttribute(
             "title",
-            Util.M.patchTableSize_LongModify(info.lines_inserted(),
-                info.lines_deleted()));
+            Util.M.patchTableSize_LongModify(info.linesInserted(),
+                info.linesDeleted()));
 
-        if (0 < info.lines_inserted()) {
+        if (0 < info.linesInserted()) {
           sb.openDiv()
             .setStyleName(R.css().inserted())
             .setAttribute("style", "width:" + i + "px")
             .closeDiv();
         }
-        if (0 < info.lines_deleted()) {
+        if (0 < info.linesDeleted()) {
           sb.openDiv()
             .setStyleName(R.css().deleted())
             .setAttribute("style", "width:" + d + "px")
@@ -786,16 +812,25 @@
       if (mode == Mode.REVIEW) {
         sb.openTh().setStyleName(R.css().reviewed()).closeTh();
       } else {
-        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+        sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
       }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTd().closeTd(); // path
       sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
 
       // delta1
-      sb.openTh().setStyleName(R.css().deltaColumn1())
-        .append(Util.M.patchTableSize_Modify(inserted, deleted))
-        .closeTh();
+      sb.openTh().setStyleName(R.css().deltaColumn1());
+      if (hasNonBinaryFile) {
+        sb.append(Util.M.patchTableSize_Modify(inserted, deleted));
+      }
+      if (hasBinaryFile) {
+        if (hasNonBinaryFile) {
+          sb.br();
+        }
+        sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
+            formatAbsBytes(bytesInserted), formatAbsBytes(bytesDeleted)));
+      }
+      sb.closeTh();
 
       // delta2
       sb.openTh().setStyleName(R.css().deltaColumn2());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
index 5a7df72..29283b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gwt.user.client.ui.Button;
@@ -24,22 +24,24 @@
 class FollowUpAction extends ActionMessageBox {
   private final String project;
   private final String branch;
+  private final String topic;
   private final String base;
 
-  FollowUpAction(Button b, String project, String branch, String key) {
+  FollowUpAction(Button b, String project, String branch, String topic, String key) {
     super(b);
     this.project = project;
     this.branch = branch;
+    this.topic = topic;
     this.base = project + "~" + branch + "~" + key;
   }
 
   @Override
   void send(String message) {
-    ChangeApi.createChange(project, branch, message, base,
+    ChangeApi.createChange(project, branch, topic, message, base,
         new GerritCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
-            Gerrit.display(PageLinks.toChange(result.legacy_id()));
+            Gerrit.display(PageLinks.toChange(result.legacyId()));
             hide();
           }
         });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
index faba10d..55f66b7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -15,7 +15,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
@@ -51,6 +51,8 @@
   private static final String REMOVE;
   private static final String DATA_ID = "data-id";
 
+  private boolean canEdit;
+
   static {
     REMOVE = DOM.createUniqueId().replace('-', '_');
     init(REMOVE);
@@ -121,9 +123,10 @@
   }
 
   void set(ChangeInfo info) {
-    this.changeId = info.legacy_id();
+    canEdit = info.hasActions() && info.actions().containsKey("hashtags");
+    this.changeId = info.legacyId();
     display(info);
-    openForm.setVisible(Gerrit.isSignedIn());
+    openForm.setVisible(canEdit);
   }
 
   @UiHandler("openForm")
@@ -165,13 +168,15 @@
               "#" + PageLinks.toChangeQuery("hashtag:\"" + hashtagName + "\""))
           .setAttribute("role", "listitem")
           .append("#").append(hashtagName)
-          .closeAnchor()
-          .openElement("button")
-          .setAttribute("title", "Remove hashtag")
-          .setAttribute("onclick", REMOVE + "(event)")
-          .append("×")
-          .closeElement("button")
-          .closeSpan();
+          .closeAnchor();
+      if (canEdit) {
+        html.openElement("button")
+            .setAttribute("title", "Remove hashtag")
+            .setAttribute("onclick", REMOVE + "(event)")
+            .append("×")
+            .closeElement("button");
+      }
+      html.closeSpan();
       if (itr.hasNext()) {
         html.append(' ');
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
index dd06c77..ba4d6cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
@@ -19,7 +19,7 @@
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     button.openAdd {
       margin: 3px 3px 0 0;
       float: right;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
index 7635d81..92a0fc8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
@@ -14,18 +14,14 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
 
@@ -33,22 +29,15 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 class History extends FlowPanel {
   private CommentLinkProcessor clp;
   private ReplyAction replyAction;
   private Change.Id changeId;
 
-  private final Set<Integer> loaded = new HashSet<>();
-  private final Map<AuthorRevision, List<CommentInfo>> byAuthor =
-      new HashMap<>();
-
-  private final List<Integer> toLoad = new ArrayList<>(4);
-  private int active;
+  private final Map<Integer, List<CommentInfo>> byAuthor = new HashMap<>();
 
   void set(CommentLinkProcessor clp, ReplyAction ra,
       Change.Id id, ChangeInfo info) {
@@ -60,9 +49,7 @@
     if (messages != null) {
       for (MessageInfo msg : Natives.asList(messages)) {
         Message ui = new Message(this, msg);
-        if (loaded.contains(msg._revisionNumber())) {
-          ui.addComments(comments(msg));
-        }
+        ui.addComments(comments(msg));
         add(ui);
       }
       autoOpen(ChangeScreen.myLastReply(info));
@@ -99,18 +86,16 @@
     replyAction.onReply(info);
   }
 
-  void addComments(int id, NativeMap<JsArray<CommentInfo>> map) {
-    loaded.add(id);
-
+  void addComments(NativeMap<JsArray<CommentInfo>> map) {
     for (String path : map.keySet()) {
       for (CommentInfo c : Natives.asList(map.get(path))) {
         c.path(path);
         if (c.author() != null) {
-          AuthorRevision k = new AuthorRevision(c.author(), id);
-          List<CommentInfo> l = byAuthor.get(k);
+          int authorId = c.author()._accountId();
+          List<CommentInfo> l = byAuthor.get(authorId);
           if (l == null) {
             l = new ArrayList<>();
-            byAuthor.put(k, l);
+            byAuthor.put(authorId, l);
           }
           l.add(c);
         }
@@ -118,58 +103,13 @@
     }
   }
 
-  void load(int revisionNumber) {
-    if (revisionNumber > 0 && loaded.add(revisionNumber)) {
-      toLoad.add(revisionNumber);
-      start();
-    }
-  }
-
-  private void start() {
-    if (active >= 2 || toLoad.isEmpty() || !isAttached()) {
-      return;
-    }
-
-    final int revisionNumber = toLoad.remove(0);
-    active++;
-    ChangeApi.revision(new PatchSet.Id(changeId, revisionNumber))
-      .view("comments")
-      .get(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-        @Override
-        public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-          addComments(revisionNumber, result);
-          update(revisionNumber);
-          --active;
-          start();
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-          loaded.remove(revisionNumber);
-          loaded.removeAll(toLoad);
-          toLoad.clear();
-          active--;
-        }
-      });
-  }
-
-  private void update(int revisionNumber) {
-    for (Widget child : getChildren()) {
-      Message ui = (Message) child;
-      MessageInfo info = ui.getMessageInfo();
-      if (info._revisionNumber() == revisionNumber) {
-        ui.addComments(comments(info));
-      }
-    }
-  }
-
   private List<CommentInfo> comments(MessageInfo msg) {
     if (msg.author() == null) {
       return Collections.emptyList();
     }
 
-    AuthorRevision k = new AuthorRevision(msg.author(), msg._revisionNumber());
-    List<CommentInfo> list = byAuthor.get(k);
+    int authorId = msg.author()._accountId();
+    List<CommentInfo> list = byAuthor.get(authorId);
     if (list == null) {
       return Collections.emptyList();
     }
@@ -187,34 +127,10 @@
     if (match.isEmpty()) {
       return Collections.emptyList();
     } else if (other.isEmpty()) {
-      byAuthor.remove(k);
+      byAuthor.remove(authorId);
     } else {
-      byAuthor.put(k, other);
+      byAuthor.put(authorId, other);
     }
     return match;
   }
-
-  private static final class AuthorRevision {
-    final int author;
-    final int revision;
-
-    AuthorRevision(AccountInfo author, int revision) {
-      this.author = author._account_id();
-      this.revision = revision;
-    }
-
-    @Override
-    public int hashCode() {
-      return author * 31 + revision;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (!(o instanceof AuthorRevision)) {
-        return false;
-      }
-      AuthorRevision b = (AuthorRevision) o;
-      return author == b.author && revision == b.revision;
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
index 927e500..6493f11 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.java
@@ -15,11 +15,15 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
+import com.google.gerrit.client.info.ChangeInfo.IncludedInInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.dom.client.TableRowElement;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.uibinder.client.UiBinder;
@@ -41,6 +45,7 @@
   private boolean loaded;
 
   @UiField Style style;
+  @UiField TableElement table;
   @UiField Element branches;
   @UiField Element tags;
 
@@ -58,6 +63,12 @@
         public void onSuccess(IncludedInInfo r) {
           branches.setInnerSafeHtml(formatList(r.branches()));
           tags.setInnerSafeHtml(formatList(r.tags()));
+          for (String n : r.externalNames()) {
+            JsArrayString external = r.external(n);
+            if (external.length() > 0) {
+              appendRow(n, external);
+            }
+          }
           loaded = true;
         }
 
@@ -82,4 +93,12 @@
     }
     return html;
   }
+
+  private void appendRow(String title, JsArrayString l) {
+    TableRowElement row = table.insertRow(-1);
+    TableCellElement th = Document.get().createTHElement();
+    th.setInnerText(title);
+    row.appendChild(th);
+    row.insertCell(-1).setInnerSafeHtml(formatList(l));
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
index 59b05f0..36ac734 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style type='com.google.gerrit.client.change.IncludedInBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.IncludedInBox.Style'>
     .includedInBox {
       min-width: 300px;
       max-width: 580px;
@@ -63,7 +63,7 @@
     }
   </ui:style>
   <g:HTMLPanel styleName='{style.includedInBox}'>
-    <table class='{style.includedInTable}'>
+    <table class='{style.includedInTable}' ui:field='table'>
       <tr>
         <th><ui:msg>Branches</ui:msg></th>
           <td ui:field='branches'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index a416894..4139348 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo.AvatarInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
@@ -115,7 +115,8 @@
 
   private Widget renderUsers(LabelInfo label) {
     Map<Integer, List<ApprovalInfo>> m = new HashMap<>(4);
-    int approved = 0, rejected = 0;
+    int approved = 0;
+    int rejected = 0;
 
     for (ApprovalInfo ai : Natives.asList(label.all())) {
       if (ai.value() != 0) {
@@ -142,7 +143,7 @@
 
       String val = LabelValue.formatValue(v.shortValue());
       html.openSpan();
-      html.setAttribute("title", label.value_text(val));
+      html.setAttribute("title", label.valueText(val));
       if (v.intValue() == approved) {
         html.setStyleName(style.label_ok());
       } else if (v.intValue() == rejected) {
@@ -171,12 +172,12 @@
 
   private static boolean isApproved(LabelInfo label, ApprovalInfo ai) {
     return label.approved() != null
-        && label.approved()._account_id() == ai._account_id();
+        && label.approved()._accountId() == ai._accountId();
   }
 
   private static boolean isRejected(LabelInfo label, ApprovalInfo ai) {
     return label.rejected() != null
-        && label.rejected()._account_id() == ai._account_id();
+        && label.rejected()._accountId() == ai._accountId();
   }
 
   private String getStyleForLabel(LabelInfo label) {
@@ -233,12 +234,12 @@
       } else if (ai.email() != null) {
         name = ai.email();
       } else {
-        name = Integer.toString(ai._account_id());
+        name = Integer.toString(ai._accountId());
       }
 
       String votableCategories = "";
       if (votable != null) {
-        Set<String> s = votable.get(ai._account_id()).votableLabels();
+        Set<String> s = votable.get(ai._accountId()).votableLabels();
         if (!s.isEmpty()) {
           StringBuilder sb = new StringBuilder(Util.C.votable());
           sb.append(" ");
@@ -253,7 +254,7 @@
       }
       html.openSpan()
           .setAttribute("role", "listitem")
-          .setAttribute(DATA_ID, ai._account_id())
+          .setAttribute(DATA_ID, ai._accountId())
           .setAttribute("title", getTitle(ai, votableCategories))
           .setStyleName(style.label_user());
       if (img != null) {
@@ -269,7 +270,7 @@
         html.closeSelf();
       }
       html.append(name);
-      if (removable.contains(ai._account_id())) {
+      if (removable.contains(ai._accountId())) {
         html.openElement("button")
             .setAttribute("title", Util.M.removeReviewer(name))
             .setAttribute("onclick", REMOVE + "(event)")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
index 8fa5a68..fc14587 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
@@ -33,15 +33,40 @@
   interface Binder extends UiBinder<HTMLPanel, LineComment> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
+  @UiField Element sideLoc;
+  @UiField Element psLoc;
+  @UiField Element psNum;
   @UiField Element fileLoc;
   @UiField Element lineLoc;
   @UiField InlineHyperlink line;
   @UiField Element message;
 
-  LineComment(CommentLinkProcessor clp, PatchSet.Id ps, CommentInfo info) {
+  LineComment(CommentLinkProcessor clp,
+      PatchSet.Id defaultPs,
+      CommentInfo info) {
     initWidget(uiBinder.createAndBindUi(this));
 
-    if (info.has_line()) {
+    PatchSet.Id ps;
+    if (info.patchSet() != defaultPs.get()) {
+      ps = new PatchSet.Id(defaultPs.getParentKey(), info.patchSet());
+      psNum.setInnerText(Integer.toString(info.patchSet()));
+      sideLoc.removeFromParent();
+      sideLoc = null;
+    } else if (info.side() == Side.PARENT) {
+      ps = defaultPs;
+      psLoc.removeFromParent();
+      psLoc = null;
+      psNum= null;
+    } else {
+      ps = defaultPs;
+      sideLoc.removeFromParent();
+      sideLoc = null;
+      psLoc.removeFromParent();
+      psLoc = null;
+      psNum= null;
+    }
+
+    if (info.hasLine()) {
       fileLoc.removeFromParent();
       fileLoc = null;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
index 8dc1245..f33ba51 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     .box {
       position: relative;
     }
@@ -29,13 +29,17 @@
       font-weight: bold;
     }
     .message {
-      margin-left: 111px;
+      margin-left: 135px;
     }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.box}'>
-    <div class='{style.location}' ui:field='fileLoc'><ui:msg>File Comment</ui:msg></div>
-    <div class='{style.location}' ui:field='lineLoc'><ui:msg>Line <c:InlineHyperlink ui:field='line'/>:</ui:msg></div>
+    <div class='{style.location}'>
+      <span ui:field='sideLoc'><ui:msg>Base, </ui:msg></span>
+      <span ui:field='psLoc'><ui:msg>PS<span ui:field='psNum'/>, </ui:msg></span>
+      <span ui:field='fileLoc'><ui:msg>File Comment</ui:msg></span>
+      <span ui:field='lineLoc'><ui:msg>Line <c:InlineHyperlink ui:field='line'/>:</ui:msg></span>
+    </div>
     <div class='{style.message}' ui:field='message'/>
   </g:HTMLPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index 22d39a7..8b559e3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -121,13 +121,9 @@
   }
 
   void setOpen(boolean open) {
-    if (open && info._revisionNumber() > 0) {
-      if (commentList == null) {
-        history.load(info._revisionNumber());
-      } else if (!commentList.isEmpty()) {
-        renderComments(commentList);
-        commentList = Collections.emptyList();
-      }
+    if (open && info._revisionNumber() > 0 && !commentList.isEmpty()) {
+      renderComments(commentList);
+      commentList = Collections.emptyList();
     }
     setName(open);
 
@@ -142,21 +138,14 @@
   }
 
   private void setName(boolean open) {
-    name.setInnerText(open ? authorName(info) : elide(authorName(info), 20));
-  }
-
-  private static String elide(final String s, final int len) {
-    if (s == null || s.length() <= len || len <= 10) {
-      return s;
-    }
-    int i = (len - 3) / 2;
-    return s.substring(0, i) + "..." + s.substring(s.length() - i);
+    name.setInnerText(open
+        ? authorName(info)
+        : com.google.gerrit.common.FormatUtil.elide(authorName(info), 20));
   }
 
   void autoOpen() {
     if (commentList == null) {
       autoOpen = true;
-      history.load(info._revisionNumber());
     } else if (!commentList.isEmpty()) {
       setOpen(true);
     }
@@ -209,7 +198,7 @@
       if (info.author().name() != null) {
         return info.author().name();
       }
-      return Gerrit.getConfig().getAnonymousCowardName();
+      return Gerrit.info().user().anonymousCowardName();
     }
     return Util.C.messageNoAuthor();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
index 42cb39b..e362c07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gerrit.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style type='com.google.gerrit.client.change.Message.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.Message.Style'>
     .messageBox {
       position: relative;
       width: 1168px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
index 3842ee6..5e2b8e3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
index 665eff5..1d76612 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
@@ -43,7 +43,6 @@
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
@@ -122,13 +121,12 @@
       RestApi call = ChangeApi.detail(changeId.get());
       ChangeList.addOptions(call, EnumSet.of(
           ListChangesOption.ALL_COMMITS,
-          ListChangesOption.ALL_REVISIONS,
-          ListChangesOption.DRAFT_COMMENTS));
+          ListChangesOption.ALL_REVISIONS));
       call.get(new AsyncCallback<ChangeInfo>() {
         @Override
         public void onSuccess(ChangeInfo result) {
           if (edit != null) {
-            edit.set_name(edit.commit().commit());
+            edit.setName(edit.commit().commit());
             result.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
           }
           render(result.revisions());
@@ -187,15 +185,6 @@
     if (r.draft()) {
       sb.append(Resources.C.draft()).append(' ');
     }
-    if (r.has_draft_comments()) {
-      sb.openSpan()
-        .addStyleName(style.draft_comment())
-        .setAttribute("title", Resources.C.draftCommentsTooltip())
-        .append(new ImageResourceRenderer()
-            .render(Gerrit.RESOURCES.draftComments()))
-        .closeSpan()
-        .append(' ');
-    }
     sb.append(r.id());
     sb.closeTd();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
index bd69cd6..7537aa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style type='com.google.gerrit.client.change.PatchSetsBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.PatchSetsBox.Style'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
 
     .revisionBox {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
index 7667559..2ca26ce 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JsArrayString;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
index 6638dbe..ebeb574 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
@@ -16,9 +16,10 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
@@ -41,12 +42,12 @@
   }
 
   void set(ChangeInfo info, String commit, ReplyAction action) {
-    if (!info.has_permitted_labels() || !info.status().isOpen()) {
+    if (!info.hasPermittedLabels() || !info.status().isOpen()) {
       // Quick approve needs at least one label on an open change.
       setVisible(false);
       return;
     }
-    if (info.revision(commit).is_edit() || info.revision(commit).draft()) {
+    if (info.revision(commit).isEdit() || info.revision(commit).draft()) {
       setVisible(false);
       return;
     }
@@ -55,52 +56,28 @@
     String qValueStr = null;
     short qValue = 0;
 
-    for (LabelInfo label : Natives.asList(info.all_labels().values())) {
-      if (!info.permitted_labels().containsKey(label.name())) {
-        continue;
-      }
-
-      JsArrayString values = info.permitted_values(label.name());
-      if (values.length() == 0) {
-        continue;
-      }
-
-      switch (label.status()) {
-        case NEED: // Label is required for submit.
-          break;
-
-        case OK: // Label already applied.
-        case MAY: // Label is not required.
-          continue;
-
-        case REJECT: // Submit cannot happen, do not quick approve.
-        case IMPOSSIBLE:
-          setVisible(false);
-          return;
-      }
-
+    int index = info.getMissingLabelIndex();
+    if (index != -1) {
+      LabelInfo label = Natives.asList(info.allLabels().values()).get(index);
+      JsArrayString values = info.permittedValues(label.name());
       String s = values.get(values.length() - 1);
       short v = LabelInfo.parseValue(s);
-      if (v > 0 && s.equals(label.max_value())) {
-        if (qName != null) {
-          // Quick approve is available for one label only.
-          setVisible(false);
-          return;
-        }
-
+      if (v > 0 && s.equals(label.maxValue())) {
         qName = label.name();
         qValueStr = s;
         qValue = v;
       }
     }
 
-    if (qName != null)  {
-      changeId = info.legacy_id();
+    if (qName != null) {
+      changeId = info.legacyId();
       revision = commit;
       input = ReviewInput.create();
+      input.drafts(DraftHandling.PUBLISH_ALL_REVISIONS);
       input.label(qName, qValue);
       replyAction = action;
       setText(qName + qValueStr);
+      setVisible(true);
     } else {
       setVisible(false);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
index 790198b..986bd78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.RebaseDialog;
 import com.google.gerrit.common.PageLinks;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 0c78a67..3be3486 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -16,11 +16,12 @@
 
 import static com.google.gerrit.common.PageLinks.op;
 
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
@@ -59,6 +60,8 @@
     String pointer();
     String row();
     String subject();
+    String strikedSubject();
+    String submittable();
     String tabPanel();
   }
 
@@ -76,6 +79,19 @@
       }
     },
 
+    SUBMITTED_TOGETHER(Resources.C.submittedTogether(),
+        Resources.C.submittedTogether()) {
+      @Override
+      String getTitle(int count) {
+        return Resources.M.submittedTogether(count);
+      }
+
+      @Override
+      String getTitle(String count) {
+        return Resources.M.submittedTogether(count);
+      }
+    },
+
     SAME_TOPIC(Resources.C.sameTopic(),
         Resources.C.sameTopicTooltip()) {
       @Override
@@ -171,6 +187,10 @@
     getTab(Tab.CHERRY_PICKS).setShowBranches(true);
     getTab(Tab.SAME_TOPIC).setShowBranches(true);
     getTab(Tab.SAME_TOPIC).setShowProjects(true);
+    getTab(Tab.SAME_TOPIC).setShowSubmittable(true);
+    getTab(Tab.SUBMITTED_TOGETHER).setShowBranches(true);
+    getTab(Tab.SUBMITTED_TOGETHER).setShowProjects(true);
+    getTab(Tab.SUBMITTED_TOGETHER).setShowSubmittable(true);
   }
 
   void set(final ChangeInfo info, final String revision) {
@@ -178,7 +198,7 @@
       setForOpenChange(info, revision);
     }
 
-    ChangeApi.revision(info.legacy_id().get(), revision).view("related")
+    ChangeApi.revision(info.legacyId().get(), revision).view("related")
         .get(new TabCallback<RelatedInfo>(Tab.RELATED_CHANGES, info.project(), revision) {
               @Override
               public JsArray<ChangeAndCommit> convert(RelatedInfo result) {
@@ -188,20 +208,28 @@
 
     StringBuilder cherryPicksQuery = new StringBuilder();
     cherryPicksQuery.append(op("project", info.project()));
-    cherryPicksQuery.append(" ").append(op("change", info.change_id()));
-    cherryPicksQuery.append(" ").append(op("-change", info.legacy_id().get()));
+    cherryPicksQuery.append(" ").append(op("change", info.changeId()));
+    cherryPicksQuery.append(" ").append(op("-change", info.legacyId().get()));
     cherryPicksQuery.append(" -is:abandoned");
     ChangeList.query(cherryPicksQuery.toString(),
         EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
         new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision));
 
-    if (info.topic() != null && !"".equals(info.topic())) {
+    // TODO(sbeller): show only on latest revision
+    ChangeApi.change(info.legacyId().get()).view("submitted_together")
+        .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+            info.project(), revision));
+
+    if (!Gerrit.info().change().isSubmitWholeTopicEnabled()
+        && info.topic() != null && !"".equals(info.topic())) {
       StringBuilder topicQuery = new StringBuilder();
       topicQuery.append("status:open");
       topicQuery.append(" ").append(op("topic", info.topic()));
-      topicQuery.append(" ").append(op("-change", info.legacy_id().get()));
       ChangeList.query(topicQuery.toString(),
-          EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
+          EnumSet.of(ListChangesOption.CURRENT_REVISION,
+                     ListChangesOption.CURRENT_COMMIT,
+                     ListChangesOption.DETAILED_LABELS,
+                     ListChangesOption.LABELS),
           new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
     }
   }
@@ -211,7 +239,7 @@
       StringBuilder conflictsQuery = new StringBuilder();
       conflictsQuery.append("status:open");
       conflictsQuery.append(" is:mergeable");
-      conflictsQuery.append(" ").append(op("conflicts", info.legacy_id().get()));
+      conflictsQuery.append(" ").append(op("conflicts", info.legacyId().get()));
       ChangeList.query(conflictsQuery.toString(),
           EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
           new TabChangeListCallback(Tab.CONFLICTING_CHANGES, info.project(), revision));
@@ -319,15 +347,17 @@
     protected JsArray<ChangeAndCommit> convert(ChangeList l) {
       JsArray<ChangeAndCommit> arr = JavaScriptObject.createArray().cast();
       for (ChangeInfo i : Natives.asList(l)) {
-        if (i.current_revision() != null && i.revisions().containsKey(i.current_revision())) {
-          RevisionInfo currentRevision = i.revision(i.current_revision());
+        if (i.currentRevision() != null && i.revisions().containsKey(i.currentRevision())) {
+          RevisionInfo currentRevision = i.revision(i.currentRevision());
           ChangeAndCommit c = ChangeAndCommit.create();
-          c.set_id(i.id());
-          c.set_commit(currentRevision.commit());
-          c.set_change_number(i.legacy_id().get());
-          c.set_revision_number(currentRevision._number());
-          c.set_branch(i.branch());
-          c.set_project(i.project());
+          c.setId(i.id());
+          c.setCommit(currentRevision.commit());
+          c.setChangeNumber(i.legacyId().get());
+          c.setRevisionNumber(currentRevision._number());
+          c.setBranch(i.branch());
+          c.setProject(i.project());
+          c.setSubmittable(i.submittable() && i.mergeable());
+          c.setStatus(i.status().asChangeStatus().toString());
           arr.push(c);
         }
       }
@@ -350,56 +380,68 @@
     public final native CommitInfo commit() /*-{ return this.commit }-*/;
     final native String branch() /*-{ return this.branch }-*/;
     final native String project() /*-{ return this.project }-*/;
+    final native boolean submittable() /*-{ return this._submittable ? true : false; }-*/;
+    final Change.Status status() {
+      String s = statusRaw();
+      return s != null ? Change.Status.valueOf(s) : null;
+    }
+    private final native String statusRaw() /*-{ return this.status; }-*/;
 
-    final native void set_id(String i)
+    final native void setId(String i)
     /*-{ if(i)this.change_id=i; }-*/;
 
-    final native void set_commit(CommitInfo c)
+    final native void setCommit(CommitInfo c)
     /*-{ if(c)this.commit=c; }-*/;
 
-    final native void set_branch(String b)
+    final native void setBranch(String b)
     /*-{ if(b)this.branch=b; }-*/;
 
-    final native void set_project(String b)
+    final native void setProject(String b)
     /*-{ if(b)this.project=b; }-*/;
 
-    public final Change.Id legacy_id() {
-      return has_change_number() ? new Change.Id(_change_number()) : null;
+    public final Change.Id legacyId() {
+      return hasChangeNumber() ? new Change.Id(_changeNumber()) : null;
     }
 
-    public final PatchSet.Id patch_set_id() {
-      return has_change_number() && has_revision_number()
-          ? new PatchSet.Id(legacy_id(), _revision_number())
+    public final PatchSet.Id patchSetId() {
+      return hasChangeNumber() && hasRevisionNumber()
+          ? new PatchSet.Id(legacyId(), _revisionNumber())
           : null;
     }
 
-    public final native boolean has_change_number()
+    public final native boolean hasChangeNumber()
     /*-{ return this.hasOwnProperty('_change_number') }-*/;
 
-    final native boolean has_revision_number()
+    final native boolean hasRevisionNumber()
     /*-{ return this.hasOwnProperty('_revision_number') }-*/;
 
-    final native boolean has_current_revision_number()
+    final native boolean hasCurrentRevisionNumber()
     /*-{ return this.hasOwnProperty('_current_revision_number') }-*/;
 
-    final native int _change_number()
+    final native int _changeNumber()
     /*-{ return this._change_number }-*/;
 
-    final native int _revision_number()
+    final native int _revisionNumber()
     /*-{ return this._revision_number }-*/;
 
-    final native int _current_revision_number()
+    final native int _currentRevisionNumber()
     /*-{ return this._current_revision_number }-*/;
 
-    final native void set_change_number(int n)
+    final native void setChangeNumber(int n)
     /*-{ this._change_number=n; }-*/;
 
-    final native void set_revision_number(int n)
+    final native void setRevisionNumber(int n)
     /*-{ this._revision_number=n; }-*/;
 
-    final native void set_current_revision_number(int n)
+    final native void setCurrentRevisionNumber(int n)
     /*-{ this._current_revision_number=n; }-*/;
 
+    final native void setSubmittable(boolean s)
+    /*-{ this._submittable=s; }-*/;
+
+    final native void setStatus(String s)
+    /*-{ if(s)this.status=s; }-*/;
+
     protected ChangeAndCommit() {
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index 96d0d10..23959e7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.change.RelatedChanges.ChangeAndCommit;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.GitwebInfo;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
@@ -68,7 +69,8 @@
       AbstractImagePrototype.create(Gerrit.RESOURCES.arrowRight()).getSafeHtml();
 
   private static final native String init(String o) /*-{
-    $wnd[o] = $entry(@com.google.gerrit.client.change.RelatedChangesTab::onOpen(Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/dom/client/Element;));
+    $wnd[o] = $entry(@com.google.gerrit.client.change.RelatedChangesTab::onOpen(
+      Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/dom/client/Element;));
     return o + '(event,this)';
   }-*/;
 
@@ -86,6 +88,7 @@
 
   private boolean showBranches;
   private boolean showProjects;
+  private boolean showSubmittable;
   private boolean showIndirectAncestors;
   private boolean registerKeys;
   private int maxHeight;
@@ -111,6 +114,10 @@
     this.showProjects = showProjects;
   }
 
+  void setShowSubmittable(boolean submittable) {
+    this.showSubmittable = submittable;
+  }
+
   void setShowIndirectAncestors(boolean showIndirectAncestors) {
     this.showIndirectAncestors = showIndirectAncestors;
   }
@@ -274,13 +281,18 @@
       sb.append(POINTER_HTML);
       sb.closeSpan();
 
-      sb.openSpan().setStyleName(RelatedChanges.R.css().subject());
+      if (info.status() == Change.Status.ABANDONED) {
+        sb.openSpan().setStyleName(RelatedChanges.R.css().strikedSubject());
+      } else {
+        sb.openSpan().setStyleName(RelatedChanges.R.css().subject());
+      }
       String url = url();
       if (url != null) {
         sb.openAnchor().setAttribute("href", url);
         if (url.startsWith("#")) {
           sb.setAttribute("onclick", OPEN);
         }
+        sb.setAttribute("title", info.commit().subject());
         if (showProjects) {
           sb.append(info.project()).append(": ");
         }
@@ -295,20 +307,28 @@
       sb.closeSpan();
 
       sb.openSpan();
-      GitwebLink gw = Gerrit.getGitwebLink();
-      if (gw != null && (!info.has_change_number() || !info.has_revision_number())) {
+      GitwebInfo gw = Gerrit.info().gitweb();
+      if (gw != null && (!info.hasChangeNumber() || !info.hasRevisionNumber())) {
         sb.setStyleName(RelatedChanges.R.css().gitweb());
         sb.setAttribute("title", gw.getLinkName());
-        sb.append('\u25CF');
+        sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
+      } else if (info.status() != null && !info.status().isOpen()) {
+        sb.setStyleName(RelatedChanges.R.css().gitweb());
+        sb.setAttribute("title", Util.toLongString(info.status()));
+        sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
       } else if (notConnected) {
         sb.setStyleName(RelatedChanges.R.css().indirect());
         sb.setAttribute("title", Resources.C.indirectAncestor());
         sb.append('~');
-      } else if (info.has_current_revision_number() && info.has_revision_number()
-          && info._current_revision_number() != info._revision_number()) {
+      } else if (info.hasCurrentRevisionNumber() && info.hasRevisionNumber()
+          && info._currentRevisionNumber() != info._revisionNumber()) {
         sb.setStyleName(RelatedChanges.R.css().notCurrent());
         sb.setAttribute("title", Util.C.notCurrent());
-        sb.append('\u25CF');
+        sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
+      } else if (showSubmittable && info.submittable()) {
+        sb.setStyleName(RelatedChanges.R.css().submittable());
+        sb.setAttribute("title", Util.C.submittable());
+        sb.append('\u2713'); // Unicode 'CHECK MARK'
       } else {
         sb.setStyleName(RelatedChanges.R.css().current());
       }
@@ -318,14 +338,14 @@
     }
 
     private String url() {
-      if (info.has_change_number() && info.has_revision_number()) {
-        PatchSet.Id id = info.patch_set_id();
+      if (info.hasChangeNumber() && info.hasRevisionNumber()) {
+        PatchSet.Id id = info.patchSetId();
         return "#" + PageLinks.toChange(
             id.getParentKey(),
             id.getId());
       }
 
-      GitwebLink gw = Gerrit.getGitwebLink();
+      GitwebInfo gw = Gerrit.info().gitweb();
       if (gw != null && project != null) {
         return gw.toRevision(project, info.commit().commit());
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
index 17b218d..9783cf4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
@@ -1,20 +1,20 @@
-//Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2015 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
+// 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
+// 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.
+// 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.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
index 77348f7..d0d8b488 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
@@ -1,23 +1,23 @@
-//Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2015 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
+// 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
+// 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.
+// 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.client.change;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
index 27849ee..17e8797 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
@@ -20,7 +20,7 @@
     xmlns:u='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
index 29622d5..7882eec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -32,6 +32,7 @@
 class ReplyAction {
   private final PatchSet.Id psId;
   private final String revision;
+  private final boolean hasDraftComments;
   private final ChangeScreen.Style style;
   private final CommentLinkProcessor clp;
   private final Widget replyButton;
@@ -46,23 +47,25 @@
   ReplyAction(
       ChangeInfo info,
       String revision,
+      boolean hasDraftComments,
       ChangeScreen.Style style,
       CommentLinkProcessor clp,
       Widget replyButton,
       Widget quickApproveButton) {
     this.psId = new PatchSet.Id(
-        info.legacy_id(),
+        info.legacyId(),
         info.revisions().get(revision)._number());
     this.revision = revision;
+    this.hasDraftComments = hasDraftComments;
     this.style = style;
     this.clp = clp;
     this.replyButton = replyButton;
     this.quickApproveButton = quickApproveButton;
 
-    boolean current = revision.equals(info.current_revision());
-    allLabels = info.all_labels();
-    permittedLabels = current && info.has_permitted_labels()
-        ? info.permitted_labels()
+    boolean current = revision.equals(info.currentRevision());
+    allLabels = info.allLabels();
+    permittedLabels = current && info.hasPermittedLabels()
+        ? info.permittedLabels()
         : NativeMap.<JsArrayString> create();
   }
 
@@ -110,11 +113,15 @@
       public void onClose(CloseEvent<PopupPanel> event) {
         if (popup == p) {
           popup = null;
+          if (hasDraftComments || replyBox.hasMessage()) {
+            replyButton.setStyleName(style.highlight());
+          }
         }
       }
     });
     p.add(replyBox);
     Window.scrollTo(0, 0);
+    replyButton.removeStyleName(style.highlight());
     p.showRelativeTo(replyButton);
     GlobalKey.dialog(p);
     popup = p;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index f9054fe..2ec4b6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -18,13 +18,13 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
@@ -140,25 +140,26 @@
   protected void onLoad() {
     commentsPanel.setVisible(false);
     post.setEnabled(false);
-    CommentApi.drafts(psId, new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-      @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        attachComments(result);
-        displayComments(result);
-        post.setEnabled(true);
-      }
+    ChangeApi.drafts(psId.getParentKey().get())
+        .get(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+          @Override
+          public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+            displayComments(result);
+            post.setEnabled(true);
+          }
 
-      @Override
-      public void onFailure(Throwable caught) {
-        post.setEnabled(true);
-      }
-    });
+          @Override
+          public void onFailure(Throwable caught) {
+            post.setEnabled(true);
+          }
+        });
 
     Scheduler.get().scheduleDeferred(new ScheduledCommand() {
       @Override
       public void execute() {
         message.setFocus(true);
-      }});
+      }
+    });
     Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
       @Override
       public boolean execute() {
@@ -167,7 +168,8 @@
           message.setCursorPos(t.length());
         }
         return false;
-      }}, 0);
+      }
+    }, 0);
   }
 
   @UiHandler("post")
@@ -180,8 +182,15 @@
     postReview();
   }
 
+  boolean hasMessage() {
+    return !message.getText().trim().isEmpty();
+  }
+
   private void postReview() {
     in.message(message.getText().trim());
+    // Don't send any comments in the request; just publish everything, even if
+    // e.g. a draft was modified in another tab since we last looked it up.
+    in.drafts(DraftHandling.PUBLISH_ALL_REVISIONS);
     in.prePost();
     ChangeApi.revision(psId.getParentKey().get(), revision)
       .view("review")
@@ -284,7 +293,7 @@
     List<LabelAndValues> checkboxes = new ArrayList<>(labels.size());
     int row = 1;
     for (LabelAndValues lv : labels) {
-      if (isCheckBox(lv.info.value_set())) {
+      if (isCheckBox(lv.info.valueSet())) {
         checkboxes.add(lv);
       } else {
         renderRadio(row++, columns, lv);
@@ -321,7 +330,7 @@
     fmt.setStyleName(row, labelHelpColumn, style.label_help());
 
     ApprovalInfo self = Gerrit.isSignedIn()
-        ? lv.info.for_user(Gerrit.getUserAccount().getId().get())
+        ? lv.info.forUser(Gerrit.getUserAccount().getId().get())
         : null;
 
     final LabelRadioGroup group =
@@ -329,7 +338,7 @@
     for (int i = 0; i < columns.size(); i++) {
       Short v = columns.get(i);
       if (lv.permitted.contains(v)) {
-        String text = lv.info.value_text(LabelValue.formatValue(v));
+        String text = lv.info.valueText(LabelValue.formatValue(v));
         LabelRadioButton b = new LabelRadioButton(group, text, v);
         if ((self != null && v == self.value()) || (self == null && v.equals(dv))) {
           b.setValue(true);
@@ -345,7 +354,7 @@
 
   private void renderCheckBox(int row, LabelAndValues lv) {
     ApprovalInfo self = Gerrit.isSignedIn()
-        ? lv.info.for_user(Gerrit.getUserAccount().getId().get())
+        ? lv.info.forUser(Gerrit.getUserAccount().getId().get())
         : null;
 
     final String id = lv.info.name();
@@ -366,7 +375,7 @@
 
     CellFormatter fmt = labelsTable.getCellFormatter();
     fmt.setStyleName(row, labelHelpColumn, style.label_help());
-    labelsTable.setText(row, labelHelpColumn, lv.info.value_text("+1"));
+    labelsTable.setText(row, labelHelpColumn, lv.info.valueText("+1"));
   }
 
   private static boolean isCheckBox(Set<Short> values) {
@@ -375,11 +384,6 @@
         && values.contains((short) 1);
   }
 
-  private void attachComments(NativeMap<JsArray<CommentInfo>> result) {
-    in.drafts(ReviewInput.DraftHandling.KEEP);
-    in.comments(result);
-  }
-
   private void displayComments(NativeMap<JsArray<CommentInfo>> m) {
     comments.clear();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
index a17d648..52f6b6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style type='com.google.gerrit.client.change.ReplyBox.Styles'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.ReplyBox.Styles'>
     .replyBox {
     }
     .label_name {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
index 5ec292b..253c9a7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
index 47d6cad..ae1b5d1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.TextAreaActionDialog;
 import com.google.gerrit.common.PageLinks;
@@ -48,7 +48,7 @@
               public void onSuccess(ChangeInfo result) {
                 sent = true;
                 hide();
-                Gerrit.display(PageLinks.toChange(result.legacy_id()));
+                Gerrit.display(PageLinks.toChange(result.legacyId()));
               }
 
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
index 7af247a..1c0486d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.admin.Util;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.groups.GroupBaseInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index f69f406..3ba7ea5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -17,12 +17,12 @@
 import com.google.gerrit.client.ConfirmationCallback;
 import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
@@ -100,7 +100,7 @@
   }
 
   void set(ChangeInfo info) {
-    this.changeId = info.legacy_id();
+    this.changeId = info.legacyId();
     display(info);
     reviewerSuggestOracle.setChange(changeId);
     openForm.setVisible(Gerrit.isSignedIn());
@@ -125,7 +125,7 @@
 
   @UiHandler("addMe")
   void onAddMe(@SuppressWarnings("unused") ClickEvent e) {
-    String accountId = String.valueOf(Gerrit.getUserAccountInfo()._account_id());
+    String accountId = String.valueOf(Gerrit.getUserAccount()._accountId());
     addReviewer(accountId, false);
   }
 
@@ -199,22 +199,22 @@
   private void display(ChangeInfo info) {
     Map<Integer, AccountInfo> r = new HashMap<>();
     Map<Integer, AccountInfo> cc = new HashMap<>();
-    for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+    for (LabelInfo label : Natives.asList(info.allLabels().values())) {
       if (label.all() != null) {
         for (ApprovalInfo ai : Natives.asList(label.all())) {
-          (ai.value() != 0 ? r : cc).put(ai._account_id(), ai);
+          (ai.value() != 0 ? r : cc).put(ai._accountId(), ai);
         }
       }
     }
     for (Integer i : r.keySet()) {
       cc.remove(i);
     }
-    cc.remove(info.owner()._account_id());
+    cc.remove(info.owner()._accountId());
 
     Set<Integer> removable = new HashSet<>();
-    if (info.removable_reviewers() != null) {
-      for (AccountInfo a : Natives.asList(info.removable_reviewers())) {
-        removable.add(a._account_id());
+    if (info.removableReviewers() != null) {
+      for (AccountInfo a : Natives.asList(info.removableReviewers())) {
+        removable.add(a._accountId());
       }
     }
 
@@ -228,8 +228,8 @@
     reviewersText.setInnerSafeHtml(rHtml);
     ccText.setInnerSafeHtml(ccHtml);
     if (Gerrit.isSignedIn()) {
-      int currentUser = Gerrit.getUserAccountInfo()._account_id();
-      boolean showAddMeButton = info.owner()._account_id() != currentUser
+      int currentUser = Gerrit.getUserAccount()._accountId();
+      boolean showAddMeButton = info.owner()._accountId() != currentUser
           && !cc.containsKey(currentUser)
           && !r.containsKey(currentUser);
       addMe.setVisible(showAddMeButton);
@@ -242,13 +242,13 @@
       LabelInfo label = change.label(name);
       if (label.all() != null) {
         for (ApprovalInfo ai : Natives.asList(label.all())) {
-          int id = ai._account_id();
+          int id = ai._accountId();
           VotableInfo ad = d.get(id);
           if (ad == null) {
             ad = new VotableInfo();
             d.put(id, ad);
           }
-          if (ai.has_value()) {
+          if (ai.hasValue()) {
             ad.votable(name);
           }
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
index 9924c1d..22e35e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -20,7 +20,7 @@
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:u='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     button.openAdd {
       margin: 3px 3px 0 0;
       float: right;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
index 09d3476..ad1043a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.api.ChangeGlue;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.SubmitInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,7 +27,7 @@
 class SubmitAction {
   static void call(ChangeInfo changeInfo, RevisionInfo revisionInfo) {
     if (ChangeGlue.onSubmitChange(changeInfo, revisionInfo)) {
-      final Change.Id changeId = changeInfo.legacy_id();
+      final Change.Id changeId = changeInfo.legacyId();
       ChangeApi.submit(
         changeId.get(), revisionInfo.name(),
         new GerritCallback<SubmitInfo>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
index 9f45678..bd9bec3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.common.PageLinks;
@@ -68,12 +68,10 @@
   }
 
   void set(ChangeInfo info, String revision) {
-    canEdit = info.has_actions()
-        && info.actions().containsKey("topic")
-        && info.actions().get("topic").enabled();
+    canEdit = info.hasActions() && info.actions().containsKey("topic");
 
     psId = new PatchSet.Id(
-        info.legacy_id(),
+        info.legacyId(),
         info.revisions().get(revision)._number());
 
     initTopicLink(info);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
index ae46b10..e7e24b4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
@@ -21,7 +21,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .show { cursor: pointer; }
     .edit, .cancel { float: right; }
   </ui:style>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
index cdbc693..c14e628 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.info.ChangeInfo.MessageInfo;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -48,7 +48,7 @@
     HashSet<Integer> seen = new HashSet<>();
     StringBuilder r = new StringBuilder();
     for (MessageInfo m : newMessages) {
-      int a = m.author() != null ? m.author()._account_id() : 0;
+      int a = m.author() != null ? m.author()._accountId() : 0;
       if (seen.add(a)) {
         if (r.length() > 0) {
           r.append(", ");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
index 1c46b8c..1d5592b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     .popup {
       position: fixed;
       bottom: 0;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
index 5820a50..b1c9761 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.ui.UserActivityMonitor;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
@@ -26,7 +26,7 @@
   private static final int MAX_PERIOD = 3 * 60 * 1000;
   private static final int IDLE_PERIOD = 2 * 3600 * 1000;
   private static final int POLL_PERIOD =
-      Gerrit.getConfig().getChangeUpdateDelay() * 1000;
+      Gerrit.info().change().updateDelay() * 1000;
 
   private final ChangeScreen screen;
   private int delay;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index 2803db3..bde9755 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -13,14 +13,14 @@
  * limitations under the License.
  */
 
-.pointer, .reviewed, .removeButton {
+.pointer, .reviewed, .restoreDelete {
   padding: 0px;
   vertical-align: top;
 }
 .pointer {
   width: 12px;
 }
-.reviewed, .removeButton {
+.reviewed {
   height: 19px;
   width: 20px;
 }
@@ -75,7 +75,7 @@
 
 .deltaColumn1 {
   white-space: nowrap;
-  text-align: right;
+  text-align: right !important;
 }
 
 .deltaColumn2 {
@@ -96,7 +96,11 @@
   background-color: #d44;
 }
 
-.removeButton button {
+.restoreDelete div {
+  white-space: nowrap;
+}
+
+.restoreDelete button {
   cursor: pointer;
   padding: 0;
   margin: 0 0 0 5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
index 2e62b98..5e0e402 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
@@ -38,13 +38,16 @@
   visibility: hidden;
 }
 
-.subject {
+.subject, .strikedSubject {
   display: inline-block;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   width: 355px;
 }
+.strikedSubject {
+  text-decoration: line-through;
+}
 
 .tabPanel .gwt-TabBarItem,
 .tabPanel .gwt-TabBarItem-selected,
@@ -68,7 +71,8 @@
 .current,
 .gitweb,
 .indirect,
-.notCurrent {
+.notCurrent,
+.submittable {
   display: inline-block;
   text-align: center;
   vertical-align: top;
@@ -87,3 +91,8 @@
 .notCurrent {
   color: #FFA62F;   /* orange */
 }
+
+.submittable {
+  color: #090;      /* green */
+  font-weight: bold;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index d9e9878..f53ecf8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -16,9 +16,12 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.JsArray;
@@ -28,8 +31,18 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
+import java.util.Set;
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
+  // If changing default options, also update in
+  // ChangeIT#defaultSearchDoesNotTouchDatabase().
+  private static final Set<ListChangesOption> MY_DASHBOARD_OPTIONS;
+  static {
+    EnumSet<ListChangesOption> options = EnumSet.copyOf(ChangeTable.OPTIONS);
+    options.add(ListChangesOption.REVIEWED);
+    MY_DASHBOARD_OPTIONS = Collections.unmodifiableSet(options);
+  }
+
   private final Account.Id ownerId;
   private final boolean mine;
   private ChangeTable table;
@@ -61,10 +74,14 @@
     incoming = new ChangeTable.Section();
     closed = new ChangeTable.Section();
 
-    outgoing.setTitleText(Util.C.outgoingReviews());
-    incoming.setTitleText(Util.C.incomingReviews());
+    String who = mine ? "self" : ownerId.toString();
+    outgoing.setTitleWidget(new InlineHyperlink(Util.C.outgoingReviews(),
+        PageLinks.toChangeQuery(queryOutgoing(who))));
+    incoming.setTitleWidget(new InlineHyperlink(Util.C.incomingReviews(),
+        PageLinks.toChangeQuery(queryIncoming(who))));
     incoming.setHighlightUnreviewed(mine);
-    closed.setTitleText(Util.C.recentlyClosed());
+    closed.setTitleWidget(new InlineHyperlink(Util.C.recentlyClosed(),
+        PageLinks.toChangeQuery(queryClosed(who))));
 
     table.addSection(outgoing);
     table.addSection(incoming);
@@ -73,24 +90,34 @@
     table.setSavePointerId("owner:" + ownerId);
   }
 
+  private static String queryOutgoing(String who) {
+    return "is:open owner:" + who;
+  }
+
+  private static String queryIncoming(String who) {
+    return "is:open reviewer:" + who + " -owner:" + who;
+  }
+
+  private static String queryClosed(String who) {
+    return "is:closed (owner:" + who + " OR reviewer:" + who + ")";
+  }
+
   @Override
   protected void onLoad() {
     super.onLoad();
 
     String who = mine ? "self" : ownerId.toString();
-    ChangeList.query(
+    ChangeList.queryMultiple(
         new ScreenLoadCallback<JsArray<ChangeList>>(this) {
           @Override
           protected void preDisplay(JsArray<ChangeList> result) {
             display(result);
           }
         },
-        mine
-          ? EnumSet.of(ListChangesOption.REVIEWED)
-          : EnumSet.noneOf(ListChangesOption.class),
-        "is:open owner:" + who,
-        "is:open reviewer:" + who + " -owner:" + who,
-        "is:closed (owner:" + who + " OR reviewer:" + who + ") -age:4w limit:10");
+        mine ? MY_DASHBOARD_OPTIONS : DashboardTable.OPTIONS,
+        queryOutgoing(who),
+        queryIncoming(who),
+        queryClosed(who) + " -age:4w limit:10");
   }
 
   @Override
@@ -144,7 +171,9 @@
       @Override
       public int compare(ChangeInfo a, ChangeInfo b) {
         int cmp = a.created().compareTo(b.created());
-        if (cmp != 0) return cmp;
+        if (cmp != 0) {
+          return cmp;
+        }
         return a._number() - b._number();
       }
     };
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 98595e7..9cafeec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.IncludedInInfo;
 import com.google.gerrit.client.rpc.CallbackGroup.Callback;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
@@ -45,15 +46,16 @@
    * new change is created as NEW.
    *
    */
-  public static void createChange(String project, String branch,
+  public static void createChange(String project, String branch, String topic,
       String subject, String base, AsyncCallback<ChangeInfo> cb) {
     CreateChangeInput input = CreateChangeInput.create();
     input.project(emptyToNull(project));
     input.branch(emptyToNull(branch));
+    input.topic(emptyToNull(topic));
     input.subject(emptyToNull(subject));
-    input.base_change(emptyToNull(base));
+    input.baseChange(emptyToNull(base));
 
-    if (Gerrit.getConfig().isAllowDraftChanges()) {
+    if (Gerrit.info().change().allowDrafts()) {
       input.status(Change.Status.DRAFT.toString());
     }
 
@@ -95,6 +97,21 @@
     return call(id, "detail");
   }
 
+  public static RestApi actions(int id, String revision) {
+    if (revision == null || revision.equals("")) {
+      revision = "current";
+    }
+    return call(id, revision, "actions");
+  }
+
+  public static RestApi comments(int id) {
+    return call(id, "comments");
+  }
+
+  public static RestApi drafts(int id) {
+    return call(id, "drafts");
+  }
+
   public static void edit(int id, AsyncCallback<EditInfo> cb) {
     edit(id).get(cb);
   }
@@ -172,7 +189,7 @@
   /** Submit a specific revision of a change. */
   public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
     SubmitInput in = SubmitInput.create();
-    in.wait_for_merge(true);
+    in.waitForMerge(true);
     call(id, commit, "submit").post(in, cb);
   }
 
@@ -234,9 +251,10 @@
     }
 
     public final native void branch(String b) /*-{ if(b)this.branch=b; }-*/;
+    public final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
     public final native void project(String p) /*-{ if(p)this.project=p; }-*/;
     public final native void subject(String s) /*-{ if(s)this.subject=s; }-*/;
-    public final native void base_change(String b) /*-{ if(b)this.base_change=b; }-*/;
+    public final native void baseChange(String b) /*-{ if(b)this.base_change=b; }-*/;
     public final native void status(String s)  /*-{ if(s)this.status=s; }-*/;
 
     protected CreateChangeInput() {
@@ -266,7 +284,7 @@
   }
 
   private static class SubmitInput extends JavaScriptObject {
-    final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
+    final native void waitForMerge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
 
     static SubmitInput create() {
       return (SubmitInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 6531129..1fb997f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -18,10 +18,10 @@
 
 public interface ChangeConstants extends Constants {
   String statusLongNew();
-  String statusLongSubmitted();
   String statusLongMerged();
   String statusLongAbandoned();
   String statusLongDraft();
+  String submittable();
   String readyToSubmit();
   String mergeConflict();
   String notCurrent();
@@ -66,7 +66,6 @@
   String keyReloadSearch();
   String keyPublishComments();
   String keyEditTopic();
-  String keyEditMessage();
   String keyAddReviewers();
   String keyExpandAllMessages();
   String keyCollapseAllMessages();
@@ -154,10 +153,6 @@
   String headingRevertMessage();
   String revertChangeTitle();
 
-  String headingEditCommitMessage();
-  String editCommitMessageToolTip();
-  String titleEditCommitMessage();
-
   String buttonCherryPickChangeBegin();
   String buttonCherryPickChangeSend();
   String headingCherryPickBranch();
@@ -206,19 +201,10 @@
   String diffAllSideBySide();
   String diffAllUnified();
 
-  String inTheFuture();
-  String month();
-  String months();
-  String year();
-  String years();
-
-  String oneSecondAgo();
-  String oneMinuteAgo();
-  String oneHourAgo();
-  String oneDayAgo();
-  String oneWeekAgo();
-  String oneMonthAgo();
-  String oneYearAgo();
-
   String votable();
+
+  String pushCertMissing();
+  String pushCertBad();
+  String pushCertOk();
+  String pushCertTrusted();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 45415c5..a5fa7b4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -1,8 +1,8 @@
 statusLongNew = Review in Progress
-statusLongSubmitted = Submitted, Merge Pending
 statusLongMerged = Merged
 statusLongAbandoned = Abandoned
 statusLongDraft = Draft
+submittable = Submittable
 readyToSubmit = Ready to Submit
 mergeConflict = Merge Conflict
 notCurrent = Not Current
@@ -46,7 +46,6 @@
 keyReloadSearch = Reload change list
 keyPublishComments = Review and publish comments
 keyEditTopic = Edit change topic
-keyEditMessage = Edit commit message
 keyAddReviewers = Add reviewers
 keyExpandAllMessages = Expand all messages
 keyCollapseAllMessages = Collapse all messages
@@ -140,10 +139,6 @@
 headingRevertMessage = Revert Commit Message:
 revertChangeTitle = Code Review - Revert Merged Change
 
-headingEditCommitMessage = Commit Message
-editCommitMessageToolTip = Edit Commit Message
-titleEditCommitMessage = Create New Patch Set
-
 buttonCherryPickChangeBegin = Cherry Pick To
 buttonCherryPickChangeSend = Cherry Pick Change
 headingCherryPickBranch = Cherry Pick to Branch:
@@ -188,18 +183,9 @@
 diffAllSideBySide = All Side-by-Side
 diffAllUnified = All Unified
 
-inTheFuture = in the future
-month = month
-months = months
-years = years
-year = year
-
-oneSecondAgo = 1 second ago
-oneMinuteAgo = 1 minute ago
-oneHourAgo = 1 hour ago
-oneDayAgo = 1 day ago
-oneWeekAgo = 1 week ago
-oneMonthAgo = 1 month ago
-oneYearAgo = 1 year ago
-
 votable = Votable:
+
+pushCertMissing = This patch set was created without a push certificate
+pushCertBad = Push certificate is invalid
+pushCertOk = Push certificate is valid, but key is not trusted
+pushCertTrusted = Push certificate is valid and key is trusted
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
index a00e329..0928cd8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
@@ -76,15 +76,15 @@
   public static void rename(int id, String path, String newPath,
       AsyncCallback<VoidResult> cb) {
     Input in = Input.create();
-    in.old_path(path);
-    in.new_path(newPath);
+    in.oldPath(path);
+    in.newPath(newPath);
     ChangeApi.edit(id).post(in, cb);
   }
 
   /** Restore (undo delete/modify) a file in the pending edit. */
   public static void restore(int id, String path, AsyncCallback<VoidResult> cb) {
     Input in = Input.create();
-    in.restore_path(path);
+    in.restorePath(path);
     ChangeApi.edit(id).post(in, cb);
   }
 
@@ -101,9 +101,9 @@
       return createObject().cast();
     }
 
-    final native void restore_path(String p) /*-{ this.restore_path=p }-*/;
-    final native void old_path(String p) /*-{ this.old_path=p }-*/;
-    final native void new_path(String p) /*-{ this.new_path=p }-*/;
+    final native void restorePath(String p) /*-{ this.restore_path=p }-*/;
+    final native void oldPath(String p) /*-{ this.old_path=p }-*/;
+    final native void newPath(String p) /*-{ this.new_path=p }-*/;
 
     protected Input() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index b1866dd..cea9142 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -14,58 +14,75 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
-import java.util.EnumSet;
+import java.util.Set;
 
 /** List of changes available from {@code /changes/}. */
 public class ChangeList extends JsArray<ChangeInfo> {
   private static final String URI = "/changes/";
-  private static final EnumSet<ListChangesOption> OPTIONS = EnumSet.of(
-      ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS);
 
-  /** Run 2 or more queries in a single remote invocation. */
-  public static void query(
-      AsyncCallback<JsArray<ChangeList>> callback,
-      EnumSet<ListChangesOption> options,
+  /** Run multiple queries in a single remote invocation. */
+  public static void queryMultiple(
+      final AsyncCallback<JsArray<ChangeList>> callback,
+      Set<ListChangesOption> options,
       String... queries) {
-    assert queries.length >= 2; // At least 2 is required for correct result.
+    if (queries.length == 0) {
+      return;
+    }
     RestApi call = new RestApi(URI);
     for (String q : queries) {
       call.addParameterRaw("q", KeyUtil.encode(q));
     }
-    OPTIONS.addAll(options);
-    addOptions(call, OPTIONS);
-    call.get(callback);
+    addOptions(call, options);
+    if (queries.length == 1) {
+      // Server unwraps a single query, so wrap it back in an array for the
+      // callback.
+      call.get(new AsyncCallback<ChangeList>() {
+        @Override
+        public void onSuccess(ChangeList result) {
+          JsArray<ChangeList> wrapped = JsArray.createArray(1).cast();
+          wrapped.set(0, result);
+          callback.onSuccess(wrapped);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          callback.onFailure(caught);
+        }
+      });
+    } else {
+      call.get(callback);
+    }
   }
 
   public static void query(String query,
-      EnumSet<ListChangesOption> options,
+      Set<ListChangesOption> options,
       AsyncCallback<ChangeList> callback) {
-    RestApi call = newQuery(query);
-    addOptions(call, options);
-    call.get(callback);
+    query(query, options, callback, 0, 0);
   }
 
-  public static void next(String query,
-      int start, int limit,
-      AsyncCallback<ChangeList> callback) {
+  public static void query(String query,
+      Set<ListChangesOption> options,
+      AsyncCallback<ChangeList> callback,
+      int start, int limit) {
     RestApi call = newQuery(query);
     if (limit > 0) {
       call.addParameter("n", limit);
     }
-    addOptions(call, OPTIONS);
+    addOptions(call, options);
     if (start != 0) {
       call.addParameter("S", start);
     }
     call.get(callback);
   }
 
-  public static void addOptions(RestApi call, EnumSet<ListChangesOption> s) {
+  public static void addOptions(RestApi call, Set<ListChangesOption> s) {
     call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 76e3211..ef74a65 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -34,6 +34,8 @@
   String patchTableComments(@PluralCount int count);
   String patchTableDrafts(@PluralCount int count);
   String patchTableSize_Modify(int insertions, int deletions);
+  String patchTableSize_ModifyBinaryFiles(String bytesInserted,
+      String bytesDeleted);
   String patchTableSize_LongModify(int insertions, int deletions);
   String patchTableSize_Lines(@PluralCount int insertions);
 
@@ -60,16 +62,5 @@
   String groupHasTooManyMembers(String group);
   String groupManyMembersConfirmation(String group, int memberCount);
 
-  String secondsAgo(long seconds);
-  String minutesAgo(long minutes);
-  String hoursAgo(long hours);
-  String daysAgo(long days);
-  String weeksAgo(long weeks);
-  String monthsAgo(long months);
-  String yearsAgo(long years);
-  String years0MonthsAgo(long years, String yearLabel);
-  String yearsMonthsAgo(long years, String yearLabel, long months,
-      String monthLabel);
-
   String insertionsAndDeletions(int insertions, int deletions);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 7069c4a..67ef2c3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -17,6 +17,7 @@
 patchTableComments = {0} comments
 patchTableDrafts = {0} drafts
 patchTableSize_Modify = +{0}, -{1}
+patchTableSize_ModifyBinaryFiles = +{0}, -{1}
 patchTableSize_LongModify = {0} inserted, {1} deleted
 patchTableSize_Lines = {0} lines
 
@@ -43,14 +44,4 @@
 groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
 groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
 
-secondsAgo = {0} seconds ago
-minutesAgo = {0} minutes ago
-hoursAgo = {0} hours ago
-daysAgo = {0} days ago
-weeksAgo = {0} weeks ago
-monthsAgo = {0} months ago
-years0MonthsAgo = {0} {1} ago
-yearsMonthsAgo = {0} {1}, {2} {3} ago
-yearsAgo = {0} years ago
-
 insertionsAndDeletions = +{0}, -{1}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 8b71448..6136825 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -18,8 +18,9 @@
 import static com.google.gerrit.client.FormatUtil.shortFormat;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -27,6 +28,7 @@
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.dom.client.Element;
@@ -45,9 +47,17 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.Set;
 
 public class ChangeTable extends NavigationTable<ChangeInfo> {
+  // If changing default options, also update in
+  // ChangeIT#defaultSearchDoesNotTouchDatabase().
+  static final Set<ListChangesOption> OPTIONS =
+      Collections.unmodifiableSet(EnumSet.of(
+          ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS));
+
   private static final int C_STAR = 1;
   private static final int C_ID = 2;
   private static final int C_SUBJECT = 3;
@@ -61,19 +71,17 @@
 
   private final List<Section> sections;
   private int columns;
-  private boolean showLegacyId;
+  private final boolean showLegacyId;
   private List<String> labelNames;
 
   public ChangeTable() {
     super(Util.C.changeItemHelp());
     columns = BASE_COLUMNS;
     labelNames = Collections.emptyList();
+    showLegacyId = Gerrit.getUserPreferences().legacycidInChangeTable();
 
     if (Gerrit.isSignedIn()) {
       keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
-      showLegacyId = Gerrit.getUserAccount()
-          .getGeneralPreferences()
-          .isLegacycidInChangeTable();
     }
 
     sections = new ArrayList<>();
@@ -118,13 +126,13 @@
 
   @Override
   protected Object getRowItemKey(final ChangeInfo item) {
-    return item.legacy_id();
+    return item.legacyId();
   }
 
   @Override
   protected void onOpenRow(final int row) {
     final ChangeInfo c = getRowItem(row);
-    final Change.Id id = c.legacy_id();
+    final Change.Id id = c.legacyId();
     Gerrit.display(PageLinks.toChange(id));
   }
 
@@ -208,10 +216,10 @@
     CellFormatter fmt = table.getCellFormatter();
     if (Gerrit.isSignedIn()) {
       table.setWidget(row, C_STAR, StarredChanges.createIcon(
-          c.legacy_id(),
+          c.legacyId(),
           c.starred()));
     }
-    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacy_id()), c));
+    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacyId()), c));
 
     String subject = Util.cropSubject(c.subject());
     table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
@@ -229,21 +237,17 @@
       table.setText(row, C_OWNER, "");
     }
 
-    table.setWidget(row, C_PROJECT, new ProjectLink(c.project_name_key()));
-    table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
+    table.setWidget(row, C_PROJECT, new ProjectLink(c.projectNameKey()));
+    table.setWidget(row, C_BRANCH, new BranchLink(c.projectNameKey(), c
         .status(), c.branch(), c.topic()));
-    if (Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getGeneralPreferences()
-            .isRelativeDateInChangeTable()) {
+    if (Gerrit.getUserPreferences().relativeDateInChangeTable()) {
       table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
     } else {
       table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
     }
 
     int col = C_SIZE;
-    if (Gerrit.isSignedIn()
-        && !Gerrit.getUserAccount().getGeneralPreferences()
-            .isSizeBarInChangeTable()) {
+    if (!Gerrit.getUserPreferences().sizeBarInChangeTable()) {
       table.setText(row, col,
           Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
     } else {
@@ -265,10 +269,8 @@
 
       String user;
       String info;
-      ReviewCategoryStrategy reviewCategoryStrategy = Gerrit.isSignedIn()
-          ? Gerrit.getUserAccount().getGeneralPreferences()
-                .getReviewCategoryStrategy()
-          : ReviewCategoryStrategy.NONE;
+      ReviewCategoryStrategy reviewCategoryStrategy =
+          Gerrit.getUserPreferences().reviewCategoryStrategy();
       if (label.rejected() != null) {
         user = label.rejected().name();
         info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
@@ -364,7 +366,7 @@
   }
 
   private static Widget getSizeWidget(ChangeInfo c) {
-    int largeChangeSize = Gerrit.getConfig().getLargeChangeSize();
+    int largeChangeSize = Gerrit.info().change().largeChange();
     int changedLines = c.insertions() + c.deletions();
     int p = 100;
     if (changedLines < largeChangeSize) {
@@ -447,7 +449,7 @@
 
   private final class TableChangeLink extends ChangeLink {
     private TableChangeLink(final String text, final ChangeInfo c) {
-      super(text, c.legacy_id());
+      super(text, c.legacyId());
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 52e5d40..9088c1c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.diff.CommentRange;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
@@ -29,7 +29,7 @@
     n.path(path);
     n.side(side);
     if (range != null) {
-      n.line(range.end_line());
+      n.line(range.endLine());
       n.range(range);
     } else if (line > 0) {
       n.line(line);
@@ -41,11 +41,11 @@
     CommentInfo n = createObject().cast();
     n.path(r.path());
     n.side(r.side());
-    n.in_reply_to(r.id());
-    if (r.has_range()) {
-      n.line(r.range().end_line());
+    n.inReplyTo(r.id());
+    if (r.hasRange()) {
+      n.line(r.range().endLine());
       n.range(r.range());
-    } else if (r.has_line()) {
+    } else if (r.hasLine()) {
       n.line(r.line());
     }
     return n;
@@ -56,12 +56,12 @@
     n.path(s.path());
     n.side(s.side());
     n.id(s.id());
-    n.in_reply_to(s.in_reply_to());
+    n.inReplyTo(s.inReplyTo());
     n.message(s.message());
-    if (s.has_range()) {
-      n.line(s.range().end_line());
+    if (s.hasRange()) {
+      n.line(s.range().endLine());
       n.range(s.range());
-    } else if (s.has_line()) {
+    } else if (s.hasLine()) {
       n.line(s.line());
     }
     return n;
@@ -71,7 +71,7 @@
   public final native void id(String i) /*-{ this.id = i }-*/;
   public final native void line(int n) /*-{ this.line = n }-*/;
   public final native void range(CommentRange r) /*-{ this.range = r }-*/;
-  public final native void in_reply_to(String i) /*-{ this.in_reply_to = i }-*/;
+  public final native void inReplyTo(String i) /*-{ this.in_reply_to = i }-*/;
   public final native void message(String m) /*-{ this.message = m }-*/;
 
   public final void side(Side side) {
@@ -81,7 +81,8 @@
 
   public final native String path() /*-{ return this.path }-*/;
   public final native String id() /*-{ return this.id }-*/;
-  public final native String in_reply_to() /*-{ return this.in_reply_to }-*/;
+  public final native String inReplyTo() /*-{ return this.in_reply_to }-*/;
+  public final native int patchSet() /*-{ return this.patch_set }-*/;
 
   public final Side side() {
     String s = sideRaw();
@@ -108,8 +109,8 @@
 
   public final native AccountInfo author() /*-{ return this.author }-*/;
   public final native int line() /*-{ return this.line || 0 }-*/;
-  public final native boolean has_line() /*-{ return this.hasOwnProperty('line') }-*/;
-  public final native boolean has_range() /*-{ return this.hasOwnProperty('range') }-*/;
+  public final native boolean hasLine() /*-{ return this.hasOwnProperty('line') }-*/;
+  public final native boolean hasRange() /*-{ return this.hasOwnProperty('range') }-*/;
   public final native CommentRange range() /*-{ return this.range }-*/;
   public final native String message() /*-{ return this.message }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
index 9e97c56..26e11ae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -20,14 +20,12 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.http.client.URL;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 import java.util.ArrayList;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.ListIterator;
 
@@ -104,33 +102,19 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-
-    if (queries.size() == 1) {
-      ChangeList.next(queries.get(0),
-          0, 0,
-          new GerritCallback<ChangeList>() {
-            @Override
-            public void onSuccess(ChangeList result) {
-              updateColumnsForLabels(result);
-              sections.get(0).display(result);
-              finishDisplay();
+    ChangeList.queryMultiple(
+        new GerritCallback<JsArray<ChangeList>>() {
+          @Override
+          public void onSuccess(JsArray<ChangeList> result) {
+            List<ChangeList> cls = Natives.asList(result);
+            updateColumnsForLabels(cls.toArray(new ChangeList[cls.size()]));
+            for (int i = 0; i < cls.size(); i++) {
+              sections.get(i).display(cls.get(i));
             }
-        });
-    } else if (! queries.isEmpty()) {
-      ChangeList.query(
-          new GerritCallback<JsArray<ChangeList>>() {
-            @Override
-            public void onSuccess(JsArray<ChangeList> result) {
-              List<ChangeList> cls = Natives.asList(result);
-              updateColumnsForLabels(cls.toArray(new ChangeList[cls.size()]));
-              for (int i = 0; i < cls.size(); i++) {
-                sections.get(i).display(cls.get(i));
-              }
-              finishDisplay();
-            }
-          },
-          EnumSet.noneOf(ListChangesOption.class),
-          queries.toArray(new String[queries.size()]));
-    }
+            finishDisplay();
+          }
+        },
+        OPTIONS,
+        queries.toArray(new String[queries.size()]));
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 9527c36..b7b9b07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -39,15 +38,7 @@
   protected PagedSingleListScreen(String anchorToken, int start) {
     anchorPrefix = anchorToken;
     this.start = start;
-
-    if (Gerrit.isSignedIn()) {
-      final AccountGeneralPreferences p =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      final short m = p.getMaximumPageSize();
-      pageSize = 0 < m ? m : AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    } else {
-      pageSize = AccountGeneralPreferences.DEFAULT_PAGESIZE;
-    }
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 488b34b..2b3c6ae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
@@ -54,7 +55,7 @@
         if (isAttached()) {
           if (result.length() == 1 && isSingleQuery(query)) {
             ChangeInfo c = result.get(0);
-            Change.Id id = c.legacy_id();
+            Change.Id id = c.legacyId();
             Gerrit.display(PageLinks.toChange(id));
           } else {
             display(result);
@@ -74,7 +75,8 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    ChangeList.next(query, start, pageSize, loadCallback());
+    ChangeList.query(
+        query, ChangeTable.OPTIONS, loadCallback(), start, pageSize);
   }
 
   private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
index 7651495..096dbd0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -24,7 +24,7 @@
   }
 
   public static enum DraftHandling {
-    DELETE, PUBLISH, KEEP
+    DELETE, PUBLISH, KEEP, PUBLISH_ALL_REVISIONS
   }
 
   public static ReviewInput create() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
index 53d618c..48e3052 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index d34492c..e6d3dbe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -34,8 +34,6 @@
         return C.statusLongDraft();
       case NEW:
         return C.statusLongNew();
-      case SUBMITTED:
-        return C.statusLongSubmitted();
       case MERGED:
         return C.statusLongMerged();
       case ABANDONED:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
index 5dedaf0..28812ac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.client.config;
 
-import com.google.gerrit.client.account.Preferences;
-import com.google.gerrit.client.extensions.TopMenuList;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.ServerInfo;
+import com.google.gerrit.client.info.TopMenuList;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /**
@@ -34,7 +37,28 @@
     new RestApi("/config/server/top-menus").get(cb);
   }
 
-  public static void defaultPreferences(AsyncCallback<Preferences> cb) {
+  public static void defaultPreferences(AsyncCallback<AccountPreferencesInfo> cb) {
     new RestApi("/config/server/preferences").get(cb);
   }
+
+  public static void serverInfo(AsyncCallback<ServerInfo> cb) {
+    new RestApi("/config/server/info").get(cb);
+  }
+
+  public static void confirmEmail(String token, AsyncCallback<VoidResult> cb) {
+    EmailConfirmationInput input = EmailConfirmationInput.create();
+    input.setToken(token);
+    new RestApi("/config/server/email.confirm").put(input, cb);
+  }
+
+  private static class EmailConfirmationInput extends JavaScriptObject {
+    final native void setToken(String token) /*-{ this.token = token; }-*/;
+
+    static EmailConfirmationInput create() {
+      return createObject().cast();
+    }
+
+    protected EmailConfirmationInput() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index fed8f91..8ff11e8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -124,7 +124,7 @@
     padding = new ArrayList<>();
     paddingDivs = new ArrayList<>();
 
-    String diffColor = diff.meta_a() == null || diff.meta_b() == null
+    String diffColor = diff.metaA() == null || diff.metaB() == null
         ? DiffTable.style.intralineBg()
         : DiffTable.style.diff();
 
@@ -175,8 +175,8 @@
 
     colorLines(cmA, color, startA, aLen);
     colorLines(cmB, color, startB, bLen);
-    markEdit(cmA, startA, a, region.edit_a());
-    markEdit(cmB, startB, b, region.edit_b());
+    markEdit(cmA, startA, a, region.editA());
+    markEdit(cmB, startB, b, region.editB());
     addPadding(cmA, startA + aLen - 1, bLen - aLen);
     addPadding(cmB, startB + bLen - 1, aLen - bLen);
     addGutterTag(region, startA, startB);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
index e72c840..f4f1e83 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
@@ -63,7 +63,8 @@
   -webkit-user-select: initial;
   -khtml-user-select: initial;
   -moz-user-select: text;
-  -ms-user-select: initial;
+  -ms-user-select: text;
+  user-select: initial;
 }
 
 .commentBox {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
index ca56bf4..0e85a2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index 514a3be..4e1a3e1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -218,8 +218,8 @@
         info,
         expandAll);
 
-    if (info.in_reply_to() != null) {
-      PublishedBox r = published.get(info.in_reply_to());
+    if (info.inReplyTo() != null) {
+      PublishedBox r = published.get(info.inReplyTo());
       if (r != null) {
         r.setReplyBox(box);
       }
@@ -391,7 +391,8 @@
       return w;
     }
 
-    int lineA, lineB;
+    int lineA;
+    int lineB;
     if (line == 0) {
       lineA = lineB = 0;
     } else if (side == DisplaySide.A) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
index a38a6ca..9f46e9b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -38,10 +38,10 @@
         to.line() + 1, to.ch());
   }
 
-  public final native int start_line() /*-{ return this.start_line; }-*/;
-  public final native int start_character() /*-{ return this.start_character; }-*/;
-  public final native int end_line() /*-{ return this.end_line; }-*/;
-  public final native int end_character() /*-{ return this.end_character; }-*/;
+  public final native int startLine() /*-{ return this.start_line; }-*/;
+  public final native int startCharacter() /*-{ return this.start_character; }-*/;
+  public final native int endLine() /*-{ return this.end_line; }-*/;
+  public final native int endCharacter() /*-{ return this.end_character; }-*/;
 
   private final native void set(int sl, int sc, int el, int ec) /*-{
     this.start_line = sl;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index 18d673a..6d795d3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.client.diff;
 
 import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
@@ -68,16 +69,16 @@
     return this;
   }
 
-  public DiffApi ignoreWhitespace(AccountDiffPreference.Whitespace w) {
+  public DiffApi ignoreWhitespace(DiffPreferencesInfo.Whitespace w) {
     switch (w) {
       default:
       case IGNORE_NONE:
         return ignoreWhitespace(IgnoreWhitespace.NONE);
-      case IGNORE_SPACE_AT_EOL:
+      case IGNORE_TRAILING:
         return ignoreWhitespace(IgnoreWhitespace.TRAILING);
-      case IGNORE_SPACE_CHANGE:
+      case IGNORE_LEADING_AND_TRAILING:
         return ignoreWhitespace(IgnoreWhitespace.CHANGED);
-      case IGNORE_ALL_SPACE:
+      case IGNORE_ALL:
         return ignoreWhitespace(IgnoreWhitespace.ALL);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
index 9b3ac38..1e5c5e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index 7140e07..923a995 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.diff;
 
 import com.google.gerrit.client.DiffWebLinkInfo;
-import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -30,24 +30,24 @@
   public static final String GITLINK = "x-git/gitlink";
   public static final String SYMLINK = "x-git/symlink";
 
-  public final native FileMeta meta_a() /*-{ return this.meta_a; }-*/;
-  public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
-  public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
+  public final native FileMeta metaA() /*-{ return this.meta_a; }-*/;
+  public final native FileMeta metaB() /*-{ return this.meta_b; }-*/;
+  public final native JsArrayString diffHeader() /*-{ return this.diff_header; }-*/;
   public final native JsArray<Region> content() /*-{ return this.content; }-*/;
-  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<DiffWebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
   public final native boolean binary() /*-{ return this.binary || false; }-*/;
 
-  public final List<WebLinkInfo> side_by_side_web_links() {
+  public final List<WebLinkInfo> sideBySideWebLinks() {
     return filterWebLinks(DiffView.SIDE_BY_SIDE);
   }
 
-  public final List<WebLinkInfo> unified_web_links() {
+  public final List<WebLinkInfo> unifiedWebLinks() {
     return filterWebLinks(DiffView.UNIFIED_DIFF);
   }
 
   private final List<WebLinkInfo> filterWebLinks(DiffView diffView) {
     List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>();
-    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(web_links());
+    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(webLinks());
     if (allDiffWebLinks != null) {
       for (DiffWebLinkInfo webLink : allDiffWebLinks) {
         if (diffView == DiffView.SIDE_BY_SIDE
@@ -63,22 +63,22 @@
     return filteredDiffWebLinks;
   }
 
-  public final ChangeType change_type() {
-    return ChangeType.valueOf(change_typeRaw());
+  public final ChangeType changeType() {
+    return ChangeType.valueOf(changeTypeRaw());
   }
-  private final native String change_typeRaw()
+  private final native String changeTypeRaw()
   /*-{ return this.change_type }-*/;
 
-  public final IntraLineStatus intraline_status() {
-    String s = intraline_statusRaw();
+  public final IntraLineStatus intralineStatus() {
+    String s = intralineStatusRaw();
     return s != null
         ? IntraLineStatus.valueOf(s)
         : IntraLineStatus.OFF;
   }
-  private final native String intraline_statusRaw()
+  private final native String intralineStatusRaw()
   /*-{ return this.intraline_status }-*/;
 
-  public final boolean has_skip() {
+  public final boolean hasSkip() {
     JsArray<Region> c = content();
     for (int i = 0; i < c.length(); i++) {
       if (c.get(i).skip() != 0) {
@@ -88,7 +88,7 @@
     return false;
   }
 
-  public final String text_a() {
+  public final String textA() {
     StringBuilder s = new StringBuilder();
     JsArray<Region> c = content();
     for (int i = 0; i < c.length(); i++) {
@@ -103,7 +103,7 @@
     return s.toString();
   }
 
-  public final String text_b() {
+  public final String textB() {
     StringBuilder s = new StringBuilder();
     JsArray<Region> c = content();
     for (int i = 0; i < c.length(); i++) {
@@ -133,9 +133,9 @@
 
   public static class FileMeta extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
-    public final native String content_type() /*-{ return this.content_type; }-*/;
+    public final native String contentType() /*-{ return this.content_type; }-*/;
     public final native int lines() /*-{ return this.lines || 0 }-*/;
-    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+    public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
     protected FileMeta() {
     }
@@ -148,8 +148,8 @@
     public final native int skip() /*-{ return this.skip || 0; }-*/;
     public final native boolean common() /*-{ return this.common || false; }-*/;
 
-    public final native JsArray<Span> edit_a() /*-{ return this.edit_a }-*/;
-    public final native JsArray<Span> edit_b() /*-{ return this.edit_b }-*/;
+    public final native JsArray<Span> editA() /*-{ return this.edit_a }-*/;
+    public final native JsArray<Span> editB() /*-{ return this.edit_b }-*/;
 
     protected Region() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 8a9d71e..4b073f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.diff;
 
 import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -151,13 +151,13 @@
 
   void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
       boolean editExists, boolean current, boolean open, boolean binary) {
-    this.changeType = info.change_type();
-    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a(), editExists,
+    this.changeType = info.changeType();
+    patchSetSelectBoxA.setUpPatchSetNav(list, info.metaA(), editExists,
         current, open, binary);
-    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b(), editExists,
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.metaB(), editExists,
         current, open, binary);
 
-    JsArrayString hdr = info.diff_header();
+    JsArrayString hdr = info.diffHeader();
     if (hdr != null) {
       StringBuilder b = new StringBuilder();
       for (int i = 1; i < hdr.length(); i++) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index 504d9c0..e72cf27 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
     @external .CodeMirror, .CodeMirror-selectedtext;
     @external .CodeMirror-linenumber;
     @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
@@ -80,7 +80,9 @@
     .b { border-left: 1px solid #ddd; }
 
     .a .diff { background-color: #faa; }
-    .b .diff { background-color: #9f9; }
+    /* Set min-width for lineWrapping to make sure it gets enough width
+       before lineWrapping and to make sure it dosent do a ugly line wrap */
+    .b .diff { background-color: #9f9; min-width: 60em; }
     .a .intralineBg { background-color: #fee; }
     .b .intralineBg { background-color: #dfd; }
     .noIntraline .a .intralineBg { background-color: #faa; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
index 5bf56c2..21b2f50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -244,7 +244,7 @@
   }
 
   private void restoreSelection() {
-    if (getFromTo() != null && comment.in_reply_to() == null) {
+    if (getFromTo() != null && comment.inReplyTo() == null) {
       getCm().setSelection(getFromTo().from(), getFromTo().to());
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
index 0c97e8b..a363c06 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
@@ -20,7 +20,7 @@
     xmlns:e='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .draft {
       width: 45px;
       text-align: center;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index 2c551c0..eec3d0c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -16,14 +16,15 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
-import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ReviewInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.diff.DiffInfo.Region;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.FileInfo;
+import com.google.gerrit.client.info.GitwebInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -67,7 +68,7 @@
   }
 
   private static enum ReviewedState {
-    AUTO_REVIEW, LOADED;
+    AUTO_REVIEW, LOADED
   }
 
   @UiField CheckBox reviewed;
@@ -115,7 +116,8 @@
       return b.append(Util.C.commitMessage());
     }
 
-    GitwebLink gw = (project != null && commit != null) ? Gerrit.getGitwebLink() : null;
+    GitwebInfo gw = (project != null && commit != null)
+        ? Gerrit.info().gitweb() : null;
     int s = path.lastIndexOf('/') + 1;
     if (gw != null && s > 0) {
       String base = path.substring(0, s - 1);
@@ -192,7 +194,7 @@
   }
 
   void setChangeInfo(ChangeInfo info) {
-    GitwebLink gw = Gerrit.getGitwebLink();
+    GitwebInfo gw = Gerrit.info().gitweb();
     if (gw != null) {
       for (RevisionInfo rev : Natives.asList(info.revisions().values())) {
         if (patchSetId.getId().equals(rev.id())) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
index c58eef7..f13c9a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -20,7 +20,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
   .header {
     position: relative;
     height: 16px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
index 7287a65..7c8bc21 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2014 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.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
index d7c8fc9..6a18c4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .bubble {
       z-index: 150;
       white-space: nowrap;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index ef731f4..186cd98 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.WebLinkInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -116,7 +116,7 @@
         linkPanel.add(createEditIcon());
       }
     }
-    List<WebLinkInfo> webLinks = Natives.asList(meta.web_links());
+    List<WebLinkInfo> webLinks = Natives.asList(meta.webLinks());
     if (webLinks != null) {
       for (WebLinkInfo webLink : webLinks) {
         linkPanel.add(webLink.toAnchor());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
index cc8dd74..6e526ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
@@ -20,7 +20,7 @@
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='patchConstants'
       type='com.google.gerrit.client.patches.PatchConstants'/>
-  <ui:style type='com.google.gerrit.client.diff.PatchSetSelectBox.BoxStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.PatchSetSelectBox.BoxStyle'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
     .table {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index 4265203..c5b1f96 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.client.diff;
 
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.DEFAULT_CONTEXT;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_NONE;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.DEFAULT_CONTEXT;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_ALL;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_NONE;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_TRAILING;
 import static com.google.gwt.event.dom.client.KeyCodes.KEY_ESCAPE;
 
 import com.google.gerrit.client.Gerrit;
@@ -29,12 +29,14 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.NpIntTextBox;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Visibility;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyDownEvent;
@@ -54,6 +56,7 @@
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.ToggleButton;
+import com.google.gwt.user.client.ui.UIObject;
 
 import net.codemirror.mode.ModeInfo;
 import net.codemirror.mode.ModeInjector;
@@ -62,11 +65,11 @@
 import java.util.Objects;
 
 /** Displays current diff preferences. */
-class PreferencesBox extends Composite {
+public class PreferencesBox extends Composite {
   interface Binder extends UiBinder<HTMLPanel, PreferencesBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
-  interface Style extends CssResource {
+  public interface Style extends CssResource {
     String dialog();
   }
 
@@ -76,17 +79,20 @@
   private Timer updateContextTimer;
 
   @UiField Style style;
+  @UiField Element header;
   @UiField Anchor close;
   @UiField ListBox ignoreWhitespace;
   @UiField NpIntTextBox tabWidth;
   @UiField NpIntTextBox lineLength;
   @UiField NpIntTextBox context;
+  @UiField NpIntTextBox cursorBlinkRate;
   @UiField CheckBox contextEntireFile;
   @UiField ToggleButton intralineDifference;
   @UiField ToggleButton syntaxHighlighting;
   @UiField ToggleButton whitespaceErrors;
   @UiField ToggleButton showTabs;
   @UiField ToggleButton lineNumbers;
+  @UiField Element leftSideLabel;
   @UiField ToggleButton leftSide;
   @UiField ToggleButton emptyPane;
   @UiField ToggleButton topMenu;
@@ -94,18 +100,27 @@
   @UiField ToggleButton manualReview;
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
+  @UiField ToggleButton matchBrackets;
+  @UiField ToggleButton lineWrapping;
   @UiField ListBox theme;
+  @UiField Element modeLabel;
   @UiField ListBox mode;
   @UiField Button apply;
   @UiField Button save;
 
-  PreferencesBox(SideBySide view) {
+  public PreferencesBox(SideBySide view) {
     this.view = view;
 
     initWidget(uiBinder.createAndBindUi(this));
     initIgnoreWhitespace();
     initTheme();
-    initMode();
+
+    if (view != null) {
+      initMode();
+    } else {
+      UIObject.setVisible(header, false);
+      apply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+    }
   }
 
   @Override
@@ -113,61 +128,76 @@
     super.onLoad();
 
     save.setVisible(Gerrit.isSignedIn());
-    addDomHandler(new KeyDownHandler() {
-      @Override
-      public void onKeyDown(KeyDownEvent event) {
-        if (event.getNativeKeyCode() == KEY_ESCAPE
-            || event.getNativeKeyCode() == ',') {
-          close();
-        }
-      }
-    }, KeyDownEvent.getType());
 
-    updateContextTimer = new Timer() {
-      @Override
-      public void run() {
-        if (prefs.context() == WHOLE_FILE_CONTEXT) {
-          contextEntireFile.setValue(true);
+    if (view != null) {
+      addDomHandler(new KeyDownHandler() {
+        @Override
+        public void onKeyDown(KeyDownEvent event) {
+          if (event.getNativeKeyCode() == KEY_ESCAPE
+              || event.getNativeKeyCode() == ',') {
+            close();
+          }
         }
-        if (view.canRenderEntireFile(prefs)) {
-          renderEntireFile.setEnabled(true);
-          renderEntireFile.setValue(prefs.renderEntireFile());
-        } else {
-          renderEntireFile.setValue(false);
-          renderEntireFile.setEnabled(false);
+      }, KeyDownEvent.getType());
+
+      updateContextTimer = new Timer() {
+        @Override
+        public void run() {
+          if (prefs.context() == WHOLE_FILE_CONTEXT) {
+            contextEntireFile.setValue(true);
+          }
+          if (view.canRenderEntireFile(prefs)) {
+            renderEntireFile.setEnabled(true);
+            renderEntireFile.setValue(prefs.renderEntireFile());
+          } else {
+            renderEntireFile.setValue(false);
+            renderEntireFile.setEnabled(false);
+          }
+          view.setContext(prefs.context());
         }
-        view.setContext(prefs.context());
-      }
-    };
+      };
+    }
   }
 
-  void set(DiffPreferences prefs) {
+  public Style getStyle() {
+    return style;
+  }
+
+  public void set(DiffPreferences prefs) {
     this.prefs = prefs;
 
     setIgnoreWhitespace(prefs.ignoreWhitespace());
     tabWidth.setIntValue(prefs.tabSize());
-    if (Patch.COMMIT_MSG.equals(view.getPath())) {
+    if (view != null && Patch.COMMIT_MSG.equals(view.getPath())) {
       lineLength.setEnabled(false);
       lineLength.setIntValue(72);
     } else {
       lineLength.setEnabled(true);
       lineLength.setIntValue(prefs.lineLength());
     }
+    cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
     syntaxHighlighting.setValue(prefs.syntaxHighlighting());
     whitespaceErrors.setValue(prefs.showWhitespaceErrors());
     showTabs.setValue(prefs.showTabs());
     lineNumbers.setValue(prefs.showLineNumbers());
-    leftSide.setValue(view.diffTable.isVisibleA());
     emptyPane.setValue(!prefs.hideEmptyPane());
-    leftSide.setEnabled(!(prefs.hideEmptyPane()
-        && view.diffTable.getChangeType() == ChangeType.ADDED));
+    if (view != null) {
+      leftSide.setValue(view.diffTable.isVisibleA());
+      leftSide.setEnabled(!(prefs.hideEmptyPane()
+          && view.diffTable.getChangeType() == ChangeType.ADDED));
+    } else {
+      UIObject.setVisible(leftSideLabel, false);
+      leftSide.setVisible(false);
+    }
     topMenu.setValue(!prefs.hideTopMenu());
     autoHideDiffTableHeader.setValue(!prefs.autoHideDiffTableHeader());
     manualReview.setValue(prefs.manualReview());
     expandAllComments.setValue(prefs.expandAllComments());
+    matchBrackets.setValue(prefs.matchBrackets());
+    lineWrapping.setValue(prefs.lineWrapping());
     setTheme(prefs.theme());
 
-    if (view.canRenderEntireFile(prefs)) {
+    if (view == null || view.canRenderEntireFile(prefs)) {
       renderEntireFile.setValue(prefs.renderEntireFile());
       renderEntireFile.setEnabled(true);
     } else {
@@ -175,22 +205,31 @@
       renderEntireFile.setEnabled(false);
     }
 
-    mode.setEnabled(prefs.syntaxHighlighting());
-    if (prefs.syntaxHighlighting()) {
-      setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode"));
+    if (view != null) {
+      mode.setEnabled(prefs.syntaxHighlighting());
+      if (prefs.syntaxHighlighting()) {
+        setMode(view.getCmFromSide(DisplaySide.B).getStringOption("mode"));
+      }
+    } else {
+      UIObject.setVisible(modeLabel, false);
+      mode.setVisible(false);
     }
 
-    switch (view.getIntraLineStatus()) {
-      case OFF:
-      case OK:
-        intralineDifference.setValue(prefs.intralineDifference());
-        break;
+    if (view != null) {
+      switch (view.getIntraLineStatus()) {
+        case OFF:
+        case OK:
+          intralineDifference.setValue(prefs.intralineDifference());
+          break;
 
-      case TIMEOUT:
-      case FAILURE:
-        intralineDifference.setValue(false);
-        intralineDifference.setEnabled(false);
-        break;
+        case TIMEOUT:
+        case FAILURE:
+          intralineDifference.setValue(false);
+          intralineDifference.setEnabled(false);
+          break;
+      }
+    } else {
+      intralineDifference.setValue(prefs.intralineDifference());
     }
 
     if (prefs.context() == WHOLE_FILE_CONTEXT) {
@@ -207,13 +246,17 @@
   void onIgnoreWhitespace(@SuppressWarnings("unused") ChangeEvent e) {
     prefs.ignoreWhitespace(Whitespace.valueOf(
         ignoreWhitespace.getValue(ignoreWhitespace.getSelectedIndex())));
-    view.reloadDiffInfo();
+    if (view != null) {
+      view.reloadDiffInfo();
+    }
   }
 
   @UiHandler("intralineDifference")
   void onIntralineDifference(ValueChangeEvent<Boolean> e) {
     prefs.intralineDifference(e.getValue());
-    view.setShowIntraline(prefs.intralineDifference());
+    if (view != null) {
+      view.setShowIntraline(prefs.intralineDifference());
+    }
   }
 
   @UiHandler("context")
@@ -239,7 +282,9 @@
       return;
     }
     prefs.context(c);
-    updateContextTimer.schedule(200);
+    if (view != null) {
+      updateContextTimer.schedule(200);
+    }
   }
 
   @UiHandler("contextEntireFile")
@@ -257,7 +302,9 @@
       context.setFocus(true);
       context.setSelectionRange(0, context.getText().length());
     }
-    updateContextTimer.schedule(200);
+    if (view != null) {
+      updateContextTimer.schedule(200);
+    }
   }
 
   @UiHandler("tabWidth")
@@ -265,14 +312,16 @@
     String v = e.getValue();
     if (v != null && v.length() > 0) {
       prefs.tabSize(Math.max(1, Integer.parseInt(v)));
-      view.operation(new Runnable() {
-        @Override
-        public void run() {
-          int v = prefs.tabSize();
-          view.getCmFromSide(DisplaySide.A).setOption("tabSize", v);
-          view.getCmFromSide(DisplaySide.B).setOption("tabSize", v);
-        }
-      });
+      if (view != null) {
+        view.operation(new Runnable() {
+          @Override
+          public void run() {
+            int v = prefs.tabSize();
+            view.getCmFromSide(DisplaySide.A).setOption("tabSize", v);
+            view.getCmFromSide(DisplaySide.B).setOption("tabSize", v);
+          }
+        });
+      }
     }
   }
 
@@ -281,30 +330,52 @@
     String v = e.getValue();
     if (v != null && v.length() > 0) {
       prefs.lineLength(Math.max(1, Integer.parseInt(v)));
-      view.operation(new Runnable() {
-        @Override
-        public void run() {
-          view.setLineLength(prefs.lineLength());
-        }
-      });
+      if (view != null) {
+        view.operation(new Runnable() {
+          @Override
+          public void run() {
+            view.setLineLength(prefs.lineLength());
+          }
+        });
+      }
     }
   }
   @UiHandler("expandAllComments")
   void onExpandAllComments(ValueChangeEvent<Boolean> e) {
     prefs.expandAllComments(e.getValue());
-    view.getCommentManager().setExpandAllComments(prefs.expandAllComments());
+    if (view != null) {
+      view.getCommentManager().setExpandAllComments(prefs.expandAllComments());
+    }
+  }
+
+  @UiHandler("cursorBlinkRate")
+  void onCursoBlinkRate(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      // A negative value hides the cursor entirely:
+      // don't let user shoot himself in the foot.
+      prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
+      view.getCmFromSide(DisplaySide.A).setOption("cursorBlinkRate",
+          prefs.cursorBlinkRate());
+      view.getCmFromSide(DisplaySide.B).setOption("cursorBlinkRate",
+          prefs.cursorBlinkRate());
+    }
   }
 
   @UiHandler("showTabs")
   void onShowTabs(ValueChangeEvent<Boolean> e) {
     prefs.showTabs(e.getValue());
-    view.setShowTabs(prefs.showTabs());
+    if (view != null) {
+      view.setShowTabs(prefs.showTabs());
+    }
   }
 
   @UiHandler("lineNumbers")
   void onLineNumbers(ValueChangeEvent<Boolean> e) {
     prefs.showLineNumbers(e.getValue());
-    view.setShowLineNumbers(prefs.showLineNumbers());
+    if (view != null) {
+      view.setShowLineNumbers(prefs.showLineNumbers());
+    }
   }
 
   @UiHandler("leftSide")
@@ -315,29 +386,35 @@
   @UiHandler("emptyPane")
   void onHideEmptyPane(ValueChangeEvent<Boolean> e) {
     prefs.hideEmptyPane(!e.getValue());
-    view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
-    if (prefs.hideEmptyPane()) {
-      if (view.diffTable.getChangeType() == ChangeType.ADDED) {
-        leftSide.setValue(false);
-        leftSide.setEnabled(false);
+    if (view != null) {
+      view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
+      if (prefs.hideEmptyPane()) {
+        if (view.diffTable.getChangeType() == ChangeType.ADDED) {
+          leftSide.setValue(false);
+          leftSide.setEnabled(false);
+        }
+      } else {
+        leftSide.setValue(view.diffTable.isVisibleA());
+        leftSide.setEnabled(true);
       }
-    } else {
-      leftSide.setValue(view.diffTable.isVisibleA());
-      leftSide.setEnabled(true);
     }
   }
 
   @UiHandler("topMenu")
   void onTopMenu(ValueChangeEvent<Boolean> e) {
     prefs.hideTopMenu(!e.getValue());
-    Gerrit.setHeaderVisible(!prefs.hideTopMenu());
-    view.resizeCodeMirror();
+    if (view != null) {
+      Gerrit.setHeaderVisible(!prefs.hideTopMenu());
+      view.resizeCodeMirror();
+    }
   }
 
   @UiHandler("autoHideDiffTableHeader")
   void onAutoHideDiffTableHeader(ValueChangeEvent<Boolean> e) {
     prefs.autoHideDiffTableHeader(!e.getValue());
-    view.setAutoHideDiffHeader(!e.getValue());
+    if (view != null) {
+      view.setAutoHideDiffHeader(!e.getValue());
+    }
   }
 
   @UiHandler("manualReview")
@@ -348,11 +425,13 @@
   @UiHandler("syntaxHighlighting")
   void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
     prefs.syntaxHighlighting(e.getValue());
-    mode.setEnabled(prefs.syntaxHighlighting());
-    if (prefs.syntaxHighlighting()) {
-      setMode(view.getContentType());
+    if (view != null) {
+      mode.setEnabled(prefs.syntaxHighlighting());
+      if (prefs.syntaxHighlighting()) {
+        setMode(view.getContentType());
+      }
+      view.setSyntaxHighlighting(prefs.syntaxHighlighting());
     }
-    view.setSyntaxHighlighting(prefs.syntaxHighlighting());
   }
 
   @UiHandler("mode")
@@ -386,42 +465,66 @@
   @UiHandler("whitespaceErrors")
   void onWhitespaceErrors(ValueChangeEvent<Boolean> e) {
     prefs.showWhitespaceErrors(e.getValue());
-    view.operation(new Runnable() {
-      @Override
-      public void run() {
-        boolean s = prefs.showWhitespaceErrors();
-        view.getCmFromSide(DisplaySide.A).setOption("showTrailingSpace", s);
-        view.getCmFromSide(DisplaySide.B).setOption("showTrailingSpace", s);
-      }
-    });
+    if (view != null) {
+      view.operation(new Runnable() {
+        @Override
+        public void run() {
+          boolean s = prefs.showWhitespaceErrors();
+          view.getCmFromSide(DisplaySide.A).setOption("showTrailingSpace", s);
+          view.getCmFromSide(DisplaySide.B).setOption("showTrailingSpace", s);
+        }
+      });
+    }
   }
 
   @UiHandler("renderEntireFile")
   void onRenderEntireFile(ValueChangeEvent<Boolean> e) {
     prefs.renderEntireFile(e.getValue());
-    view.updateRenderEntireFile();
+    if (view != null) {
+      view.updateRenderEntireFile();
+    }
+  }
+
+  @UiHandler("matchBrackets")
+  void onMatchBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.matchBrackets(e.getValue());
+    view.getCmFromSide(DisplaySide.A).setOption("matchBrackets",
+        prefs.matchBrackets());
+    view.getCmFromSide(DisplaySide.B).setOption("matchBrackets",
+        prefs.matchBrackets());
+  }
+
+  @UiHandler("lineWrapping")
+  void onLineWrapping(ValueChangeEvent<Boolean> e) {
+    prefs.lineWrapping(e.getValue());
+    view.getCmFromSide(DisplaySide.A).setOption("lineWrapping",
+        prefs.lineWrapping());
+    view.getCmFromSide(DisplaySide.B).setOption("lineWrapping",
+        prefs.lineWrapping());
   }
 
   @UiHandler("theme")
   void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     final Theme newTheme = getSelectedTheme();
     prefs.theme(newTheme);
-    ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
-      @Override
-      public void onSuccess(Void result) {
-        view.operation(new Runnable() {
-          @Override
-          public void run() {
-            if (getSelectedTheme() == newTheme && isAttached()) {
-              String t = newTheme.name().toLowerCase();
-              view.getCmFromSide(DisplaySide.A).setOption("theme", t);
-              view.getCmFromSide(DisplaySide.B).setOption("theme", t);
-              view.setThemeStyles(newTheme.isDark());
+    if (view != null) {
+      ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          view.operation(new Runnable() {
+            @Override
+            public void run() {
+              if (getSelectedTheme() == newTheme && isAttached()) {
+                String t = newTheme.name().toLowerCase();
+                view.getCmFromSide(DisplaySide.A).setOption("theme", t);
+                view.getCmFromSide(DisplaySide.B).setOption("theme", t);
+                view.setThemeStyles(newTheme.isDark());
+              }
             }
-          }
-        });
-      }
-    });
+          });
+        }
+      });
+    }
   }
 
   private Theme getSelectedTheme() {
@@ -438,15 +541,14 @@
     AccountApi.putDiffPreferences(prefs, new GerritCallback<DiffPreferences>() {
       @Override
       public void onSuccess(DiffPreferences result) {
-        AccountDiffPreference p = Gerrit.getAccountDiffPreference();
-        if (p == null) {
-          p = AccountDiffPreference.createDefault(Gerrit.getUserAccount().getId());
-        }
+        DiffPreferencesInfo p = new DiffPreferencesInfo();
         result.copyTo(p);
-        Gerrit.setAccountDiffPreference(p);
+        Gerrit.setDiffPreferences(p);
       }
     });
-    close();
+    if (view != null) {
+      close();
+    }
   }
 
   @UiHandler("close")
@@ -479,14 +581,14 @@
         PatchUtil.C.whitespaceIGNORE_NONE(),
         IGNORE_NONE.name());
     ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_SPACE_AT_EOL(),
-        IGNORE_SPACE_AT_EOL.name());
+        PatchUtil.C.whitespaceIGNORE_TRAILING(),
+        IGNORE_TRAILING.name());
     ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_SPACE_CHANGE(),
-        IGNORE_SPACE_CHANGE.name());
+        PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(),
+        IGNORE_LEADING_AND_TRAILING.name());
     ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_ALL_SPACE(),
-        IGNORE_ALL_SPACE.name());
+        PatchUtil.C.whitespaceIGNORE_ALL(),
+        IGNORE_ALL.name());
   }
 
   private void initMode() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
index 62d2eac..7dbbc21 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:style type='com.google.gerrit.client.diff.PreferencesBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.PreferencesBox.Style'>
     @external .gwt-TextBox;
     @external .gwt-ToggleButton .html-face;
     @external .gwt-ToggleButton-up;
@@ -32,7 +32,8 @@
       color: #ffffff;
       font-family: arial,sans-serif;
       font-weight: bold;
-      overflow: hidden;
+      overflow: auto !important;
+      bottom: 0;
       text-align: left;
       text-shadow: 1px 1px 7px #000000;
       min-width: 300px;
@@ -155,15 +156,17 @@
   </ui:style>
 
   <g:HTMLPanel styleName='{style.box}'>
-    <table style='width: 100%'>
-      <tr>
-        <td><ui:msg>Diff Preferences</ui:msg></td>
-        <td style='text-align: right'>
-          <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
-        </td>
-      </tr>
-    </table>
-    <hr/>
+    <div ui:field='header'>
+      <table style='width: 100%'>
+        <tr>
+          <td><ui:msg>Diff Preferences</ui:msg></td>
+          <td style='text-align: right'>
+            <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
+          </td>
+        </tr>
+      </table>
+      <hr/>
+    </div>
     <table class='{style.table}'>
       <tr>
         <th><ui:msg>Theme</ui:msg></th>
@@ -194,6 +197,12 @@
           or <g:CheckBox ui:field='contextEntireFile'>entire file</g:CheckBox></ui:msg></td>
       </tr>
       <tr>
+        <th><ui:msg>Cursor Blink Rate</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='cursorBlinkRate'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
         <th><ui:msg>Intraline Difference</ui:msg></th>
         <td><g:ToggleButton ui:field='intralineDifference'>
           <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
@@ -208,7 +217,7 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
-        <th><ui:msg>Language</ui:msg></th>
+        <th><div ui:field='modeLabel'><ui:msg>Language</ui:msg></div></th>
         <td><g:ListBox ui:field='mode'/></td>
       </tr>
       <tr>
@@ -240,7 +249,7 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
-        <th><ui:msg>Left Side</ui:msg></th>
+        <th><div ui:field='leftSideLabel'><ui:msg>Left Side</ui:msg></div></th>
         <td><g:ToggleButton ui:field='leftSide'>
           <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
           <g:downFace><ui:msg>Show</ui:msg></g:downFace>
@@ -282,6 +291,20 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Match Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='matchBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Line Wrapping</ui:msg></th>
+        <td><g:ToggleButton ui:field='lineWrapping'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <td></td>
         <td>
           <g:Button ui:field='apply' styleName='{style.apply}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
index 7d74c2b..48b4c3c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -205,7 +205,7 @@
       if (info.author().name() != null) {
         return info.author().name();
       }
-      return Gerrit.getConfig().getAnonymousCowardName();
+      return Gerrit.info().user().anonymousCowardName();
     }
     return Util.C.messageNoAuthor();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
index bcc34e2..46b76ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:c='urn:import:com.google.gerrit.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style type='com.google.gerrit.client.diff.PublishedBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.PublishedBox.Style'>
     .avatar {
       position: absolute;
       width: 26px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 0894ed8..0fdc519 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.diff;
 
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
 import static java.lang.Double.POSITIVE_INFINITY;
 
 import com.google.gerrit.client.Dispatcher;
@@ -24,12 +24,13 @@
 import com.google.gerrit.client.change.ChangeScreen;
 import com.google.gerrit.client.change.FileTable;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
-import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.diff.DiffInfo.FileMeta;
 import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo.EditInfo;
+import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -150,7 +151,7 @@
     this.startSide = startSide;
     this.startLine = startLine;
 
-    prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
+    prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
     handlers = new ArrayList<>(6);
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     header = new Header(keysNavigation, base, revision, path);
@@ -238,11 +239,11 @@
         changeStatus = info.status();
         info.revisions().copyKeysIntoChildren("name");
         if (edit != null) {
-          edit.set_name(edit.commit().commit());
-          info.set_edit(edit);
+          edit.setName(edit.commit().commit());
+          info.setEdit(edit);
           info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
         }
-        String currentRevision = info.current_revision();
+        String currentRevision = info.currentRevision();
         boolean current = currentRevision != null &&
             revision.get() == info.revision(currentRevision)._number();
         JsArray<RevisionInfo> list = info.revisions().values();
@@ -572,8 +573,8 @@
       diffTable.addStyleName(DiffTable.style.showLineNumbers());
     }
 
-    cmA = newCM(diff.meta_a(), diff.text_a(), diffTable.cmA);
-    cmB = newCM(diff.meta_b(), diff.text_b(), diffTable.cmB);
+    cmA = newCM(diff.metaA(), diff.textA(), diffTable.cmA);
+    cmB = newCM(diff.metaB(), diff.textB(), diffTable.cmB);
 
     cmA.extras().side(DisplaySide.A);
     cmB.extras().side(DisplaySide.B);
@@ -603,7 +604,7 @@
             chunkManager.getLineMapper());
 
     prefsAction = new PreferencesAction(this, prefs);
-    header.init(prefsAction, getLinks(), diff.side_by_side_web_links());
+    header.init(prefsAction, getLinks(), diff.sideBySideWebLinks());
     scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
 
     if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
@@ -635,12 +636,12 @@
       Element parent) {
     return CodeMirror.create(parent, Configuration.create()
       .set("readOnly", true)
-      .set("cursorBlinkRate", 0)
+      .set("cursorBlinkRate", prefs.cursorBlinkRate())
       .set("cursorHeight", 0.85)
       .set("lineNumbers", prefs.showLineNumbers())
       .set("tabSize", prefs.tabSize())
       .set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null)
-      .set("lineWrapping", false)
+      .set("lineWrapping", prefs.lineWrapping())
       .set("scrollbarStyle", "overlay")
       .set("styleSelectedText", true)
       .set("showTrailingSpace", prefs.showWhitespaceErrors())
@@ -651,7 +652,7 @@
   }
 
   DiffInfo.IntraLineStatus getIntraLineStatus() {
-    return diff.intraline_status();
+    return diff.intralineStatus();
   }
 
   boolean renderEntireFile() {
@@ -665,7 +666,7 @@
   }
 
   String getContentType() {
-    return getContentType(diff.meta_b());
+    return getContentType(diff.metaB());
   }
 
   void setThemeStyles(boolean d) {
@@ -718,8 +719,8 @@
         @Override
         public void onSuccess(Void result) {
           if (prefs.syntaxHighlighting()) {
-            cmA.setOption("mode", getContentType(diff.meta_a()));
-            cmB.setOption("mode", getContentType(diff.meta_b()));
+            cmA.setOption("mode", getContentType(diff.metaA()));
+            cmB.setOption("mode", getContentType(diff.metaB()));
           }
         }
 
@@ -926,7 +927,7 @@
     int offset = 6;
 
     // Adjust for merge commits, which have two parent lines
-    if (diff.text_b().startsWith("Merge")) {
+    if (diff.textB().startsWith("Merge")) {
       offset += 1;
     }
 
@@ -987,8 +988,8 @@
 
   private String getContentType(DiffInfo.FileMeta meta) {
     if (prefs.syntaxHighlighting() && meta != null
-        && meta.content_type() != null) {
-     ModeInfo m = ModeInfo.findMode(meta.content_type(), path);
+        && meta.contentType() != null) {
+     ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
      return m != null ? m.mime() : null;
    }
    return null;
@@ -996,8 +997,8 @@
 
   private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
     new ModeInjector()
-      .add(getContentType(diffInfo.meta_a()))
-      .add(getContentType(diffInfo.meta_b()))
+      .add(getContentType(diffInfo.metaA()))
+      .add(getContentType(diffInfo.metaB()))
       .inject(cb);
   }
 
@@ -1047,8 +1048,8 @@
           @Override
           public void onSuccess(DiffInfo info) {
             new ModeInjector()
-              .add(getContentType(info.meta_a()))
-              .add(getContentType(info.meta_b()))
+              .add(getContentType(info.metaA()))
+              .add(getContentType(info.metaB()))
               .inject(CallbackGroup.<Void> emptyCallback());
           }
 
@@ -1089,8 +1090,8 @@
   }
 
   private static FileSize bucketFileSize(DiffInfo diff) {
-    FileMeta a = diff.meta_a();
-    FileMeta b = diff.meta_b();
+    FileMeta a = diff.metaA();
+    FileMeta b = diff.metaB();
     FileSize[] sizes = FileSize.values();
     for (int i = sizes.length - 1; 0 <= i; i--) {
       FileSize s = sizes[i];
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
index da5b351..a4c2eb9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style>
+  <ui:style gss='false'>
     .sbs {
       margin-left: -5px;
       margin-right: -5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
index 4c12cc0..258eec6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2013 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.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
index 0ff23a7..bf3c425 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
@@ -16,7 +16,7 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
     .skipBar {
       background-color: #def;
       height: 1.3em;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
index 4972f5d..5376588 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.patches.SkippedLine;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
@@ -39,13 +39,14 @@
   }
 
   void render(int context, DiffInfo diff) {
-    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+    if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
       return;
     }
 
     JsArray<Region> regions = diff.content();
     List<SkippedLine> skips = new ArrayList<>();
-    int lineA = 0, lineB = 0;
+    int lineA = 0;
+    int lineB = 0;
     for (int i = 0; i < regions.length(); i++) {
       Region current = regions.get(i);
       if (current.ab() != null || current.common() || current.skip() > 0) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java
index 6235186..fd63ffc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocInfo.java
@@ -22,6 +22,10 @@
   public final native String title() /*-{ return this.title; }-*/;
   public final native String url() /*-{ return this.url; }-*/;
 
+  public static DocInfo create() {
+    return (DocInfo) createObject();
+  }
+
   protected DocInfo() {
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
index cea9106..4a6ecab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
@@ -15,172 +15,24 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.client.info.DownloadInfo.DownloadCommandInfo;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
 
-public abstract class DownloadCommandLink extends Anchor implements ClickHandler {
-  public static class CopyableCommandLinkFactory {
-    protected CopyableLabel copyLabel = null;
-    protected Widget widget;
+public class DownloadCommandLink extends Anchor implements ClickHandler {
+  private final CopyableLabel copyLabel;
+  private final String command;
 
-    public class CheckoutCommandLink extends DownloadCommandLink {
-      public CheckoutCommandLink () {
-        super(DownloadCommand.CHECKOUT, "checkout");
-      }
+  public DownloadCommandLink(CopyableLabel copyLabel,
+      DownloadCommandInfo commandInfo) {
+    super(commandInfo.name());
+    this.copyLabel = copyLabel;
+    this.command = commandInfo.command();
 
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git fetch " + link.getUrlData()
-            + " && git checkout FETCH_HEAD");
-      }
-    }
-
-    public class PullCommandLink extends DownloadCommandLink {
-      public PullCommandLink() {
-        super(DownloadCommand.PULL, "pull");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git pull " + link.getUrlData());
-      }
-    }
-
-    public class CherryPickCommandLink extends DownloadCommandLink {
-      public CherryPickCommandLink() {
-        super(DownloadCommand.CHERRY_PICK, "cherry-pick");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git fetch " + link.getUrlData()
-            + " && git cherry-pick FETCH_HEAD");
-      }
-    }
-
-    public class FormatPatchCommandLink extends DownloadCommandLink {
-      public FormatPatchCommandLink() {
-        super(DownloadCommand.FORMAT_PATCH, "patch");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git fetch " + link.getUrlData()
-            + " && git format-patch -1 --stdout FETCH_HEAD");
-      }
-    }
-
-    public class RepoCommandLink extends DownloadCommandLink {
-      String projectName;
-      String ref;
-      public RepoCommandLink(String project, String ref) {
-        super(DownloadCommand.REPO_DOWNLOAD, "repo download");
-        this.projectName = project;
-        this.ref = ref;
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(false);
-        final StringBuilder r = new StringBuilder();
-        r.append("repo download ");
-        r.append(projectName);
-        r.append(" ");
-        r.append(ref);
-        copyLabel.setText(r.toString());
-      }
-    }
-
-    public class CloneCommandLink extends DownloadCommandLink {
-      public CloneCommandLink() {
-        super(DownloadCommand.CHECKOUT, "clone");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git clone " + link.getUrlData());
-      }
-    }
-
-    public class CloneWithCommitMsgHookCommandLink extends DownloadCommandLink {
-      private final Project.NameKey project;
-
-      public CloneWithCommitMsgHookCommandLink(Project.NameKey project) {
-        super(DownloadCommand.CHECKOUT, "clone with commit-msg hook");
-        this.project = project;
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-
-        String sshPort = null;
-        String sshAddr = Gerrit.getConfig().getSshdAddress();
-        int p = sshAddr.lastIndexOf(':');
-        if (p != -1 && !sshAddr.endsWith(":")) {
-          sshPort = sshAddr.substring(p + 1);
-        }
-
-        StringBuilder cmd = new StringBuilder();
-        cmd.append("git clone ");
-        cmd.append(link.getUrlData());
-        cmd.append(" && scp -p ");
-        if (sshPort != null) {
-          cmd.append("-P ");
-          cmd.append(sshPort);
-          cmd.append(" ");
-        }
-        cmd.append(Gerrit.getUserAccount().getUserName());
-        cmd.append("@");
-
-        if (sshAddr.startsWith("*:") || p == -1) {
-          cmd.append(Window.Location.getHostName());
-        } else {
-          cmd.append(sshAddr.substring(0, p));
-        }
-
-        cmd.append(":hooks/commit-msg ");
-
-        p = project.get().lastIndexOf('/');
-        if (p != -1) {
-          cmd.append(project.get().substring(p + 1));
-        } else {
-          cmd.append(project.get());
-        }
-
-        cmd.append("/.git/hooks/");
-
-        copyLabel.setText(cmd.toString());
-      }
-    }
-
-    public CopyableCommandLinkFactory(CopyableLabel label, Widget widget) {
-      copyLabel = label;
-      this.widget = widget;
-    }
-  }
-
-  final DownloadCommand cmdType;
-
-  public DownloadCommandLink(DownloadCommand cmdType,
-      String text) {
-    super(text);
-    this.cmdType = cmdType;
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
     addClickHandler(this);
@@ -192,31 +44,11 @@
     event.stopPropagation();
 
     select();
-
-    if (Gerrit.isSignedIn()) {
-      // If the user is signed-in, remember this choice for future panels.
-      //
-      AccountGeneralPreferences pref =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      pref.setDownloadCommand(cmdType);
-      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
-          new AsyncCallback<VoidResult>() {
-            @Override
-            public void onFailure(Throwable caught) {
-            }
-
-            @Override
-            public void onSuccess(VoidResult result) {
-            }
-          });
-    }
-  }
-
-  public DownloadCommand getCmdType() {
-    return cmdType;
   }
 
   void select() {
+    copyLabel.setText(command);
+
     DownloadCommandPanel parent = (DownloadCommandPanel) getParent();
     for (Widget w : parent) {
       if (w != this && w instanceof DownloadCommandLink) {
@@ -226,6 +58,4 @@
     parent.setCurrentCommand(this);
     addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
   }
-
-  protected abstract void setCurrentUrl(DownloadUrlLink link);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
index d17d6c2..21c33e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
@@ -15,15 +15,12 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
 
 public class DownloadCommandPanel extends FlowPanel {
   private DownloadCommandLink currentCommand;
-  private DownloadUrlLink currentUrl;
 
   public DownloadCommandPanel() {
     setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
@@ -34,19 +31,20 @@
     return getWidgetCount() == 0;
   }
 
-  public void select(AccountGeneralPreferences.DownloadCommand cmdType) {
+  public void select() {
     DownloadCommandLink first = null;
 
     for (Widget w : this) {
       if (w instanceof DownloadCommandLink) {
-        final DownloadCommandLink d = (DownloadCommandLink) w;
-        if (first == null) {
-          first = d;
-        }
-        if (d.cmdType == cmdType) {
+        DownloadCommandLink d = (DownloadCommandLink) w;
+        if (currentCommand != null
+            && d.getText().equals(currentCommand.getText())) {
           d.select();
           return;
         }
+        if (first == null) {
+          first = d;
+        }
       }
     }
 
@@ -57,22 +55,7 @@
     }
   }
 
-  void setCurrentUrl(DownloadUrlLink link) {
-    currentUrl = link;
-    update();
-  }
-
   void setCurrentCommand(DownloadCommandLink cmd) {
     currentCommand = cmd;
-    update();
-  }
-
-  private void update() {
-    if (currentCommand != null && currentUrl != null) {
-      currentCommand.setCurrentUrl(currentUrl);
-    } else if (currentCommand != null &&
-        currentCommand.getCmdType().equals(DownloadCommand.REPO_DOWNLOAD)) {
-      currentCommand.setCurrentUrl(null);
-    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.java
deleted file mode 100644
index a0313ad..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2008 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.client.download;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface DownloadMessages extends Messages {
-  String anonymousDownload(String protocol);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.properties
deleted file mode 100644
index 3e34da2..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.properties
+++ /dev/null
@@ -1 +0,0 @@
-anonymousDownload = Anonymous {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
index 350dbed..7119bd2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
@@ -15,51 +15,32 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.client.info.DownloadInfo.DownloadCommandInfo;
+import com.google.gerrit.client.info.DownloadInfo.DownloadSchemeInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
-import java.util.Set;
+import java.util.List;
 
 public abstract class DownloadPanel extends FlowPanel {
-  protected String projectName;
+  protected final String project;
 
-  protected Set<DownloadScheme> allowedSchemes =
-      Gerrit.getConfig().getDownloadSchemes();
-  protected Set<DownloadCommand> allowedCommands =
-      Gerrit.getConfig().getDownloadCommands();
-  protected DownloadCommandLink.CopyableCommandLinkFactory cmdLinkfactory;
+  private final DownloadCommandPanel commands = new DownloadCommandPanel();
+  private final DownloadUrlPanel urls = new DownloadUrlPanel();
+  private final CopyableLabel copyLabel = new CopyableLabel("");
 
-  protected DownloadCommandPanel commands = new DownloadCommandPanel();
-  protected DownloadUrlPanel urls = new DownloadUrlPanel(commands);
-  protected CopyableLabel copyLabel = new CopyableLabel("");
-
-  public DownloadPanel(String project, String ref, boolean allowAnonymous) {
-    this.projectName = project;
-
+  public DownloadPanel(String project, boolean allowAnonymous) {
+    this.project = project;
     copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
-    urls.add(DownloadUrlLink.createDownloadUrlLinks(project, ref, allowAnonymous));
-    cmdLinkfactory = new DownloadCommandLink.CopyableCommandLinkFactory(
-        copyLabel, urls);
+    urls.add(DownloadUrlLink.createDownloadUrlLinks(allowAnonymous, this));
 
-    populateDownloadCommandLinks();
     setupWidgets();
   }
 
-  protected void setupWidgets() {
-    if (!commands.isEmpty()) {
-      final AccountGeneralPreferences pref;
-      if (Gerrit.isSignedIn()) {
-        pref = Gerrit.getUserAccount().getGeneralPreferences();
-      } else {
-        pref = new AccountGeneralPreferences();
-        pref.resetToDefaults();
-      }
-      commands.select(pref.getDownloadCommand());
-      urls.select(pref.getDownloadUrl());
+  private void setupWidgets() {
+    if (!urls.isEmpty()) {
+      urls.select(Gerrit.getUserPreferences().downloadScheme());
 
       FlowPanel p = new FlowPanel();
       p.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeader());
@@ -74,5 +55,14 @@
     }
   }
 
-  protected abstract void populateDownloadCommandLinks();
+  void populateDownloadCommandLinks(DownloadSchemeInfo schemeInfo) {
+    commands.clear();
+    for (DownloadCommandInfo cmd : getCommands(schemeInfo)) {
+      commands.add(new DownloadCommandLink(copyLabel, cmd));
+    }
+    commands.select();
+  }
+
+  protected abstract List<DownloadCommandInfo> getCommands(
+      DownloadSchemeInfo schemeInfo);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
index ce5c060..2a3ca5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
@@ -15,214 +15,52 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.DownloadInfo.DownloadSchemeInfo;
 import com.google.gwt.aria.client.Roles;
-import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 public class DownloadUrlLink extends Anchor implements ClickHandler {
-  public static class DownloadRefUrlLink extends DownloadUrlLink {
-    protected String projectName;
-    protected String ref;
-
-    protected DownloadRefUrlLink(DownloadScheme urlType,
-        String text, String project, String ref) {
-      super(urlType, text);
-      this.projectName = project;
-      this.ref = ref;
-    }
-
-    protected void appendRef(StringBuilder r) {
-      if (ref != null) {
-        r.append(" ");
-        r.append(ref);
-      }
-    }
-  }
-
-  public static class AnonGitLink extends DownloadRefUrlLink {
-    public AnonGitLink(String project, String ref) {
-      super(DownloadScheme.ANON_GIT, Util.M.anonymousDownload("Git"), project, ref);
-    }
-
-    @Override
-    public String getUrlData() {
-      StringBuilder r = new StringBuilder();
-      r.append(Gerrit.getConfig().getGitDaemonUrl());
-      r.append(projectName);
-      appendRef(r);
-      return r.toString();
-    }
-  }
-
-  public static class AnonHttpLink extends DownloadRefUrlLink {
-    public AnonHttpLink(String project, String ref) {
-      super(DownloadScheme.ANON_HTTP, Util.M.anonymousDownload("HTTP"), project, ref);
-    }
-
-    @Override
-    public String getUrlData() {
-      StringBuilder r = new StringBuilder();
-      if (Gerrit.getConfig().getGitHttpUrl() != null) {
-        r.append(Gerrit.getConfig().getGitHttpUrl());
-      } else {
-        r.append(hostPageUrl);
-      }
-      r.append(projectName);
-      appendRef(r);
-      return r.toString();
-    }
-  }
-
-  public static class SshLink extends DownloadRefUrlLink {
-    public SshLink(String project, String ref) {
-      super(DownloadScheme.SSH, "SSH", project, ref);
-    }
-
-    @Override
-    public String getUrlData() {
-      String sshAddr = Gerrit.getConfig().getSshdAddress();
-      final StringBuilder r = new StringBuilder();
-      r.append("ssh://");
-      r.append(Gerrit.getUserAccount().getUserName());
-      r.append("@");
-      if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
-        r.append(Window.Location.getHostName());
-      }
-      if (sshAddr.startsWith("*")) {
-        sshAddr = sshAddr.substring(1);
-      }
-      r.append(sshAddr);
-      r.append("/");
-      r.append(projectName);
-      appendRef(r);
-      return r.toString();
-    }
-  }
-
-  public static class HttpLink extends DownloadRefUrlLink {
-    protected boolean anonymous;
-
-    public HttpLink(String project, String ref, boolean anonymous) {
-      super(DownloadScheme.HTTP, "HTTP", project, ref);
-      this.anonymous = anonymous;
-    }
-
-    @Override
-    public String getUrlData() {
-      final StringBuilder r = new StringBuilder();
-      if (Gerrit.getConfig().getGitHttpUrl() != null
-          && (anonymous || siteReliesOnHttp())) {
-        r.append(Gerrit.getConfig().getGitHttpUrl());
-      } else {
-        String base = hostPageUrl;
-        int p = base.indexOf("://");
-        int s = base.indexOf('/', p + 3);
-        if (s < 0) {
-          s = base.length();
-        }
-        String host = base.substring(p + 3, s);
-        if (host.contains("@")) {
-          host = host.substring(host.indexOf('@') + 1);
-        }
-
-        r.append(base.substring(0, p + 3));
-        r.append(Gerrit.getUserAccount().getUserName());
-        r.append('@');
-        r.append(host);
-        r.append(base.substring(s));
-      }
-      r.append(projectName);
-      appendRef(r);
-      return r.toString();
-    }
-  }
-
-  public static boolean siteReliesOnHttp() {
-    return Gerrit.getConfig().getGitHttpUrl() != null
-        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
-        && !Gerrit.getConfig().siteHasUsernames();
-  }
-
-  public static List<DownloadUrlLink> createDownloadUrlLinks(String project,
-      String ref, boolean allowAnonymous) {
+  public static List<DownloadUrlLink> createDownloadUrlLinks(
+      boolean allowAnonymous, DownloadPanel downloadPanel) {
     List<DownloadUrlLink> urls = new ArrayList<>();
-    Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
-
-    if (allowAnonymous
-        && Gerrit.getConfig().getGitDaemonUrl() != null
-        && (allowedSchemes.contains(DownloadScheme.ANON_GIT) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.AnonGitLink(project, ref));
-    }
-
-    if (allowAnonymous
-        && (allowedSchemes.contains(DownloadScheme.ANON_HTTP) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.AnonHttpLink(project, ref));
-    }
-
-    if (Gerrit.getConfig().getSshdAddress() != null
-        && hasUserName()
-        && (allowedSchemes.contains(DownloadScheme.SSH) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.SshLink(project, ref));
-    }
-
-    if ((hasUserName() || siteReliesOnHttp())
-        && (allowedSchemes.contains(DownloadScheme.HTTP)
-            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.HttpLink(project, ref, allowAnonymous));
+    for (String s : Gerrit.info().download().schemes()) {
+      DownloadSchemeInfo scheme = Gerrit.info().download().scheme(s);
+      if (scheme.isAuthRequired() && !allowAnonymous) {
+        continue;
+      }
+      urls.add(new DownloadUrlLink(downloadPanel, scheme, s));
     }
     return urls;
   }
 
-  private static boolean hasUserName() {
-    return Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0;
-  }
+  private final DownloadPanel downloadPanel;
+  private final DownloadSchemeInfo schemeInfo;
+  private final String schemeName;
 
-  protected DownloadScheme urlType;
-  protected String urlData;
-  protected String hostPageUrl = GWT.getHostPageBaseURL();
-
-  public DownloadUrlLink(DownloadScheme urlType, String text, String urlData) {
-    this(text);
-    this.urlType = urlType;
-    this.urlData = urlData;
-  }
-
-  public DownloadUrlLink(DownloadScheme urlType, String text) {
-    this(text);
-    this.urlType = urlType;
-  }
-
-  public DownloadUrlLink(String text) {
-    super(text);
+  public DownloadUrlLink(DownloadPanel downloadPanel,
+      DownloadSchemeInfo schemeInfo, String schemeName) {
+    super(schemeName);
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
     addClickHandler(this);
 
-    if (!hostPageUrl.endsWith("/")) {
-      hostPageUrl += "/";
-    }
+    this.downloadPanel = downloadPanel;
+    this.schemeInfo = schemeInfo;
+    this.schemeName = schemeName;
   }
 
-  public String getUrlData() {
-    return urlData;
+  public String getSchemeName() {
+    return schemeName;
   }
 
   @Override
@@ -232,33 +70,33 @@
 
     select();
 
-    if (Gerrit.isSignedIn()) {
-      // If the user is signed-in, remember this choice for future panels.
-      //
-      AccountGeneralPreferences pref =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      pref.setDownloadUrl(urlType);
-      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
-          new AsyncCallback<VoidResult>() {
+    AccountPreferencesInfo prefs = Gerrit.getUserPreferences();
+    if (Gerrit.isSignedIn() && !schemeName.equals(prefs.downloadScheme())) {
+      prefs.downloadScheme(schemeName);
+      AccountPreferencesInfo in = AccountPreferencesInfo.create();
+      in.downloadScheme(schemeName);
+      AccountApi.self().view("preferences")
+          .put(in, new AsyncCallback<JavaScriptObject>() {
             @Override
-            public void onFailure(Throwable caught) {
+            public void onSuccess(JavaScriptObject result) {
             }
 
             @Override
-            public void onSuccess(VoidResult result) {
+            public void onFailure(Throwable caught) {
             }
           });
     }
   }
 
   void select() {
+    downloadPanel.populateDownloadCommandLinks(schemeInfo);
+
     DownloadUrlPanel parent = (DownloadUrlPanel) getParent();
     for (Widget w : parent) {
       if (w != this && w instanceof DownloadUrlLink) {
         w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
       }
     }
-    parent.setCurrentUrl(this);
     addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
index 8d07498..541c6f3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
@@ -23,10 +22,8 @@
 import java.util.Collection;
 
 public class DownloadUrlPanel extends FlowPanel {
-  private final DownloadCommandPanel commandPanel;
 
-  public DownloadUrlPanel(final DownloadCommandPanel commandPanel) {
-    this.commandPanel = commandPanel;
+  public DownloadUrlPanel() {
     setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
     Roles.getTablistRole().set(getElement());
   }
@@ -35,7 +32,7 @@
     return getWidgetCount() == 0;
   }
 
-  public void select(AccountGeneralPreferences.DownloadScheme urlType) {
+  public void select(String schemeName) {
     DownloadUrlLink first = null;
 
     for (Widget w : this) {
@@ -44,7 +41,7 @@
         if (first == null) {
           first = d;
         }
-        if (d.urlType == urlType) {
+        if (d.getSchemeName().equals(schemeName)) {
           d.select();
           return;
         }
@@ -58,10 +55,6 @@
     }
   }
 
-  void setCurrentUrl(DownloadUrlLink link) {
-    commandPanel.setCurrentUrl(link);
-  }
-
   public void add(Collection<DownloadUrlLink> links) {
     for (Widget link: links) {
       add(link);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/Util.java
deleted file mode 100644
index 510361b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/Util.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2008 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.client.download;
-
-import com.google.gwt.core.client.GWT;
-
-public class Util {
-  public static final DownloadMessages M = GWT.create(DownloadMessages.class);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
index c833c5d..dda5fc2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
@@ -19,7 +19,7 @@
 import com.google.gwt.core.client.JsArray;
 
 public class EditFileInfo extends JavaScriptObject {
-  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<DiffWebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
   protected EditFileInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java
new file mode 100644
index 0000000..d297f4d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesAction.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2014 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.client.editor;
+
+import com.google.gerrit.client.account.EditPreferences;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+class EditPreferencesAction {
+  private final EditScreen view;
+  private final EditPreferences prefs;
+  private PopupPanel popup;
+  private EditPreferencesBox current;
+
+  EditPreferencesAction(EditScreen view, EditPreferences prefs) {
+    this.view = view;
+    this.prefs = prefs;
+  }
+
+  void show() {
+    if (popup != null) {
+      hide();
+      return;
+    }
+
+    current = new EditPreferencesBox(view);
+    current.set(prefs);
+
+    popup = new PopupPanel(true, false);
+    popup.setStyleName(current.style.dialog());
+    popup.add(current);
+    popup.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        view.getEditor().focus();
+        popup = null;
+        current = null;
+      }
+    });
+    popup.setPopupPositionAndShow(new PositionCallback() {
+      @Override
+      public void setPosition(int offsetWidth, int offsetHeight) {
+        popup.setPopupPosition(300, 120);
+      }
+    });
+  }
+
+  void hide() {
+    if (popup != null) {
+      popup.hide();
+      popup = null;
+      current = null;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
new file mode 100644
index 0000000..7a439b5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.java
@@ -0,0 +1,324 @@
+// Copyright (C) 2014 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.client.editor;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.account.EditPreferences;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.NpIntTextBox;
+import com.google.gerrit.extensions.client.KeyMapType;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ToggleButton;
+import com.google.gwt.user.client.ui.UIObject;
+
+import net.codemirror.theme.ThemeLoader;
+
+/** Displays current edit preferences. */
+public class EditPreferencesBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, EditPreferencesBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  public interface Style extends CssResource {
+    String dialog();
+  }
+
+  private final EditScreen view;
+  private EditPreferences prefs;
+
+  @UiField Style style;
+  @UiField Element header;
+  @UiField Anchor close;
+  @UiField NpIntTextBox tabWidth;
+  @UiField NpIntTextBox lineLength;
+  @UiField NpIntTextBox cursorBlinkRate;
+  @UiField ToggleButton topMenu;
+  @UiField ToggleButton syntaxHighlighting;
+  @UiField ToggleButton showTabs;
+  @UiField ToggleButton whitespaceErrors;
+  @UiField ToggleButton lineNumbers;
+  @UiField ToggleButton matchBrackets;
+  @UiField ToggleButton lineWrapping;
+  @UiField ToggleButton autoCloseBrackets;
+  @UiField ListBox theme;
+  @UiField ListBox keyMap;
+  @UiField Button apply;
+  @UiField Button save;
+
+  public EditPreferencesBox(EditScreen view) {
+    this.view = view;
+    initWidget(uiBinder.createAndBindUi(this));
+    initTheme();
+    initKeyMapType();
+
+    if (view == null) {
+      UIObject.setVisible(header, false);
+      apply.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+    }
+  }
+
+  public Style getStyle() {
+    return style;
+  }
+
+  public void set(EditPreferences prefs) {
+    this.prefs = prefs;
+
+    tabWidth.setIntValue(prefs.tabSize());
+    lineLength.setIntValue(prefs.lineLength());
+    cursorBlinkRate.setIntValue(prefs.cursorBlinkRate());
+    topMenu.setValue(!prefs.hideTopMenu());
+    syntaxHighlighting.setValue(prefs.syntaxHighlighting());
+    showTabs.setValue(prefs.showTabs());
+    whitespaceErrors.setValue(prefs.showWhitespaceErrors());
+    lineNumbers.setValue(prefs.hideLineNumbers());
+    matchBrackets.setValue(prefs.matchBrackets());
+    lineWrapping.setValue(prefs.lineWrapping());
+    autoCloseBrackets.setValue(prefs.autoCloseBrackets());
+    setTheme(prefs.theme());
+    setKeyMapType(prefs.keyMapType());
+  }
+
+  @UiHandler("tabWidth")
+  void onTabWidth(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      prefs.tabSize(Math.max(1, Integer.parseInt(v)));
+      if (view != null) {
+        view.getEditor().setOption("tabSize", v);
+      }
+    }
+  }
+
+  @UiHandler("lineLength")
+  void onLineLength(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      prefs.lineLength(Math.max(1, Integer.parseInt(v)));
+      if (view != null) {
+        view.setLineLength(prefs.lineLength());
+      }
+    }
+  }
+
+  @UiHandler("cursorBlinkRate")
+  void onCursoBlinkRate(ValueChangeEvent<String> e) {
+    String v = e.getValue();
+    if (v != null && v.length() > 0) {
+      // A negative value hides the cursor entirely:
+      // don't let user shoot himself in the foot.
+      prefs.cursorBlinkRate(Math.max(0, Integer.parseInt(v)));
+      if (view != null) {
+        view.getEditor().setOption("cursorBlinkRate", prefs.cursorBlinkRate());
+      }
+    }
+  }
+
+  @UiHandler("topMenu")
+  void onTopMenu(ValueChangeEvent<Boolean> e) {
+    prefs.hideTopMenu(!e.getValue());
+    if (view != null) {
+      Gerrit.setHeaderVisible(!prefs.hideTopMenu());
+      view.resizeCodeMirror();
+    }
+  }
+
+  @UiHandler("showTabs")
+  void onShowTabs(ValueChangeEvent<Boolean> e) {
+    prefs.showTabs(e.getValue());
+    if (view != null) {
+      view.setShowTabs(prefs.showTabs());
+    }
+  }
+
+  @UiHandler("whitespaceErrors")
+  void onshowTrailingSpace(ValueChangeEvent<Boolean> e) {
+    prefs.showWhitespaceErrors(e.getValue());
+    if (view != null) {
+      view.setShowWhitespaceErrors(prefs.showWhitespaceErrors());
+    }
+  }
+
+  @UiHandler("lineNumbers")
+  void onLineNumbers(ValueChangeEvent<Boolean> e) {
+    prefs.hideLineNumbers(e.getValue());
+    if (view != null) {
+      view.setShowLineNumbers(prefs.hideLineNumbers());
+    }
+  }
+
+  @UiHandler("syntaxHighlighting")
+  void onSyntaxHighlighting(ValueChangeEvent<Boolean> e) {
+    prefs.syntaxHighlighting(e.getValue());
+    if (view != null) {
+      view.setSyntaxHighlighting(prefs.syntaxHighlighting());
+    }
+  }
+
+  @UiHandler("matchBrackets")
+  void onMatchBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.matchBrackets(e.getValue());
+    if (view != null) {
+      view.getEditor().setOption("matchBrackets", prefs.matchBrackets());
+    }
+  }
+
+  @UiHandler("lineWrapping")
+  void onLineWrapping(ValueChangeEvent<Boolean> e) {
+    prefs.lineWrapping(e.getValue());
+    if (view != null) {
+      view.getEditor().setOption("lineWrapping", prefs.lineWrapping());
+    }
+  }
+
+  @UiHandler("autoCloseBrackets")
+  void onCloseBrackets(ValueChangeEvent<Boolean> e) {
+    prefs.autoCloseBrackets(e.getValue());
+    if (view != null) {
+      view.getEditor().setOption("autoCloseBrackets", prefs.autoCloseBrackets());
+    }
+  }
+
+  @UiHandler("theme")
+  void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
+    final Theme newTheme = Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
+    prefs.theme(newTheme);
+    if (view != null) {
+      ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          view.getEditor().operation(new Runnable() {
+            @Override
+            public void run() {
+              String t = newTheme.name().toLowerCase();
+              view.getEditor().setOption("theme", t);
+            }
+          });
+        }
+      });
+    }
+  }
+
+  @UiHandler("keyMap")
+  void onKeyMap(@SuppressWarnings("unused") ChangeEvent e) {
+    KeyMapType keyMapType = KeyMapType.valueOf(
+        keyMap.getValue(keyMap.getSelectedIndex()));
+    prefs.keyMapType(keyMapType);
+    if (view != null) {
+      view.getEditor().setOption("keyMap", keyMapType.name().toLowerCase());
+    }
+  }
+
+  @UiHandler("apply")
+  void onApply(@SuppressWarnings("unused") ClickEvent e) {
+    close();
+  }
+
+  @UiHandler("save")
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
+    AccountApi.putEditPreferences(prefs, new GerritCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult n) {
+        prefs.copyTo(Gerrit.getEditPreferences());
+      }
+    });
+    close();
+  }
+
+  @UiHandler("close")
+  void onClose(ClickEvent e) {
+    e.preventDefault();
+    close();
+  }
+
+  private void close() {
+    ((PopupPanel) getParent()).hide();
+  }
+
+  private void setTheme(Theme v) {
+    String name = v != null ? v.name() : Theme.DEFAULT.name();
+    for (int i = 0; i < theme.getItemCount(); i++) {
+      if (theme.getValue(i).equals(name)) {
+        theme.setSelectedIndex(i);
+        return;
+      }
+    }
+    theme.setSelectedIndex(0);
+  }
+
+  private void initTheme() {
+    theme.addItem(
+        Theme.DEFAULT.name().toLowerCase(),
+        Theme.DEFAULT.name());
+    theme.addItem(
+        Theme.ECLIPSE.name().toLowerCase(),
+        Theme.ECLIPSE.name());
+    theme.addItem(
+        Theme.ELEGANT.name().toLowerCase(),
+        Theme.ELEGANT.name());
+    theme.addItem(
+        Theme.NEAT.name().toLowerCase(),
+        Theme.NEAT.name());
+    theme.addItem(
+        Theme.MIDNIGHT.name().toLowerCase(),
+        Theme.MIDNIGHT.name());
+    theme.addItem(
+        Theme.NIGHT.name().toLowerCase(),
+        Theme.NIGHT.name());
+    theme.addItem(
+        Theme.TWILIGHT.name().toLowerCase(),
+        Theme.TWILIGHT.name());
+  }
+
+  private void setKeyMapType(KeyMapType v) {
+    String name = v != null ? v.name() : KeyMapType.DEFAULT.name();
+    for (int i = 0; i < keyMap.getItemCount(); i++) {
+      if (keyMap.getValue(i).equals(name)) {
+        keyMap.setSelectedIndex(i);
+        return;
+      }
+    }
+    keyMap.setSelectedIndex(0);
+  }
+
+  private void initKeyMapType() {
+    keyMap.addItem(
+        KeyMapType.DEFAULT.name().toLowerCase(),
+        KeyMapType.DEFAULT.name());
+    keyMap.addItem(
+        KeyMapType.EMACS.name().toLowerCase(),
+        KeyMapType.EMACS.name());
+    keyMap.addItem(
+        KeyMapType.VIM.name().toLowerCase(),
+        KeyMapType.VIM.name());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
new file mode 100644
index 0000000..3d0692c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditPreferencesBox.ui.xml
@@ -0,0 +1,262 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:x='urn:import:com.google.gerrit.client.ui'>
+  <ui:style type='com.google.gerrit.client.editor.EditPreferencesBox.Style'>
+    @external .gwt-TextBox;
+    @external .gwt-ToggleButton .html-face;
+    @external .gwt-ToggleButton-up;
+    @external .gwt-ToggleButton-up-hovering;
+    @external .gwt-ToggleButton-up-disabled;
+    @external .gwt-ToggleButton-down;
+    @external .gwt-ToggleButton-down-hovering;
+    @external .gwt-ToggleButton-down-disabled;
+
+    .dialog {
+      background: rgba(0, 0, 0, 0.85) none repeat scroll 0 50%;
+      color: #ffffff;
+      font-family: arial,sans-serif;
+      font-weight: bold;
+      overflow: auto !important;
+      bottom: 0;
+      text-align: left;
+      text-shadow: 1px 1px 7px #000000;
+      min-width: 300px;
+      z-index: 200;
+      border-radius: 10px;
+    }
+
+    @if user.agent safari {
+      .dialog {
+        \-webkit-border-radius: 10px;
+      }
+    }
+
+    @if user.agent gecko1_8 {
+      .dialog {
+        \-moz-border-radius: 10px;
+      }
+    }
+
+    .box { margin: 10px; }
+    .box .gwt-TextBox { padding: 0; }
+    .context { vertical-align: bottom; }
+
+    .table tr { min-height: 23px; }
+    .table th,
+    .table td {
+      white-space: nowrap;
+      color: #ffffff;
+    }
+    .table th {
+      padding-right: 8px;
+      text-align: right;
+    }
+
+    .box a,
+    .box a:visited,
+    .box a:hover {
+      color: #dddd00;
+    }
+
+    .box .gwt-ToggleButton {
+      position: relative;
+      height: 19px;
+      width: 140px;
+      background: #fff;
+      color: #000;
+      text-shadow: none;
+    }
+    .box .gwt-ToggleButton .html-face {
+      position: absolute;
+      top: 0;
+      width: 68px;
+      height: 17px;
+      line-height: 17px;
+      text-align: center;
+      border-width: 1px;
+    }
+
+    .box .gwt-ToggleButton-up,
+    .box .gwt-ToggleButton-up-hovering,
+    .box .gwt-ToggleButton-up-disabled,
+    .box .gwt-ToggleButton-down,
+    .box .gwt-ToggleButton-down-hovering,
+    .box .gwt-ToggleButton-down-disabled {
+      padding: 0;
+      border: 0;
+    }
+    .box .gwt-ToggleButton-up .html-face,
+    .box .gwt-ToggleButton-up-hovering .html-face {
+      left: 0;
+      background: #cacaca;
+      border-style: outset;
+    }
+    .box .gwt-ToggleButton-down .html-face,
+    .box .gwt-ToggleButton-down-hovering .html-face {
+      right: 0;
+      background: #bcf;
+      border-style: inset;
+    }
+
+    .box button {
+      margin: 6px 3px 0 0;
+      border-color: rgba(0, 0, 0, 0.1);
+      text-align: center;
+      font-size: 8pt;
+      font-weight: bold;
+      border: 1px solid;
+      cursor: pointer;
+      color: #444;
+      background-color: #f5f5f5;
+      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+      -webkit-border-radius: 2px;
+      -webkit-box-sizing: content-box;
+    }
+    .box button div {
+      color: #444;
+      height: 10px;
+      min-width: 54px;
+      line-height: 10px;
+      white-space: nowrap;
+    }
+
+    button.apply {
+      background-color: #4d90fe;
+      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+    }
+    button.apply div { color: #fff; }
+
+    button.save {
+      margin-left: 10px;
+      color: #d14836;
+      background-color: #d14836;
+      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
+    }
+    button.save div { color: #fff; }
+  </ui:style>
+
+  <g:HTMLPanel styleName='{style.box}'>
+    <div ui:field='header'>
+      <table style='width: 100%'>
+        <tr>
+          <td><ui:msg>Edit Preferences</ui:msg></td>
+          <td style='text-align: right'>
+            <g:Anchor ui:field='close' href='javascript:;'><ui:msg>Close</ui:msg></g:Anchor>
+          </td>
+        </tr>
+      </table>
+      <hr/>
+    </div>
+    <table class='{style.table}'>
+      <tr>
+        <th><ui:msg>Theme</ui:msg></th>
+        <td><g:ListBox ui:field='theme'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Key Map</ui:msg></th>
+        <td><g:ListBox ui:field='keyMap'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Tab Width</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='tabWidth'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Columns</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='lineLength'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Cursor Blink Rate</ui:msg></th>
+        <td><x:NpIntTextBox ui:field='cursorBlinkRate'
+            visibleLength='4'
+            alignment='RIGHT'/></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Top Menu</ui:msg></th>
+        <td><g:ToggleButton ui:field='topMenu'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Syntax Highlighting</ui:msg></th>
+        <td><g:ToggleButton ui:field='syntaxHighlighting'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Show Tabs</ui:msg></th>
+        <td><g:ToggleButton ui:field='showTabs'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+      <th><ui:msg>Whitespace Errors</ui:msg></th>
+        <td><g:ToggleButton ui:field='whitespaceErrors'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Line Numbers</ui:msg></th>
+        <td><g:ToggleButton ui:field='lineNumbers'>
+          <g:upFace><ui:msg>Hide</ui:msg></g:upFace>
+          <g:downFace><ui:msg>Show</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Match Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='matchBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Line Wrapping</ui:msg></th>
+        <td><g:ToggleButton ui:field='lineWrapping'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <th><ui:msg>Auto Close Brackets</ui:msg></th>
+        <td><g:ToggleButton ui:field='autoCloseBrackets'>
+          <g:upFace><ui:msg>Off</ui:msg></g:upFace>
+          <g:downFace><ui:msg>On</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
+        <td></td>
+        <td>
+          <g:Button ui:field='apply' styleName='{style.apply}'>
+            <div><ui:msg>Apply</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='save' styleName='{style.save}'>
+            <div><ui:msg>Save</ui:msg></div>
+          </g:Button>
+        </td>
+      </tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index efdbe44..a546c62 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -22,14 +22,14 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.JumpKeys;
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.account.DiffPreferences;
+import com.google.gerrit.client.account.EditPreferences;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeEditApi;
-import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.diff.DiffApi;
 import com.google.gerrit.client.diff.DiffInfo;
-import com.google.gerrit.client.diff.FileInfo;
 import com.google.gerrit.client.diff.Header;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -42,6 +42,7 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.KeyMapType;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -87,7 +88,8 @@
   private final PatchSet.Id revision;
   private final String path;
   private final int startLine;
-  private DiffPreferences prefs;
+  private EditPreferences prefs;
+  private EditPreferencesAction editPrefsAction;
   private CodeMirror cm;
   private HttpResponse<NativeString> content;
   private EditFileInfo editFileInfo;
@@ -113,7 +115,7 @@
     this.revision = patch.getParentKey();
     this.path = patch.get();
     this.startLine = startLine - 1;
-    prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
+    setRequiresSignIn(true);
     add(uiBinder.createAndBindUi(this));
     addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
   }
@@ -129,6 +131,8 @@
   protected void onLoad() {
     super.onLoad();
 
+    prefs = EditPreferences.create(Gerrit.getEditPreferences());
+
     CallbackGroup group1 = new CallbackGroup();
     final CallbackGroup group2 = new CallbackGroup();
     final CallbackGroup group3 = new CallbackGroup();
@@ -163,7 +167,6 @@
           }
         }));
 
-
     if (revision.get() == 0) {
       ChangeEditApi.getMeta(revision, path,
           group1.add(new AsyncCallback<EditFileInfo>() {
@@ -185,7 +188,7 @@
         .get(group1.add(new AsyncCallback<DiffInfo>() {
           @Override
           public void onSuccess(DiffInfo diffInfo) {
-            diffLinks = diffInfo.web_links();
+            diffLinks = diffInfo.webLinks();
           }
 
           @Override
@@ -224,7 +227,6 @@
       @Override
       protected void preDisplay(Void result) {
         initEditor(content);
-        content = null;
 
         renderLinks(editFileInfo, diffLinks);
         editFileInfo = null;
@@ -237,9 +239,18 @@
   @Override
   public void registerKeys() {
     super.registerKeys();
-    cm.addKeyMap(KeyMap.create()
+    KeyMap localKeyMap = KeyMap.create();
+    localKeyMap
         .on("Ctrl-L", gotoLine())
-        .on("Cmd-L", gotoLine()));
+        .on("Cmd-L", gotoLine())
+        .on("Cmd-S", save());
+
+    // TODO(davido): Find a better way to prevent key maps collisions
+    if (prefs.keyMapType() != KeyMapType.EMACS) {
+      localKeyMap.on("Ctrl-S", save());
+    }
+
+    cm.addKeyMap(localKeyMap);
   }
 
   private Runnable gotoLine() {
@@ -298,9 +309,8 @@
 
     cm.adjustHeight(header.getOffsetHeight());
     cm.on("cursorActivity", updateCursorPosition());
-    cm.extras().showTabs(prefs.showTabs());
-    cm.extras().lineLength(
-        Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
+    setShowTabs(prefs.showTabs());
+    setLineLength(prefs.lineLength());
     cm.refresh();
     cm.focus();
 
@@ -308,6 +318,7 @@
       cm.scrollToLine(startLine);
     }
     updateActiveLine();
+    editPrefsAction = new EditPreferencesAction(this, prefs);
   }
 
   @Override
@@ -327,6 +338,15 @@
     JumpKeys.enable(true);
   }
 
+  CodeMirror getEditor() {
+    return cm;
+  }
+
+  @UiHandler("editSettings")
+  void onEditSetting(@SuppressWarnings("unused") ClickEvent e) {
+    editPrefsAction.show();
+  }
+
   @UiHandler("save")
   void onSave(@SuppressWarnings("unused") ClickEvent e) {
     save().run();
@@ -340,6 +360,52 @@
     }
   }
 
+  void setLineLength(int length) {
+    cm.extras().lineLength(
+        Patch.COMMIT_MSG.equals(path) ? 72 : length);
+  }
+
+  void setShowLineNumbers(boolean show) {
+    cm.setOption("lineNumbers", show);
+  }
+
+  void setShowWhitespaceErrors(final boolean show) {
+    cm.operation(new Runnable() {
+      @Override
+      public void run() {
+        cm.setOption("showTrailingSpace", show);
+      }
+    });
+  }
+
+  void setShowTabs(boolean show) {
+    cm.extras().showTabs(show);
+  }
+
+  void resizeCodeMirror() {
+    cm.adjustHeight(header.getOffsetHeight());
+  }
+
+  void setSyntaxHighlighting(boolean b) {
+    ModeInfo modeInfo = ModeInfo.findMode(content.getContentType(), path);
+    final String mode = modeInfo != null ? modeInfo.mode() : null;
+    if (b && mode != null && !mode.isEmpty()) {
+      injectMode(mode, new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          cm.setOption("mode", mode);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          prefs.syntaxHighlighting(false);
+        }
+      });
+    } else {
+      cm.setOption("mode", (String) null);
+    }
+  }
+
   private void upToChange() {
     Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
   }
@@ -356,20 +422,19 @@
     cm = CodeMirror.create(editor, Configuration.create()
         .set("value", content)
         .set("readOnly", false)
-        .set("cursorBlinkRate", 0)
+        .set("cursorBlinkRate", prefs.cursorBlinkRate())
         .set("cursorHeight", 0.85)
-        .set("lineNumbers", true)
+        .set("lineNumbers", prefs.hideLineNumbers())
         .set("tabSize", prefs.tabSize())
         .set("lineWrapping", false)
+        .set("matchBrackets", prefs.matchBrackets())
+        .set("autoCloseBrackets", prefs.autoCloseBrackets())
         .set("scrollbarStyle", "overlay")
         .set("styleSelectedText", true)
-        .set("showTrailingSpace", true)
-        .set("keyMap", "default")
+        .set("showTrailingSpace", prefs.showWhitespaceErrors())
+        .set("keyMap", prefs.keyMapType().name().toLowerCase())
         .set("theme", prefs.theme().name().toLowerCase())
         .set("mode", mode != null ? mode.mode() : null));
-    cm.addKeyMap(KeyMap.create()
-        .on("Cmd-S", save())
-        .on("Ctrl-S", save()));
   }
 
   private void renderLinks(EditFileInfo editInfo,
@@ -377,7 +442,7 @@
     renderLinksToDiff();
 
     if (editInfo != null) {
-      renderLinks(Natives.asList(editInfo.web_links()));
+      renderLinks(Natives.asList(editInfo.webLinks()));
     } else if (diffLinks != null) {
       renderLinks(Natives.asList(diffLinks));
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
index 93c3bb9..88af398 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
@@ -16,7 +16,8 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
+  <ui:style gss='false'>
     @external .CodeMirror, .CodeMirror-cursor;
 
     .header {
@@ -115,6 +116,13 @@
       padding-top: 2px;
       padding-right: 3px;
     }
+
+    .preferences {
+      position: relative;
+      top: 2px;
+      cursor: pointer;
+      outline: none;
+    }
   </ui:style>
   <g:HTMLPanel styleName='{style.header}'>
     <div class='{style.headerLine}' ui:field='header'>
@@ -135,6 +143,13 @@
        <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
        <div class='{style.navigation}'>
          <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
+         <g:Image
+             ui:field='editSettings'
+             styleName='{style.preferences}'
+             resource='{ico.gear}'
+             title='Edit screen preferences'>
+            <ui:attribute name='title'/>
+         </g:Image>
        </div>
     </div>
     <div ui:field='editor' />
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 8d961ca..a511550 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -48,6 +48,10 @@
   font-family: norm-font;
 }
 
+button {
+  padding: 1px 6px;
+}
+
 .gerritBody {
   font-size: small;
   padding-left: 5px;
@@ -114,6 +118,10 @@
   cursor: pointer;
 }
 
+.extensionPanel {
+  padding-top: 10px;
+}
+
 /** MenuScreen **/
 .menuScreenMenuBar {
   background: topMenuColor;
@@ -297,13 +305,22 @@
   white-space: nowrap;
   display: inline;
 }
-.searchPanel .gwt-TextBox {
+.searchPanel .searchTextBox {
   font-size: 9pt;
+  margin: 5.286px 3px 0 0;
 }
-.searchPanel .gwt-Button {
-  font-size: 9pt;
-  margin-left: 2px;
-  padding: 3px 6px;
+.searchPanel .searchButton {
+  text-align: center;
+  font-size: 8pt;
+  font-weight: bold;
+  cursor: pointer;
+  border: 2px solid;
+  color: #FFF;
+  border-color: rgba(0, 0, 0, 0.15);
+  height: 14px;
+  background-color: #53A93F;
+  border-radius: 2px;
+  box-sizing: content-box;
 }
 .suggestBoxPopup {
   z-index: 200;
@@ -345,7 +362,7 @@
     opacity: 0.80;
   }
 }
-@if user.agent ie6 ie8 {
+@if user.agent ie8 {
   /* IE just doesn't do opacity the way we want, make our dialog
    * stand out in a way that it can't be missed against the page
    */
@@ -613,6 +630,10 @@
   padding-right: 3px;
 }
 
+.nowrap {
+  white-space: nowrap;
+}
+
 
 /** PatchContentTable **/
 .patchContentTable {
@@ -929,7 +950,14 @@
   margin-right: 0.5em;
 }
 .downloadLinkCopyLabel .gwt-TextBox {
-  width: 30em;
+  width: 40em;
+}
+.downloadLinkCopyLabel span {
+  width: 40em;
+  white-space: nowrap;
+  display: inline-block;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 /** UnifiedScreen **/
@@ -1007,18 +1035,6 @@
 .accountInfoBlock .gwt-Button {
   margin-left: 10px;
 }
-.accountContactPrivacyDetails {
-  margin-left: 10px;
-  margin-top: 5px;
-  margin-bottom: 5px;
-  width: 40em;
-}
-.accountContactOnFile {
-  margin-left: 10px;
-  margin-top: 5px;
-  margin-bottom: 5px;
-  font-weight: bold;
-}
 
 .addWatchPanel {
   margin-top: 10px;
@@ -1050,6 +1066,10 @@
   width: 100%;
 }
 
+.sshKeyTable td.dataCell, .sshKeyTable td.iconCell {
+  vertical-align: top;
+}
+
 .createProjectPanel {
   margin-bottom: 10px;
   background-color: trimColor;
@@ -1242,6 +1262,10 @@
   cursor: pointer;
 }
 
+.branchTableDeleteButton {
+  margin-top: 5px;
+}
+
 .branchTablePrevNextLinks {
   position: relative;
 }
@@ -1294,3 +1318,10 @@
   font-size: 7pt;
   padding: 1px;
 }
+
+/* List Screens */
+.pagingLink {
+  font-size: 18px;
+  margin-top: 5px;
+  margin-bottom: 15px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
index 0b178f2..93be87b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.groups;
 
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
@@ -125,7 +125,7 @@
     } else {
       MemberInput input = MemberInput.create();
       for (String member : members) {
-        input.add_member(member);
+        input.addMember(member);
       }
       members(group).post(input, cb);
     }
@@ -139,7 +139,7 @@
     } else {
       MemberInput in = MemberInput.create();
       for (Integer id : ids) {
-        in.add_member(id.toString());
+        in.addMember(id.toString());
       }
       group(group).view("members.delete").post(in, cb);
     }
@@ -172,7 +172,7 @@
     } else {
       IncludedGroupInput input = IncludedGroupInput.create();
       for (String includedGroup : includedGroups) {
-        input.add_group(includedGroup);
+        input.addGroup(includedGroup);
       }
       groups(group).post(input, cb);
     }
@@ -187,12 +187,18 @@
     } else {
       IncludedGroupInput in = IncludedGroupInput.create();
       for (AccountGroup.UUID g : ids) {
-        in.add_group(g.get());
+        in.addGroup(g.get());
       }
       group(group).view("groups.delete").post(in, cb);
     }
   }
 
+  /** Get audit log of a group. */
+  public static void getAuditLog(AccountGroup.UUID group,
+      AsyncCallback<JsArray<GroupAuditEventInfo>> cb) {
+    group(group).view("log.audit").get(cb);
+  }
+
   private static RestApi members(AccountGroup.UUID group) {
     return group(group).view("members");
   }
@@ -235,7 +241,7 @@
 
   private static class MemberInput extends JavaScriptObject {
     final native void init() /*-{ this.members = []; }-*/;
-    final native void add_member(String n) /*-{ this.members.push(n); }-*/;
+    final native void addMember(String n) /*-{ this.members.push(n); }-*/;
 
     static MemberInput create() {
       MemberInput m = (MemberInput) createObject();
@@ -249,7 +255,7 @@
 
   private static class IncludedGroupInput extends JavaScriptObject {
     final native void init() /*-{ this.groups = []; }-*/;
-    final native void add_group(String n) /*-{ this.groups.push(n); }-*/;
+    final native void addGroup(String n) /*-{ this.groups.push(n); }-*/;
 
     static IncludedGroupInput create() {
       IncludedGroupInput g = (IncludedGroupInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
new file mode 100644
index 0000000..f62ae22
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 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.client.groups;
+
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+
+public class GroupAuditEventInfo extends JavaScriptObject {
+  public enum Type {
+    ADD_USER, REMOVE_USER, ADD_GROUP, REMOVE_GROUP
+  }
+
+  public final Timestamp date() {
+    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
+  }
+
+  public final Type type() {
+    return Type.valueOf(typeRaw());
+  }
+
+  public final native AccountInfo user() /*-{ return this.user; }-*/;
+  public final native AccountInfo memberAsUser() /*-{ return this.member; }-*/;
+  public final native GroupInfo memberAsGroup() /*-{ return this.member; }-*/;
+
+  private final native String dateRaw() /*-{ return this.date; }-*/;
+  private final native String typeRaw() /*-{ return this.type; }-*/;
+
+  protected GroupAuditEventInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index f1e4e87..6142e5b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.groups;
 
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
index 94e5e58..a24e1dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
@@ -19,7 +19,7 @@
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
-/** Groups available from {@code /groups/} or {@code /accounts/{id}/groups}. */
+/** Groups available from {@code /groups/} or {@code /accounts/[id]/groups}. */
 public class GroupList extends JsArray<GroupInfo> {
   public static void my(AsyncCallback<GroupList> callback) {
     new RestApi("/accounts/self/groups").get(callback);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
index d788a1d..d63c212 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.groups;
 
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index efeb2ec..5b9203a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2008 The Android Open Source Project
+// Copyright (C) 2008 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.
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
@@ -28,11 +28,11 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.client.PrettyFormatter;
 import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -286,8 +286,8 @@
   }
 
   protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) {
-    AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
-    dp.setShowWhitespaceErrors(false);
+    DiffPreferencesInfo dp = s.getDiffPrefs();
+    dp.showWhitespaceErrors = false;
 
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
     f.setDiffPrefs(dp);
@@ -299,7 +299,7 @@
   }
 
   protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
-    AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
+    DiffPreferencesInfo dp = s.getDiffPrefs();
 
     SparseFileContent b = s.getB();
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
@@ -546,7 +546,7 @@
   private CommentEditorPanel findOrCreateCommentEditor(final int suggestRow,
       final int column, final PatchLineComment newComment, final boolean create) {
     int row = suggestRow;
-    int spans[] = new int[column + 1];
+    int[] spans = new int[column + 1];
     FIND_ROW: while (row < table.getRowCount()) {
       int col = 0;
       for (int cell = 0; row < table.getRowCount()
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 2538102..81494c0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -71,7 +71,7 @@
     comment = plc;
 
     addStyleName(Gerrit.RESOURCES.css().commentEditorPanel());
-    setAuthorNameText(Gerrit.getUserAccountInfo(), PatchUtil.C.draft());
+    setAuthorNameText(Gerrit.getUserAccount(), PatchUtil.C.draft());
     setMessageText(plc.getMessage());
     addDoubleClickHandler(this);
 
@@ -345,7 +345,7 @@
     if (c.getLine() > 0) {
       i.line(c.getLine());
     }
-    i.in_reply_to(c.getParentUuid());
+    i.inReplyTo(c.getParentUuid());
     i.message(c.getMessage());
     return i;
   }
@@ -359,7 +359,7 @@
             i.id()),
         i.line(),
         Gerrit.getUserAccount().getId(),
-        i.in_reply_to(),
+        i.inReplyTo(),
         i.updated());
     p.setMessage(i.message());
     p.setSide((short) (i.side() == Side.PARENT ? 0 : 1));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
index ca81537..f1bf3de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
@@ -20,7 +20,7 @@
 
 
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style>
+  <ui:style gss='false'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
     @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index 2a04c38..c7f9859 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -54,7 +54,7 @@
   private final KeyCommandSet keys;
   private final Grid table;
 
-  private KeyCommand cmds[] = new KeyCommand[2];
+  private KeyCommand[] cmds = new KeyCommand[2];
 
   NavLinks(KeyCommandSet kcs, PatchSet.Id forPatch) {
     patchSetId = forPatch;
@@ -99,6 +99,7 @@
 
     /* setup the cells */
     if (link != null) {
+      link.addStyleName(Gerrit.RESOURCES.css().nowrap());
       table.setWidget(0, nav.col, link);
     } else {
       table.clearCell(0, nav.col);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 39aadc3..422a4dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -69,9 +69,9 @@
   String commentCancelEdit();
 
   String whitespaceIGNORE_NONE();
-  String whitespaceIGNORE_SPACE_AT_EOL();
-  String whitespaceIGNORE_SPACE_CHANGE();
-  String whitespaceIGNORE_ALL_SPACE();
+  String whitespaceIGNORE_TRAILING();
+  String whitespaceIGNORE_LEADING_AND_TRAILING();
+  String whitespaceIGNORE_ALL();
 
   String previousFileHelp();
   String nextFileHelp();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 2f68822..aa6177b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -50,9 +50,9 @@
 commentCancelEdit = Cancel comment edit
 
 whitespaceIGNORE_NONE=None
-whitespaceIGNORE_SPACE_AT_EOL=At Line End
-whitespaceIGNORE_SPACE_CHANGE=Leading, At Line End
-whitespaceIGNORE_ALL_SPACE=All
+whitespaceIGNORE_TRAILING=At Line End
+whitespaceIGNORE_LEADING_AND_TRAILING=Leading, At Line End
+whitespaceIGNORE_ALL=All
 
 previousFileHelp = Previous file
 nextFileHelp = Next file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 264e043..b7ba64b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.NodeList;
 import com.google.gwt.dom.client.OptionElement;
@@ -156,7 +156,7 @@
   public void setEnableSmallFileFeatures(final boolean on) {
     enableSmallFileFeatures = on;
     if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(getValue().isSyntaxHighlighting());
+      syntaxHighlighting.setValue(getValue().syntaxHighlighting);
     } else {
       syntaxHighlighting.setValue(false);
     }
@@ -181,7 +181,7 @@
   public void setEnableIntralineDifference(final boolean on) {
     enableIntralineDifference = on;
     if (enableIntralineDifference) {
-      intralineDifference.setValue(getValue().isIntralineDifference());
+      intralineDifference.setValue(getValue().intralineDifference);
     } else {
       intralineDifference.setValue(false);
     }
@@ -197,36 +197,36 @@
     syntaxHighlighting.setTitle(title);
   }
 
-  public AccountDiffPreference getValue() {
+  public DiffPreferencesInfo getValue() {
     return listenablePrefs.get();
   }
 
-  public void setValue(final AccountDiffPreference dp) {
+  public void setValue(final DiffPreferencesInfo dp) {
     listenablePrefs.set(dp);
     display();
   }
 
   protected void display() {
-    final AccountDiffPreference dp = getValue();
-    setIgnoreWhitespace(dp.getIgnoreWhitespace());
+    final DiffPreferencesInfo dp = getValue();
+    setIgnoreWhitespace(dp.ignoreWhitespace);
     if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(dp.isSyntaxHighlighting());
+      syntaxHighlighting.setValue(dp.syntaxHighlighting);
     } else {
       syntaxHighlighting.setValue(false);
     }
-    setContext(dp.getContext());
+    setContext(dp.context);
 
-    tabWidth.setIntValue(dp.getTabSize());
-    colWidth.setIntValue(dp.getLineLength());
-    intralineDifference.setValue(dp.isIntralineDifference());
-    whitespaceErrors.setValue(dp.isShowWhitespaceErrors());
-    showLineEndings.setValue(dp.isShowLineEndings());
-    showTabs.setValue(dp.isShowTabs());
-    skipDeleted.setValue(dp.isSkipDeleted());
-    skipUncommented.setValue(dp.isSkipUncommented());
-    expandAllComments.setValue(dp.isExpandAllComments());
-    retainHeader.setValue(dp.isRetainHeader());
-    manualReview.setValue(dp.isManualReview());
+    tabWidth.setIntValue(dp.tabSize);
+    colWidth.setIntValue(dp.lineLength);
+    intralineDifference.setValue(dp.intralineDifference);
+    whitespaceErrors.setValue(dp.showWhitespaceErrors);
+    showLineEndings.setValue(dp.showLineEndings);
+    showTabs.setValue(dp.showTabs);
+    skipDeleted.setValue(dp.skipDeleted);
+    skipUncommented.setValue(dp.skipUncommented);
+    expandAllComments.setValue(dp.expandAllComments);
+    retainHeader.setValue(dp.retainHeader);
+    manualReview.setValue(dp.manualReview);
   }
 
   @UiHandler("update")
@@ -244,22 +244,21 @@
       new ErrorDialog(PatchUtil.C.illegalNumberOfColumns()).center();
       return;
     }
-
-    AccountDiffPreference dp = new AccountDiffPreference(getValue());
-    dp.setIgnoreWhitespace(getIgnoreWhitespace());
-    dp.setContext(getContext());
-    dp.setTabSize(tabWidth.getIntValue());
-    dp.setLineLength(colWidth.getIntValue());
-    dp.setSyntaxHighlighting(syntaxHighlighting.getValue());
-    dp.setIntralineDifference(intralineDifference.getValue());
-    dp.setShowWhitespaceErrors(whitespaceErrors.getValue());
-    dp.setShowLineEndings(showLineEndings.getValue());
-    dp.setShowTabs(showTabs.getValue());
-    dp.setSkipDeleted(skipDeleted.getValue());
-    dp.setSkipUncommented(skipUncommented.getValue());
-    dp.setExpandAllComments(expandAllComments.getValue());
-    dp.setRetainHeader(retainHeader.getValue());
-    dp.setManualReview(manualReview.getValue());
+    DiffPreferencesInfo dp = getValue();
+    dp.ignoreWhitespace = getIgnoreWhitespace();
+    dp.context = getContext();
+    dp.tabSize = tabWidth.getIntValue();
+    dp.lineLength = colWidth.getIntValue();
+    dp.syntaxHighlighting = syntaxHighlighting.getValue();
+    dp.intralineDifference = intralineDifference.getValue();
+    dp.showWhitespaceErrors = whitespaceErrors.getValue();
+    dp.showLineEndings = showLineEndings.getValue();
+    dp.showTabs = showTabs.getValue();
+    dp.skipDeleted = skipDeleted.getValue();
+    dp.skipUncommented = skipUncommented.getValue();
+    dp.expandAllComments = expandAllComments.getValue();
+    dp.retainHeader = retainHeader.getValue();
+    dp.manualReview = manualReview.getValue();
 
     listenablePrefs.set(dp);
   }
@@ -289,18 +288,18 @@
   private void initIgnoreWhitespace(ListBox ws) {
     ws.addItem(PatchUtil.C.whitespaceIGNORE_NONE(), //
         Whitespace.IGNORE_NONE.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_SPACE_AT_EOL(), //
-        Whitespace.IGNORE_SPACE_AT_EOL.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_SPACE_CHANGE(), //
-        Whitespace.IGNORE_SPACE_CHANGE.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_ALL_SPACE(), //
-        Whitespace.IGNORE_ALL_SPACE.name());
+    ws.addItem(PatchUtil.C.whitespaceIGNORE_TRAILING(), //
+        Whitespace.IGNORE_TRAILING.name());
+    ws.addItem(PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(), //
+        Whitespace.IGNORE_LEADING_AND_TRAILING.name());
+    ws.addItem(PatchUtil.C.whitespaceIGNORE_ALL(), //
+        Whitespace.IGNORE_ALL.name());
   }
 
   private void initContext(ListBox context) {
-    for (final short v : AccountDiffPreference.CONTEXT_CHOICES) {
+    for (final short v : DiffPreferencesInfo.CONTEXT_CHOICES) {
       final String label;
-      if (v == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+      if (v == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
         label = Util.C.contextWholeFile();
       } else {
         label = Util.M.lines(v);
@@ -314,7 +313,7 @@
     if (0 <= sel) {
       return Whitespace.valueOf(ignoreWhitespace.getValue(sel));
     }
-    return getValue().getIgnoreWhitespace();
+    return getValue().ignoreWhitespace;
   }
 
   private void setIgnoreWhitespace(Whitespace s) {
@@ -327,12 +326,12 @@
     ignoreWhitespace.setSelectedIndex(0);
   }
 
-  private short getContext() {
+  private int getContext() {
     final int sel = context.getSelectedIndex();
     if (0 <= sel) {
       return Short.parseShort(context.getValue(sel));
     }
-    return getValue().getContext();
+    return getValue().context;
   }
 
   private void setContext(int ctx) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
index 586b767..5164302 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
@@ -22,7 +22,7 @@
   ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
   ui:generateLocales='default,en'
   >
-<ui:style>
+<ui:style gss='false'>
   @external .gwt-TextBox;
   @external .gwt-ListBox;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
index 338e950..8977876 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
@@ -18,7 +18,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
     @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
index 178a583..d84f799 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
@@ -65,9 +65,9 @@
       new PatchValidator() {
         @Override
         public boolean isValid(Patch patch) {
-          return !((listenablePrefs.get().isSkipDeleted()
+          return !((listenablePrefs.get().skipDeleted
               && patch.getChangeType().equals(ChangeType.DELETED))
-              || (listenablePrefs.get().isSkipUncommented()
+              || (listenablePrefs.get().skipUncommented
               && patch.getCommentCount() == 0));
         }
 
@@ -262,8 +262,8 @@
   private static boolean isUnifiedPatchLink(final Patch patch) {
     return (patch.getPatchType().equals(PatchType.BINARY)
         || (Gerrit.isSignedIn()
-            && Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
-            .equals(DiffView.UNIFIED_DIFF)));
+            && Gerrit.getUserPreferences().diffView()
+                .equals(DiffView.UNIFIED_DIFF)));
   }
 
   private static String getFileNameOnly(Patch patch) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 10c029f..b3617d5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -294,7 +294,7 @@
   private void appendImageDifferences(final PatchScript script,
       final SafeHtmlBuilder nc) {
     final boolean syntaxHighlighting =
-        script.getDiffPrefs().isSyntaxHighlighting();
+        script.getDiffPrefs().syntaxHighlighting;
     if (script.getDisplayMethodA() == DisplayMethod.IMG) {
       final String url = getUrlA();
       appendImageLine(nc, url, syntaxHighlighting, false);
@@ -310,7 +310,7 @@
     final SparseHtmlFile a = getSparseHtmlFileA(script);
     final SparseHtmlFile b = getSparseHtmlFileB(script);
     final boolean syntaxHighlighting =
-        script.getDiffPrefs().isSyntaxHighlighting();
+        script.getDiffPrefs().syntaxHighlighting;
     for (final EditList.Hunk hunk : script.getHunks()) {
       appendHunkHeader(nc, hunk);
       while (hunk.next()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
index a5c1484..e587aac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
-import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.diff.DiffApi;
 import com.google.gerrit.client.diff.DiffInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -31,9 +31,9 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.client.PrettyFactory;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
@@ -125,10 +125,10 @@
     lastScript = null;
   }
 
-  private void update(AccountDiffPreference dp) {
+  private void update(DiffPreferencesInfo dp) {
     // Did the user just turn on auto-review?
-    if (!reviewedPanels.getValue() && prefs.getOld().isManualReview()
-        && !dp.isManualReview()) {
+    if (!reviewedPanels.getValue() && prefs.getOld().manualReview
+        && !dp.manualReview) {
       reviewedPanels.setValue(true);
       reviewedPanels.setReviewedByCurrentUser(true);
     }
@@ -152,25 +152,25 @@
     }
   }
 
-  private boolean canReuse(AccountDiffPreference dp, PatchScript last) {
-    if (last.getDiffPrefs().getIgnoreWhitespace() != dp.getIgnoreWhitespace()) {
+  private boolean canReuse(DiffPreferencesInfo dp, PatchScript last) {
+    if (last.getDiffPrefs().ignoreWhitespace != dp.ignoreWhitespace) {
       // Whitespace ignore setting requires server computation.
       return false;
     }
 
-    final int ctx = dp.getContext();
-    if (ctx == AccountDiffPreference.WHOLE_FILE_CONTEXT && !last.getA().isWholeFile()) {
+    final int ctx = dp.context;
+    if (ctx == DiffPreferencesInfo.WHOLE_FILE_CONTEXT
+        && !last.getA().isWholeFile()) {
       // We don't have the entire file here, so we can't render it.
       return false;
     }
 
-    if (last.getDiffPrefs().getContext() < ctx && !last.getA().isWholeFile()) {
+    if (last.getDiffPrefs().context < ctx && !last.getA().isWholeFile()) {
       // We don't have sufficient context.
       return false;
     }
 
-    if (dp.isSyntaxHighlighting()
-        && !last.getA().isWholeFile()) {
+    if (dp.syntaxHighlighting && !last.getA().isWholeFile()) {
       // We need the whole file to syntax highlight accurately.
       return false;
     }
@@ -251,7 +251,7 @@
   }
 
   private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
-    return diffInfo.unified_web_links();
+    return diffInfo.unifiedWebLinks();
   }
 
   private String getSideBySideDiffUrl() {
@@ -425,15 +425,15 @@
     }
 
     if (script.isHugeFile()) {
-      AccountDiffPreference dp = script.getDiffPrefs();
-      int context = dp.getContext();
-      if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+      DiffPreferencesInfo dp = script.getDiffPrefs();
+      int context = dp.context;
+      if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
         context = Short.MAX_VALUE;
       } else if (context > Short.MAX_VALUE) {
         context = Short.MAX_VALUE;
       }
-      dp.setContext((short) Math.min(context, LARGE_FILE_CONTEXT));
-      dp.setSyntaxHighlighting(false);
+      dp.context = Math.min(context, LARGE_FILE_CONTEXT);
+      dp.syntaxHighlighting = false;
       script.setDiffPrefs(dp);
     }
 
@@ -453,7 +453,7 @@
 
     if (Gerrit.isSignedIn()) {
       boolean isReviewed = false;
-      if (isFirst && !prefs.get().isManualReview()) {
+      if (isFirst && !prefs.get().manualReview) {
         isReviewed = true;
         reviewedPanels.setReviewedByCurrentUser(isReviewed);
       } else {
@@ -476,9 +476,9 @@
     super.onShowView();
     if (prefsHandler == null) {
       prefsHandler = prefs.addValueChangeHandler(
-          new ValueChangeHandler<AccountDiffPreference>() {
+          new ValueChangeHandler<DiffPreferencesInfo>() {
             @Override
-            public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
+            public void onValueChange(ValueChangeEvent<DiffPreferencesInfo> event) {
               update(event.getValue());
             }
           });
@@ -491,7 +491,7 @@
       new ErrorDialog(PatchUtil.C.intralineTimeout()).setText(
           Gerrit.C.warnTitle()).show();
     }
-    if (topView != null && prefs.get().isRetainHeader()) {
+    if (topView != null && prefs.get().retainHeader) {
       setTopView(topView);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
index 6c1a841..8d166a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -14,25 +14,15 @@
 
 package com.google.gerrit.client.projects;
 
-import com.google.gerrit.client.WebLinkInfo;
-import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.info.ActionInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 
-public class BranchInfo extends JavaScriptObject {
-  public final String getShortName() {
-    return ref().startsWith(Branch.R_HEADS)
-        ? ref().substring(Branch.R_HEADS.length())
-        : ref();
-  }
-
-  public final native String ref() /*-{ return this.ref; }-*/;
-  public final native String revision() /*-{ return this.revision; }-*/;
+public class BranchInfo extends RefInfo {
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
-  public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
   protected BranchInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 7aa5be5..322a354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.projects;
 
 import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.info.ActionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
@@ -35,23 +35,29 @@
   public final native String description()
   /*-{ return this.description }-*/;
 
-  public final native InheritedBooleanInfo require_change_id()
+  public final native InheritedBooleanInfo requireChangeId()
   /*-{ return this.require_change_id; }-*/;
 
-  public final native InheritedBooleanInfo use_content_merge()
+  public final native InheritedBooleanInfo useContentMerge()
   /*-{ return this.use_content_merge; }-*/;
 
-  public final native InheritedBooleanInfo use_contributor_agreements()
+  public final native InheritedBooleanInfo useContributorAgreements()
   /*-{ return this.use_contributor_agreements; }-*/;
 
-  public final native InheritedBooleanInfo create_new_change_for_all_not_in_target()
+  public final native InheritedBooleanInfo createNewChangeForAllNotInTarget()
   /*-{ return this.create_new_change_for_all_not_in_target; }-*/;
 
-  public final native InheritedBooleanInfo use_signed_off_by()
+  public final native InheritedBooleanInfo useSignedOffBy()
   /*-{ return this.use_signed_off_by; }-*/;
 
-  public final SubmitType submit_type() {
-    return SubmitType.valueOf(submit_typeRaw());
+  public final native InheritedBooleanInfo enableSignedPush()
+  /*-{ return this.enable_signed_push; }-*/;
+
+  public final native InheritedBooleanInfo requireSignedPush()
+  /*-{ return this.require_signed_push; }-*/;
+
+  public final SubmitType submitType() {
+    return SubmitType.valueOf(submitTypeRaw());
   }
 
   public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
@@ -63,7 +69,7 @@
   public final native NativeMap<ActionInfo> actions()
   /*-{ return this.actions; }-*/;
 
-  private final native String submit_typeRaw()
+  private final native String submitTypeRaw()
   /*-{ return this.submit_type }-*/;
 
   public final ProjectState state() {
@@ -75,7 +81,7 @@
   private final native String stateRaw()
   /*-{ return this.state }-*/;
 
-  public final native MaxObjectSizeLimitInfo max_object_size_limit()
+  public final native MaxObjectSizeLimitInfo maxObjectSizeLimit()
   /*-{ return this.max_object_size_limit; }-*/;
 
   private final native NativeMap<CommentLinkInfo> commentlinks0()
@@ -131,13 +137,13 @@
     public final native boolean value()
     /*-{ return this.value ? true : false; }-*/;
 
-    public final native boolean inherited_value()
+    public final native boolean inheritedValue()
     /*-{ return this.inherited_value ? true : false; }-*/;
 
-    public final InheritableBoolean configured_value() {
-      return InheritableBoolean.valueOf(configured_valueRaw());
+    public final InheritableBoolean configuredValue() {
+      return InheritableBoolean.valueOf(configuredValueRaw());
     }
-    private final native String configured_valueRaw()
+    private final native String configuredValueRaw()
     /*-{ return this.configured_value }-*/;
 
     public final void setConfiguredValue(InheritableBoolean v) {
@@ -152,8 +158,8 @@
 
   public static class MaxObjectSizeLimitInfo extends JavaScriptObject {
     public final native String value() /*-{ return this.value; }-*/;
-    public final native String inherited_value() /*-{ return this.inherited_value; }-*/;
-    public final native String configured_value() /*-{ return this.configured_value }-*/;
+    public final native String inheritedValue() /*-{ return this.inherited_value; }-*/;
+    public final native String configuredValue() /*-{ return this.configured_value }-*/;
 
     protected MaxObjectSizeLimitInfo() {
     }
@@ -179,8 +185,8 @@
 
   public static class ConfigParameterValue extends JavaScriptObject {
     final native void init() /*-{ this.values = []; }-*/;
-    final native void add_value(String v) /*-{ this.values.push(v); }-*/;
-    final native void set_value(String v) /*-{ if(v)this.value = v; }-*/;
+    final native void addValue(String v) /*-{ this.values.push(v); }-*/;
+    final native void setValue(String v) /*-{ if(v)this.value = v; }-*/;
     public static ConfigParameterValue create() {
       ConfigParameterValue v = createObject().cast();
       return v;
@@ -189,13 +195,13 @@
     public final ConfigParameterValue values(String[] values) {
       init();
       for (String v : values) {
-        add_value(v);
+        addValue(v);
       }
       return this;
     }
 
     public final ConfigParameterValue value(String v) {
-      set_value(v);
+      setValue(v);
       return this;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
index c22b007..6a1ba11 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.projects;
 
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -61,7 +61,7 @@
   }
 
   public static void add(ChangeInfo info) {
-    instance.changeToProject.put(info.legacy_id().get(), info.project());
+    instance.changeToProject.put(info.legacyId().get(), info.project());
   }
 
   private final LinkedHashMap<String, Entry> cache;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index b121d07..fffdd3f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -44,6 +44,32 @@
         .put(input, cb);
   }
 
+  private static RestApi getRestApi(Project.NameKey name, String viewName,
+      int limit, int start, String match) {
+    RestApi call = project(name).view(viewName);
+    call.addParameter("n", limit);
+    call.addParameter("s", start);
+    if (match != null) {
+      if (match.startsWith("^")) {
+        call.addParameter("r", match);
+      } else {
+        call.addParameter("m", match);
+      }
+    }
+    return call;
+  }
+
+  /** Retrieve all visible tags of the project */
+  public static void getTags(Project.NameKey name,
+      AsyncCallback<JsArray<TagInfo>> cb) {
+    project(name).view("tags").get(cb);
+  }
+
+  public static void getTags(Project.NameKey name, int limit, int start,
+      String match, AsyncCallback<JsArray<TagInfo>> cb) {
+    getRestApi(name, "tags", limit, start, match).get(cb);
+  }
+
   /** Create a new branch */
   public static void createBranch(Project.NameKey name, String ref,
       String revision, AsyncCallback<BranchInfo> cb) {
@@ -60,17 +86,7 @@
 
   public static void getBranches(Project.NameKey name, int limit, int start,
        String match, AsyncCallback<JsArray<BranchInfo>> cb) {
-    RestApi call = project(name).view("branches");
-    call.addParameter("n", limit);
-    call.addParameter("s", start);
-    if (match != null) {
-      if (match.startsWith("^")) {
-        call.addParameter("r", match);
-      } else {
-        call.addParameter("m", match);
-      }
-    }
-    call.get(cb);
+    getRestApi(name, "branches", limit, start, match).get(cb);
   }
 
   /**
@@ -84,7 +100,7 @@
     } else {
       DeleteBranchesInput d = DeleteBranchesInput.create();
       for (String ref : refs) {
-        d.add_branch(ref);
+        d.addBranch(ref);
       }
       project(name).view("branches:delete").post(d, cb);
     }
@@ -99,7 +115,10 @@
       InheritableBoolean useContributorAgreements,
       InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
       InheritableBoolean createNewChangeForAllNotInTarget,
-      InheritableBoolean requireChangeId, String maxObjectSizeLimit,
+      InheritableBoolean requireChangeId,
+      InheritableBoolean enableSignedPush,
+      InheritableBoolean requireSignedPush,
+      String maxObjectSizeLimit,
       SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
       AsyncCallback<ConfigInfo> cb) {
@@ -110,6 +129,12 @@
     in.setUseSignedOffBy(useSignedOffBy);
     in.setRequireChangeId(requireChangeId);
     in.setCreateNewChangeForAllNotInTarget(createNewChangeForAllNotInTarget);
+    if (enableSignedPush != null) {
+      in.setEnableSignedPush(enableSignedPush);
+    }
+    if (requireSignedPush != null) {
+      in.setRequireSignedPush(requireSignedPush);
+    }
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -230,6 +255,18 @@
     private final native void setCreateNewChangeForAllNotInTargetRaw(String v)
     /*-{ if(v)this.create_new_change_for_all_not_in_target=v; }-*/;
 
+    final void setEnableSignedPush(InheritableBoolean v) {
+      setEnableSignedPushRaw(v.name());
+    }
+    private final native void setEnableSignedPushRaw(String v)
+    /*-{ if(v)this.enable_signed_push=v; }-*/;
+
+    final void setRequireSignedPush(InheritableBoolean v) {
+      setRequireSignedPushRaw(v.name());
+    }
+    private final native void setRequireSignedPushRaw(String v)
+    /*-{ if(v)this.require_signed_push=v; }-*/;
+
     final native void setMaxObjectSizeLimit(String l)
     /*-{ if(l)this.max_object_size_limit=l; }-*/;
 
@@ -317,6 +354,6 @@
     }
 
     final native void init() /*-{ this.branches = []; }-*/;
-    final native void add_branch(String b) /*-{ this.branches.push(b); }-*/;
+    final native void addBranch(String b) /*-{ this.branches.push(b); }-*/;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
index fe9872c..00a4034 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.projects;
 
-import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.info.WebLinkInfo;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -30,7 +30,7 @@
 
   public final native String name() /*-{ return this.name; }-*/;
   public final native String description() /*-{ return this.description; }-*/;
-  public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
   public final ProjectState state() {
     return ProjectState.valueOf(getStringState());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
new file mode 100644
index 0000000..053dbd3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/RefInfo.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 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.client.projects;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class RefInfo extends JavaScriptObject {
+  public final String getShortName() {
+    return RefNames.shortName(ref());
+  }
+
+  public final native String ref() /*-{ return this.ref; }-*/;
+  public final native String revision() /*-{ return this.revision; }-*/;
+
+  protected RefInfo() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
similarity index 70%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
index 4413603..ee1d1af 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/TagInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.client.projects;
 
-import com.google.gwt.core.client.JsArray;
+public class TagInfo extends RefInfo {
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+  // TODO(dpursehouse) add extra tag-related fields (message, tagger, etc)
+  protected TagInfo() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index 771423e..a0e25ad 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -21,6 +21,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.common.data.HostPageData;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.Scheduler;
@@ -449,7 +450,7 @@
     }
     req.setHeader("Accept", JSON_TYPE);
     if (Gerrit.getXGerritAuth() != null) {
-      req.setHeader("X-Gerrit-Auth", Gerrit.getXGerritAuth());
+      req.setHeader(HostPageData.XSRF_HEADER_NAME, Gerrit.getXGerritAuth());
     }
     return req;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index cdad972..288549f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.reviewdb.client.Account;
@@ -71,8 +71,8 @@
       return ai.email();
     } else if (ai.name() != null) {
       return ai.name();
-    } else if (ai._account_id() != 0) {
-      return "" + ai._account_id();
+    } else if (ai._accountId() != 0) {
+      return "" + ai._accountId();
     } else {
       return "";
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index 78350db..60c23df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
index c9a0590..6af4b78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 /** Link to the open changes of a project. */
 public class BranchLink extends InlineHyperlink {
@@ -61,10 +61,10 @@
       String branch, String topic) {
     String query = PageLinks.projectQuery(project, status);
 
-    if (branch.startsWith(Branch.R_REFS)) {
-      if (branch.startsWith(Branch.R_HEADS)) {
+    if (branch.startsWith(RefNames.REFS)) {
+      if (branch.startsWith(RefNames.REFS_HEADS)) {
         query += " " + PageLinks.op("branch", //
-            branch.substring(Branch.R_HEADS.length()));
+            branch.substring(RefNames.REFS_HEADS.length()));
       } else {
         query += " " + PageLinks.op("ref", branch);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
index 00269f9..f69e042 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -82,7 +82,7 @@
     return newBranch.getText();
   }
 
-  class BranchSuggestion implements Suggestion {
+  static class BranchSuggestion implements Suggestion {
     private BranchInfo branch;
 
     public BranchSuggestion(BranchInfo branch) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 748cd3c..77f40df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.AvatarImage;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
index 021f39c..e398e78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -19,12 +19,12 @@
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
@@ -34,6 +34,7 @@
 public abstract class CreateChangeDialog extends TextAreaActionDialog {
   private SuggestBox newChange;
   private List<BranchInfo> branches;
+  private TextBox topic;
 
   public CreateChangeDialog(Project.NameKey project) {
     super(Util.C.dialogCreateChangeTitle(),
@@ -46,6 +47,15 @@
           }
         });
 
+    topic = new TextBox();
+    topic.setWidth("100%");
+    topic.getElement().getStyle().setProperty("boxSizing", "border-box");
+    FlowPanel newTopicPanel = new FlowPanel();
+    newTopicPanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    newTopicPanel.add(topic);
+    panel.insert(newTopicPanel, 0);
+    panel.insert(new SmallHeading(Util.C.newChangeTopicSuggestion()), 0);
+
     newChange = new SuggestBox(new HighlightSuggestOracle() {
       @Override
       protected void onRequestSuggestions(Request request, Callback done) {
@@ -61,14 +71,13 @@
 
     newChange.setWidth("100%");
     newChange.getElement().getStyle().setProperty("boxSizing", "border-box");
-    message.setCharacterWidth(70);
-
-    FlowPanel mwrap = new FlowPanel();
-    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    mwrap.add(newChange);
-
-    panel.insert(mwrap, 0);
+    FlowPanel newChangePanel = new FlowPanel();
+    newChangePanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    newChangePanel.add(newChange);
+    panel.insert(newChangePanel, 0);
     panel.insert(new SmallHeading(Util.C.newChangeBranchSuggestion()), 0);
+
+    message.setCharacterWidth(70);
   }
 
   @Override
@@ -82,7 +91,11 @@
     return newChange.getText();
   }
 
-  class BranchSuggestion implements Suggestion {
+  public String getDestinationTopic() {
+    return topic.getText();
+  }
+
+  static class BranchSuggestion implements Suggestion {
     private BranchInfo branch;
 
     public BranchSuggestion(BranchInfo branch) {
@@ -91,10 +104,7 @@
 
     @Override
     public String getDisplayString() {
-      if (branch.ref().startsWith(Branch.R_HEADS)) {
-        return branch.ref().substring(Branch.R_HEADS.length());
-      }
-      return branch.ref();
+      return branch.getShortName();
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java
similarity index 96%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java
index 59feba8..76ad0e7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java
@@ -21,7 +21,7 @@
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-public class FancyFlexTableImplIE6 extends FancyFlexTableImpl {
+public class FancyFlexTableImplIE8 extends FancyFlexTableImpl {
   @Override
   public void resetHtml(final FlexTable myTable, final SafeHtml bodyHtml) {
     final Element oldBody = getBodyElement(myTable);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
index 27bc107..0c31f41 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 public class ListenableAccountDiffPreference
-    extends ListenableOldValue<AccountDiffPreference> {
+    extends ListenableOldValue<DiffPreferencesInfo> {
 
   public ListenableAccountDiffPreference() {
     reset();
@@ -33,7 +33,7 @@
           new GerritCallback<VoidResult>() {
         @Override
         public void onSuccess(VoidResult result) {
-          Gerrit.setAccountDiffPreference(get());
+          Gerrit.setDiffPreferences(get());
           cb.onSuccess(result);
         }
 
@@ -46,10 +46,10 @@
   }
 
   public void reset() {
-    if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) {
-      set(Gerrit.getAccountDiffPreference());
+    if (Gerrit.isSignedIn() && Gerrit.getDiffPreferences() != null) {
+      set(Gerrit.getDiffPreferences());
     } else {
-      set(AccountDiffPreference.createDefault(null));
+      set(DiffPreferencesInfo.defaults());
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
index cf49ce7..d944690 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
@@ -49,6 +49,11 @@
   }
 
   @Override
+  protected FlowPanel getBody() {
+    return body;
+  }
+
+  @Override
   protected void add(final Widget w) {
     body.add(w);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
index b57a58e..0933153 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
@@ -89,6 +89,7 @@
       try {
         intValue = Integer.parseInt(getText());
       } catch (NumberFormatException e) {
+        // Ignored
       }
     }
     return intValue;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java
new file mode 100644
index 0000000..e4ad903
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PagingHyperlink.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 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.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.admin.Util;
+
+public class PagingHyperlink extends Hyperlink {
+
+  public static PagingHyperlink createPrev() {
+    return new PagingHyperlink(Util.C.pagedListPrev());
+  }
+
+  public static PagingHyperlink createNext() {
+    return new PagingHyperlink(Util.C.pagedListNext());
+  }
+
+  private PagingHyperlink(String text) {
+    super(text, true, "");
+    setStyleName(Gerrit.RESOURCES.css().pagingLink());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
index 86c31f2..8b58403 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -168,11 +168,8 @@
     } else {
       popup.setPopupPositionAndShow(popupPosition);
       GlobalKey.dialog(popup);
-      try {
-        GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
-            KeyCodes.KEY_ESCAPE, popup));
-      } catch (Throwable e) {
-      }
+      GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
+          KeyCodes.KEY_ESCAPE, popup));
       projectsTab.setRegisterKeys(true);
       projectsTab.finishDisplay();
       filterTxt.setFocus(true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index 6637957..bb55b69 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -65,8 +65,9 @@
   }
 
   public void displaySubset(ProjectMap projects, int fromIndex, int toIndex) {
-    while (1 < table.getRowCount())
+    while (1 < table.getRowCount()) {
       table.removeRow(table.getRowCount() - 1);
+    }
 
     List<ProjectInfo> list = Natives.asList(projects.values());
     Collections.sort(list, new Comparator<ProjectInfo>() {
@@ -75,8 +76,9 @@
         return a.name().compareTo(b.name());
       }
     });
-    for(ProjectInfo p : list.subList(fromIndex, toIndex))
+    for (ProjectInfo p : list.subList(fromIndex, toIndex)) {
       insert(table.getRowCount(), p);
+    }
 
     finishDisplay();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
index 5f47d98..1a9fc4b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.ui.CheckBox;
@@ -29,13 +32,14 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
-import java.util.LinkedList;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 public abstract class RebaseDialog extends CommentedActionDialog {
   private final SuggestBox base;
-  private final CheckBox cb;
-  private List<ChangeInfo> changes;
+  private final CheckBox changeParent;
+  private List<ChangeInfo> candidateChanges;
   private final boolean sendEnabled;
 
   public RebaseDialog(final String project, final String branch,
@@ -44,17 +48,19 @@
     this.sendEnabled = sendEnabled;
     sendButton.setText(Util.C.buttonRebaseChangeSend());
 
-    // create the suggestion box
+    // Create the suggestion box to filter over a list of recent changes
+    // open on the same branch. The list of candidates is primed by the
+    // changeParent CheckBox (below) getting enabled by the user.
     base = new SuggestBox(new HighlightSuggestOracle() {
       @Override
       protected void onRequestSuggestions(Request request, Callback done) {
         String query = request.getQuery().toLowerCase();
-        LinkedList<ChangeSuggestion> suggestions = new LinkedList<>();
-        for (final ChangeInfo ci : changes) {
-          if (changeId.equals(ci.legacy_id())) {
+        List<ChangeSuggestion> suggestions = new ArrayList<>();
+        for (ChangeInfo ci : candidateChanges) {
+          if (changeId.equals(ci.legacyId())) {
             continue;  // do not suggest current change
           }
-          String id = String.valueOf(ci.legacy_id().get());
+          String id = String.valueOf(ci.legacyId().get());
           if (id.contains(query) || ci.subject().toLowerCase().contains(query)) {
             suggestions.add(new ChangeSuggestion(ci));
             if (suggestions.size() >= 50) { // limit to 50 suggestions
@@ -69,21 +75,32 @@
         Util.C.rebasePlaceholderMessage());
     base.setStyleName(Gerrit.RESOURCES.css().rebaseSuggestBox());
 
-    // the checkbox which must be clicked before the change list is populated
-    cb = new CheckBox(Util.C.rebaseConfirmMessage());
-    cb.addClickHandler(new ClickHandler() {
+    // The changeParent checkbox must be clicked to load into browser memory
+    // a list of open changes from the same project and same branch that this
+    // change may rebase onto.
+    changeParent = new CheckBox(Util.C.rebaseConfirmMessage());
+    changeParent.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
-        boolean checked = ((CheckBox) event.getSource()).getValue();
-        if (checked) {
-          ChangeList.next("project:" + project + " AND branch:" + branch
-              + " AND is:open NOT age:90d", 0, 1000,
+        if (changeParent.getValue()) {
+          ChangeList.query(
+              PageLinks.projectQuery(new Project.NameKey(project))
+                  + " " + PageLinks.op("branch", branch)
+                  + " is:open -age:90d",
+              Collections.<ListChangesOption> emptySet(),
               new GerritCallback<ChangeList>() {
                 @Override
                 public void onSuccess(ChangeList result) {
-                  changes = Natives.asList(result);
+                  candidateChanges = Natives.asList(result);
                   updateControls(true);
                 }
+
+                @Override
+                public void onFailure(Throwable err) {
+                  updateControls(false);
+                  changeParent.setValue(false);
+                  super.onFailure(err);
+                }
               });
         } else {
           updateControls(false);
@@ -92,7 +109,7 @@
     });
 
     // add the checkbox and suggestbox widgets to the content panel
-    contentPanel.add(cb);
+    contentPanel.add(changeParent);
     contentPanel.add(base);
     contentPanel.setStyleName(Gerrit.RESOURCES.css().rebaseContentPanel());
   }
@@ -124,7 +141,7 @@
   }
 
   public String getBase() {
-    return cb.getValue() ? base.getText() : null;
+    return changeParent.getValue() ? base.getText() : null;
   }
 
   private static class ChangeSuggestion implements Suggestion {
@@ -136,12 +153,12 @@
 
     @Override
     public String getDisplayString() {
-      return String.valueOf(change.legacy_id().get()) + ": " + change.subject();
+      return String.valueOf(change.legacyId().get()) + ": " + change.subject();
     }
 
     @Override
     public String getReplacementString() {
-      return String.valueOf(change.legacy_id().get());
+      return String.valueOf(change.legacyId().get());
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
index 26c0ce6..2d7736b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
@@ -29,7 +29,7 @@
 
   @Override
   protected void onRequestSuggestions(Request req, Callback cb) {
-    if (req.getQuery().length() >= Gerrit.getConfig().getSuggestFrom()) {
+    if (req.getQuery().length() >= Gerrit.info().suggest().from()) {
       _onRequestSuggestions(req, cb);
     } else {
       List<Suggestion> none = Collections.emptyList();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
index 0f5e12a..a220ea0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
@@ -29,4 +29,5 @@
   String dialogCreateChangeTitle();
   String dialogCreateChangeHeading();
   String newChangeBranchSuggestion();
+  String newChangeTopicSuggestion();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
index a0845d9..736e210 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
@@ -10,3 +10,4 @@
 dialogCreateChangeTitle = Create Change
 dialogCreateChangeHeading = Description
 newChangeBranchSuggestion = Select branch for new change
+newChangeTopicSuggestion = Enter topic for new change
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 1c9ad3c..639e5e7 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -112,9 +112,24 @@
   }-*/;
 
   public enum LineClassWhere {
-    TEXT { @Override String value() { return "text"; } },
-    BACKGROUND { @Override String value() { return "background"; } },
-    WRAP { @Override String value() { return "wrap"; } };
+    TEXT {
+      @Override
+      String value() {
+        return "text";
+      }
+    },
+    BACKGROUND {
+      @Override
+      String value() {
+        return "background";
+      }
+    },
+    WRAP {
+      @Override
+      String value() {
+        return "wrap";
+      }
+    };
     abstract String value();
   }
 
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
index 2d69015..eac8510 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
@@ -35,8 +35,8 @@
 
     public static FromTo create(CommentRange range) {
       return create(
-          Pos.create(range.start_line() - 1, range.start_character()),
-          Pos.create(range.end_line() - 1, range.end_character()));
+          Pos.create(range.startLine() - 1, range.startCharacter()),
+          Pos.create(range.endLine() - 1, range.endCharacter()));
     }
 
     public final native Pos from() /*-{ return this.from }-*/;
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
index d6b194b..debf4b7 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -62,6 +62,7 @@
       Modes.I.php(),
       Modes.I.pig(),
       Modes.I.properties(),
+      Modes.I.puppet(),
       Modes.I.python(),
       Modes.I.r(),
       Modes.I.rst(),
@@ -72,8 +73,10 @@
       Modes.I.soy(),
       Modes.I.sql(),
       Modes.I.stex(),
+      Modes.I.tcl(),
       Modes.I.velocity(),
       Modes.I.verilog(),
+      Modes.I.vhdl(),
       Modes.I.xml(),
       Modes.I.yaml(),
     });
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
index e511be5..42990b8 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -47,6 +47,7 @@
   @Source("php.js") @DoNotEmbed DataResource php();
   @Source("pig.js") @DoNotEmbed DataResource pig();
   @Source("properties.js") @DoNotEmbed DataResource properties();
+  @Source("puppet.js") @DoNotEmbed DataResource puppet();
   @Source("python.js") @DoNotEmbed DataResource python();
   @Source("r.js") @DoNotEmbed DataResource r();
   @Source("rst.js") @DoNotEmbed DataResource rst();
@@ -60,6 +61,7 @@
   @Source("tcl.js") @DoNotEmbed DataResource tcl();
   @Source("velocity.js") @DoNotEmbed DataResource velocity();
   @Source("verilog.js") @DoNotEmbed DataResource verilog();
+  @Source("vhdl.js") @DoNotEmbed DataResource vhdl();
   @Source("xml.js") @DoNotEmbed DataResource xml();
   @Source("yaml.js") @DoNotEmbed DataResource yaml();
 
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
new file mode 100644
index 0000000..0f659c9a
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2015 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.client;
+
+import static com.google.gerrit.client.FormatUtil.formatBytes;
+import static org.junit.Assert.assertEquals;
+
+import com.googlecode.gwt.test.GwtModule;
+import com.googlecode.gwt.test.GwtTest;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+@GwtModule("com.google.gerrit.GerritGwtUI")
+@Ignore
+public class FormatUtilTest extends GwtTest {
+  @Test
+  public void testFormatBytes() {
+    assertEquals("+/- 0 B", formatBytes(0));
+    assertEquals("+27 B", formatBytes(27));
+    assertEquals("+999 B", formatBytes(999));
+    assertEquals("+1000 B", formatBytes(1000));
+    assertEquals("+1023 B", formatBytes(1023));
+    assertEquals("+1.0 KiB", formatBytes(1024));
+    assertEquals("+1.7 KiB", formatBytes(1728));
+    assertEquals("+108.0 KiB", formatBytes(110592));
+    assertEquals("+6.8 MiB", formatBytes(7077888));
+    assertEquals("+432.0 MiB", formatBytes(452984832));
+    assertEquals("+27.0 GiB", formatBytes(28991029248L));
+    assertEquals("+1.7 TiB", formatBytes(1855425871872L));
+    assertEquals("+8.0 EiB", formatBytes(9223372036854775807L));
+
+    assertEquals("-27 B", formatBytes(-27));
+    assertEquals("-1.7 MiB", formatBytes(-1728));
+  }
+}
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index 3345018..3085054 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -12,7 +12,9 @@
     '//gerrit-common:annotations',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:linker_server',
     '//gerrit-gwtexpui:server',
+    '//gerrit-launcher:launcher',
     '//gerrit-patch-jgit:server',
     '//gerrit-prettify:server',
     '//gerrit-reviewdb:server',
@@ -34,7 +36,7 @@
     '//lib/jgit:jgit',
     '//lib/jgit:jgit-servlet',
     '//lib/log:api',
-    '//lib/lucene:core',
+    '//lib/lucene:core-and-backward-codecs',
   ],
   provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
@@ -56,6 +58,8 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//gerrit-util-http:http',
+    '//gerrit-util-http:testutil',
+    '//lib:jimfs',
     '//lib:junit',
     '//lib:gson',
     '//lib:gwtorm',
@@ -64,8 +68,10 @@
     '//lib:truth',
     '//lib/easymock:easymock',
     '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
+    '//lib/joda:joda-time',
   ],
   source_under_test = [':httpd'],
   # TODO(sop) Remove after Buck supports Eclipse
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
index 3b48b65..bcc5842 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -15,8 +15,11 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.StopPluginListener;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
 
 import java.io.IOException;
@@ -37,17 +40,62 @@
       protected void configureServlets() {
         DynamicSet.setOf(binder(), AllRequestFilter.class);
         filter("/*").through(FilterProxy.class);
+
+        bind(StopPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(FilterProxy.class);
       }
     };
   }
 
   @Singleton
-  static class FilterProxy implements Filter {
+  static class FilterProxy implements Filter, StopPluginListener {
     private final DynamicSet<AllRequestFilter> filters;
 
+    private DynamicSet<AllRequestFilter> initializedFilters;
+    private FilterConfig filterConfig;
+
     @Inject
     FilterProxy(DynamicSet<AllRequestFilter> filters) {
       this.filters = filters;
+      this.initializedFilters = new DynamicSet<>();
+      this.filterConfig = null;
+    }
+
+    /**
+     * Initializes a filter if needed
+     *
+     * @param filter The filter that should get initialized
+     * @return {@code true} iff filter is now initialized
+     * @throws ServletException if filter itself fails to init
+     */
+    private synchronized boolean initFilterIfNeeded(AllRequestFilter filter)
+        throws ServletException {
+      boolean ret = true;
+      if (filters.contains(filter)) {
+        // Regardless of whether or not the caller checked filter's
+        // containment in initializedFilters, we better re-check as we're now
+        // synchronized.
+        if (!initializedFilters.contains(filter)) {
+          filter.init(filterConfig);
+          initializedFilters.add(filter);
+        }
+      } else {
+        ret = false;
+      }
+      return ret;
+    }
+
+    private synchronized void cleanUpInitializedFilters() {
+      Iterable<AllRequestFilter> filtersToCleanUp  = initializedFilters;
+      initializedFilters = new DynamicSet<>();
+      for (AllRequestFilter filter : filtersToCleanUp) {
+        if (filters.contains(filter)) {
+          initializedFilters.add(filter);
+        } else {
+          filter.destroy();
+        }
+      }
     }
 
     @Override
@@ -58,28 +106,67 @@
         @Override
         public void doFilter(ServletRequest req, ServletResponse res)
             throws IOException, ServletException {
-          if (itr.hasNext()) {
-            itr.next().doFilter(req, res, this);
-          } else {
-            last.doFilter(req, res);
+          while (itr.hasNext()) {
+            AllRequestFilter filter = itr.next();
+            // To avoid {@code synchronized} on the the whole filtering (and
+            // thereby killing concurrency), we start the below disjunction
+            // with an unsynchronized check for containment. This
+            // unsynchronized check is always correct if no filters got
+            // initialized/cleaned concurrently behind our back.
+            // The case of concurrently initialized filters is saved by the
+            // call to initFilterIfNeeded. So that's fine too.
+            // The case of concurrently cleaned filters between the {@code if}
+            // condition and the call to {@code doFilter} is not saved by
+            // anything. If a filter is getting removed concurrently while
+            // another thread is in those two lines, doFilter might (but need
+            // not) fail.
+            //
+            // Since this failure only occurs if a filter is deleted
+            // (e.g.: a plugin reloaded) exactly when a thread is in those
+            // two lines, and it only breaks a single request, we're ok with
+            // it, given that this is really both really improbable and also
+            // the "proper" fix for it would basically kill concurrency of
+            // webrequests.
+            if (initializedFilters.contains(filter)
+                || initFilterIfNeeded(filter)) {
+              filter.doFilter(req, res, this);
+              return;
+            }
           }
+          last.doFilter(req, res);
         }
       }.doFilter(req, res);
     }
 
     @Override
     public void init(FilterConfig config) throws ServletException {
+      // Plugins that provide AllRequestFilters might get loaded later at
+      // runtime, long after this init method had been called. To allow to
+      // correctly init such plugins' AllRequestFilters, we keep the
+      // FilterConfig around, and reuse it to lazy init the AllRequestFilters.
+      filterConfig = config;
+
       for (AllRequestFilter f: filters) {
-        f.init(config);
+        initFilterIfNeeded(f);
       }
     }
 
     @Override
-    public void destroy() {
-      for (AllRequestFilter f: filters) {
-        f.destroy();
+    public synchronized void destroy() {
+      Iterable<AllRequestFilter> filtersToDestroy  = initializedFilters;
+      initializedFilters = new DynamicSet<>();
+      for (AllRequestFilter filter: filtersToDestroy) {
+        filter.destroy();
       }
     }
+
+    @Override
+    public void onStopPlugin(Plugin plugin) {
+      // In order to allow properly garbage collection, we need to scrub
+      // initializedFilters clean of filters stemming from plugins as they
+      // get unloaded.
+      cleanUpInitializedFilters();
+    }
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index d315fa3..a1cfec7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
+import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.httpd.WebSessionManager.Key;
 import com.google.gerrit.httpd.WebSessionManager.Val;
 import com.google.gerrit.reviewdb.client.Account;
@@ -80,7 +81,7 @@
           val = manager.createVal(key, val);
         }
 
-        String token = request.getHeader("X-Gerrit-Auth");
+        String token = request.getHeader(HostPageData.XSRF_HEADER_NAME);
         if (val != null && token != null && token.equals(val.getAuth())) {
           okPaths.add(AccessPath.REST_API);
         }
@@ -136,7 +137,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     if (user == null) {
       if (isSignedIn()) {
         user = identified.create(val.getAccountId());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index 4bd9ef5..f291621 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -57,8 +57,6 @@
  */
 @Singleton
 class ContainerAuthFilter implements Filter {
-  public static final String REALM_NAME = "Gerrit Code Review";
-
   private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final Config config;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
deleted file mode 100644
index 4b0e46c..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ /dev/null
@@ -1,194 +0,0 @@
-// 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.
-
-package com.google.gerrit.httpd;
-
-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.GerritConfig;
-import com.google.gerrit.common.data.GitwebConfig;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.change.ArchiveFormat;
-import com.google.gerrit.server.change.GetArchive;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.DownloadConfig;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.contact.ContactStore;
-import com.google.gerrit.server.mail.EmailSender;
-import com.google.gerrit.server.ssh.SshInfo;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-import org.eclipse.jgit.lib.Config;
-
-import java.net.MalformedURLException;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import javax.servlet.ServletContext;
-
-class GerritConfigProvider implements Provider<GerritConfig> {
-  private final Realm realm;
-  private final Config cfg;
-  private final AuthConfig authConfig;
-  private final DownloadConfig downloadConfig;
-  private final GetArchive.AllowedFormats archiveFormats;
-  private final GitWebConfig gitWebConfig;
-  private final AllProjectsName wildProject;
-  private final SshInfo sshInfo;
-
-  private EmailSender emailSender;
-  private final ContactStore contactStore;
-  private final ServletContext servletContext;
-  private final String anonymousCowardName;
-
-  @Inject
-  GerritConfigProvider(final Realm r, @GerritServerConfig final Config gsc,
-      final AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
-      final SshInfo si, final ContactStore cs,
-      final ServletContext sc, final DownloadConfig dc,
-      final GetArchive.AllowedFormats af,
-      final @AnonymousCowardName String acn) {
-    realm = r;
-    cfg = gsc;
-    authConfig = ac;
-    downloadConfig = dc;
-    archiveFormats = af;
-    gitWebConfig = gwc;
-    sshInfo = si;
-    wildProject = wp;
-    contactStore = cs;
-    servletContext = sc;
-    anonymousCowardName = acn;
-  }
-
-  @Inject(optional = true)
-  void setEmailSender(final EmailSender d) {
-    emailSender = d;
-  }
-
-  private GerritConfig create() throws MalformedURLException {
-    final GerritConfig config = new GerritConfig();
-    switch (authConfig.getAuthType()) {
-      case LDAP:
-      case LDAP_BIND:
-        config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
-        config.setRegisterText(cfg.getString("auth", null, "registertext"));
-        config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
-        config.setHttpPasswordSettingsEnabled(!authConfig.isGitBasicAuth());
-        break;
-
-      case CUSTOM_EXTENSION:
-        config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
-        config.setRegisterText(cfg.getString("auth", null, "registertext"));
-        config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
-        config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
-        break;
-
-      case HTTP:
-      case HTTP_LDAP:
-        config.setLoginUrl(cfg.getString("auth", null, "loginurl"));
-        config.setLoginText(cfg.getString("auth", null, "logintext"));
-        break;
-
-      case CLIENT_SSL_CERT_LDAP:
-      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-      case OAUTH:
-      case OPENID:
-      case OPENID_SSO:
-        break;
-    }
-    config.setSwitchAccountUrl(cfg.getString("auth", null, "switchAccountUrl"));
-    config.setUseContributorAgreements(cfg.getBoolean("auth",
-        "contributoragreements", false));
-    config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
-    config.setGitHttpUrl(cfg.getString("gerrit", null, "gitHttpUrl"));
-    config.setUseContactInfo(contactStore != null && contactStore.isEnabled());
-    config.setDownloadSchemes(downloadConfig.getDownloadSchemes());
-    config.setDownloadCommands(downloadConfig.getDownloadCommands());
-    config.setAuthType(authConfig.getAuthType());
-    config.setWildProject(wildProject);
-    config.setDocumentationAvailable(servletContext
-        .getResource("/Documentation/index.html") != null);
-    config.setAnonymousCowardName(anonymousCowardName);
-    config.setSuggestFrom(cfg.getInt("suggest", "from", 0));
-    config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
-        cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
-    config.setLargeChangeSize(cfg.getInt("change", "largeChange", 500));
-    config.setArchiveFormats(Lists.newArrayList(Iterables.transform(
-        archiveFormats.getAllowed(),
-        new Function<ArchiveFormat, String>() {
-          @Override
-          public String apply(ArchiveFormat in) {
-            return in.getShortName();
-          }
-        })));
-
-    config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
-    config.setReportBugText(cfg.getString("gerrit", null, "reportBugText"));
-
-    final Set<Account.FieldName> fields = new HashSet<>();
-    for (final Account.FieldName n : Account.FieldName.values()) {
-      if (realm.allowsEdit(n)) {
-        fields.add(n);
-      }
-    }
-    if (emailSender != null && emailSender.isEnabled()
-        && realm.allowsEdit(Account.FieldName.REGISTER_NEW_EMAIL)) {
-      fields.add(Account.FieldName.REGISTER_NEW_EMAIL);
-    }
-    config.setEditableAccountFields(fields);
-
-    if (gitWebConfig.getUrl() != null) {
-      config.setGitwebLink(new GitwebConfig(gitWebConfig.getUrl(), gitWebConfig
-          .getGitWebType()));
-    }
-
-    if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
-      config.setSshdAddress(sshInfo.getHostKeys().get(0).getHost());
-    }
-
-    String replyTitle =
-        Optional.fromNullable(cfg.getString("change", null, "replyTooltip"))
-        .or("Reply and score")
-        + " (Shortcut: a)";
-    String replyLabel =
-        Optional.fromNullable(cfg.getString("change", null, "replyLabel"))
-        .or("Reply")
-        + "\u2026";
-    config.setReplyTitle(replyTitle);
-    config.setReplyLabel(replyLabel);
-
-    config.setAllowDraftChanges(cfg.getBoolean("change", "allowDrafts", true));
-
-    return config;
-  }
-
-  @Override
-  public GerritConfig get() {
-    try {
-      return create();
-    } catch (MalformedURLException e) {
-      throw new ProvisionException("Cannot create GerritConfig instance", e);
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
index 94b8f29..019efb4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
@@ -72,7 +72,7 @@
       throws IOException, ServletException {
     CurrentUser user = userProvider.get();
     if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser who = (IdentifiedUser) user;
+      IdentifiedUser who = user.asIdentifiedUser();
       if (who.getUserName() != null && !who.getUserName().isEmpty()) {
         req.setAttribute(REQ_ATTR_KEY, who.getUserName());
       } else {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index edd2594..e1810ef 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
@@ -55,8 +55,7 @@
   }
 
   private boolean isHttpEnabled(){
-    return downloadConfig.getDownloadSchemes().contains(DownloadScheme.DEFAULT_DOWNLOADS)
-        || downloadConfig.getDownloadSchemes().contains(DownloadScheme.ANON_HTTP)
-        || downloadConfig.getDownloadSchemes().contains(DownloadScheme.HTTP);
+    return downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.ANON_HTTP)
+        || downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.HTTP);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 43cd741..89d62dd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -63,8 +63,6 @@
 import org.eclipse.jgit.transport.resolver.UploadPackFactory;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -158,13 +156,6 @@
     public Repository open(HttpServletRequest req, String projectName)
         throws RepositoryNotFoundException, ServiceNotAuthorizedException,
         ServiceNotEnabledException {
-      try {
-        // TODO: remove this code when Guice fixes its issue 745
-        projectName = URLDecoder.decode(projectName, "UTF-8");
-      } catch (UnsupportedEncodingException e) {
-        // leave it encoded
-      }
-
       while (projectName.endsWith("/")) {
         projectName = projectName.substring(0, projectName.length() - 1);
       }
@@ -186,7 +177,7 @@
         throw new RepositoryNotFoundException(projectName);
       }
 
-      CurrentUser user = pc.getCurrentUser();
+      CurrentUser user = pc.getUser();
       user.setAccessPath(AccessPath.GIT);
 
       if (!pc.isVisible()) {
@@ -301,12 +292,12 @@
         throws ServiceNotAuthorizedException {
       final ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
 
-      if (!(pc.getCurrentUser().isIdentifiedUser())) {
+      if (!(pc.getUser().isIdentifiedUser())) {
         // Anonymous users are not permitted to push.
         throw new ServiceNotAuthorizedException();
       }
 
-      final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
+      final IdentifiedUser user = pc.getUser().asIdentifiedUser();
       final ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
       ReceivePack rp = rc.getReceivePack();
       rp.setRefLogIdent(user.newRefLogIdent());
@@ -376,14 +367,13 @@
         return;
       }
 
-      if (!(pc.getCurrentUser().isIdentifiedUser())) {
+      if (!(pc.getUser().isIdentifiedUser())) {
         chain.doFilter(request, response);
         return;
       }
 
       AdvertisedObjectsCacheKey cacheKey = AdvertisedObjectsCacheKey.create(
-          ((IdentifiedUser) pc.getCurrentUser()).getAccountId(),
-          projectName);
+          pc.getUser().getAccountId(), projectName);
 
       if (isGet) {
         cache.invalidate(cacheKey);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
deleted file mode 100644
index 9d47977..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ /dev/null
@@ -1,239 +0,0 @@
-// 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.
-
-package com.google.gerrit.httpd;
-
-import com.google.gerrit.common.data.GitWebType;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-
-public class GitWebConfig {
-  private static final Logger log = LoggerFactory.getLogger(GitWebConfig.class);
-
-  private final String url;
-  private final File gitweb_cgi;
-  private final File gitweb_css;
-  private final File gitweb_js;
-  private final File git_logo_png;
-  private GitWebType type;
-
-  @Inject
-  GitWebConfig(final SitePaths sitePaths, @GerritServerConfig final Config cfg) {
-    final String cfgUrl = cfg.getString("gitweb", null, "url");
-    final String cfgCgi = cfg.getString("gitweb", null, "cgi");
-
-    type = GitWebType.fromName(cfg.getString("gitweb", null, "type"));
-    if (type == null) {
-      url = null;
-      gitweb_cgi = null;
-      gitweb_css = null;
-      gitweb_js = null;
-      git_logo_png = null;
-      return;
-    }
-
-    type.setLinkName(cfg.getString("gitweb", null, "linkname"));
-    type.setBranch(cfg.getString("gitweb", null, "branch"));
-    type.setProject(cfg.getString("gitweb", null, "project"));
-    type.setRevision(cfg.getString("gitweb", null, "revision"));
-    type.setRootTree(cfg.getString("gitweb", null, "roottree"));
-    type.setFile(cfg.getString("gitweb", null, "file"));
-    type.setFileHistory(cfg.getString("gitweb", null, "filehistory"));
-    type.setLinkDrafts(cfg.getBoolean("gitweb", null, "linkdrafts", true));
-    type.setUrlEncode(cfg.getBoolean("gitweb", null, "urlencode", true));
-    String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
-    if (pathSeparator != null) {
-      if (pathSeparator.length() == 1) {
-        char c = pathSeparator.charAt(0);
-        if (isValidPathSeparator(c)) {
-          type.setPathSeparator(c);
-        } else {
-          log.warn("Invalid value specified for gitweb.pathSeparator: " + c);
-        }
-      } else {
-        log.warn("Value specified for gitweb.pathSeparator is not a single character:" + pathSeparator);
-      }
-    }
-
-    if (type.getBranch() == null) {
-      log.warn("No Pattern specified for gitweb.branch, disabling.");
-      type = null;
-    } else if (type.getProject() == null) {
-      log.warn("No Pattern specified for gitweb.project, disabling.");
-      type = null;
-    } else if (type.getRevision() == null) {
-      log.warn("No Pattern specified for gitweb.revision, disabling.");
-      type = null;
-    } else if (type.getRootTree() == null) {
-      log.warn("No Pattern specified for gitweb.roottree, disabling.");
-      type = null;
-    } else if (type.getFile() == null) {
-      log.warn("No Pattern specified for gitweb.file, disabling.");
-      type = null;
-    } else if (type.getFileHistory() == null) {
-      log.warn("No Pattern specified for gitweb.filehistory, disabling.");
-      type = null;
-    }
-
-    if ((cfgUrl != null && cfgUrl.isEmpty())
-        || (cfgCgi != null && cfgCgi.isEmpty())) {
-      // Either setting was explicitly set to the empty string disabling
-      // gitweb for this server. Disable the configuration.
-      //
-      url = null;
-      gitweb_cgi = null;
-      gitweb_css = null;
-      gitweb_js = null;
-      git_logo_png = null;
-      return;
-    }
-
-    if ((cfgUrl != null) && (cfgCgi == null || cfgCgi.isEmpty())) {
-      // Use an externally managed gitweb instance, and not an internal one.
-      //
-      url = cfgUrl;
-      gitweb_cgi = null;
-      gitweb_css = null;
-      gitweb_js = null;
-      git_logo_png = null;
-      return;
-    }
-
-    final File pkgCgi = new File("/usr/lib/cgi-bin/gitweb.cgi");
-    String[] resourcePaths = {"/usr/share/gitweb/static", "/usr/share/gitweb",
-        "/var/www/static", "/var/www"};
-    File cgi;
-
-    if (cfgCgi != null) {
-      // Use the CGI script configured by the administrator, failing if it
-      // cannot be used as specified.
-      //
-      cgi = sitePaths.resolve(cfgCgi);
-      if (!cgi.isFile()) {
-        throw new IllegalStateException("Cannot find gitweb.cgi: " + cgi);
-      }
-      if (!cgi.canExecute()) {
-        throw new IllegalStateException("Cannot execute gitweb.cgi: " + cgi);
-      }
-
-      if (!cgi.equals(pkgCgi)) {
-        // Assume the administrator pointed us to the distribution,
-        // which also has the corresponding CSS and logo file.
-        //
-        String absPath = cgi.getParentFile().getAbsolutePath();
-        resourcePaths = new String[] {absPath + "/static", absPath};
-      }
-
-    } else if (pkgCgi.isFile() && pkgCgi.canExecute()) {
-      // Use the OS packaged CGI.
-      //
-      log.debug("Assuming gitweb at " + pkgCgi);
-      cgi = pkgCgi;
-
-    } else {
-      log.warn("gitweb not installed (no " + pkgCgi + " found)");
-      cgi = null;
-      resourcePaths = new String[] {};
-    }
-
-    File css = null, js = null, logo = null;
-    for (String path : resourcePaths) {
-      File dir = new File(path);
-      css = new File(dir, "gitweb.css");
-      js = new File(dir, "gitweb.js");
-      logo = new File(dir, "git-logo.png");
-      if (css.isFile() && logo.isFile()) {
-        break;
-      }
-    }
-
-    if (cfgUrl == null || cfgUrl.isEmpty()) {
-      url = cgi != null ? "gitweb" : null;
-    } else {
-      url = cgi != null ? cfgUrl : null;
-    }
-    gitweb_cgi = cgi;
-    gitweb_css = css;
-    gitweb_js = js;
-    git_logo_png = logo;
-  }
-
-  /** @return GitWebType for gitweb viewer. */
-  public GitWebType getGitWebType() {
-    return type;
-  }
-
-  /**
-   * @return URL of the entry point into gitweb. This URL may be relative to our
-   *         context if gitweb is hosted by ourselves; or absolute if its hosted
-   *         elsewhere; or null if gitweb has not been configured.
-   */
-  public String getUrl() {
-    return url;
-  }
-
-  /** @return local path to the CGI executable; null if we shouldn't execute. */
-  public File getGitwebCGI() {
-    return gitweb_cgi;
-  }
-
-  /** @return local path of the {@code gitweb.css} matching the CGI. */
-  public File getGitwebCSS() {
-    return gitweb_css;
-  }
-
-  /** @return local path of the {@code gitweb.js} for the CGI. */
-  public File getGitwebJS() {
-    return gitweb_js;
-  }
-
-  /** @return local path of the {@code git-logo.png} for the CGI. */
-  public File getGitLogoPNG() {
-    return git_logo_png;
-  }
-
-  /**
-   * Determines if a given character can be used unencoded in an URL as a
-   * replacement for the path separator '/'.
-   *
-   * Reasoning: http://www.ietf.org/rfc/rfc1738.txt § 2.2:
-   *
-   * ... only alphanumerics, the special characters "$-_.+!*'(),", and
-   *  reserved characters used for their reserved purposes may be used
-   * unencoded within a URL.
-   *
-   * The following characters might occur in file names, however:
-   *
-   * alphanumeric characters,
-   *
-   * "$-_.+!',"
-   */
-  static boolean isValidPathSeparator(char c) {
-    switch (c) {
-      case '*':
-      case '(':
-      case ')':
-        return true;
-      default:
-        return false;
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 1a2d3f6..95f4536 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.ByteStreams;
+
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -21,14 +25,13 @@
 import org.xml.sax.SAXException;
 
 import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 import java.util.zip.GZIPOutputStream;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -36,7 +39,6 @@
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.transform.OutputKeys;
 import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMSource;
@@ -49,21 +51,21 @@
 /** Utility functions to deal with HTML using W3C DOM operations. */
 public class HtmlDomUtil {
   /** Standard character encoding we prefer (UTF-8). */
-  public static final String ENC = "UTF-8";
+  public static final Charset ENC = UTF_8;
 
   /** DOCTYPE for a standards mode HTML document. */
   public static final String HTML_STRICT =
       "-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd";
 
   /** Convert a document to a UTF-8 byte sequence. */
-  public static byte[] toUTF8(final Document hostDoc) throws IOException {
+  public static byte[] toUTF8(Document hostDoc) throws IOException {
     return toString(hostDoc).getBytes(ENC);
   }
 
   /** Compress the document. */
-  public static byte[] compress(final byte[] raw) throws IOException {
-    final ByteArrayOutputStream out = new ByteArrayOutputStream();
-    final GZIPOutputStream gz = new GZIPOutputStream(out);
+  public static byte[] compress(byte[] raw) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    GZIPOutputStream gz = new GZIPOutputStream(out);
     gz.write(raw);
     gz.finish();
     gz.flush();
@@ -71,43 +73,39 @@
   }
 
   /** Convert a document to a String, assuming later encoding to UTF-8. */
-  public static String toString(final Document hostDoc) throws IOException {
+  public static String toString(Document hostDoc) throws IOException {
     try {
-      final StringWriter out = new StringWriter();
-      final DOMSource domSource = new DOMSource(hostDoc);
-      final StreamResult streamResult = new StreamResult(out);
-      final TransformerFactory tf = TransformerFactory.newInstance();
-      final Transformer serializer = tf.newTransformer();
-      serializer.setOutputProperty(OutputKeys.ENCODING, ENC);
+      StringWriter out = new StringWriter();
+      DOMSource domSource = new DOMSource(hostDoc);
+      StreamResult streamResult = new StreamResult(out);
+      TransformerFactory tf = TransformerFactory.newInstance();
+      Transformer serializer = tf.newTransformer();
+      serializer.setOutputProperty(OutputKeys.ENCODING, ENC.name());
       serializer.setOutputProperty(OutputKeys.METHOD, "html");
       serializer.setOutputProperty(OutputKeys.INDENT, "no");
       serializer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
           HtmlDomUtil.HTML_STRICT);
       serializer.transform(domSource, streamResult);
       return out.toString();
-    } catch (TransformerConfigurationException e) {
-      final IOException r = new IOException("Error transforming page");
-      r.initCause(e);
-      throw r;
     } catch (TransformerException e) {
-      final IOException r = new IOException("Error transforming page");
+      IOException r = new IOException("Error transforming page");
       r.initCause(e);
       throw r;
     }
   }
 
   /** Find an element by its "id" attribute; null if no element is found. */
-  public static Element find(final Node parent, final String name) {
-    final NodeList list = parent.getChildNodes();
+  public static Element find(Node parent, String name) {
+    NodeList list = parent.getChildNodes();
     for (int i = 0; i < list.getLength(); i++) {
-      final Node n = list.item(i);
+      Node n = list.item(i);
       if (n instanceof Element) {
-        final Element e = (Element) n;
+        Element e = (Element) n;
         if (name.equals(e.getAttribute("id"))) {
           return e;
         }
       }
-      final Element r = find(n, name);
+      Element r = find(n, name);
       if (r != null) {
         return r;
       }
@@ -116,9 +114,8 @@
   }
 
   /** Append an HTML &lt;input type="hidden"&gt; to the form. */
-  public static void addHidden(final Element form, final String name,
-      final String value) {
-    final Element in = form.getOwnerDocument().createElement("input");
+  public static void addHidden(Element form, String name, String value) {
+    Element in = form.getOwnerDocument().createElement("input");
     in.setAttribute("type", "hidden");
     in.setAttribute("name", name);
     in.setAttribute("value", value);
@@ -135,51 +132,38 @@
   }
 
   /** Clone a document so it can be safely modified on a per-request basis. */
-  public static Document clone(final Document doc) throws IOException {
-    final Document d;
+  public static Document clone(Document doc) throws IOException {
+    Document d;
     try {
       d = newBuilder().newDocument();
     } catch (ParserConfigurationException e) {
       throw new IOException("Cannot clone document");
     }
-    final Node n = d.importNode(doc.getDocumentElement(), true);
+    Node n = d.importNode(doc.getDocumentElement(), true);
     d.appendChild(n);
     return d;
   }
 
   /** Parse an XHTML file from our CLASSPATH and return the instance. */
-  public static Document parseFile(final Class<?> context, final String name)
+  public static Document parseFile(Class<?> context, String name)
       throws IOException {
-    final InputStream in;
-
-    in = context.getResourceAsStream(name);
-    if (in == null) {
-      return null;
-    }
-    try {
-      try {
-        try {
-          final Document doc = newBuilder().parse(in);
-          compact(doc);
-          return doc;
-        } catch (SAXException e) {
-          throw new IOException("Error reading " + name, e);
-        } catch (ParserConfigurationException e) {
-          throw new IOException("Error reading " + name, e);
-        }
-      } finally {
-        in.close();
+    try (InputStream in = context.getResourceAsStream(name)) {
+      if (in == null) {
+        return null;
       }
-    } catch (IOException e) {
+      Document doc = newBuilder().parse(in);
+      compact(doc);
+      return doc;
+    } catch (SAXException | ParserConfigurationException | IOException e) {
       throw new IOException("Error reading " + name, e);
     }
   }
 
-  private static void compact(final Document doc) {
+  private static void compact(Document doc) {
     try {
-      final String expr = "//text()[normalize-space(.) = '']";
-      final XPathFactory xp = XPathFactory.newInstance();
-      final XPathExpression e = xp.newXPath().compile(expr);
+      String expr = "//text()[normalize-space(.) = '']";
+      XPathFactory xp = XPathFactory.newInstance();
+      XPathExpression e = xp.newXPath().compile(expr);
       NodeList empty = (NodeList) e.evaluate(doc, XPathConstants.NODESET);
       for (int i = 0; i < empty.getLength(); i++) {
         Node node = empty.item(i);
@@ -191,78 +175,50 @@
   }
 
   /** Read a Read a UTF-8 text file from our CLASSPATH and return it. */
-  public static String readFile(final Class<?> context, final String name)
+  public static String readFile(Class<?> context, String name)
       throws IOException {
-    final InputStream in = context.getResourceAsStream(name);
-    if (in == null) {
-      return null;
-    }
-    try {
-      return asString(in);
+    try (InputStream in = context.getResourceAsStream(name)) {
+      if (in == null) {
+        return null;
+      }
+      return new String(ByteStreams.toByteArray(in), ENC);
     } catch (IOException e) {
       throw new IOException("Error reading " + name, e);
     }
   }
 
   /** Parse an XHTML file from the local drive and return the instance. */
-  public static Document parseFile(final File path) throws IOException {
-    try {
-      final InputStream in = new FileInputStream(path);
-      try {
-        try {
-          final Document doc = newBuilder().parse(in);
-          compact(doc);
-          return doc;
-        } catch (SAXException e) {
-          throw new IOException("Error reading " + path, e);
-        } catch (ParserConfigurationException e) {
-          throw new IOException("Error reading " + path, e);
-        }
-      } finally {
-        in.close();
-      }
-    } catch (FileNotFoundException e) {
+  public static Document parseFile(Path path) throws IOException {
+    try (InputStream in = Files.newInputStream(path)) {
+      Document doc = newBuilder().parse(in);
+      compact(doc);
+      return doc;
+    } catch (NoSuchFileException e) {
       return null;
-    } catch (IOException e) {
+    } catch (SAXException | ParserConfigurationException | IOException e) {
       throw new IOException("Error reading " + path, e);
     }
   }
 
   /** Read a UTF-8 text file from the local drive. */
-  public static String readFile(final File parentDir, final String name)
+  public static String readFile(Path parentDir, String name)
       throws IOException {
     if (parentDir == null) {
       return null;
     }
-    final File path = new File(parentDir, name);
-    try {
-      return asString(new FileInputStream(path));
-    } catch (FileNotFoundException e) {
+    Path path = parentDir.resolve(name);
+    try (InputStream in = Files.newInputStream(path)) {
+      return new String(ByteStreams.toByteArray(in), ENC);
+    } catch (NoSuchFileException e) {
       return null;
     } catch (IOException e) {
       throw new IOException("Error reading " + path, e);
     }
   }
 
-  private static String asString(final InputStream in)
-      throws UnsupportedEncodingException, IOException {
-    try {
-      final StringBuilder w = new StringBuilder();
-      final InputStreamReader r = new InputStreamReader(in, ENC);
-      final char[] buf = new char[512];
-      int n;
-      while ((n = r.read(buf)) > 0) {
-        w.append(buf, 0, n);
-      }
-      return w.toString();
-    } finally {
-      in.close();
-    }
-  }
-
   private static DocumentBuilder newBuilder()
       throws ParserConfigurationException {
-    final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
     factory.setValidating(false);
     factory.setExpandEntityReferences(false);
     factory.setIgnoringComments(true);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index ada3ebf..88584eb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -78,7 +78,7 @@
       final HttpServletResponse rsp) throws IOException {
 
     final String sid = webSession.get().getSessionId();
-    final CurrentUser currentUser = webSession.get().getCurrentUser();
+    final CurrentUser currentUser = webSession.get().getUser();
     final String what = "sign out";
     final long when = TimeUtil.nowMs();
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index 47593aa..adad03f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -34,8 +34,8 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
-    return session.get().getCurrentUser();
+  public CurrentUser getUser() {
+    return session.get().getUser();
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 8c469a9..6e6324e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.common.base.MoreObjects;
@@ -26,6 +27,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.AuthenticationFailedException;
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
@@ -168,6 +170,10 @@
         rsp.sendError(SC_UNAUTHORIZED);
         return false;
       }
+    } catch (AuthenticationFailedException e) {
+      log.warn("Authentication failed for " + username + ": " + e.getMessage());
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
     } catch (AccountException e) {
       log.warn("Authentication failed for " + username, e);
       rsp.sendError(SC_UNAUTHORIZED);
@@ -183,10 +189,10 @@
   }
 
   private String encoding(HttpServletRequest req) {
-    return MoreObjects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
+    return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
   }
 
-  class Response extends HttpServletResponseWrapper {
+  static class Response extends HttpServletResponseWrapper {
     private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
 
     Response(HttpServletResponse rsp) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index 12de344..38dd118 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
@@ -36,7 +37,6 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Collections;
@@ -189,25 +189,17 @@
   }
 
   private static String H(String data) {
-    try {
-      MessageDigest md = newMD5();
-      md.update(data.getBytes("UTF-8"));
-      return LHEX(md.digest());
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("UTF-8 encoding not available", e);
-    }
+    MessageDigest md = newMD5();
+    md.update(data.getBytes(UTF_8));
+    return LHEX(md.digest());
   }
 
   private static String KD(String secret, String data) {
-    try {
-      MessageDigest md = newMD5();
-      md.update(secret.getBytes("UTF-8"));
-      md.update((byte) ':');
-      md.update(data.getBytes("UTF-8"));
-      return LHEX(md.digest());
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("UTF-8 encoding not available", e);
-    }
+    MessageDigest md = newMD5();
+    md.update(secret.getBytes(UTF_8));
+    md.update((byte) ':');
+    md.update(data.getBytes(UTF_8));
+    return LHEX(md.digest());
   }
 
   private static MessageDigest newMD5() {
@@ -260,8 +252,12 @@
       } else {
         int space = auth.indexOf(' ', eq + 1);
         int comma = auth.indexOf(',', eq + 1);
-        if (space < 0) space = auth.length();
-        if (comma < 0) comma = auth.length();
+        if (space < 0) {
+          space = auth.length();
+        }
+        if (comma < 0) {
+          comma = auth.length();
+        }
 
         final int e = Math.min(space, comma);
         value = auth.substring(eq + 1, e);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 614184a..f6b79e5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -84,7 +84,7 @@
         return;
       }
 
-      CurrentUser self = session.get().getCurrentUser();
+      CurrentUser self = session.get().getUser();
       if (!self.getCapabilities().canRunAs()) {
         replyError(req, res,
             SC_FORBIDDEN,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 86debdd..4b3eca7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -21,9 +21,8 @@
 import com.google.gerrit.httpd.raw.CatServlet;
 import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.httpd.raw.LegacyGerritServlet;
-import com.google.gerrit.httpd.raw.RobotsServlet;
 import com.google.gerrit.httpd.raw.SshInfoServlet;
-import com.google.gerrit.httpd.raw.StaticServlet;
+import com.google.gerrit.httpd.raw.StaticModule;
 import com.google.gerrit.httpd.raw.ToolServlet;
 import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
 import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
@@ -77,7 +76,6 @@
       serve("/signout").with(HttpLogoutServlet.class);
     }
     serve("/ssh_info").with(SshInfoServlet.class);
-    serve("/static/*").with(StaticServlet.class);
 
     serve("/Main.class").with(notFound());
     serve("/com/google/gerrit/launcher/*").with(notFound());
@@ -106,7 +104,7 @@
 
     filter("/Documentation/").through(QueryDocumentationFilter.class);
 
-    serve("/robots.txt").with(RobotsServlet.class);
+    install(new StaticModule());
   }
 
   private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 7831161..9e425e9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -17,26 +17,24 @@
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.httpd.auth.become.BecomeAnyAccountModule;
 import com.google.gerrit.httpd.auth.container.HttpAuthModule;
 import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
-import com.google.gerrit.httpd.gitweb.GitWebModule;
+import com.google.gerrit.httpd.gitweb.GitwebModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
 import com.google.inject.ProvisionException;
 import com.google.inject.servlet.RequestScoped;
 
@@ -45,25 +43,18 @@
 public class WebModule extends LifecycleModule {
   private final AuthConfig authConfig;
   private final boolean wantSSL;
-  private final GitWebConfig gitWebConfig;
+  private final GitwebCgiConfig gitwebCgiConfig;
   private final GerritOptions options;
 
   @Inject
-  WebModule(final AuthConfig authConfig,
-      @CanonicalWebUrl @Nullable final String canonicalUrl,
+  WebModule(AuthConfig authConfig,
+      @CanonicalWebUrl @Nullable String canonicalUrl,
       GerritOptions options,
-      final Injector creatingInjector) {
+      GitwebCgiConfig gitwebCgiConfig) {
     this.authConfig = authConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
     this.options = options;
-
-    this.gitWebConfig =
-        creatingInjector.createChildInjector(new AbstractModule() {
-          @Override
-          protected void configure() {
-            bind(GitWebConfig.class);
-          }
-        }).getInstance(GitWebConfig.class);
+    this.gitwebCgiConfig = gitwebCgiConfig;
   }
 
   @Override
@@ -84,13 +75,10 @@
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module(options.enableMasterFeatures()));
 
-    bind(GitWebConfig.class).toInstance(gitWebConfig);
-    if (gitWebConfig.getGitwebCGI() != null) {
-      install(new GitWebModule());
+    if (gitwebCgiConfig.getGitwebCgi() != null) {
+      install(new GitwebModule());
     }
 
-    bind(GerritConfigProvider.class);
-    bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
     DynamicSet.setOf(binder(), WebUiPlugin.class);
 
     install(new AsyncReceiveCommits.Module());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index 3349cc1..b2d32fc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -25,7 +25,7 @@
   public String getXGerritAuth();
   public boolean isValidXGerritAuth(String keyIn);
   public AccountExternalId.Key getLastLoginExternalId();
-  public CurrentUser getCurrentUser();
+  public CurrentUser getUser();
   public void login(AuthResult res, boolean rememberMe);
 
   /** Set the user account for this current request only. */
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 5db620c..0250939 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -201,6 +201,10 @@
       this.auth = auth;
     }
 
+    public long getExpiresAt() {
+      return expiresAt;
+    }
+
     Account.Id getAccountId() {
       return accountId;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index b8a8092..56c5cbd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -54,8 +54,6 @@
 @SuppressWarnings("serial")
 @Singleton
 class BecomeAnyAccountLoginServlet extends HttpServlet {
-  private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
-
   private final SchemaFactory<ReviewDb> schema;
   private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
@@ -104,13 +102,10 @@
         throw new ServletException(e);
       }
       rsp.setContentType("text/html");
-      rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+      rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
       rsp.setContentLength(raw.length);
-      final OutputStream out = rsp.getOutputStream();
-      try {
+      try (OutputStream out = rsp.getOutputStream()) {
         out.write(raw);
-      } finally {
-        out.close();
       }
       return;
     }
@@ -120,14 +115,6 @@
       final StringBuilder rdr = new StringBuilder();
       rdr.append(req.getContextPath());
       rdr.append("/");
-      if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
-        if (rdr.indexOf("?") < 0) {
-          rdr.append("?");
-        } else {
-          rdr.append("&");
-        }
-        rdr.append("gwt.codesvr=").append(req.getParameter("gwt.codesvr"));
-      }
 
       if (res.isNew()) {
         rdr.append('#' + PageLinks.REGISTER);
@@ -138,14 +125,14 @@
 
     } else {
       rsp.setContentType("text/html");
-      rsp.setCharacterEncoding(HtmlDomUtil.ENC);
-      final Writer out = rsp.getWriter();
-      out.write("<html>");
-      out.write("<body>");
-      out.write("<h1>Account Not Found</h1>");
-      out.write("</body>");
-      out.write("</html>");
-      out.close();
+      rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
+      try (Writer out = rsp.getWriter()) {
+        out.write("<html>");
+        out.write("<body>");
+        out.write("<h1>Account Not Found</h1>");
+        out.write("</body>");
+        out.write("</html>");
+      }
     }
   }
 
@@ -155,16 +142,9 @@
     if (doc == null) {
       throw new FileNotFoundException("No " + pageName + " in webapp");
     }
-    if (!IS_DEV) {
-      final Element devmode = HtmlDomUtil.find(doc, "gwtdevmode");
-      if (devmode != null) {
-        devmode.getParentNode().removeChild(devmode);
-      }
-    }
 
     Element userlistElement = HtmlDomUtil.find(doc, "userlist");
-    ReviewDb db = schema.open();
-    try {
+    try (ReviewDb db = schema.open()) {
       ResultSet<Account> accounts = db.accounts().firstNById(100);
       for (Account a : accounts) {
         String displayName;
@@ -184,8 +164,6 @@
         userlistElement.appendChild(linkElement);
         userlistElement.appendChild(doc.createElement("br"));
       }
-    } finally {
-      db.close();
     }
 
     return HtmlDomUtil.toUTF8(doc);
@@ -206,15 +184,10 @@
   }
 
   private AuthResult byUserName(final String userName) {
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        AccountExternalId.Key key =
-            new AccountExternalId.Key(SCHEME_USERNAME, userName);
-        return auth(db.accountExternalIds().get(key));
-      } finally {
-        db.close();
-      }
+    try (ReviewDb db = schema.open()) {
+      AccountExternalId.Key key =
+          new AccountExternalId.Key(SCHEME_USERNAME, userName);
+      return auth(db.accountExternalIds().get(key));
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
       return null;
@@ -222,14 +195,9 @@
   }
 
   private AuthResult byPreferredEmail(final String email) {
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        List<Account> matches = db.accounts().byPreferredEmail(email).toList();
-        return matches.size() == 1 ? auth(matches.get(0)) : null;
-      } finally {
-        db.close();
-      }
+    try (ReviewDb db = schema.open()) {
+      List<Account> matches = db.accounts().byPreferredEmail(email).toList();
+      return matches.size() == 1 ? auth(matches.get(0)) : null;
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
       return null;
@@ -243,13 +211,8 @@
     } catch (NumberFormatException nfe) {
       return null;
     }
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        return auth(db.accounts().get(id));
-      } finally {
-        db.close();
-      }
+    try (ReviewDb db = schema.open()) {
+      return auth(db.accounts().get(id));
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
       return null;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index 8947a38..bd7014a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -21,8 +21,8 @@
 
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.RemoteUserUtil;
+import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.config.AuthConfig;
@@ -112,13 +112,10 @@
 
       CacheHeaders.setNotCacheable(rsp);
       rsp.setContentType("text/html");
-      rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+      rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
       rsp.setContentLength(tosend.length);
-      final OutputStream out = rsp.getOutputStream();
-      try {
+      try (OutputStream out = rsp.getOutputStream()) {
         out.write(tosend);
-      } finally {
-        out.close();
       }
     }
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 6d8a0cda..bfbf1ff 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.auth.container;
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -102,14 +103,10 @@
       final byte[] bin = HtmlDomUtil.toUTF8(doc);
       rsp.setStatus(HttpServletResponse.SC_FORBIDDEN);
       rsp.setContentType("text/html");
-      rsp.setCharacterEncoding("UTF-8");
+      rsp.setCharacterEncoding(UTF_8.name());
       rsp.setContentLength(bin.length);
-      final ServletOutputStream out = rsp.getOutputStream();
-      try {
+      try (ServletOutputStream out = rsp.getOutputStream()) {
         out.write(bin);
-      } finally {
-        out.flush();
-        out.close();
       }
       return;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
index 8652ef0..4c7ce7b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
@@ -1,4 +1,4 @@
-//Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2011 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.
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index aea89ba..a5b4f7a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -92,13 +92,10 @@
     byte[] bin = HtmlDomUtil.toUTF8(doc);
     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     res.setContentType("text/html");
-    res.setCharacterEncoding("UTF-8");
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(bin.length);
-    ServletOutputStream out = res.getOutputStream();
-    try {
+    try (ServletOutputStream out = res.getOutputStream()) {
       out.write(bin);
-    } finally {
-      out.close();
     }
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
index 41aa552..b5365ad 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
@@ -14,16 +14,19 @@
 
 package com.google.gerrit.httpd.gitweb;
 
-import com.google.gerrit.httpd.GitWebConfig;
+import static com.google.gerrit.common.FileUtil.lastModified;
+
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.util.IO;
-
-import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletOutputStream;
@@ -38,16 +41,16 @@
   private final byte[] raw;
 
   @Inject
-  GitLogoServlet(final GitWebConfig gitWebConfig) throws IOException {
+  GitLogoServlet(GitwebCgiConfig cfg) throws IOException {
     byte[] png;
-    final File src = gitWebConfig.getGitLogoPNG();
+    Path src = cfg.getGitLogoPng();
     if (src != null) {
-      try {
-        png = IO.readFully(src);
-      } catch (FileNotFoundException e) {
+      try (InputStream in = Files.newInputStream(src)) {
+        png = ByteStreams.toByteArray(in);
+      } catch (NoSuchFileException e) {
         png = null;
       }
-      modified = src.lastModified();
+      modified = lastModified(src);
     } else {
       modified = -1;
       png = null;
@@ -69,11 +72,8 @@
       rsp.setDateHeader("Last-Modified", modified);
       CacheHeaders.setCacheable(req, rsp, 5, TimeUnit.MINUTES);
 
-      final ServletOutputStream os = rsp.getOutputStream();
-      try {
+      try (ServletOutputStream os = rsp.getOutputStream()) {
         os.write(raw);
-      } finally {
-        os.close();
       }
     } else {
       CacheHeaders.setNotCacheable(rsp);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
similarity index 77%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
index 4a39b97..abcc4fe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
@@ -14,16 +14,19 @@
 
 package com.google.gerrit.httpd.gitweb;
 
-import com.google.gerrit.httpd.GitWebConfig;
+import static com.google.gerrit.common.FileUtil.lastModified;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletOutputStream;
@@ -32,9 +35,9 @@
 import javax.servlet.http.HttpServletResponse;
 
 @SuppressWarnings("serial")
-abstract class GitWebCssServlet extends HttpServlet {
+abstract class GitwebCssServlet extends HttpServlet {
   @Singleton
-  static class Site extends GitWebCssServlet {
+  static class Site extends GitwebCssServlet {
     @Inject
     Site(SitePaths paths) throws IOException {
       super(paths.site_css);
@@ -42,28 +45,26 @@
   }
 
   @Singleton
-  static class Default extends GitWebCssServlet {
+  static class Default extends GitwebCssServlet {
     @Inject
-    Default(GitWebConfig gwc) throws IOException {
-      super(gwc.getGitwebCSS());
+    Default(GitwebCgiConfig gwcc) throws IOException {
+      super(gwcc.getGitwebCss());
     }
   }
 
-  private static final String ENC = "UTF-8";
-
   private final long modified;
   private final byte[] raw_css;
   private final byte[] gz_css;
 
-  GitWebCssServlet(final File src)
+  GitwebCssServlet(final Path src)
       throws IOException {
     if (src != null) {
-      final File dir = src.getParentFile();
-      final String name = src.getName();
+      final Path dir = src.getParent();
+      final String name = src.getFileName().toString();
       final String raw = HtmlDomUtil.readFile(dir, name);
       if (raw != null) {
-        modified = src.lastModified();
-        raw_css = raw.getBytes(ENC);
+        modified = lastModified(src);
+        raw_css = raw.getBytes(UTF_8);
         gz_css = HtmlDomUtil.compress(raw_css);
       } else {
         modified = -1L;
@@ -87,7 +88,7 @@
       final HttpServletResponse rsp) throws IOException {
     if (raw_css != null) {
       rsp.setContentType("text/css");
-      rsp.setCharacterEncoding(ENC);
+      rsp.setCharacterEncoding(UTF_8.name());
       final byte[] toSend;
       if (RPCServletUtils.acceptsGzipEncoding(req)) {
         rsp.setHeader("Content-Encoding", "gzip");
@@ -99,11 +100,8 @@
       rsp.setDateHeader("Last-Modified", modified);
       CacheHeaders.setCacheable(req, rsp, 5, TimeUnit.MINUTES);
 
-      final ServletOutputStream os = rsp.getOutputStream();
-      try {
+      try (ServletOutputStream os = rsp.getOutputStream()) {
         os.write(toSend);
-      } finally {
-        os.close();
       }
     } else {
       CacheHeaders.setNotCacheable(rsp);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
similarity index 73%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
index d71732a..f30eb52 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
@@ -14,16 +14,19 @@
 
 package com.google.gerrit.httpd.gitweb;
 
-import com.google.gerrit.httpd.GitWebConfig;
+import static com.google.gerrit.common.FileUtil.lastModified;
+
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.server.config.GitwebCgiConfig;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.util.IO;
-
-import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletOutputStream;
@@ -33,21 +36,21 @@
 
 @SuppressWarnings("serial")
 @Singleton
-class GitWebJavaScriptServlet extends HttpServlet {
+class GitwebJavaScriptServlet extends HttpServlet {
   private final long modified;
   private final byte[] raw;
 
   @Inject
-  GitWebJavaScriptServlet(final GitWebConfig gitWebConfig) throws IOException {
+  GitwebJavaScriptServlet(GitwebCgiConfig gitwebCgiConfig) throws IOException {
     byte[] png;
-    final File src = gitWebConfig.getGitwebJS();
+    Path src = gitwebCgiConfig.getGitwebJs();
     if (src != null) {
-      try {
-        png = IO.readFully(src);
-      } catch (FileNotFoundException e) {
+      try (InputStream in = Files.newInputStream(src)) {
+        png = ByteStreams.toByteArray(in);
+      } catch (NoSuchFileException e) {
         png = null;
       }
-      modified = src.lastModified();
+      modified = lastModified(src);
     } else {
       modified = -1;
       png = null;
@@ -69,11 +72,8 @@
       rsp.setDateHeader("Last-Modified", modified);
       CacheHeaders.setCacheable(req, rsp, 5, TimeUnit.MINUTES);
 
-      final ServletOutputStream os = rsp.getOutputStream();
-      try {
+      try (ServletOutputStream os = rsp.getOutputStream()) {
         os.write(raw);
-      } finally {
-        os.close();
       }
     } else {
       CacheHeaders.setNotCacheable(rsp);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java
similarity index 73%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebModule.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java
index 14ccf89..21a4d65 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebModule.java
@@ -16,13 +16,13 @@
 
 import com.google.inject.servlet.ServletModule;
 
-public class GitWebModule extends ServletModule {
+public class GitwebModule extends ServletModule {
   @Override
   protected void configureServlets() {
-    serve("/gitweb").with(GitWebServlet.class);
+    serveRegex("^/(?:a/)?gitweb").with(GitwebServlet.class);
     serve("/gitweb-logo.png").with(GitLogoServlet.class);
-    serve("/gitweb.js").with(GitWebJavaScriptServlet.class);
-    serve("/gitweb-default.css").with(GitWebCssServlet.Default.class);
-    serve("/gitweb-site.css").with(GitWebCssServlet.Site.class);
+    serve("/gitweb.js").with(GitwebJavaScriptServlet.class);
+    serve("/gitweb-default.css").with(GitwebCssServlet.Default.class);
+    serve("/gitweb-site.css").with(GitwebCssServlet.Site.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
similarity index 84%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index 573725c..b80b69f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -29,24 +29,30 @@
 
 package com.google.gerrit.httpd.gitweb;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.GitwebCgiConfig;
+import com.google.gerrit.server.config.GitwebConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,7 +61,6 @@
 import java.io.BufferedReader;
 import java.io.EOFException;
 import java.io.File;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -63,6 +68,8 @@
 import java.io.PrintWriter;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -76,15 +83,15 @@
 /** Invokes {@code gitweb.cgi} for the project given in {@code p}. */
 @SuppressWarnings("serial")
 @Singleton
-class GitWebServlet extends HttpServlet {
+class GitwebServlet extends HttpServlet {
   private static final Logger log =
-      LoggerFactory.getLogger(GitWebServlet.class);
+      LoggerFactory.getLogger(GitwebServlet.class);
 
   private static final String PROJECT_LIST_ACTION = "project_list";
 
   private final Set<String> deniedActions;
   private final int bufferSize = 8192;
-  private final File gitwebCgi;
+  private final Path gitwebCgi;
   private final URI gitwebUrl;
   private final LocalDiskRepositoryManager repoManager;
   private final ProjectControl.Factory projectControl;
@@ -93,21 +100,24 @@
   private final EnvList _env;
 
   @Inject
-  GitWebServlet(final LocalDiskRepositoryManager repoManager,
-      final ProjectControl.Factory projectControl,
-      final Provider<AnonymousUser> anonymousUserProvider,
-      final Provider<CurrentUser> userProvider,
-      final SitePaths site,
-      final GerritConfig gerritConfig, final GitWebConfig gitWebConfig)
+  GitwebServlet(LocalDiskRepositoryManager repoManager,
+      ProjectControl.Factory projectControl,
+      Provider<AnonymousUser> anonymousUserProvider,
+      Provider<CurrentUser> userProvider,
+      SitePaths site,
+      @GerritServerConfig Config cfg,
+      SshInfo sshInfo,
+      GitwebConfig gitwebConfig,
+      GitwebCgiConfig gitwebCgiConfig)
       throws IOException {
     this.repoManager = repoManager;
     this.projectControl = projectControl;
     this.anonymousUserProvider = anonymousUserProvider;
     this.userProvider = userProvider;
-    this.gitwebCgi = gitWebConfig.getGitwebCGI();
+    this.gitwebCgi = gitwebCgiConfig.getGitwebCgi();
     this.deniedActions = new HashSet<>();
 
-    final String url = gitWebConfig.getUrl();
+    final String url = gitwebConfig.getUrl();
     if ((url != null) && (!url.equals("gitweb"))) {
       URI uri = null;
       try {
@@ -125,7 +135,7 @@
     deniedActions.add("project_index");
 
     _env = new EnvList();
-    makeSiteConfig(site, gerritConfig);
+    makeSiteConfig(site, cfg, sshInfo);
 
     if (!_env.envMap.containsKey("SystemRoot")) {
       String os = System.getProperty("os.name");
@@ -143,30 +153,32 @@
     }
   }
 
-  private void makeSiteConfig(final SitePaths site,
-      final GerritConfig gerritConfig) throws IOException {
-    if (!site.tmp_dir.exists()) {
-      site.tmp_dir.mkdirs();
+  private void makeSiteConfig(SitePaths site, Config cfg, SshInfo sshInfo)
+      throws IOException {
+    if (!Files.exists(site.tmp_dir)) {
+      Files.createDirectories(site.tmp_dir);
     }
-    File myconf = File.createTempFile("gitweb_config", ".perl", site.tmp_dir);
+    Path myconf = Files.createTempFile(site.tmp_dir, "gitweb_config", ".perl");
 
     // To make our configuration file only readable or writable by us;
     // this reduces the chances of someone tampering with the file.
     //
-    myconf.setWritable(false, false /* all */);
-    myconf.setReadable(false, false /* all */);
-    myconf.setExecutable(false, false /* all */);
+    // TODO(dborowitz): Is there a portable way to do this with NIO?
+    File myconfFile = myconf.toFile();
+    myconfFile.setWritable(false, false /* all */);
+    myconfFile.setReadable(false, false /* all */);
+    myconfFile.setExecutable(false, false /* all */);
 
-    myconf.setWritable(true, true /* owner only */);
-    myconf.setReadable(true, true /* owner only */);
+    myconfFile.setWritable(true, true /* owner only */);
+    myconfFile.setReadable(true, true /* owner only */);
 
-    myconf.deleteOnExit();
+    myconfFile.deleteOnExit();
 
     _env.set("GIT_DIR", ".");
-    _env.set("GITWEB_CONFIG", myconf.getAbsolutePath());
+    _env.set("GITWEB_CONFIG", myconf.toAbsolutePath().toString());
 
-    final PrintWriter p = new PrintWriter(new FileWriter(myconf));
-    try {
+    try (PrintWriter p =
+        new PrintWriter(Files.newBufferedWriter(myconf, UTF_8))) {
       p.print("# Autogenerated by Gerrit Code Review \n");
       p.print("# DO NOT EDIT\n");
       p.print("\n");
@@ -174,12 +186,12 @@
       // We are mounted at the same level in the context as the main
       // UI, so we can include the same header and footer scheme.
       //
-      final File hdr = site.site_header;
-      if (hdr.isFile()) {
+      Path hdr = site.site_header;
+      if (Files.isRegularFile(hdr)) {
         p.print("$site_header = " + quoteForPerl(hdr) + ";\n");
       }
-      final File ftr = site.site_footer;
-      if (ftr.isFile()) {
+      Path ftr = site.site_footer;
+      if (Files.isRegularFile(ftr)) {
         p.print("$site_footer = " + quoteForPerl(ftr) + ";\n");
       }
 
@@ -192,8 +204,8 @@
       p.print("$logo = 'gitweb-logo.png';\n");
       p.print("$javascript = 'gitweb.js';\n");
       p.print("@stylesheets = ('gitweb-default.css');\n");
-      final File css = site.site_css;
-      if (css.isFile()) {
+      Path css = site.site_css;
+      if (Files.isRegularFile(css)) {
         p.print("push @stylesheets, 'gitweb-site.css';\n");
       }
 
@@ -226,8 +238,8 @@
 
       // Generate URLs using anonymous git://
       //
-      if (gerritConfig.getGitDaemonUrl() != null) {
-        String url = gerritConfig.getGitDaemonUrl();
+      String url = cfg.getString("gerrit", null, "canonicalGitUrl");
+      if (url != null) {
         if (url.endsWith("/")) {
           url = url.substring(0, url.length() - 1);
         }
@@ -240,8 +252,8 @@
 
       // Generate URLs using authenticated ssh://
       //
-      if (gerritConfig.getSshdAddress() != null) {
-        String sshAddr = gerritConfig.getSshdAddress();
+      if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
+        String sshAddr = sshInfo.getHostKeys().get(0).getHost();
         p.print("if ($ENV{'GERRIT_USER_NAME'}) {\n");
         p.print("  push @git_base_url_list, join('', 'ssh://'");
         p.print(", $ENV{'GERRIT_USER_NAME'}");
@@ -258,7 +270,7 @@
       }
 
       // Link back to Gerrit (when possible, to matching review record).
-      // Supported Gitweb's hash values are:
+      // Supported gitweb's hash values are:
       // - (missing),
       // - HEAD,
       // - refs/heads/<branch>,
@@ -269,9 +281,9 @@
       p.print("  my $h = shift;\n");
       p.print("  my $q;\n");
       p.print("  if (!$h || $h eq 'HEAD') {\n");
-      p.print("    $q = qq{#q,project:$ENV{'GERRIT_PROJECT_NAME'}};\n");
+      p.print("    $q = qq{#/q/project:$ENV{'GERRIT_PROJECT_NAME'}};\n");
       p.print("  } elsif ($h =~ /^refs\\/heads\\/([-\\w]+)$/) {\n");
-      p.print("    $q = qq{#q,project:$ENV{'GERRIT_PROJECT_NAME'}");
+      p.print("    $q = qq{#/q/project:$ENV{'GERRIT_PROJECT_NAME'}");
       p.print("+branch:$1};\n"); // wrapped
       p.print("  } elsif ($h =~ /^refs\\/changes\\/\\d{2}\\/(\\d+)\\/\\d+$/) ");
       p.print("{\n"); // wrapped
@@ -294,15 +306,15 @@
       // If the administrator has created a site-specific gitweb_config,
       // load that before we perform any final overrides.
       //
-      final File sitecfg = site.site_gitweb;
-      if (sitecfg.isFile()) {
+      Path sitecfg = site.site_gitweb;
+      if (Files.isRegularFile(sitecfg)) {
         p.print("$GITWEB_CONFIG = " + quoteForPerl(sitecfg) + ";\n");
         p.print("if (-e $GITWEB_CONFIG) {\n");
         p.print("  do " + quoteForPerl(sitecfg) + ";\n");
         p.print("}\n");
       }
 
-      final File root = repoManager.getBasePath();
+      Path root = repoManager.getBasePath();
       p.print("$projectroot = " + quoteForPerl(root) + ";\n");
 
       // Permit exporting only the project we were started for.
@@ -326,18 +338,16 @@
       //
       p.print("$feature{'forks'}{'override'} = 0;\n");
       p.print("$feature{'forks'}{'default'} = [0];\n");
-    } finally {
-      p.close();
     }
 
-    myconf.setReadOnly();
+    myconfFile.setReadOnly();
   }
 
-  private String quoteForPerl(File value) {
-    return quoteForPerl(value.getAbsolutePath());
+  private static String quoteForPerl(Path value) {
+    return quoteForPerl(value.toAbsolutePath().toString());
   }
 
-  private String quoteForPerl(String value) {
+  private static String quoteForPerl(String value) {
     if (value == null || value.isEmpty()) {
       return "''";
     }
@@ -405,19 +415,13 @@
       return;
     }
 
-    final Repository repo;
-    try {
-      repo = repoManager.openRepository(nameKey);
+
+    try (Repository repo = repoManager.openRepository(nameKey)) {
+      CacheHeaders.setNotCacheable(rsp);
+      exec(req, rsp, project);
     } catch (RepositoryNotFoundException e) {
       getServletContext().log("Cannot open repository", e);
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-    try {
-      CacheHeaders.setNotCacheable(rsp);
-      exec(req, rsp, project);
-    } finally {
-      repo.close();
     }
   }
 
@@ -455,9 +459,10 @@
   private void exec(final HttpServletRequest req,
       final HttpServletResponse rsp, final ProjectControl project) throws IOException {
     final Process proc =
-        Runtime.getRuntime().exec(new String[] {gitwebCgi.getAbsolutePath()},
+        Runtime.getRuntime().exec(
+            new String[] {gitwebCgi.toAbsolutePath().toString()},
             makeEnv(req, project),
-            gitwebCgi.getAbsoluteFile().getParentFile());
+            gitwebCgi.toAbsolutePath().getParent().toFile());
 
     copyStderrToLog(proc.getErrorStream());
     if (0 < req.getContentLength()) {
@@ -466,25 +471,15 @@
       proc.getOutputStream().close();
     }
 
-    try {
-      final InputStream in;
+    try (InputStream in = new BufferedInputStream(proc.getInputStream(), bufferSize)) {
+      readCgiHeaders(rsp, in);
 
-      in = new BufferedInputStream(proc.getInputStream(), bufferSize);
-      try {
-        readCgiHeaders(rsp, in);
-
-        final OutputStream out = rsp.getOutputStream();
-        try {
-          final byte[] buf = new byte[bufferSize];
-          int n;
-          while ((n = in.read(buf)) > 0) {
-            out.write(buf, 0, n);
-          }
-        } finally {
-          out.close();
+      try (OutputStream out = rsp.getOutputStream()) {
+        final byte[] buf = new byte[bufferSize];
+        int n;
+        while ((n = in.read(buf)) > 0) {
+          out.write(buf, 0, n);
         }
-      } finally {
-        in.close();
       }
     } catch (IOException e) {
       // The browser has probably closed its input stream. We don't
@@ -535,7 +530,7 @@
     //
     env.set("REQUEST_METHOD", req.getMethod());
     env.set("SCRIPT_NAME", req.getContextPath() + req.getServletPath());
-    env.set("SCRIPT_FILENAME", gitwebCgi.getAbsolutePath());
+    env.set("SCRIPT_FILENAME", gitwebCgi.toAbsolutePath().toString());
     env.set("SERVER_NAME", req.getServerName());
     env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
     env.set("SERVER_PROTOCOL", req.getProtocol());
@@ -556,8 +551,8 @@
     }
 
     String remoteUser = null;
-    if (project.getCurrentUser().isIdentifiedUser()) {
-      final IdentifiedUser u = (IdentifiedUser) project.getCurrentUser();
+    if (project.getUser().isIdentifiedUser()) {
+      final IdentifiedUser u = project.getUser().asIdentifiedUser();
       final String user = u.getUserName();
       env.set("GERRIT_USER_NAME", user);
       if (user != null && !user.isEmpty()) {
@@ -569,7 +564,7 @@
     env.set("REMOTE_USER", remoteUser);
 
     // Override CGI settings using alternative URI provided by gitweb.url.
-    // This is required to trick Gitweb into thinking that it's served under
+    // This is required to trick gitweb into thinking that it's served under
     // different URL. Setting just $my_uri on the perl's side isn't enough,
     // because few actions (atom, blobdiff_plain, commitdiff_plain) rely on
     // URL returned by $cgi->self_url().
@@ -634,29 +629,24 @@
           log.debug("Unexpected error copying input to CGI", e);
         }
       }
-    }, "GitWeb-InputFeeder").start();
+    }, "Gitweb-InputFeeder").start();
   }
 
   private void copyStderrToLog(final InputStream in) {
     new Thread(new Runnable() {
       @Override
       public void run() {
-        try {
-          final BufferedReader br =
-              new BufferedReader(new InputStreamReader(in, "ISO-8859-1"));
-          try {
-            String line;
-            while ((line = br.readLine()) != null) {
-              log.error("CGI: " + line);
-            }
-          } finally {
-            br.close();
+        try (BufferedReader br =
+            new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) {
+          String line;
+          while ((line = br.readLine()) != null) {
+            log.error("CGI: " + line);
           }
         } catch (IOException e) {
           log.debug("Unexpected error copying stderr from CGI", e);
         }
       }
-    }, "GitWeb-ErrorLogger").start();
+    }, "Gitweb-ErrorLogger").start();
   }
 
   private static Enumeration<String> enumerateHeaderNames(HttpServletRequest req) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java
index 47ef520..6afe52a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ContextMapper.java
@@ -45,7 +45,7 @@
     return base + name;
   }
 
-  private class WrappedRequest extends HttpServletRequestWrapper {
+  private static class WrappedRequest extends HttpServletRequestWrapper {
     private final String contextPath;
     private final String pathInfo;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 4c9c59d..4e635b2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
 import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
 import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Optional;
@@ -24,6 +26,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.io.ByteStreams;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.resources.Resource;
@@ -55,14 +58,14 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
@@ -303,7 +306,7 @@
       }
       if (!entry.isPresent() && file.endsWith("/index.html")) {
         String pfx = file.substring(0, file.length() - "index.html".length());
-        long pluginLastModified = holder.plugin.getSrcFile().lastModified();
+        long pluginLastModified = lastModified(holder.plugin.getSrcFile());
         if (hasUpToDateCachedResource(rsc, pluginLastModified)) {
           rsc.send(req, res);
         } else {
@@ -409,18 +412,18 @@
 
     if (about != null) {
       InputStreamReader isr = new InputStreamReader(scanner.getInputStream(about));
-      BufferedReader reader = new BufferedReader(isr);
       StringBuilder aboutContent = new StringBuilder();
-      String line;
-      while ((line = reader.readLine()) != null) {
-        line = line.trim();
-        if (line.isEmpty()) {
-          aboutContent.append("\n");
-        } else {
-          aboutContent.append(line).append("\n");
+      try (BufferedReader reader = new BufferedReader(isr)) {
+        String line;
+        while ((line = reader.readLine()) != null) {
+          line = line.trim();
+          if (line.isEmpty()) {
+            aboutContent.append("\n");
+          } else {
+            aboutContent.append(line).append("\n");
+          }
         }
       }
-      reader.close();
 
       // Only append the About section if there was anything in it
       if (aboutContent.toString().trim().length() > 0) {
@@ -466,13 +469,13 @@
     m.appendTail(sb);
 
     byte[] html = new MarkdownFormatter()
-      .markdownToDocHtml(sb.toString(), "UTF-8");
+      .markdownToDocHtml(sb.toString(), UTF_8.name());
     resourceCache.put(cacheKey, new SmallResource(html)
         .setContentType("text/html")
-        .setCharacterEncoding("UTF-8")
+        .setCharacterEncoding(UTF_8.name())
         .setLastModified(lastModifiedTime));
     res.setContentType("text/html");
-    res.setCharacterEncoding("UTF-8");
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(html.length);
     res.setDateHeader("Last-Modified", lastModifiedTime);
     res.getOutputStream().write(html);
@@ -524,7 +527,7 @@
       charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     }
     if (charEnc == null) {
-      charEnc = "UTF-8";
+      charEnc = UTF_8.name();
     }
     return new MarkdownFormatter().extractTitleFromMarkdown(
           readWholeEntry(scanner, entry),
@@ -551,7 +554,7 @@
     }
 
     String txtmd = RawParseUtils.decode(
-        Charset.forName(encoding != null ? encoding : "UTF-8"),
+        Charset.forName(encoding != null ? encoding : UTF_8.name()),
         rawmd);
     long time = entry.getTime();
     if (0 < time) {
@@ -611,12 +614,12 @@
 
   private void sendJsPlugin(Plugin plugin, PluginResourceKey key,
       HttpServletRequest req, HttpServletResponse res) throws IOException {
-    File pluginFile = plugin.getSrcFile();
+    Path path = plugin.getSrcFile();
     if (req.getRequestURI().endsWith(getJsPluginPath(plugin))
-        && pluginFile.exists()) {
-      res.setHeader("Content-Length", Long.toString(pluginFile.length()));
+        && Files.exists(path)) {
+      res.setHeader("Content-Length", Long.toString(Files.size(path)));
       res.setContentType("application/javascript");
-      writeToResponse(res, new FileInputStream(pluginFile));
+      writeToResponse(res, Files.newInputStream(path));
     } else {
       resourceCache.put(key, Resource.NOT_FOUND);
       Resource.NOT_FOUND.send(req, res);
@@ -624,25 +627,15 @@
   }
 
   private static String getJsPluginPath(Plugin plugin) {
-    return String.format("/plugins/%s/static/%s", plugin.getName(), plugin.getSrcFile()
-        .getName());
+    return String.format("/plugins/%s/static/%s", plugin.getName(),
+        plugin.getSrcFile().getFileName());
   }
 
-  private void writeToResponse(HttpServletResponse res, InputStream in)
+  private void writeToResponse(HttpServletResponse res, InputStream inputStream)
       throws IOException {
-    try {
-      OutputStream out = res.getOutputStream();
-      try {
-        byte[] tmp = new byte[1024];
-        int n;
-        while ((n = in.read(tmp)) > 0) {
-          out.write(tmp, 0, n);
-        }
-      } finally {
-        out.close();
-      }
-    } finally {
-      in.close();
+    try (OutputStream out = res.getOutputStream();
+        InputStream in = inputStream) {
+      ByteStreams.copy(in, out);
     }
   }
 
@@ -669,9 +662,9 @@
     }
 
     private static String getPrefix(Plugin plugin, String attr, String def) {
-      File srcFile = plugin.getSrcFile();
+      Path path = plugin.getSrcFile();
       PluginContentScanner scanner = plugin.getContentScanner();
-      if (srcFile == null || scanner == PluginContentScanner.EMPTY) {
+      if (path == null || scanner == PluginContentScanner.EMPTY) {
         return def;
       }
       try {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index 72fc008..ce696a1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -15,47 +15,23 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.common.base.Optional;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import eu.medsea.mimeutil.MimeType;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.NB;
-
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.security.MessageDigest;
-import java.security.SecureRandom;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -73,26 +49,17 @@
 @SuppressWarnings("serial")
 @Singleton
 public class CatServlet extends HttpServlet {
-  private static final MimeType ZIP = new MimeType("application/zip");
   private final Provider<ReviewDb> requestDb;
-  private final GitRepositoryManager repoManager;
-  private final SecureRandom rng;
-  private final FileTypeRegistry registry;
   private final Provider<CurrentUser> userProvider;
   private final ChangeControl.GenericFactory changeControl;
   private final ChangeEditUtil changeEditUtil;
 
   @Inject
-  CatServlet(GitRepositoryManager grm,
-      Provider<ReviewDb> sf,
-      FileTypeRegistry ftr,
+  CatServlet(Provider<ReviewDb> sf,
       ChangeControl.GenericFactory ccf,
       Provider<CurrentUser> usrprv,
       ChangeEditUtil ceu) {
     requestDb = sf;
-    repoManager = grm;
-    rng = new SecureRandom();
-    registry = ftr;
     changeControl = ccf;
     userProvider = usrprv;
     changeEditUtil = ceu;
@@ -129,7 +96,6 @@
 
       if (c < 0) {
         side = 0;
-
       } else {
         try {
           side = Integer.parseInt(keyStr.substring(c + 1));
@@ -149,15 +115,11 @@
     }
 
     final Change.Id changeId = patchKey.getParentKey().getParentKey();
-    final Project project;
-    final String revision;
+    String revision;
     try {
       final ReviewDb db = requestDb.get();
       final ChangeControl control = changeControl.validateFor(changeId,
           userProvider.get());
-
-      project = control.getProject();
-
       if (patchKey.getParentKey().get() == 0) {
         // change edit
         try {
@@ -189,186 +151,9 @@
       return;
     }
 
-    final Repository repo;
-    try {
-      repo = repoManager.openRepository(project.getNameKey());
-    } catch (RepositoryNotFoundException e) {
-      getServletContext().log("Cannot open repository", e);
-      rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-
-    final ObjectLoader blobLoader;
-    final RevCommit fromCommit;
-    final String suffix;
-    final String path = patchKey.getFileName();
-    try (ObjectReader reader = repo.newObjectReader();
-        RevWalk rw = new RevWalk(reader)) {
-      final RevCommit c;
-
-      c = rw.parseCommit(ObjectId.fromString(revision));
-      if (side == 0) {
-        fromCommit = c;
-        suffix = "new";
-
-      } else if (1 <= side && side - 1 < c.getParentCount()) {
-        fromCommit = rw.parseCommit(c.getParent(side - 1));
-        if (c.getParentCount() == 1) {
-          suffix = "old";
-        } else {
-          suffix = "old" + side;
-        }
-
-      } else {
-        rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-        return;
-      }
-
-      try (TreeWalk tw = TreeWalk.forPath(reader, path, fromCommit.getTree())) {
-        if (tw == null) {
-          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-          return;
-        }
-
-        if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
-          blobLoader = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
-
-        } else {
-          rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-          return;
-        }
-      }
-    } catch (IOException e) {
-      getServletContext().log("Cannot read repository", e);
-      rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    } catch (RuntimeException e) {
-      getServletContext().log("Cannot read repository", e);
-      rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    } finally {
-      repo.close();
-    }
-
-    final byte[] raw =
-        blobLoader.isLarge() ? null : blobLoader.getCachedBytes();
-    final long when = fromCommit.getCommitTime() * 1000L;
-
-    rsp.setDateHeader("Last-Modified", when);
-    CacheHeaders.setNotCacheable(rsp);
-
-    OutputStream out;
-    @SuppressWarnings("resource")
-    ZipOutputStream zo;
-
-    final MimeType contentType = registry.getMimeType(path, raw);
-    if (!registry.isSafeInline(contentType)) {
-      // The content may not be safe to transmit inline, as a browser might
-      // interpret it as HTML or JavaScript hosted by this site. Such code
-      // might then run in the site's security domain, and may be able to use
-      // the user's cookies to perform unauthorized actions.
-      //
-      // Usually, wrapping the content into a ZIP file forces the browser to
-      // save the content to the local system instead.
-      //
-
-      rsp.setContentType(ZIP.toString());
-      rsp.setHeader("Content-Disposition", "attachment; filename=\""
-          + safeFileName(path, suffix) + ".zip" + "\"");
-
-      zo = new ZipOutputStream(rsp.getOutputStream());
-
-      final ZipEntry e = new ZipEntry(safeFileName(path, rand(req, suffix)));
-      e.setComment(fromCommit.name() + ":" + path);
-      e.setSize(blobLoader.getSize());
-      e.setTime(when);
-      zo.putNextEntry(e);
-      out = zo;
-
-    } else {
-      rsp.setContentType(contentType.toString());
-      rsp.setHeader("Content-Length", "" + blobLoader.getSize());
-
-      out = rsp.getOutputStream();
-      zo = null;
-    }
-
-    if (raw != null) {
-      out.write(raw);
-    } else {
-      blobLoader.copyTo(out);
-    }
-
-    if (zo != null) {
-      zo.closeEntry();
-    }
-    out.close();
+    String path = patchKey.getFileName();
+    String restUrl = String.format("%s/changes/%d/revisions/%s/files/%s/download?parent=%d",
+        req.getContextPath(), changeId.get(), revision, Url.encode(path), side);
+    rsp.sendRedirect(restUrl);
   }
-
-  private static String safeFileName(String fileName, final String suffix) {
-    // Convert a file path (e.g. "src/Init.c") to a safe file name with
-    // no meta-characters that might be unsafe on any given platform.
-    //
-    final int slash = fileName.lastIndexOf('/');
-    if (slash >= 0) {
-      fileName = fileName.substring(slash + 1);
-    }
-
-    final StringBuilder r = new StringBuilder(fileName.length());
-    for (int i = 0; i < fileName.length(); i++) {
-      final char c = fileName.charAt(i);
-      if (c == '_' || c == '-' || c == '.' || c == '@') {
-        r.append(c);
-
-      } else if ('0' <= c && c <= '9') {
-        r.append(c);
-
-      } else if ('A' <= c && c <= 'Z') {
-        r.append(c);
-
-      } else if ('a' <= c && c <= 'z') {
-        r.append(c);
-
-      } else if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
-        r.append('-');
-
-      } else {
-        r.append('_');
-      }
-    }
-    fileName = r.toString();
-
-    final int ext = fileName.lastIndexOf('.');
-    if (ext <= 0) {
-      return fileName + "_" + suffix;
-
-    } else {
-      return fileName.substring(0, ext) + "_" + suffix
-          + fileName.substring(ext);
-    }
-  }
-
-  private String rand(final HttpServletRequest req, final String suffix)
-      throws UnsupportedEncodingException {
-    // Produce a random suffix that is difficult (or nearly impossible)
-    // for an attacker to guess in advance. This reduces the risk that
-    // an attacker could upload a *.class file and have us send a ZIP
-    // that can be invoked through an applet tag in the victim's browser.
-    //
-    final MessageDigest md = Constants.newMessageDigest();
-    final byte[] buf = new byte[8];
-
-    NB.encodeInt32(buf, 0, req.getRemotePort());
-    md.update(req.getRemoteAddr().getBytes("UTF-8"));
-    md.update(buf, 0, 4);
-
-    NB.encodeInt64(buf, 0, TimeUtil.nowMs());
-    md.update(buf, 0, 8);
-
-    rng.nextBytes(buf);
-    md.update(buf, 0, 8);
-
-    return suffix + "-" + ObjectId.fromRaw(md.digest()).name();
-  }
-
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
new file mode 100644
index 0000000..826ff95
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DirectoryDocServlet.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.cache.Cache;
+
+import java.nio.file.Path;
+
+class DirectoryDocServlet extends ResourceServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final Path doc;
+
+  DirectoryDocServlet(Cache<Path, Resource> cache, Path unpackedWar) {
+    super(cache, true);
+    this.doc = unpackedWar.resolve("Documentation");
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) {
+    return doc.resolve(pathInfo);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DirectoryGwtUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DirectoryGwtUiServlet.java
new file mode 100644
index 0000000..336faad
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/DirectoryGwtUiServlet.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+
+class DirectoryGwtUiServlet extends ResourceServlet {
+  private static final long serialVersionUID = 1L;
+
+  private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
+  private final Path ui;
+
+  DirectoryGwtUiServlet(Cache<Path, Resource> cache, Path unpackedWar,
+      boolean dev) throws IOException {
+    super(cache, false);
+    ui = unpackedWar.resolve("gerrit_ui");
+    if (!Files.exists(ui)) {
+      Files.createDirectory(ui);
+    }
+    if (dev) {
+      ui.toFile().deleteOnExit();
+    }
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) {
+    return ui.resolve(pathInfo);
+  }
+
+  @Override
+  protected FileTime getLastModifiedTime(Path p) {
+    // Return initialization time of this class, since the GWT outputs from the
+    // build process all have mtimes of 1980/1/1.
+    return NOW;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 90c5ff4..cac402b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -14,22 +14,30 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.common.primitives.Bytes;
 import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.HostPageData;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.GetDiffPreferences;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -40,6 +48,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -47,17 +56,19 @@
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StringWriter;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -68,12 +79,12 @@
 public class HostPageServlet extends HttpServlet {
   private static final Logger log =
       LoggerFactory.getLogger(HostPageServlet.class);
-  private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
+
   private static final String HPD_ID = "gerrit_hostpagedata";
+  private static final int DEFAULT_JS_LOAD_TIMEOUT = 5000;
 
   private final Provider<CurrentUser> currentUser;
   private final DynamicItem<WebSession> session;
-  private final GerritConfig config;
   private final DynamicSet<WebUiPlugin> plugins;
   private final DynamicSet<MessageOfTheDay> messages;
   private final HostPageData.Theme signedOutTheme;
@@ -82,33 +93,43 @@
   private final Document template;
   private final String noCacheName;
   private final boolean refreshHeaderFooter;
-  private final StaticServlet staticServlet;
+  private final SiteStaticDirectoryServlet staticServlet;
   private final boolean isNoteDbEnabled;
+  private final Integer pluginsLoadTimeout;
+  private final GetDiffPreferences getDiff;
+  private final AuthConfig authConfig;
   private volatile Page page;
 
   @Inject
-  HostPageServlet(final Provider<CurrentUser> cu, final DynamicItem<WebSession> w,
-      final SitePaths sp, final ThemeFactory themeFactory,
-      final GerritConfig gc, final ServletContext servletContext,
-      final DynamicSet<WebUiPlugin> webUiPlugins,
-      final DynamicSet<MessageOfTheDay> motd,
-      @GerritServerConfig final Config cfg,
-      final StaticServlet ss,
-      final NotesMigration migration)
+  HostPageServlet(
+      Provider<CurrentUser> cu,
+      DynamicItem<WebSession> w,
+      SitePaths sp,
+      ThemeFactory themeFactory,
+      ServletContext servletContext,
+      DynamicSet<WebUiPlugin> webUiPlugins,
+      DynamicSet<MessageOfTheDay> motd,
+      @GerritServerConfig Config cfg,
+      AuthConfig authCfg,
+      SiteStaticDirectoryServlet ss,
+      NotesMigration migration,
+      GetDiffPreferences diffPref)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
-    config = gc;
     plugins = webUiPlugins;
     messages = motd;
     signedOutTheme = themeFactory.getSignedOutTheme();
     signedInTheme = themeFactory.getSignedInTheme();
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    authConfig = authCfg;
     staticServlet = ss;
     isNoteDbEnabled = migration.enabled();
+    pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
+    getDiff = diffPref;
 
-    final String pageName = "HostPage.html";
+    String pageName = "HostPage.html";
     template = HtmlDomUtil.parseFile(getClass(), pageName);
     if (template == null) {
       throw new FileNotFoundException("No " + pageName + " in webapp");
@@ -122,81 +143,70 @@
     }
 
     String src = "gerrit_ui/gerrit_ui.nocache.js";
-    if (!IS_DEV) {
-      Element devmode = HtmlDomUtil.find(template, "gwtdevmode");
-      if (devmode != null) {
-        devmode.getParentNode().removeChild(devmode);
-      }
-
-      InputStream in = servletContext.getResourceAsStream("/" + src);
+    try (InputStream in = servletContext.getResourceAsStream("/" + src)) {
       if (in != null) {
         Hasher md = Hashing.md5().newHasher();
-        try {
-          try {
-            final byte[] buf = new byte[1024];
-            int n;
-            while ((n = in.read(buf)) > 0) {
-              md.putBytes(buf, 0, n);
-            }
-          } finally {
-            in.close();
-          }
-        } catch (IOException e) {
-          throw new IOException("Failed reading " + src, e);
+        byte[] buf = new byte[1024];
+        int n;
+        while ((n = in.read(buf)) > 0) {
+          md.putBytes(buf, 0, n);
         }
         src += "?content=" + md.hash().toString();
       } else {
         log.debug("No " + src + " in webapp root; keeping noncache.js URL");
       }
+    } catch (IOException e) {
+      throw new IOException("Failed reading " + src, e);
     }
 
     noCacheName = src;
     page = new Page();
   }
 
-  private void json(final Object data, final StringWriter w) {
+  private static int getPluginsLoadTimeout(Config cfg) {
+    long cfgValue =
+        ConfigUtil.getTimeUnit(cfg, "plugins", null, "jsLoadTimeout",
+            DEFAULT_JS_LOAD_TIMEOUT, TimeUnit.MILLISECONDS);
+    if (cfgValue < 0) {
+      return 0;
+    }
+    return (int) cfgValue;
+  }
+
+  private void json(Object data, StringWriter w) {
     JsonServlet.defaultGsonBuilder().create().toJson(data, w);
   }
 
   private Page get() {
     Page p = page;
-    if (refreshHeaderFooter && p.isStale()) {
-      final Page newPage;
-      try {
-        newPage = new Page();
-      } catch (IOException e) {
-        log.error("Cannot refresh site header/footer", e);
-        return p;
+    try {
+      if (refreshHeaderFooter && p.isStale()) {
+        p = new Page();
+        page = p;
       }
-      p = newPage;
-      page = p;
+    } catch (IOException e) {
+      log.error("Cannot refresh site header/footer", e);
     }
     return p;
   }
 
   @Override
-  protected void doGet(final HttpServletRequest req,
-      final HttpServletResponse rsp) throws IOException {
-    final Page.Content page = select(req);
-    final StringWriter w = new StringWriter();
-    final CurrentUser user = currentUser.get();
+  protected void doGet(HttpServletRequest req,
+      HttpServletResponse rsp) throws IOException {
+    Page.Content page = select(req);
+    StringWriter w = new StringWriter();
+    CurrentUser user = currentUser.get();
     if (user.isIdentifiedUser()) {
-      w.write(HPD_ID + ".account=");
-      json(((IdentifiedUser) user).getAccount(), w);
-      w.write(";");
-
-      w.write(HPD_ID + ".xGerritAuth=");
-      json(session.get().getXGerritAuth(), w);
-      w.write(";");
-
+      setXGerritAuthCookie(req, rsp, session.get());
       w.write(HPD_ID + ".accountDiffPref=");
-      json(((IdentifiedUser) user).getAccountDiffPreference(), w);
+      json(getDiffPreferences(user.asIdentifiedUser()), w);
       w.write(";");
 
       w.write(HPD_ID + ".theme=");
       json(signedInTheme, w);
       w.write(";");
     } else {
+      setXGerritAuthCookie(req, rsp, null);
       w.write(HPD_ID + ".theme=");
       json(signedOutTheme, w);
       w.write(";");
@@ -204,9 +214,9 @@
     plugins(w);
     messages(w);
 
-    final byte[] hpd = w.toString().getBytes("UTF-8");
-    final byte[] raw = Bytes.concat(page.part1, hpd, page.part2);
-    final byte[] tosend;
+    byte[] hpd = w.toString().getBytes(UTF_8);
+    byte[] raw = Bytes.concat(page.part1, hpd, page.part2);
+    byte[] tosend;
     if (RPCServletUtils.acceptsGzipEncoding(req)) {
       rsp.setHeader("Content-Encoding", "gzip");
       tosend = HtmlDomUtil.compress(raw);
@@ -216,16 +226,39 @@
 
     CacheHeaders.setNotCacheable(rsp);
     rsp.setContentType("text/html");
-    rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+    rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
     rsp.setContentLength(tosend.length);
-    final OutputStream out = rsp.getOutputStream();
-    try {
+    try (OutputStream out = rsp.getOutputStream()) {
       out.write(tosend);
-    } finally {
-      out.close();
     }
   }
 
+  private void setXGerritAuthCookie(HttpServletRequest req,
+      HttpServletResponse rsp, WebSession session) {
+    String v = session != null ? session.getXGerritAuth() : "";
+    Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, v);
+    c.setPath("/");
+    c.setHttpOnly(false);
+    c.setSecure(authConfig.getCookieSecure() && isSecure(req));
+    c.setMaxAge(session != null
+        ? -1 // Set the cookie for this browser session.
+        : 0); // Remove the cookie (expire immediately).
+    rsp.addCookie(c);
+  }
+
+  private static boolean isSecure(HttpServletRequest req) {
+    return req.isSecure() || "https".equals(req.getScheme());
+  }
+
+  private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) {
+    try {
+      return getDiff.apply(new AccountResource(user));
+    } catch (AuthException | ConfigInvalidException | IOException e) {
+      log.warn("Cannot query account diff preferences", e);
+    }
+    return DiffPreferencesInfo.defaults();
+  }
+
   private void plugins(StringWriter w) {
     List<String> urls = Lists.newArrayList();
     for (WebUiPlugin u : plugins) {
@@ -273,7 +306,7 @@
       String src = e.getAttribute("src");
       if (src != null && src.startsWith("static/")) {
         String name = src.substring("static/".length());
-        StaticServlet.Resource r = staticServlet.getResource(name);
+        ResourceServlet.Resource r = staticServlet.getResource(name);
         if (r != null) {
           e.setAttribute("src", src + "?e=" + r.etag);
         }
@@ -288,16 +321,16 @@
   }
 
   private static class FileInfo {
-    private final File path;
+    private final Path path;
     private final long time;
 
-    FileInfo(final File p) {
+    FileInfo(Path p) {
       path = p;
-      time = path.lastModified();
+      time = lastModified(path);
     }
 
     boolean isStale() {
-      return time != path.lastModified();
+      return time != lastModified(path);
     }
   }
 
@@ -315,17 +348,17 @@
       header = injectXmlFile(hostDoc, "gerrit_header", site.site_header);
       footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
 
-      final HostPageData pageData = new HostPageData();
+      HostPageData pageData = new HostPageData();
       pageData.version = Version.getVersion();
-      pageData.config = config;
       pageData.isNoteDbEnabled = isNoteDbEnabled;
+      pageData.pluginsLoadTimeout = pluginsLoadTimeout;
 
-      final StringWriter w = new StringWriter();
+      StringWriter w = new StringWriter();
       w.write("var " + HPD_ID + "=");
       json(pageData, w);
       w.write(";");
 
-      final Element data = HtmlDomUtil.find(hostDoc, HPD_ID);
+      Element data = HtmlDomUtil.find(hostDoc, HPD_ID);
       asScript(data);
       data.appendChild(hostDoc.createTextNode(w.toString()));
       data.appendChild(hostDoc.createComment(HPD_ID));
@@ -344,7 +377,7 @@
       return css.isStale() || header.isStale() || footer.isStale();
     }
 
-    private void asScript(final Element scriptNode) {
+    private void asScript(Element scriptNode) {
       scriptNode.setAttribute("type", "text/javascript");
       scriptNode.setAttribute("language", "javascript");
     }
@@ -354,20 +387,20 @@
       final byte[] part2;
 
       Content(Document hostDoc) throws IOException {
-        final String raw = HtmlDomUtil.toString(hostDoc);
-        final int p = raw.indexOf("<!--" + HPD_ID);
+        String raw = HtmlDomUtil.toString(hostDoc);
+        int p = raw.indexOf("<!--" + HPD_ID);
         if (p < 0) {
           throw new IOException("No tag in transformed host page HTML");
         }
-        part1 = raw.substring(0, p).getBytes("UTF-8");
-        part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes("UTF-8");
+        part1 = raw.substring(0, p).getBytes(UTF_8);
+        part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes(UTF_8);
       }
     }
 
-    private FileInfo injectCssFile(final Document hostDoc, final String id,
-        final File src) throws IOException {
-      final FileInfo info = new FileInfo(src);
-      final Element banner = HtmlDomUtil.find(hostDoc, id);
+    private FileInfo injectCssFile(Document hostDoc, String id, Path src)
+        throws IOException {
+      FileInfo info = new FileInfo(src);
+      Element banner = HtmlDomUtil.find(hostDoc, id);
       if (banner == null) {
         return info;
       }
@@ -376,7 +409,8 @@
         banner.removeChild(banner.getFirstChild());
       }
 
-      String css = HtmlDomUtil.readFile(src.getParentFile(), src.getName());
+      String css =
+          HtmlDomUtil.readFile(src.getParent(), src.getFileName().toString());
       if (css == null) {
         return info;
       }
@@ -385,10 +419,10 @@
       return info;
     }
 
-    private FileInfo injectXmlFile(final Document hostDoc, final String id,
-        final File src) throws IOException {
-      final FileInfo info = new FileInfo(src);
-      final Element banner = HtmlDomUtil.find(hostDoc, id);
+    private FileInfo injectXmlFile(Document hostDoc, String id, Path src)
+        throws IOException {
+      FileInfo info = new FileInfo(src);
+      Element banner = HtmlDomUtil.find(hostDoc, id);
       if (banner == null) {
         return info;
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
index 9c267a8..95a247f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
@@ -68,13 +68,10 @@
 
     CacheHeaders.setNotCacheable(rsp);
     rsp.setContentType("text/html");
-    rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+    rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
     rsp.setContentLength(tosend.length);
-    final OutputStream out = rsp.getOutputStream();
-    try {
+    try (OutputStream out = rsp.getOutputStream()) {
       out.write(tosend);
-    } finally {
-      out.close();
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
new file mode 100644
index 0000000..a5bc6c6
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
@@ -0,0 +1,231 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.escape.Escaper;
+import com.google.common.html.HtmlEscapers;
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gwtexpui.linker.server.UserAgentRule;
+import com.google.gwtexpui.server.CacheHeaders;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class RecompileGwtUiFilter implements Filter {
+  private static final Logger log =
+      LoggerFactory.getLogger(RecompileGwtUiFilter.class);
+
+  private final boolean gwtuiRecompile =
+      System.getProperty("gerrit.disable-gwtui-recompile") == null;
+  private final UserAgentRule rule = new UserAgentRule();
+  private final Set<String> uaInitialized = new HashSet<>();
+  private final Path unpackedWar;
+  private final Path gen;
+  private final Path root;
+
+  private String lastTarget;
+  private long lastTime;
+
+  RecompileGwtUiFilter(Path buckOut, Path unpackedWar) {
+    this.unpackedWar = unpackedWar;
+    gen = buckOut.resolve("gen");
+    root = buckOut.getParent();
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse res,
+      FilterChain chain) throws IOException, ServletException {
+    String pkg = "gerrit-gwtui";
+    String target = "ui_" + rule.select((HttpServletRequest) request);
+    if (gwtuiRecompile || !uaInitialized.contains(target)) {
+      String rule = "//" + pkg + ":" + target;
+      // TODO(davido): instead of assuming specific Buck's internal
+      // target directory for gwt_binary() artifacts, ask Buck for
+      // the location of user agent permutation GWT zip, e. g.:
+      // $ buck targets --show_output //gerrit-gwtui:ui_safari \
+      //    | awk '{print $2}'
+      String child = String.format("%s/__gwt_binary_%s__", pkg, target);
+      File zip = gen.resolve(child).resolve(target + ".zip").toFile();
+
+      synchronized (this) {
+        try {
+          build(root, gen, rule);
+        } catch (BuildFailureException e) {
+          displayFailure(rule, e.why, (HttpServletResponse) res);
+          return;
+        }
+
+        if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
+          lastTarget = target;
+          lastTime = zip.lastModified();
+          unpack(zip, unpackedWar.toFile());
+        }
+      }
+      uaInitialized.add(target);
+    }
+    chain.doFilter(request, res);
+  }
+
+  private void displayFailure(String rule, byte[] why, HttpServletResponse res)
+      throws IOException {
+    res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    res.setContentType("text/html");
+    res.setCharacterEncoding(UTF_8.name());
+    CacheHeaders.setNotCacheable(res);
+
+    Escaper html = HtmlEscapers.htmlEscaper();
+    try (PrintWriter w = res.getWriter()) {
+      w.write("<html><title>BUILD FAILED</title><body>");
+      w.format("<h1>%s FAILED</h1>", html.escape(rule));
+      w.write("<pre>");
+      w.write(html.escape(RawParseUtils.decode(why)));
+      w.write("</pre>");
+      w.write("</body></html>");
+    }
+  }
+
+  @Override
+  public void init(FilterConfig config) {
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  private static void unpack(File srcwar, File dstwar) throws IOException {
+    try (ZipFile zf = new ZipFile(srcwar)) {
+      final Enumeration<? extends ZipEntry> e = zf.entries();
+      while (e.hasMoreElements()) {
+        final ZipEntry ze = e.nextElement();
+        final String name = ze.getName();
+
+        if (ze.isDirectory()
+          || name.startsWith("WEB-INF/")
+          || name.startsWith("META-INF/")
+          || name.startsWith("com/google/gerrit/launcher/")
+          || name.equals("Main.class")) {
+          continue;
+        }
+
+        final File rawtmp = new File(dstwar, name);
+        mkdir(rawtmp.getParentFile());
+        rawtmp.deleteOnExit();
+
+        try (FileOutputStream rawout = new FileOutputStream(rawtmp);
+            InputStream in = zf.getInputStream(ze)) {
+          final byte[] buf = new byte[4096];
+          int n;
+          while ((n = in.read(buf, 0, buf.length)) > 0) {
+            rawout.write(buf, 0, n);
+          }
+        }
+      }
+    }
+  }
+
+  private static void build(Path root, Path gen, String target)
+      throws IOException, BuildFailureException {
+    log.info("buck build " + target);
+    Properties properties = loadBuckProperties(gen);
+    String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
+    ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
+        .directory(root.toFile())
+        .redirectErrorStream(true);
+    if (properties.containsKey("PATH")) {
+      proc.environment().put("PATH", properties.getProperty("PATH"));
+    }
+    long start = TimeUtil.nowMs();
+    Process rebuild = proc.start();
+    byte[] out;
+    try (InputStream in = rebuild.getInputStream()) {
+      out = ByteStreams.toByteArray(in);
+    } finally {
+      rebuild.getOutputStream().close();
+    }
+
+    int status;
+    try {
+      status = rebuild.waitFor();
+    } catch (InterruptedException e) {
+      throw new InterruptedIOException("interrupted waiting for " + buck);
+    }
+    if (status != 0) {
+      throw new BuildFailureException(out);
+    }
+
+    long time = TimeUtil.nowMs() - start;
+    log.info(String.format("UPDATED    %s in %.3fs", target, time / 1000.0));
+  }
+
+  private static Properties loadBuckProperties(Path gen)
+      throws FileNotFoundException, IOException {
+    Properties properties = new Properties();
+    try (InputStream in = new FileInputStream(
+        gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
+      properties.load(in);
+    }
+    return properties;
+  }
+
+  @SuppressWarnings("serial")
+  private static class BuildFailureException extends Exception {
+    final byte[] why;
+
+    BuildFailureException(byte[] why) {
+      this.why = why;
+    }
+  }
+
+  private static void mkdir(File dir) throws IOException {
+    if (!dir.isDirectory()) {
+      mkdir(dir.getParentFile());
+      if (!dir.mkdir()) {
+        throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
+      }
+      dir.deleteOnExit();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
new file mode 100644
index 0000000..8116404
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -0,0 +1,335 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
+import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
+import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Base class for serving static resources.
+ * <p>
+ * Supports caching, ETags, basic content type detection, and limited gzip
+ * compression.
+ */
+public abstract class ResourceServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+
+  private static final Logger log =
+      LoggerFactory.getLogger(ResourceServlet.class);
+
+  private static final int CACHE_FILE_SIZE_LIMIT_BYTES = 100 << 10;
+
+  private static final String JS = "application/x-javascript";
+  private static final ImmutableMap<String, String> MIME_TYPES =
+      ImmutableMap.<String, String> builder()
+        .put("css", "text/css")
+        .put("gif", "image/gif")
+        .put("htm", "text/html")
+        .put("html", "text/html")
+        .put("ico", "image/x-icon")
+        .put("jpeg", "image/jpeg")
+        .put("jpg", "image/jpeg")
+        .put("js", JS)
+        .put("pdf", "application/pdf")
+        .put("png", "image/png")
+        .put("rtf", "text/rtf")
+        .put("svg", "image/svg+xml")
+        .put("text", "text/plain")
+        .put("tif", "image/tiff")
+        .put("tiff", "image/tiff")
+        .put("txt", "text/plain")
+        .build();
+
+  protected static String contentType(String name) {
+    int dot = name.lastIndexOf('.');
+    String ext = 0 < dot ? name.substring(dot + 1) : "";
+    String type = MIME_TYPES.get(ext);
+    return type != null ? type : "application/octet-stream";
+  }
+
+  private final Cache<Path, Resource> cache;
+  private final boolean refresh;
+  private final int cacheFileSizeLimitBytes;
+
+  protected ResourceServlet(Cache<Path, Resource> cache, boolean refresh) {
+    this(cache, refresh, CACHE_FILE_SIZE_LIMIT_BYTES);
+  }
+
+  @VisibleForTesting
+  ResourceServlet(Cache<Path, Resource> cache, boolean refresh,
+      int cacheFileSizeLimitBytes) {
+    this.cache = checkNotNull(cache, "cache");
+    this.refresh = refresh;
+    this.cacheFileSizeLimitBytes = cacheFileSizeLimitBytes;
+  }
+
+  /**
+   * Get the resource path on the filesystem that should be served for this
+   * request.
+   *
+   * @param pathInfo result of {@link HttpServletRequest#getPathInfo()}.
+   * @return path where static content can be found.
+   */
+  protected abstract Path getResourcePath(String pathInfo);
+
+  protected FileTime getLastModifiedTime(Path p) throws IOException {
+    return Files.getLastModifiedTime(p);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException {
+    String name;
+    if (req.getPathInfo() == null) {
+      name = "/";
+    } else {
+      name = CharMatcher.is('/').trimFrom(req.getPathInfo());
+    }
+    if (isUnreasonableName(name)) {
+      notFound(rsp);
+      return;
+    }
+    Path p = getResourcePath(name);
+    if (p == null) {
+      notFound(rsp);
+      return;
+    }
+
+    Resource r = cache.getIfPresent(p);
+    try {
+      if (r == null) {
+        if (maybeStream(p, req, rsp)) {
+          return; // Bypass cache for large resource.
+        }
+        r = cache.get(p, newLoader(p));
+      }
+      if (refresh && r.isStale(p, this)) {
+        cache.invalidate(p);
+        r = cache.get(p, newLoader(p));
+      }
+    } catch (ExecutionException e) {
+      log.warn("Cannot load static resource " + req.getPathInfo(), e);
+      CacheHeaders.setNotCacheable(rsp);
+      rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
+      return;
+    }
+    if (r == Resource.NOT_FOUND) {
+      notFound(rsp); // Cached not found response.
+      return;
+    }
+
+    String e = req.getParameter("e");
+    if (e != null && !r.etag.equals(e)) {
+      CacheHeaders.setNotCacheable(rsp);
+      rsp.setStatus(SC_NOT_FOUND);
+      return;
+    } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+      rsp.setStatus(SC_NOT_MODIFIED);
+      return;
+    }
+
+    byte[] tosend = r.raw;
+    if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
+      byte[] gz = HtmlDomUtil.compress(tosend);
+      if ((gz.length + 24) < tosend.length) {
+        rsp.setHeader(CONTENT_ENCODING, "gzip");
+        tosend = gz;
+      }
+    }
+    if (!CacheHeaders.hasCacheHeader(rsp)) {
+      if (e != null && r.etag.equals(e)) {
+        CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
+      } else {
+        CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+      }
+    }
+    rsp.setHeader(ETAG, r.etag);
+    rsp.setContentType(r.contentType);
+    rsp.setContentLength(tosend.length);
+    try (OutputStream out = rsp.getOutputStream()) {
+      out.write(tosend);
+    }
+  }
+
+  @Nullable
+  Resource getResource(String name) {
+    try {
+      Path p = getResourcePath(name);
+      if (p == null) {
+        log.warn(String.format("Path doesn't exist %s", name));
+        return null;
+      }
+      return cache.get(p, newLoader(p));
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot load static resource %s", name), e);
+      return null;
+    }
+  }
+
+  private static void notFound(HttpServletResponse rsp) {
+    rsp.setStatus(SC_NOT_FOUND);
+    CacheHeaders.setNotCacheable(rsp);
+  }
+
+  /**
+   * Maybe stream a path to the response, depending on the properties of the
+   * file and cache headers in the request.
+   *
+   * @param p path to stream
+   * @param req HTTP request.
+   * @param rsp HTTP response.
+   * @return true if the response was written (either the file contents or an
+   *     error); false if the path is too small to stream and should be cached.
+   */
+  private boolean maybeStream(Path p, HttpServletRequest req,
+      HttpServletResponse rsp) throws IOException {
+    try {
+      if (Files.size(p) < cacheFileSizeLimitBytes) {
+        return false;
+      }
+    } catch (NoSuchFileException e) {
+      cache.put(p, Resource.NOT_FOUND);
+      notFound(rsp);
+      return true;
+    }
+
+    long lastModified = FileUtil.lastModified(p);
+    if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
+      rsp.setStatus(SC_NOT_MODIFIED);
+      return true;
+    }
+
+    if (lastModified > 0) {
+      rsp.setDateHeader(LAST_MODIFIED, lastModified);
+    }
+    if (!CacheHeaders.hasCacheHeader(rsp)) {
+      CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+    }
+    rsp.setContentType(contentType(p.toString()));
+
+    OutputStream out = rsp.getOutputStream();
+    GZIPOutputStream gz = null;
+    if (RPCServletUtils.acceptsGzipEncoding(req)) {
+      rsp.setHeader(CONTENT_ENCODING, "gzip");
+      gz = new GZIPOutputStream(out);
+      out = gz;
+    }
+    Files.copy(p, out);
+    if (gz != null) {
+      gz.finish();
+    }
+    return true;
+  }
+
+
+  private static boolean isUnreasonableName(String name) {
+    return name.length() < 1
+      || name.contains("\\") // no windows/dos style paths
+      || name.startsWith("../") // no "../etc/passwd"
+      || name.contains("/../") // no "foo/../etc/passwd"
+      || name.contains("/./") // "foo/./foo" is insane to ask
+      || name.contains("//"); // windows UNC path can be "//..."
+  }
+
+  private Callable<Resource> newLoader(final Path p) {
+    return new Callable<Resource>() {
+      @Override
+      public Resource call() throws IOException {
+        try {
+          return new Resource(
+              getLastModifiedTime(p),
+              contentType(p.toString()),
+              Files.readAllBytes(p));
+        } catch (NoSuchFileException e) {
+          return Resource.NOT_FOUND;
+        }
+      }
+    };
+  }
+
+  static class Resource {
+    static final Resource NOT_FOUND =
+        new Resource(FileTime.fromMillis(0), "", new byte[] {});
+
+    final FileTime lastModified;
+    final String contentType;
+    final String etag;
+    final byte[] raw;
+
+    Resource(FileTime lastModified, String contentType, byte[] raw) {
+      this.lastModified = checkNotNull(lastModified, "lastModified");
+      this.contentType = checkNotNull(contentType, "contentType");
+      this.raw = checkNotNull(raw, "raw");
+      this.etag = Hashing.md5().hashBytes(raw).toString();
+    }
+
+    boolean isStale(Path p, ResourceServlet rs) throws IOException {
+      FileTime t;
+      try {
+        t = rs.getLastModifiedTime(p);
+      } catch (NoSuchFileException e) {
+        return this != NOT_FOUND;
+      }
+      return t.toMillis() == 0
+          || lastModified.toMillis() == 0
+          || !lastModified.equals(t);
+    }
+  }
+
+  static class Weigher
+      implements com.google.common.cache.Weigher<Path, Resource> {
+    @Override
+    public int weigh(Path p, Resource r) {
+      return 2 * p.toString().length() + r.raw.length;
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
deleted file mode 100644
index 1d8e74d..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.raw;
-
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * This class provides a mechanism to use a configurable robots.txt file,
- * outside of the .war of the application. In order to configure it add the
- * following to the {@code httpd} section of the {@code gerrit.conf}
- * file:
- *
- * <pre>
- * [httpd]
- *         robotsFile = etc/myrobots.txt
- * </pre>
- *
- * If the specified file name is relative it will resolved as a sub directory of
- * the site directory, if it is absolute it will be used as is.
- *
- * If the specified file doesn't exist or isn't readable the servlet will
- * default to the {@code robots.txt} file bundled with the .war file of the
- * application.
- */
-@SuppressWarnings("serial")
-@Singleton
-public class RobotsServlet extends HttpServlet {
-  private static final Logger log =
-      LoggerFactory.getLogger(RobotsServlet.class);
-
-  private final File robotsFile;
-
-  @Inject
-  RobotsServlet(@GerritServerConfig final Config config, final SitePaths sitePaths) {
-    File file = sitePaths.resolve(
-      config.getString("httpd", null, "robotsFile"));
-    if (file != null && (!file.exists() || !file.canRead())) {
-      log.warn("Cannot read httpd.robotsFile, using default");
-      file = null;
-    }
-    robotsFile = file;
-  }
-
-  @Override
-  protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp)
-      throws IOException {
-    rsp.setContentType("text/plain");
-    InputStream in = openRobotsFile();
-    try {
-      OutputStream out = rsp.getOutputStream();
-      try {
-        ByteStreams.copy(in, out);
-      } finally {
-        out.close();
-      }
-    } finally {
-      in.close();
-    }
-  }
-
-  private InputStream openRobotsFile() {
-    if (robotsFile != null) {
-      try {
-        return new FileInputStream(robotsFile);
-      } catch (IOException e) {
-        log.warn("Cannot read " + robotsFile + "; using default", e);
-      }
-    }
-    return getServletContext().getResourceAsStream("/robots.txt");
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
new file mode 100644
index 0000000..d75a523
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.cache.Cache;
+
+import java.nio.file.Path;
+
+/** Serve a single static file, regardless of path. */
+class SingleFileServlet extends ResourceServlet{
+  private static final long serialVersionUID = 1L;
+
+  private final Path path;
+
+  SingleFileServlet(Cache<Path, Resource> cache, Path path, boolean refresh) {
+    super(cache, refresh);
+    this.path = path;
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) {
+    return path;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
new file mode 100644
index 0000000..cf99d3c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+
+/** Sends static content from the site 's {@code static/} subdirectory. */
+@Singleton
+public class SiteStaticDirectoryServlet extends ResourceServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final Path staticBase;
+
+  @Inject
+  SiteStaticDirectoryServlet(
+      SitePaths site,
+      @GerritServerConfig Config cfg,
+      @Named(StaticModule.CACHE) Cache<Path, Resource> cache) {
+    super(cache, cfg.getBoolean("site", "refreshHeaderFooter", true));
+    Path p;
+    try {
+      p = site.static_dir.toRealPath().normalize();
+    } catch (IOException e) {
+      p = site.static_dir.toAbsolutePath().normalize();
+    }
+    staticBase = p;
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) {
+    Path p = staticBase.resolve(pathInfo);
+    try {
+      p = p.toRealPath().normalize();
+      if (!p.startsWith(staticBase)) {
+        return null;
+      }
+      return p;
+    } catch (IOException e) {
+      return null;
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
index 83120e0..f570cb6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
@@ -32,20 +34,22 @@
 /**
  * Servlet hosting an SSH daemon on another port. During a standard HTTP GET
  * request the servlet returns the hostname and port number back to the client
- * in the form {@code ${host} ${port}}.
+ * in the form <code>${host} ${port}</code>.
  * <p>
- * Use a Git URL such as {@code ssh://${email}@${host}:${port}/${path}},
+ * Use a Git URL such as <code>ssh://${email}@${host}:${port}/${path}</code>,
  * e.g. {@code ssh://sop@google.com@gerrit.com:8010/tools/gerrit.git} to
  * access the SSH daemon itself.
  * <p>
  * Versions of Git before 1.5.3 may require setting the username and port
  * properties in the user's {@code ~/.ssh/config} file, and using a host
- * alias through a URL such as <code>gerrit-alias:/tools/gerrit.git:
+ * alias through a URL such as {@code gerrit-alias:/tools/gerrit.git}:
  * <pre>
+ * {@code
  * Host gerrit-alias
  *  User sop@google.com
  *  Hostname gerrit.com
  *  Port 8010
+ * }
  * </pre>
  */
 @SuppressWarnings("serial")
@@ -86,13 +90,10 @@
     }
 
     CacheHeaders.setNotCacheable(rsp);
-    rsp.setCharacterEncoding("UTF-8");
+    rsp.setCharacterEncoding(UTF_8.name());
     rsp.setContentType("text/plain");
-    final PrintWriter w = rsp.getWriter();
-    try {
+    try (PrintWriter w = rsp.getWriter()) {
       w.write(out);
-    } finally {
-      w.close();
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
new file mode 100644
index 0000000..79681df
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -0,0 +1,264 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.isReadable;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Key;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.google.inject.servlet.ServletModule;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class StaticModule extends ServletModule {
+  private static final Logger log =
+      LoggerFactory.getLogger(StaticModule.class);
+
+  private static final String DOC_SERVLET = "DocServlet";
+  private static final String FAVICON_SERVLET = "FaviconServlet";
+  private static final String GWT_UI_SERVLET = "GwtUiServlet";
+  private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
+
+  static final String CACHE = "static_content";
+
+  private final FileSystem warFs;
+  private final Path buckOut;
+  private final Path unpackedWar;
+  private final boolean development;
+
+  public StaticModule() {
+    File launcherLoadedFrom = getLauncherLoadedFrom();
+    if (launcherLoadedFrom != null
+        && launcherLoadedFrom.getName().endsWith(".jar")) {
+      // Special case: unpacked war archive deployed in container.
+      // The path is something like:
+      // <container>/<gerrit>/WEB-INF/lib/launcher.jar
+      // Switch to exploded war case with <container>/webapp>/<gerrit>
+      // root directory
+      warFs = null;
+      unpackedWar = java.nio.file.Paths.get(launcherLoadedFrom
+          .getParentFile()
+          .getParentFile()
+          .getParentFile()
+          .toURI());
+      buckOut = null;
+      development = false;
+      return;
+    }
+
+    warFs = getDistributionArchive();
+    if (warFs == null) {
+      buckOut = getDeveloperBuckOut();
+      unpackedWar = makeWarTempDir();
+      development = true;
+    } else {
+      buckOut = null;
+      unpackedWar = null;
+      development = false;
+    }
+  }
+
+  @Override
+  protected void configureServlets() {
+    serveRegex("^/Documentation/(.+)$").with(named(DOC_SERVLET));
+    serve("/static/*").with(SiteStaticDirectoryServlet.class);
+    serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
+    serve("/favicon.ico").with(named(FAVICON_SERVLET));
+    serveGwtUi();
+    install(new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE, Path.class, Resource.class)
+            .maximumWeight(1 << 20)
+            .weigher(ResourceServlet.Weigher.class);
+      }
+    });
+  }
+
+  private void serveGwtUi() {
+    serveRegex("^/gerrit_ui/(?!rpc/)(.*)$")
+        .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET)));
+    if (development) {
+      filter("/").through(new RecompileGwtUiFilter(buckOut, unpackedWar));
+    }
+  }
+
+  @Provides
+  @Singleton
+  @Named(DOC_SERVLET)
+  HttpServlet getDocServlet(@Named(CACHE) Cache<Path, Resource> cache) {
+    if (warFs != null) {
+      return new WarDocServlet(cache, warFs);
+    } else if (unpackedWar != null && !development) {
+      return new DirectoryDocServlet(cache, unpackedWar);
+    } else {
+      return new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest req,
+            HttpServletResponse resp) throws IOException {
+          resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+      };
+    }
+  }
+
+  @Provides
+  @Singleton
+  @Named(GWT_UI_SERVLET)
+  HttpServlet getGwtUiServlet(@Named(CACHE) Cache<Path, Resource> cache)
+      throws IOException {
+    if (warFs != null) {
+      return new WarGwtUiServlet(cache, warFs);
+    } else {
+      return new DirectoryGwtUiServlet(cache, unpackedWar, development);
+    }
+  }
+
+  @Provides
+  @Singleton
+  @Named(ROBOTS_TXT_SERVLET)
+  HttpServlet getRobotsTxtServlet(@GerritServerConfig Config cfg,
+      SitePaths sitePaths, @Named(CACHE) Cache<Path, Resource> cache) {
+    Path configPath = sitePaths.resolve(
+        cfg.getString("httpd", null, "robotsFile"));
+    if (configPath != null) {
+      if (exists(configPath) && isReadable(configPath)) {
+        return new SingleFileServlet(cache, configPath, true);
+      } else {
+        log.warn("Cannot read httpd.robotsFile, using default");
+      }
+    }
+    if (warFs != null) {
+      return new SingleFileServlet(cache, warFs.getPath("/robots.txt"), false);
+    } else {
+      return new SingleFileServlet(cache, webappSourcePath("robots.txt"), true);
+    }
+  }
+
+  @Provides
+  @Singleton
+  @Named(FAVICON_SERVLET)
+  HttpServlet getFaviconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
+    if (warFs != null) {
+      return new SingleFileServlet(cache, warFs.getPath("/favicon.ico"), false);
+    } else {
+      return new SingleFileServlet(
+          cache, webappSourcePath("favicon.ico"), true);
+    }
+  }
+
+  private Path webappSourcePath(String name) {
+    if (unpackedWar != null) {
+      return unpackedWar.resolve(name);
+    }
+    return buckOut.resolveSibling("gerrit-war").resolve("src").resolve("main")
+        .resolve("webapp").resolve(name);
+  }
+
+  private static Key<HttpServlet> named(String name) {
+    return Key.get(HttpServlet.class, Names.named(name));
+  }
+
+  private static FileSystem getDistributionArchive() {
+    try {
+      return GerritLauncher.getDistributionArchiveFileSystem();
+    } catch (IOException e) {
+      if ((e instanceof FileNotFoundException)
+          && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
+        return null;
+      } else {
+        ProvisionException pe =
+            new ProvisionException("Error reading gerrit.war");
+        pe.initCause(e);
+        throw pe;
+      }
+    }
+  }
+
+  private static File getLauncherLoadedFrom() {
+    try {
+      return GerritLauncher.getDistributionArchive();
+    } catch (IOException e) {
+      if ((e instanceof FileNotFoundException)
+          && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
+        return null;
+      } else {
+        ProvisionException pe =
+            new ProvisionException("Error reading gerrit.war");
+        pe.initCause(e);
+        throw pe;
+      }
+    }
+  }
+
+  private static Path getDeveloperBuckOut() {
+    try {
+      return GerritLauncher.getDeveloperBuckOut();
+    } catch (FileNotFoundException e) {
+      return null;
+    }
+  }
+
+  private static Path makeWarTempDir() {
+    // Obtain our local temporary directory, but it comes back as a file
+    // so we have to switch it to be a directory post creation.
+    //
+    try {
+      File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
+      if (!dstwar.delete() || !dstwar.mkdir()) {
+        throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
+      }
+
+      // Jetty normally refuses to serve out of a symlinked directory, as
+      // a security feature. Try to resolve out any symlinks in the path.
+      //
+      try {
+        return dstwar.getCanonicalFile().toPath();
+      } catch (IOException e) {
+        return dstwar.getAbsoluteFile().toPath();
+      }
+    } catch (IOException e) {
+      ProvisionException pe =
+          new ProvisionException("Cannot create war tempdir");
+      pe.initCause(e);
+      throw pe;
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
deleted file mode 100644
index 52b7a5c9..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
+++ /dev/null
@@ -1,260 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.raw;
-
-import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
-import static com.google.common.net.HttpHeaders.ETAG;
-import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
-
-import com.google.common.base.CharMatcher;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.cache.Weigher;
-import com.google.common.collect.Maps;
-import com.google.common.hash.Hashing;
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtexpui.server.CacheHeaders;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-
-/** Sends static content from the site 's {@code static/} subdirectory. */
-@SuppressWarnings("serial")
-@Singleton
-public class StaticServlet extends HttpServlet {
-  private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
-  private static final String JS = "application/x-javascript";
-  private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
-  static {
-    MIME_TYPES.put("html", "text/html");
-    MIME_TYPES.put("htm", "text/html");
-    MIME_TYPES.put("js", JS);
-    MIME_TYPES.put("css", "text/css");
-    MIME_TYPES.put("rtf", "text/rtf");
-    MIME_TYPES.put("txt", "text/plain");
-    MIME_TYPES.put("text", "text/plain");
-    MIME_TYPES.put("pdf", "application/pdf");
-    MIME_TYPES.put("jpeg", "image/jpeg");
-    MIME_TYPES.put("jpg", "image/jpeg");
-    MIME_TYPES.put("gif", "image/gif");
-    MIME_TYPES.put("png", "image/png");
-    MIME_TYPES.put("tiff", "image/tiff");
-    MIME_TYPES.put("tif", "image/tiff");
-    MIME_TYPES.put("svg", "image/svg+xml");
-  }
-
-  private static String contentType(final String name) {
-    final int dot = name.lastIndexOf('.');
-    final String ext = 0 < dot ? name.substring(dot + 1) : "";
-    final String type = MIME_TYPES.get(ext);
-    return type != null ? type : "application/octet-stream";
-  }
-
-  private final File staticBase;
-  private final String staticBasePath;
-  private final boolean refresh;
-  private final LoadingCache<String, Resource> cache;
-
-  @Inject
-  StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
-    File f;
-    try {
-      f = site.static_dir.getCanonicalFile();
-    } catch (IOException e) {
-      f = site.static_dir.getAbsoluteFile();
-    }
-    staticBase = f;
-    staticBasePath = staticBase.getPath() + File.separator;
-    refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
-    cache = CacheBuilder.newBuilder()
-        .maximumWeight(1 << 20)
-        .weigher(new Weigher<String, Resource>() {
-          @Override
-          public int weigh(String name, Resource r) {
-            return 2 * name.length() + r.raw.length;
-          }
-        })
-        .build(new CacheLoader<String, Resource>() {
-          @Override
-          public Resource load(String name) throws Exception {
-            return loadResource(name);
-          }
-        });
-  }
-
-  @Nullable
-  Resource getResource(String name) {
-    try {
-      return cache.get(name);
-    } catch (ExecutionException e) {
-      log.warn(String.format("Cannot load static resource %s", name), e);
-      return null;
-    }
-  }
-
-  private Resource getResource(HttpServletRequest req) throws ExecutionException {
-    String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
-    if (isUnreasonableName(name)) {
-      return Resource.NOT_FOUND;
-    }
-
-    Resource r = cache.get(name);
-    if (r == Resource.NOT_FOUND) {
-      return Resource.NOT_FOUND;
-    }
-
-    if (refresh && r.isStale()) {
-      cache.invalidate(name);
-      r = cache.get(name);
-    }
-    return r;
-  }
-
-  private static boolean isUnreasonableName(String name) {
-    if (name.length() < 1) return true;
-    if (name.contains("\\")) return true; // no windows/dos style paths
-    if (name.startsWith("../")) return true; // no "../etc/passwd"
-    if (name.contains("/../")) return true; // no "foo/../etc/passwd"
-    if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
-    if (name.contains("//")) return true; // windows UNC path can be "//..."
-    return false; // is a reasonable name
-  }
-
-  @Override
-  protected void doGet(final HttpServletRequest req,
-      final HttpServletResponse rsp) throws IOException {
-    Resource r;
-    try {
-      r = getResource(req);
-    } catch (ExecutionException e) {
-      log.warn(String.format(
-          "Cannot load static resource %s",
-          req.getPathInfo()), e);
-      CacheHeaders.setNotCacheable(rsp);
-      rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-
-    String e = req.getParameter("e");
-    if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
-      CacheHeaders.setNotCacheable(rsp);
-      rsp.setStatus(SC_NOT_FOUND);
-      return;
-    } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
-      rsp.setStatus(SC_NOT_MODIFIED);
-      return;
-    }
-
-    byte[] tosend = r.raw;
-    if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
-      byte[] gz = HtmlDomUtil.compress(tosend);
-      if ((gz.length + 24) < tosend.length) {
-        rsp.setHeader(CONTENT_ENCODING, "gzip");
-        tosend = gz;
-      }
-    }
-    if (e != null && r.etag.equals(e)) {
-      CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
-    } else {
-      CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
-    }
-    rsp.setHeader(ETAG, r.etag);
-    rsp.setContentType(r.contentType);
-    rsp.setContentLength(tosend.length);
-    final OutputStream out = rsp.getOutputStream();
-    try {
-      out.write(tosend);
-    } finally {
-      out.close();
-    }
-  }
-
-  private Resource loadResource(String name) throws IOException {
-    File p = new File(staticBase, name);
-    try {
-      p = p.getCanonicalFile();
-    } catch (IOException e) {
-      return Resource.NOT_FOUND;
-    }
-    if (!p.getPath().startsWith(staticBasePath)) {
-      return Resource.NOT_FOUND;
-    }
-
-    long ts = p.lastModified();
-    FileInputStream in;
-    try {
-      in = new FileInputStream(p);
-    } catch (FileNotFoundException e) {
-      return Resource.NOT_FOUND;
-    }
-
-    byte[] raw;
-    try {
-      raw = ByteStreams.toByteArray(in);
-    } finally {
-      in.close();
-    }
-    return new Resource(p, ts, contentType(name), raw);
-  }
-
-  static class Resource {
-    static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
-
-    final File src;
-    final long lastModified;
-    final String contentType;
-    final String etag;
-    final byte[] raw;
-
-    Resource(File src, long lastModified, String contentType, byte[] raw) {
-      this.src = src;
-      this.lastModified = lastModified;
-      this.contentType = contentType;
-      this.etag = Hashing.md5().hashBytes(raw).toString();
-      this.raw = raw;
-    }
-
-    boolean isStale() {
-      return lastModified != src.lastModified();
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
index 16509ed..5c6480a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.httpd.HtmlDomUtil.compress;
 import static com.google.gerrit.httpd.HtmlDomUtil.newDocument;
 import static com.google.gerrit.httpd.HtmlDomUtil.toUTF8;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static org.eclipse.jgit.util.HttpSupport.HDR_CACHE_CONTROL;
 import static org.eclipse.jgit.util.HttpSupport.HDR_EXPIRES;
@@ -82,11 +83,8 @@
     rsp.setHeader(HDR_CACHE_CONTROL, "no-cache, must-revalidate");
     rsp.setContentType("application/octet-stream");
     rsp.setContentLength(tosend.length);
-    final OutputStream out = rsp.getOutputStream();
-    try {
+    try (OutputStream out = rsp.getOutputStream()) {
       out.write(tosend);
-    } finally {
-      out.close();
     }
   }
 
@@ -146,13 +144,10 @@
     rsp.setHeader(HDR_PRAGMA, "no-cache");
     rsp.setHeader(HDR_CACHE_CONTROL, "no-cache, must-revalidate");
     rsp.setContentType("text/html");
-    rsp.setCharacterEncoding("UTF-8");
+    rsp.setCharacterEncoding(UTF_8.name());
     rsp.setContentLength(tosend.length);
-    final OutputStream out = rsp.getOutputStream();
-    try {
+    try (OutputStream out = rsp.getOutputStream()) {
       out.write(tosend);
-    } finally {
-      out.close();
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
new file mode 100644
index 0000000..ad23314
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.cache.Cache;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+
+class WarDocServlet extends ResourceServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final FileSystem warFs;
+
+  WarDocServlet(Cache<Path, Resource> cache, FileSystem warFs) {
+    super(cache, false);
+    this.warFs = warFs;
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) {
+    return warFs.getPath("/Documentation/" + pathInfo);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java
new file mode 100644
index 0000000..45952cc
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarGwtUiServlet.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+
+class WarGwtUiServlet extends ResourceServlet {
+  private static final long serialVersionUID = 1L;
+
+  private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
+  private final FileSystem warFs;
+
+  WarGwtUiServlet(Cache<Path, Resource> cache, FileSystem warFs) {
+    super(cache, false);
+    this.warFs = warFs;
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) {
+    return warFs.getPath("/gerrit_ui/" + pathInfo);
+  }
+
+  @Override
+  protected FileTime getLastModifiedTime(Path p) {
+    // Return initialization time of this class, since the GWT outputs from the
+    // build process all have mtimes of 1980/1/1.
+    return NOW;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 6f4cc08..91a16b8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -46,7 +46,7 @@
 import com.google.common.math.IntMath;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.audit.HttpAuditEvent;
+import com.google.gerrit.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -142,6 +142,8 @@
   private static final String JSON_TYPE = "application/json";
   private static final String FORM_TYPE = "application/x-www-form-urlencoded";
 
+  private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
+
   /**
    * Garbage prefix inserted before JSON output to prevent XSSI.
    * <p>
@@ -202,6 +204,8 @@
     Object result = null;
     Multimap<String, String> params = LinkedHashMultimap.create();
     Object inputRequestBody = null;
+    RestResource rsrc = TopLevelResource.INSTANCE;
+    ViewData viewData = null;
 
     try {
       checkUserSession(req);
@@ -211,8 +215,8 @@
       CapabilityUtils.checkRequiresCapability(globals.currentUser,
           null, rc.getClass());
 
-      RestResource rsrc = TopLevelResource.INSTANCE;
-      ViewData viewData = new ViewData(null, null);
+      viewData = new ViewData(null, null);
+
       if (path.isEmpty()) {
         if (isGetOrHead(req)) {
           viewData = new ViewData(null, rc.list());
@@ -313,15 +317,15 @@
         return;
       }
 
-      if (viewData.view instanceof RestModifyView<?, ?>) {
+      if (viewData.view instanceof RestReadView<?> && isGetOrHead(req)) {
+        result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
+      } else if (viewData.view instanceof RestModifyView<?, ?>) {
         @SuppressWarnings("unchecked")
         RestModifyView<RestResource, Object> m =
             (RestModifyView<RestResource, Object>) viewData.view;
 
         inputRequestBody = parseRequest(req, inputType(m));
         result = m.apply(rsrc, inputRequestBody);
-      } else if (viewData.view instanceof RestReadView<?>) {
-        result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
       } else {
         throw new ResourceNotFoundException();
       }
@@ -330,7 +334,7 @@
         @SuppressWarnings("rawtypes")
         Response<?> r = (Response) result;
         status = r.statusCode();
-        configureCaching(req, res, rsrc, r.caching());
+        configureCaching(req, res, rsrc, viewData.view, r.caching());
       } else if (result instanceof Response.Redirect) {
         CacheHeaders.setNotCacheable(res);
         res.sendRedirect(((Response.Redirect) result).location());
@@ -384,10 +388,10 @@
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
     } finally {
-      globals.auditService.dispatch(new HttpAuditEvent(globals.webSession.get()
-          .getSessionId(), globals.currentUser.get(), req.getRequestURI(),
-          auditStartTs, params, req.getMethod(), inputRequestBody, status,
-          result));
+      globals.auditService.dispatch(new ExtendedHttpAuditEvent(globals.webSession.get()
+          .getSessionId(), globals.currentUser.get(), req,
+          auditStartTs, params, inputRequestBody, status,
+          result, rsrc, viewData == null ? null : viewData.view));
     }
   }
 
@@ -429,8 +433,9 @@
     return false;
   }
 
-  private static <T> void configureCaching(HttpServletRequest req,
-      HttpServletResponse res, RestResource rsrc, CacheControl c) {
+  private static <R extends RestResource> void configureCaching(
+      HttpServletRequest req, HttpServletResponse res, R rsrc,
+      RestView<R> view, CacheControl c) {
     if (isGetOrHead(req)) {
       switch (c.getType()) {
         case NONE:
@@ -438,13 +443,13 @@
           CacheHeaders.setNotCacheable(res);
           break;
         case PRIVATE:
-          addResourceStateHeaders(res, rsrc);
+          addResourceStateHeaders(res, rsrc, view);
           CacheHeaders.setCacheablePrivate(res,
               c.getAge(), c.getUnit(),
               c.isMustRevalidate());
           break;
         case PUBLIC:
-          addResourceStateHeaders(res, rsrc);
+          addResourceStateHeaders(res, rsrc, view);
           CacheHeaders.setCacheable(req, res,
               c.getAge(), c.getUnit(),
               c.isMustRevalidate());
@@ -455,12 +460,12 @@
     }
   }
 
-  private static void addResourceStateHeaders(
-      HttpServletResponse res, RestResource rsrc) {
-    if (rsrc instanceof RestResource.HasETag) {
-      res.setHeader(
-          HttpHeaders.ETAG,
-          ((RestResource.HasETag) rsrc).getETag());
+  private static  <R extends RestResource> void addResourceStateHeaders(
+      HttpServletResponse res, R rsrc, RestView<R> view) {
+    if (view instanceof ETagView) {
+      res.setHeader(HttpHeaders.ETAG, ((ETagView<R>) view).getETag(rsrc));
+    } else if (rsrc instanceof RestResource.HasETag) {
+      res.setHeader(HttpHeaders.ETAG, ((RestResource.HasETag) rsrc).getETag());
     }
     if (rsrc instanceof RestResource.HasLastModified) {
       res.setDateHeader(
@@ -471,7 +476,7 @@
 
   private void checkPreconditions(HttpServletRequest req)
       throws PreconditionFailedException {
-    if ("*".equals(req.getHeader("If-None-Match"))) {
+    if ("*".equals(req.getHeader(HttpHeaders.IF_NONE_MATCH))) {
       throw new PreconditionFailedException("Resource already exists");
     }
   }
@@ -517,9 +522,8 @@
       IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
       InstantiationException, InvocationTargetException, MethodNotAllowedException {
     if (isType(JSON_TYPE, req.getContentType())) {
-      BufferedReader br = req.getReader();
-      try {
-        JsonReader json = new JsonReader(br);
+      try (BufferedReader br = req.getReader();
+          JsonReader json = new JsonReader(br)) {
         json.setLenient(true);
 
         JsonToken first;
@@ -532,8 +536,6 @@
           return parseString(json.nextString(), type);
         }
         return OutputFormat.JSON.newGson().fromJson(json, type);
-      } finally {
-        br.close();
       }
     } else if (("PUT".equals(req.getMethod()) || "POST".equals(req.getMethod()))
         && acceptsRawInput(type)) {
@@ -543,8 +545,7 @@
     } else if (hasNoBody(req)) {
       return createInstance(type);
     } else if (isType("text/plain", req.getContentType())) {
-      BufferedReader br = req.getReader();
-      try {
+      try (BufferedReader br = req.getReader()) {
         char[] tmp = new char[256];
         StringBuilder sb = new StringBuilder();
         int n;
@@ -552,8 +553,6 @@
           sb.append(tmp, 0, n);
         }
         return parseString(sb.toString(), type);
-      } finally {
-        br.close();
       }
     } else if ("POST".equals(req.getMethod())
         && isType(FORM_TYPE, req.getContentType())) {
@@ -656,7 +655,7 @@
       Multimap<String, String> config,
       Object result)
       throws IOException {
-    TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+    TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
     buf.write(JSON_MAGIC);
     Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
     Gson gson = newGson(config, req);
@@ -669,7 +668,7 @@
     w.flush();
     replyBinaryResult(req, res, asBinaryResult(buf)
       .setContentType(JSON_TYPE)
-      .setCharacterEncoding(UTF_8.name()));
+      .setCharacterEncoding(UTF_8));
   }
 
   private static Gson newGson(Multimap<String, String> config,
@@ -767,11 +766,8 @@
       }
 
       if (req == null || !"HEAD".equals(req.getMethod())) {
-        OutputStream dst = res.getOutputStream();
-        try {
+        try (OutputStream dst = res.getOutputStream()) {
           bin.writeTo(dst);
-        } finally {
-          dst.close();
         }
       }
     } finally {
@@ -781,7 +777,7 @@
 
   private static BinaryResult stackJsonString(HttpServletResponse res,
       final BinaryResult src) throws IOException {
-    TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+    TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
     buf.write(JSON_MAGIC);
     try(Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
         JsonWriter json = new JsonWriter(w)) {
@@ -794,7 +790,7 @@
     res.setHeader("X-FYI-Content-Type", src.getContentType());
     return asBinaryResult(buf)
       .setContentType(JSON_TYPE)
-      .setCharacterEncoding(UTF_8.name());
+      .setCharacterEncoding(UTF_8);
   }
 
   private static BinaryResult stackBase64(HttpServletResponse res,
@@ -822,14 +818,16 @@
     }
     res.setHeader("X-FYI-Content-Encoding", "base64");
     res.setHeader("X-FYI-Content-Type", src.getContentType());
-    return b64.setContentType("text/plain").setCharacterEncoding("ISO-8859-1");
+    return b64.setContentType("text/plain").setCharacterEncoding(ISO_8859_1);
   }
 
   private static BinaryResult stackGzip(HttpServletResponse res,
       final BinaryResult src) throws IOException {
     BinaryResult gz;
     long len = src.getContentLength();
-    if (256 <= len && len <= (10 << 20)) {
+    if (len < 256) {
+      return src; // Do not compress very small payloads.
+    } else if (len <= (10 << 20)) {
       gz = compress(src);
       if (len <= gz.getContentLength()) {
         return src;
@@ -958,7 +956,8 @@
       if (user instanceof AnonymousUser) {
         throw new AuthException("Authentication required");
       } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
-        throw new AuthException("Invalid authentication method. In order to authenticate, prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
+        throw new AuthException("Invalid authentication method. In order to authenticate, "
+            + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
       }
     }
     user.setAccessPath(AccessPath.REST_API);
@@ -1002,7 +1001,7 @@
     if (err != null) {
       RequestUtil.setErrorTraceAttribute(req, err);
     }
-    configureCaching(req, res, null, c);
+    configureCaching(req, res, null, null, c);
     res.setStatus(statusCode);
     replyText(req, res, msg);
   }
@@ -1053,23 +1052,28 @@
     return false;
   }
 
+  private static int base64MaxSize(long n) {
+    return 4 * IntMath.divide((int) n, 3, CEILING);
+  }
+
   private static BinaryResult base64(BinaryResult bin)
       throws IOException {
-    int max = 4 * IntMath.divide((int) bin.getContentLength(), 3, CEILING);
-    TemporaryBuffer.Heap buf = heap(max);
-    OutputStream encoded = BaseEncoding.base64().encodingStream(
-        new OutputStreamWriter(buf, ISO_8859_1));
-    bin.writeTo(encoded);
-    encoded.close();
+    int maxSize = base64MaxSize(bin.getContentLength());
+    int estSize = Math.min(base64MaxSize(HEAP_EST_SIZE), maxSize);
+    TemporaryBuffer.Heap buf = heap(estSize, maxSize);
+    try (OutputStream encoded = BaseEncoding.base64().encodingStream(
+        new OutputStreamWriter(buf, ISO_8859_1))) {
+      bin.writeTo(encoded);
+    }
     return asBinaryResult(buf);
   }
 
   private static BinaryResult compress(BinaryResult bin)
       throws IOException {
-    TemporaryBuffer.Heap buf = heap(20 << 20);
-    GZIPOutputStream gz = new GZIPOutputStream(buf);
-    bin.writeTo(gz);
-    gz.close();
+    TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, 20 << 20);
+    try (GZIPOutputStream gz = new GZIPOutputStream(buf)) {
+      bin.writeTo(gz);
+    }
     return asBinaryResult(buf).setContentType(bin.getContentType());
   }
 
@@ -1083,8 +1087,8 @@
     }.setContentLength(buf.length());
   }
 
-  private static Heap heap(int max) {
-    return new TemporaryBuffer.Heap(max);
+  private static Heap heap(int est, int max) {
+    return new TemporaryBuffer.Heap(est, max);
   }
 
   @SuppressWarnings("serial")
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 73b546b..547bf45 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -42,13 +41,10 @@
 
   protected Account.Id getAccountId() {
     CurrentUser u = currentUser.get();
-    if (u.isIdentifiedUser()) {
-      return ((IdentifiedUser) u).getAccountId();
-    }
-    return null;
+    return u.isIdentifiedUser() ? u.getAccountId() : null;
   }
 
-  protected CurrentUser getCurrentUser() {
+  protected CurrentUser getUser() {
     return currentUser.get();
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 01f2df3..3b064e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -134,7 +134,7 @@
       Audit note = method.getAnnotation(Audit.class);
       if (note != null) {
         final String sid = call.getWebSession().getSessionId();
-        final CurrentUser username = call.getWebSession().getCurrentUser();
+        final CurrentUser username = call.getWebSession().getUser();
         final Multimap<String, ?> args =
             extractParams(note, call);
         final String what = extractWhat(note, call);
@@ -166,14 +166,18 @@
   }
 
   private String extractWhat(final Audit note, final GerritCall call) {
-    String methodClass = call.getMethodClass().getName();
-    methodClass = methodClass.substring(methodClass.lastIndexOf(".")+1);
+    Class<?> methodClass = call.getMethodClass();
+    String methodClassName = methodClass != null
+        ? methodClass.getName()
+        : "<UNKNOWN_CLASS>";
+    methodClassName =
+        methodClassName.substring(methodClassName.lastIndexOf(".") + 1);
     String what = note.action();
     if (what.length() == 0) {
       what = call.getMethod().getName();
     }
 
-    return methodClass + "." + what;
+    return methodClassName + "." + what;
   }
 
   static class GerritCall extends ActiveCall {
@@ -275,7 +279,7 @@
       } else if (session.isSignedIn() && session.isValidXGerritAuth(keyIn)) {
         // The session must exist, and must be using this token.
         //
-        session.getCurrentUser().setAccessPath(AccessPath.JSON_RPC);
+        session.getUser().setAccessPath(AccessPath.JSON_RPC);
         return true;
       }
       return false;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
index 19a02a5..1b2b990 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
@@ -66,13 +66,8 @@
       if (r != null) {
         callback.onSuccess(r);
       }
-    } catch (NoSuchProjectException e) {
-      callback.onFailure(new NoSuchEntityException());
-
-    } catch (NoSuchRefException e) {
-      callback.onFailure(new NoSuchEntityException());
-
-    } catch (NoSuchChangeException e) {
+    } catch (NoSuchProjectException | NoSuchChangeException
+        | NoSuchRefException e) {
       callback.onFailure(new NoSuchEntityException());
 
     } catch (OrmException e) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 5b28a61..c0fb86b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.data.SystemInfoService;
 import com.google.gerrit.server.project.ProjectCache;
@@ -46,16 +45,14 @@
 
   private final List<HostKey> hostKeys;
   private final Provider<HttpServletRequest> httpRequest;
-  private final Provider<GerritConfig> config;
   private final ProjectCache projectCache;
 
   @Inject
-  SystemInfoServiceImpl(final SshInfo daemon,
-      final Provider<HttpServletRequest> hsr, final Provider<GerritConfig> cfg,
-      final ProjectCache pc) {
+  SystemInfoServiceImpl(SshInfo daemon,
+      Provider<HttpServletRequest> hsr,
+      ProjectCache pc) {
     hostKeys = daemon.getHostKeys();
     httpRequest = hsr;
-    config = cfg;
     projectCache = pc;
   }
 
@@ -95,9 +92,4 @@
     log.error("Client UI JavaScript error: User-Agent=" + ua + ": " + message);
     callback.onSuccess(VoidResult.INSTANCE);
   }
-
-  @Override
-  public void gerritConfig(final AsyncCallback<GerritConfig> callback) {
-    callback.onSuccess(config.get());
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index d0fb504..62778eb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.httpd.rpc.RpcServletModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.config.FactoryModule;
 
 public class AccountModule extends RpcServletModule {
   public AccountModule() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index b2a6afc..e0d63c8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -17,32 +17,22 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.contact.ContactStore;
-import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -56,17 +46,12 @@
 
 class AccountSecurityImpl extends BaseServiceImplementation implements
     AccountSecurity {
-  private final ContactStore contactStore;
   private final Realm realm;
   private final ProjectCache projectCache;
   private final Provider<IdentifiedUser> user;
-  private final EmailTokenVerifier emailTokenVerifier;
   private final AccountByEmailCache byEmailCache;
   private final AccountCache accountCache;
-  private final AccountManager accountManager;
-  private final boolean useContactInfo;
 
-  private final ChangeUserName.CurrentUser changeUserNameFactory;
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
 
@@ -76,30 +61,21 @@
 
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
-      final Provider<CurrentUser> currentUser, final ContactStore cs,
+      final Provider<CurrentUser> currentUser,
       final Realm r, final Provider<IdentifiedUser> u,
-      final EmailTokenVerifier etv, final ProjectCache pc,
+      final ProjectCache pc,
       final AccountByEmailCache abec, final AccountCache uac,
-      final AccountManager am,
-      final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
       final ChangeHooks hooks, final GroupCache groupCache,
       final AuditService auditService) {
     super(schema, currentUser);
-    contactStore = cs;
     realm = r;
     user = u;
-    emailTokenVerifier = etv;
     projectCache = pc;
     byEmailCache = abec;
     accountCache = uac;
-    accountManager = am;
     this.auditService = auditService;
-
-    useContactInfo = contactStore != null && contactStore.isEnabled();
-
-    this.changeUserNameFactory = changeUserNameFactory;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
     this.hooks = hooks;
@@ -107,20 +83,6 @@
   }
 
   @Override
-  public void changeUserName(final String newName,
-      final AsyncCallback<VoidResult> callback) {
-    if (realm.allowsEdit(Account.FieldName.USER_NAME)) {
-      if (newName == null || !newName.matches(Account.USER_NAME_PATTERN)) {
-        callback.onFailure(new InvalidUserNameException());
-      }
-      Handler.wrap(changeUserNameFactory.create(newName)).to(callback);
-    } else {
-      callback.onFailure(
-          new PermissionDeniedException("Not allowed to change username"));
-    }
-  }
-
-  @Override
   public void myExternalIds(AsyncCallback<List<AccountExternalId>> callback) {
     externalIdDetailFactory.create().to(callback);
   }
@@ -133,7 +95,7 @@
 
   @Override
   public void updateContact(final String name, final String emailAddr,
-      final ContactInformation info, final AsyncCallback<Account> callback) {
+      final AsyncCallback<Account> callback) {
     run(callback, new Action<Account>() {
       @Override
       public Account run(ReviewDb db) throws OrmException, Failure {
@@ -148,19 +110,6 @@
           throw new Failure(new PermissionDeniedException("Email address must be verified"));
         }
         me.setPreferredEmail(Strings.emptyToNull(emailAddr));
-        if (useContactInfo) {
-          if (ContactInformation.hasAddress(info)
-              || (me.isContactFiled() && ContactInformation.hasData(info))) {
-            me.setContactFiled(TimeUtil.nowTs());
-          }
-          if (ContactInformation.hasData(info)) {
-            try {
-              contactStore.store(me, info);
-            } catch (ContactInformationStoreException e) {
-              throw new Failure(e);
-            }
-          }
-        }
         db.accounts().update(Collections.singleton(me));
         if (!eq(oldEmail, me.getPreferredEmail())) {
           byEmailCache.evict(oldEmail);
@@ -221,25 +170,4 @@
       }
     });
   }
-
-  @Override
-  public void validateEmail(final String tokenString,
-      final AsyncCallback<VoidResult> callback) {
-    try {
-      EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(tokenString);
-      Account.Id currentUser = user.get().getAccountId();
-      if (currentUser.equals(token.getAccountId())) {
-        accountManager.link(currentUser, token.toAuthRequest());
-        callback.onSuccess(VoidResult.INSTANCE);
-      } else {
-        throw new EmailTokenVerifier.InvalidTokenException();
-      }
-    } catch (EmailTokenVerifier.InvalidTokenException e) {
-      callback.onFailure(e);
-    } catch (AccountException e) {
-      callback.onFailure(e);
-    } catch (OrmException e) {
-      callback.onFailure(e);
-    }
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index e1c9e3c..63ec075 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -19,15 +19,17 @@
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
@@ -39,6 +41,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -48,24 +53,24 @@
 class AccountServiceImpl extends BaseServiceImplementation implements
     AccountService {
   private final Provider<IdentifiedUser> currentUser;
-  private final AccountCache accountCache;
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
-  private final ChangeQueryBuilder queryBuilder;
+  private final Provider<ChangeQueryBuilder> queryBuilder;
+  private final SetDiffPreferences setDiff;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
-      final AccountCache accountCache,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
-      final ChangeQueryBuilder queryBuilder) {
+      final Provider<ChangeQueryBuilder> queryBuilder,
+      SetDiffPreferences setDiff) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
-    this.accountCache = accountCache;
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
     this.queryBuilder = queryBuilder;
+    this.setDiff = setDiff;
   }
 
   @Override
@@ -79,35 +84,21 @@
   }
 
   @Override
-  public void changePreferences(final AccountGeneralPreferences pref,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      @Override
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final Account a = db.accounts().get(getAccountId());
-        if (a == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-        a.setGeneralPreferences(pref);
-        db.accounts().update(Collections.singleton(a));
-        accountCache.evict(a.getId());
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  @Override
-  public void changeDiffPreferences(final AccountDiffPreference diffPref,
+  public void changeDiffPreferences(final DiffPreferencesInfo diffPref,
       AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>(){
       @Override
       public VoidResult run(ReviewDb db) throws OrmException {
-        if (!diffPref.getAccountId().equals(getAccountId())) {
-          throw new IllegalArgumentException("diffPref.getAccountId() "
-              + diffPref.getAccountId() + " doesn't match"
-              + " the accountId of the signed in user " + getAccountId());
+        if (!getUser().isIdentifiedUser()) {
+          throw new IllegalArgumentException("Not authenticated");
         }
-        db.accountDiffPreferences().upsert(Collections.singleton(diffPref));
+        IdentifiedUser me = getUser().asIdentifiedUser();
+        try {
+          setDiff.apply(new AccountResource(me), diffPref);
+        } catch (AuthException | BadRequestException | ConfigInvalidException
+            | IOException e) {
+          throw new OrmException("Cannot save diff preferences", e);
+        }
         return VoidResult.INSTANCE;
       }
     });
@@ -156,7 +147,7 @@
 
         if (filter != null) {
           try {
-            queryBuilder.parse(filter);
+            queryBuilder.get().parse(filter);
           } catch (QueryParseException badFilter) {
             throw new InvalidQueryException(badFilter.getMessage(), filter);
           }
@@ -164,7 +155,7 @@
 
         AccountProjectWatch watch =
             new AccountProjectWatch(new AccountProjectWatch.Key(
-                ((IdentifiedUser) ctl.getCurrentUser()).getAccountId(),
+                ctl.getUser().getAccountId(),
                 nameKey, filter));
         try {
           db.accountProjectWatches().insert(Collections.singleton(watch));
@@ -201,8 +192,9 @@
       public VoidResult run(final ReviewDb db) throws OrmException, Failure {
         final Account.Id me = getAccountId();
         for (final AccountProjectWatch.Key keyId : keys) {
-          if (!me.equals(keyId.getParentKey()))
+          if (!me.equals(keyId.getParentKey())) {
             throw new Failure(new NoSuchEntityException());
+          }
         }
 
         db.accountProjectWatches().deleteKeys(keys);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index 712f5a2..0fff8ce 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -34,7 +34,7 @@
 import java.util.Map;
 
 class AgreementInfoFactory extends Handler<AgreementInfo> {
-  private final Logger log = LoggerFactory.getLogger(getClass());
+  private static final Logger log = LoggerFactory.getLogger(AgreementInfoFactory.class);
 
   interface Factory {
     AgreementInfoFactory create();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
index d0c042c..37ca524 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.data.ChangeDetailService;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.inject.Inject;
@@ -38,7 +38,7 @@
 
   @Override
   public void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id id,
-      AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback) {
+      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback) {
     patchSetDetail.create(baseId, id, diffPrefs).to(callback);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
index 840137a..d8adfe3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.httpd.rpc.RpcServletModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.config.FactoryModule;
 
 public class ChangeModule extends RpcServletModule {
   public ChangeModule() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index f4a6727..d2dc384 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -31,7 +31,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
@@ -68,7 +67,7 @@
     PatchSetDetailFactory create(
         @Assisted("psIdBase") @Nullable PatchSet.Id psIdBase,
         @Assisted("psIdNew") PatchSet.Id psIdNew,
-        @Nullable AccountDiffPreference diffPrefs);
+        @Nullable DiffPreferencesInfo diffPrefs);
   }
 
   private final PatchSetInfoFactory infoFactory;
@@ -79,10 +78,10 @@
   private final PatchLineCommentsUtil plcUtil;
   private final ChangeEditUtil editUtil;
 
-  private Project.NameKey projectKey;
+  private Project.NameKey project;
   private final PatchSet.Id psIdBase;
   private final PatchSet.Id psIdNew;
-  private final AccountDiffPreference diffPrefs;
+  private final DiffPreferencesInfo diffPrefs;
   private ObjectId oldId;
   private ObjectId newId;
 
@@ -99,7 +98,7 @@
       ChangeEditUtil editUtil,
       @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
       @Assisted("psIdNew") final PatchSet.Id psIdNew,
-      @Assisted @Nullable final AccountDiffPreference diffPrefs) {
+      @Assisted @Nullable final DiffPreferencesInfo diffPrefs) {
     this.infoFactory = psif;
     this.db = db;
     this.patchListCache = patchListCache;
@@ -134,7 +133,7 @@
         throw new NoSuchEntityException();
       }
     }
-    projectKey = control.getProject().getNameKey();
+    project = control.getProject().getNameKey();
     final PatchList list;
 
     try {
@@ -146,7 +145,7 @@
           newId = toObjectId(psIdNew);
         }
 
-        list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+        list = listFor(keyFor(diffPrefs.ignoreWhitespace));
       } else { // OK, means use base to compare
         list = patchListCache.get(control.getChange(), patchSet);
       }
@@ -172,18 +171,18 @@
 
     detail = new PatchSetDetail();
     detail.setPatchSet(patchSet);
-    detail.setProject(projectKey);
+    detail.setProject(project);
 
     detail.setInfo(infoFactory.get(db, patchSet.getId()));
     detail.setPatches(patches);
 
-    final CurrentUser user = control.getCurrentUser();
+    final CurrentUser user = control.getUser();
     if (user.isIdentifiedUser() && edit == null) {
       // If we are signed in, compute the number of draft comments by the
       // current user on each of these patch files. This way they can more
       // quickly locate where they have pending drafts, and review them.
       //
-      final Account.Id me = ((IdentifiedUser) user).getAccountId();
+      final Account.Id me = user.getAccountId();
       for (PatchLineComment c
           : plcUtil.draftByPatchSetAuthor(db, psIdNew, me, notes)) {
         final Patch p = byKey.get(c.getKey().getParentKey());
@@ -218,12 +217,12 @@
     }
   }
 
-  private PatchListKey keyFor(final Whitespace whitespace) {
-    return new PatchListKey(projectKey, oldId, newId, whitespace);
+  private PatchListKey keyFor(Whitespace whitespace) {
+    return new PatchListKey(oldId, newId, whitespace);
   }
 
   private PatchList listFor(PatchListKey key)
       throws PatchListNotAvailableException {
-    return patchListCache.get(key);
+    return patchListCache.get(key, project);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 9257271..f9180f7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.common.data.PatchDetailService;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -48,7 +48,7 @@
 
   @Override
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
-      final PatchSet.Id psb, final AccountDiffPreference dp,
+      final PatchSet.Id psb, final DiffPreferencesInfo dp,
       final AsyncCallback<PatchScript> callback) {
     if (psb == null) {
       callback.onFailure(new NoSuchEntityException());
@@ -60,7 +60,7 @@
       public PatchScript call() throws Exception {
         ChangeControl control = changeControlFactory.validateFor(
             patchKey.getParentKey().getParentKey(),
-            getCurrentUser());
+            getUser());
         return patchScriptFactoryFactory.create(
             control, patchKey.getFileName(), psa, psb, dp).call();
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 3615610..aee238d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc.project;
 
 import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
@@ -23,11 +24,13 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -64,6 +67,7 @@
 
   private final Project.NameKey projectName;
   private ProjectControl pc;
+  private WebLinks webLinks;
 
   @Inject
   ProjectAccessFactory(final GroupBackend groupBackend,
@@ -72,6 +76,7 @@
       final GroupControl.Factory groupControlFactory,
       final MetaDataUpdate.Server metaDataUpdateFactory,
       final AllProjectsName allProjectsName,
+      final WebLinks webLinks,
 
       @Assisted final Project.NameKey name) {
     this.groupBackend = groupBackend;
@@ -80,6 +85,7 @@
     this.groupControlFactory = groupControlFactory;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allProjectsName = allProjectsName;
+    this.webLinks = webLinks;
 
     this.projectName = name;
   }
@@ -209,9 +215,17 @@
     detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
     detail.setGroupInfo(buildGroupInfo(local));
     detail.setLabelTypes(pc.getLabelTypes());
+    detail.setFileHistoryLinks(getConfigFileLogLinks(projectName.get()));
     return detail;
   }
 
+  private List<WebLinkInfoCommon> getConfigFileLogLinks(String projectName) {
+    FluentIterable<WebLinkInfoCommon> links =
+        webLinks.getFileHistoryLinksCommon(projectName, RefNames.REFS_CONFIG,
+            ProjectConfig.PROJECT_CONFIG);
+    return links.isEmpty() ? null : links.toList();
+  }
+
   private Map<AccountGroup.UUID, GroupInfo> buildGroupInfo(List<AccessSection> local) {
     Map<AccountGroup.UUID, GroupInfo> infos = new HashMap<>();
     for (AccessSection section : local) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 829035b..ba4f012 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -146,9 +146,7 @@
               "You are not allowed to change the parent project since you are "
               + "not an administrator. You may save the modifications for review "
               + "so that an administrator can approve them.", e);
-        } catch (ResourceConflictException e) {
-          throw new UpdateParentFailedException(e.getMessage(), e);
-        } catch (UnprocessableEntityException e) {
+        } catch (ResourceConflictException | UnprocessableEntityException e) {
           throw new UpdateParentFailedException(e.getMessage(), e);
         }
         config.getProject().setParentName(parentProjectName);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index bd5f940..a89f52e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.httpd.rpc.RpcServletModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.config.FactoryModule;
 
 public class ProjectModule extends RpcServletModule {
   public ProjectModule() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index af6c0da..5b3b064 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -35,8 +36,11 @@
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -47,7 +51,9 @@
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 import java.util.List;
@@ -68,6 +74,7 @@
   private final ProjectCache projectCache;
   private final ChangesCollection changes;
   private final ChangeInserter.Factory changeInserterFactory;
+  private final BatchUpdate.Factory updateFactory;
 
   @Inject
   ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
@@ -79,6 +86,7 @@
       AllProjectsNameProvider allProjects,
       ChangesCollection changes,
       ChangeInserter.Factory changeInserterFactory,
+      BatchUpdate.Factory updateFactory,
       Provider<SetParent> setParent,
 
       @Assisted("projectName") Project.NameKey projectName,
@@ -95,6 +103,7 @@
     this.projectCache = projectCache;
     this.changes = changes;
     this.changeInserterFactory = changeInserterFactory;
+    this.updateFactory = updateFactory;
   }
 
   @Override
@@ -118,9 +127,21 @@
             config.getProject().getNameKey(),
             RefNames.REFS_CONFIG),
         TimeUtil.nowTs());
-    ChangeInserter ins =
-        changeInserterFactory.create(ctl, change, commit);
-    ins.insert();
+    try (RevWalk rw = new RevWalk(md.getRepository());
+        ObjectInserter objInserter = md.getRepository().newObjectInserter();
+        BatchUpdate bu = updateFactory.create(
+          db, change.getProject(), ctl.getUser(),
+          change.getCreatedOn())) {
+      bu.setRepository(md.getRepository(), rw, objInserter);
+      bu.insertChange(
+          changeInserterFactory.create(
+                ctl.controlForRef(change.getDest().get()), change, commit)
+              .setValidatePolicy(CommitValidators.Policy.NONE)
+              .setUpdateRef(false)); // Created by commitToNewRef.
+      bu.execute();
+    } catch (UpdateException | RestApiException e) {
+      throw new IOException(e);
+    }
 
     ChangeResource rsrc;
     try {
@@ -150,7 +171,7 @@
       AddReviewerInput input = new AddReviewerInput();
       input.reviewer = projectOwners;
       reviewersProvider.get().apply(rsrc, input);
-    } catch (Exception e) {
+    } catch (IOException | OrmException | RestApiException e) {
       // one of the owner groups is not visible to the user and this it why it
       // can't be added as reviewer
     }
@@ -166,7 +187,7 @@
         AddReviewerInput input = new AddReviewerInput();
         input.reviewer = r.getGroup().getUUID().get();
         reviewersProvider.get().apply(rsrc, input);
-      } catch (Exception e) {
+      } catch (IOException | OrmException | RestApiException e) {
         // ignore
       }
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
index 321f032..9c067de 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.template;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -27,8 +29,8 @@
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 @Singleton
 public class SiteHeaderFooter {
@@ -43,13 +45,13 @@
     this.refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
     this.sitePaths = sitePaths;
 
-    Template t = new Template(sitePaths);
     try {
+      Template t = new Template(sitePaths);
       t.load();
+      template = t;
     } catch (IOException e) {
       log.warn("Cannot load site header or footer", e);
     }
-    template = t;
   }
 
   public Document parse(Class<?> clazz, String name) throws IOException {
@@ -118,8 +120,8 @@
 
     void load() throws IOException {
       css = HtmlDomUtil.readFile(
-          cssFile.path.getParentFile(),
-          cssFile.path.getName());
+          cssFile.path.getParent(),
+          cssFile.path.getFileName().toString());
       header = readXml(headerFile);
       footer = readXml(footerFile);
     }
@@ -135,16 +137,16 @@
   }
 
   private static class FileInfo {
-    final File path;
+    final Path path;
     final long time;
 
-    FileInfo(File p) {
+    FileInfo(Path p) {
       path = p;
-      time = path.lastModified();
+      time = lastModified(p);
     }
 
     boolean isStale() {
-      return time != path.lastModified();
+      return time != lastModified(path);
     }
   }
 }
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
index c660311..23d5856 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
@@ -1,34 +1,6 @@
 <html>
   <head>
     <title>Gerrit Code Review</title>
-    <script id="gwtdevmode">
-      (function () {
-        var pn = 'gwt.codesvr';
-        var cn = 'gerrit_ui.' + pn;
-
-        var p_start = window.location.search.indexOf(pn + '=');
-        if (p_start != -1) {
-          p_start = p_start + pn.length + 1;
-          var p_end = window.location.search.indexOf(";", p_start);
-          if (p_end == -1) p_end = window.location.search.length;
-          var v = window.location.search.substring(p_start, p_end);
-
-          var e = new Date();
-          e.setDate(e.getDate() + 1);
-          document.cookie = cn + "=" + v + ';expires=' + e.toGMTString();
-
-        } else if (document.cookie.length != 0) {
-          var c_start = document.cookie.indexOf(cn + '=');
-          if (c_start != -1) {
-            c_start = c_start + cn.length + 1;
-            var c_end = document.cookie.indexOf(";", c_start);
-            if (c_end == -1) c_end = document.cookie.length;
-            var v = document.cookie.substring(c_start, c_end);
-            window.location.replace('?' + pn + '=' + v + document.location.hash);
-          }
-        }
-      })();
-    </script>
     <style id="gerrit_sitecss" type="text/css"></style>
   </head>
   <body>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index 9f3fa1e..c0d8446 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -2,34 +2,6 @@
   <head>
     <title>Gerrit Code Review</title>
     <meta name="gwt:property" content="locale=en_US" />
-    <script id="gwtdevmode">
-      (function () {
-        var pn = 'gwt.codesvr';
-        var cn = 'gerrit_ui.' + pn;
-
-        var p_start = window.location.search.indexOf(pn + '=');
-        if (p_start != -1) {
-          p_start = p_start + pn.length + 1;
-          var p_end = window.location.search.indexOf(";", p_start);
-          if (p_end == -1) p_end = window.location.search.length;
-          var v = window.location.search.substring(p_start, p_end);
-
-          var e = new Date();
-          e.setDate(e.getDate() + 1);
-          document.cookie = cn + "=" + v + ';expires=' + e.toGMTString();
-
-        } else if (document.cookie.length != 0) {
-          var c_start = document.cookie.indexOf(cn + '=');
-          if (c_start != -1) {
-            c_start = c_start + cn.length + 1;
-            var c_end = document.cookie.indexOf(";", c_start);
-            if (c_end == -1) c_end = document.cookie.length;
-            var v = document.cookie.substring(c_start, c_end);
-            window.location.replace('?' + pn + '=' + v + document.location.hash);
-          }
-        }
-      })();
-    </script>
     <script id="gerrit_hostpagedata"></script>
     <style id="gerrit_sitecss" type="text/css"></style>
     <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
@@ -38,7 +10,7 @@
     <div id="gerrit_topmenu"></div>
     <div id="gerrit_header"></div>
     <div id="gerrit_startinggerrit" style="margin-left: 10px;">
-      <p>Loading <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ...</p>
+      <p>Loading <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a> ...</p>
       <noscript>
         <p>Gerrit requires a JavaScript enabled browser.</p>
       </noscript>
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
new file mode 100644
index 0000000..94f3768
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -0,0 +1,368 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.newCapture;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+import com.google.inject.Key;
+import com.google.inject.util.Providers;
+
+import org.easymock.Capture;
+import org.easymock.EasyMockSupport;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class AllRequestFilterFilterProxyTest {
+  /**
+   * Set of filters for FilterProxy
+   * <p/>
+   * This set is used to as set of filters when fetching an
+   * {@link AllRequestFilter.FilterProxy} instance through
+   * {@link #getFilterProxy()}.
+   */
+  private DynamicSet<AllRequestFilter> filters;
+
+  @Before
+  public void setUp() throws Exception {
+    // Force starting each test with an initially empty set of filters.
+    // Filters get added by the tests themselves.
+    filters = new DynamicSet<>();
+  }
+
+  // The wrapping of {@link #getFilterProxy()} and
+  // {@link #addFilter(AllRequestFilter)} into separate methods may seem
+  // overengineered at this point. However, if in the future we decide to not
+  // test the inner class directly, but rather test from the outside using
+  // Guice Injectors, it is now sufficient to change only those two methods,
+  // and we need not mess with the individual tests.
+
+  /**
+   * Obtain a FilterProxy with a known DynamicSet of filters
+   * <p/>
+   * The returned {@link AllRequestFilter.FilterProxy} can have new filters
+   * added dynamically by calling {@link #addFilter(AllRequestFilter)}.
+   */
+  private AllRequestFilter.FilterProxy getFilterProxy() {
+    return new AllRequestFilter.FilterProxy(filters);
+  }
+
+  /**
+   * Add a filter to created FilterProxy instances
+   * <p/>
+   * This method adds the given filter to all
+   * {@link AllRequestFilter.FilterProxy} instances created by
+   * {@link #getFilterProxy()}.
+   */
+  private ReloadableRegistrationHandle<AllRequestFilter> addFilter(
+      final AllRequestFilter filter) {
+    Key<AllRequestFilter> key = Key.get(AllRequestFilter.class);
+    return filters.add(key, Providers.of(filter));
+  }
+
+  @Test
+  public void testNoFilters() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
+
+    FilterChain chain = ems.createMock(FilterChain.class);
+    chain.doFilter(req, res);
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testSingleFilterNoBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock("config", FilterConfig.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
+
+    FilterChain chain = ems.createMock("chain", FilterChain.class);
+
+    AllRequestFilter filter = ems.createStrictMock("filter", AllRequestFilter.class);
+    filter.init(config);
+    filter.doFilter(eq(req), eq(res), anyObject(FilterChain.class));
+    filter.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filter);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testSingleFilterBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock(FilterChain.class);
+
+    Capture<FilterChain> capturedChain = newCapture();
+
+    AllRequestFilter filter = mockControl.createMock(AllRequestFilter.class);
+    filter.init(config);
+    filter.doFilter(eq(req), eq(res), capture(capturedChain));
+    chain.doFilter(req, res);
+    filter.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filter);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    capturedChain.getValue().doFilter(req, res);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testTwoFiltersNoBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock(FilterChain.class);
+
+    AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
+
+    AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
+    filterA.init(config);
+    filterB.init(config);
+    filterA.doFilter(eq(req), eq(res), anyObject(FilterChain.class));
+    filterA.destroy();
+    filterB.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filterA);
+    addFilter(filterB);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testTwoFiltersBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock(FilterChain.class);
+
+    Capture<FilterChain> capturedChainA = newCapture();
+    Capture<FilterChain> capturedChainB = newCapture();
+
+    AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
+    AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
+
+    filterA.init(config);
+    filterB.init(config);
+    filterA.doFilter(eq(req), eq(res), capture(capturedChainA));
+    filterB.doFilter(eq(req), eq(res), capture(capturedChainB));
+    chain.doFilter(req, res);
+    filterA.destroy();
+    filterB.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filterA);
+    addFilter(filterB);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    capturedChainA.getValue().doFilter(req, res);
+    capturedChainB.getValue().doFilter(req, res);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testPostponedLoading() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req1 = new FakeHttpServletRequest();
+    HttpServletRequest req2 = new FakeHttpServletRequest();
+    HttpServletResponse res1 = new FakeHttpServletResponse();
+    HttpServletResponse res2 = new FakeHttpServletResponse();
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock("chain", FilterChain.class);
+
+    Capture<FilterChain> capturedChainA1 = newCapture();
+    Capture<FilterChain> capturedChainA2 = newCapture();
+    Capture<FilterChain> capturedChainB = newCapture();
+
+    AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
+    AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
+
+    filterA.init(config);
+    filterA.doFilter(eq(req1), eq(res1), capture(capturedChainA1));
+    chain.doFilter(req1, res1);
+
+    filterA.doFilter(eq(req2), eq(res2), capture(capturedChainA2));
+    filterB.init(config); // <-- This is crucial part. filterB got loaded
+    // after filterProxy's init finished. Nonetheless filterB gets initialized.
+    filterB.doFilter(eq(req2), eq(res2), capture(capturedChainB));
+    chain.doFilter(req2, res2);
+
+    filterA.destroy();
+    filterB.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filterA);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req1, res1, chain);
+    capturedChainA1.getValue().doFilter(req1, res1);
+
+    addFilter(filterB); // <-- Adds filter after filterProxy's init got called.
+    filterProxy.doFilter(req2, res2, chain);
+    capturedChainA2.getValue().doFilter(req2, res2);
+    capturedChainB.getValue().doFilter(req2, res2);
+
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testDynamicUnloading() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req1 = new FakeHttpServletRequest();
+    HttpServletRequest req2 = new FakeHttpServletRequest();
+    HttpServletRequest req3 = new FakeHttpServletRequest();
+    HttpServletResponse res1 = new FakeHttpServletResponse();
+    HttpServletResponse res2 = new FakeHttpServletResponse();
+    HttpServletResponse res3 = new FakeHttpServletResponse();
+
+    Plugin plugin = ems.createMock(Plugin.class);
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock("chain", FilterChain.class);
+
+    Capture<FilterChain> capturedChainA1 = newCapture();
+    Capture<FilterChain> capturedChainB1 = newCapture();
+    Capture<FilterChain> capturedChainB2 = newCapture();
+
+    AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
+    AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
+
+    filterA.init(config);
+    filterB.init(config);
+
+    filterA.doFilter(eq(req1), eq(res1), capture(capturedChainA1));
+    filterB.doFilter(eq(req1), eq(res1), capture(capturedChainB1));
+    chain.doFilter(req1, res1);
+
+    filterA.destroy(); // Cleaning up of filterA after it got unloaded
+
+    filterB.doFilter(eq(req2), eq(res2), capture(capturedChainB2));
+    chain.doFilter(req2, res2);
+
+    filterB.destroy(); // Cleaning up of filterA after it got unloaded
+
+    chain.doFilter(req3, res3);
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    ReloadableRegistrationHandle<AllRequestFilter> handleFilterA =
+        addFilter(filterA);
+    ReloadableRegistrationHandle<AllRequestFilter> handleFilterB =
+        addFilter(filterB);
+
+    filterProxy.init(config);
+
+    // Request #1 with filterA and filterB
+    filterProxy.doFilter(req1, res1, chain);
+    capturedChainA1.getValue().doFilter(req1, res1);
+    capturedChainB1.getValue().doFilter(req1, res1);
+
+    // Unloading filterA
+    handleFilterA.remove();
+    filterProxy.onStopPlugin(plugin);
+
+    // Request #1 only with filterB
+    filterProxy.doFilter(req2, res2, chain);
+    capturedChainA1.getValue().doFilter(req2, res2);
+
+    // Unloading filterB
+    handleFilterB.remove();
+    filterProxy.onStopPlugin(plugin);
+
+    // Request #1 with no additional filters
+    filterProxy.doFilter(req3, res3, chain);
+
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+}
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
index e032823..9559e13 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.httpd.plugins;
 
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 
 import org.junit.Test;
 
@@ -34,16 +33,17 @@
     ContextMapper classUnderTest = new ContextMapper(CONTEXT);
 
     HttpServletRequest originalRequest =
-        createMockRequest("/plugins/", PLUGIN_NAME + "/" + RESOURCE);
+        createFakeRequest("/plugins/", PLUGIN_NAME + "/" + RESOURCE);
 
     HttpServletRequest result =
         classUnderTest.create(originalRequest, PLUGIN_NAME);
 
-    assertEquals(CONTEXT + "/plugins/" + PLUGIN_NAME, result.getContextPath());
-    assertEquals("", result.getServletPath());
-    assertEquals("/" + RESOURCE, result.getPathInfo());
-    assertEquals(CONTEXT + "/plugins/" + PLUGIN_NAME + "/" + RESOURCE,
-        result.getRequestURI());
+    assertThat(result.getContextPath())
+        .isEqualTo(CONTEXT + "/plugins/" + PLUGIN_NAME);
+    assertThat(result.getServletPath()).isEqualTo("");
+    assertThat(result.getPathInfo()).isEqualTo("/" + RESOURCE);
+    assertThat(result.getRequestURI())
+        .isEqualTo(CONTEXT + "/plugins/" + PLUGIN_NAME + "/" + RESOURCE);
   }
 
   @Test
@@ -51,29 +51,22 @@
     ContextMapper classUnderTest = new ContextMapper(CONTEXT);
 
     HttpServletRequest originalRequest =
-        createMockRequest("/a/plugins/", PLUGIN_NAME + "/" + RESOURCE);
+        createFakeRequest("/a/plugins/", PLUGIN_NAME + "/" + RESOURCE);
 
     HttpServletRequest result =
         classUnderTest.create(originalRequest, PLUGIN_NAME);
 
-    assertEquals(CONTEXT + "/a/plugins/" + PLUGIN_NAME,
-        result.getContextPath());
-    assertEquals("", result.getServletPath());
-    assertEquals("/" + RESOURCE, result.getPathInfo());
-    assertEquals(CONTEXT + "/a/plugins/" + PLUGIN_NAME + "/" + RESOURCE,
-        result.getRequestURI());
+    assertThat(result.getContextPath())
+        .isEqualTo(CONTEXT + "/a/plugins/" + PLUGIN_NAME);
+    assertThat(result.getServletPath()).isEqualTo("");
+    assertThat(result.getPathInfo()).isEqualTo("/" + RESOURCE);
+    assertThat(result.getRequestURI())
+        .isEqualTo(CONTEXT + "/a/plugins/" + PLUGIN_NAME + "/" + RESOURCE);
   }
 
-  private static HttpServletRequest createMockRequest(String servletPath,
+  private static FakeHttpServletRequest createFakeRequest(String servletPath,
       String pathInfo) {
-    HttpServletRequest req = createNiceMock(HttpServletRequest.class);
-    expect(req.getContextPath()).andStubReturn(CONTEXT);
-    expect(req.getServletPath()).andStubReturn(servletPath);
-    expect(req.getPathInfo()).andStubReturn(pathInfo);
-    String uri = CONTEXT + servletPath + pathInfo;
-    expect(req.getRequestURI()).andStubReturn(uri);
-    replay(req);
-
-    return req;
+    return new FakeHttpServletRequest(
+        "gerrit.example.com", 80, CONTEXT, servletPath).setPathInfo(pathInfo);
   }
 }
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
new file mode 100644
index 0000000..3eb3088
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/ResourceServletTest.java
@@ -0,0 +1,336 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+
+import org.joda.time.format.ISODateTimeFormat;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.zip.GZIPInputStream;
+
+public class ResourceServletTest {
+  private static Cache<Path, Resource> newCache(int size) {
+    return CacheBuilder.newBuilder()
+      .maximumSize(size)
+      .recordStats()
+      .build();
+  }
+
+  private static class Servlet extends ResourceServlet {
+    private static final long serialVersionUID = 1L;
+
+    private final FileSystem fs;
+
+    private Servlet(FileSystem fs, Cache<Path, Resource> cache,
+        boolean refresh) {
+      super(cache, refresh);
+      this.fs = fs;
+    }
+
+    private Servlet(FileSystem fs, Cache<Path, Resource> cache,
+        boolean refresh, int cacheFileSizeLimitBytes) {
+      super(cache, refresh, cacheFileSizeLimitBytes);
+      this.fs = fs;
+    }
+
+    @Override
+    protected Path getResourcePath(String pathInfo) {
+      return fs.getPath("/" + CharMatcher.is('/').trimLeadingFrom(pathInfo));
+    }
+  }
+
+  private FileSystem fs;
+  private AtomicLong ts;
+
+  @Before
+  public void setUp() {
+    fs = Jimfs.newFileSystem(Configuration.unix());
+    ts = new AtomicLong(ISODateTimeFormat.dateTime().parseMillis(
+        "2010-01-30T12:00:00.000-08:00"));
+  }
+
+  @Test
+  public void notFoundWithoutRefresh() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, false);
+
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(request("/notfound"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND);
+    assertNotCacheable(res);
+    assertCacheHits(cache, 0, 1);
+
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/notfound"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND);
+    assertNotCacheable(res);
+    assertCacheHits(cache, 1, 1);
+  }
+
+  @Test
+  public void notFoundWithRefresh() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, true);
+
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(request("/notfound"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND);
+    assertNotCacheable(res);
+    assertCacheHits(cache, 0, 1);
+
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/notfound"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND);
+    assertNotCacheable(res);
+    assertCacheHits(cache, 1, 1);
+  }
+
+  @Test
+  public void smallFileWithRefresh() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, true);
+
+    writeFile("/foo", "foo1");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertCacheable(res, true);
+    assertHasETag(res);
+    // Miss on getIfPresent, miss on get.
+    assertCacheHits(cache, 0, 2);
+
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertCacheable(res, true);
+    assertHasETag(res);
+    assertCacheHits(cache, 1, 2);
+
+    writeFile("/foo", "foo2");
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo2");
+    assertCacheable(res, true);
+    assertHasETag(res);
+    // Hit, invalidate, miss.
+    assertCacheHits(cache, 2, 3);
+  }
+
+  @Test
+  public void smallFileWithoutRefresh() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, false);
+
+    writeFile("/foo", "foo1");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertCacheable(res, false);
+    assertHasETag(res);
+    // Miss on getIfPresent, miss on get.
+    assertCacheHits(cache, 0, 2);
+
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertCacheable(res, false);
+    assertHasETag(res);
+    assertCacheHits(cache, 1, 2);
+
+    writeFile("/foo", "foo2");
+    res = new FakeHttpServletResponse();
+    servlet.doGet(request("/foo"), res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertCacheable(res, false);
+    assertHasETag(res);
+    assertCacheHits(cache, 2, 2);
+  }
+
+  @Test
+  public void verySmallFileDoesntBotherWithGzip() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, true);
+    writeFile("/foo", "foo1");
+
+    FakeHttpServletRequest req = request("/foo")
+        .addHeader("Accept-Encoding", "gzip");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(req, res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getHeader("Content-Encoding")).isNull();
+    assertThat(res.getActualBodyString()).isEqualTo("foo1");
+    assertHasETag(res);
+    assertCacheable(res, true);
+  }
+
+  @Test
+  public void smallFileWithGzip() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, true);
+    String content = Strings.repeat("a", 100);
+    writeFile("/foo", content);
+
+    FakeHttpServletRequest req = request("/foo")
+        .addHeader("Accept-Encoding", "gzip");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(req, res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getHeader("Content-Encoding")).isEqualTo("gzip");
+    assertThat(gunzip(res.getActualBody())).isEqualTo(content);
+    assertHasETag(res);
+    assertCacheable(res, true);
+  }
+
+  @Test
+  public void largeFileBypassesCacheRegardlessOfRefreshParamter()
+      throws Exception {
+    for (boolean refresh : Lists.newArrayList(true, false)) {
+      Cache<Path, Resource> cache = newCache(1);
+      Servlet servlet = new Servlet(fs, cache, refresh, 3);
+
+      writeFile("/foo", "foo1");
+      FakeHttpServletResponse res = new FakeHttpServletResponse();
+      servlet.doGet(request("/foo"), res);
+      assertThat(res.getStatus()).isEqualTo(SC_OK);
+      assertThat(res.getActualBodyString()).isEqualTo("foo1");
+      assertThat(res.getHeader("Last-Modified")).isNotNull();
+      assertCacheable(res, refresh);
+      assertHasLastModified(res);
+      assertCacheHits(cache, 0, 1);
+
+      writeFile("/foo", "foo1");
+      res = new FakeHttpServletResponse();
+      servlet.doGet(request("/foo"), res);
+      assertThat(res.getStatus()).isEqualTo(SC_OK);
+      assertThat(res.getActualBodyString()).isEqualTo("foo1");
+      assertThat(res.getHeader("Last-Modified")).isNotNull();
+      assertCacheable(res, refresh);
+      assertHasLastModified(res);
+      assertCacheHits(cache, 0, 2);
+
+      writeFile("/foo", "foo2");
+      res = new FakeHttpServletResponse();
+      servlet.doGet(request("/foo"), res);
+      assertThat(res.getStatus()).isEqualTo(SC_OK);
+      assertThat(res.getActualBodyString()).isEqualTo("foo2");
+      assertThat(res.getHeader("Last-Modified")).isNotNull();
+      assertCacheable(res, refresh);
+      assertHasLastModified(res);
+      assertCacheHits(cache, 0, 3);
+    }
+  }
+
+  @Test
+  public void largeFileWithGzip() throws Exception {
+    Cache<Path, Resource> cache = newCache(1);
+    Servlet servlet = new Servlet(fs, cache, true, 3);
+    String content = Strings.repeat("a", 100);
+    writeFile("/foo", content);
+
+    FakeHttpServletRequest req = request("/foo")
+        .addHeader("Accept-Encoding", "gzip");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.doGet(req, res);
+    assertThat(res.getStatus()).isEqualTo(SC_OK);
+    assertThat(res.getHeader("Content-Encoding")).isEqualTo("gzip");
+    assertThat(gunzip(res.getActualBody())).isEqualTo(content);
+    assertHasLastModified(res);
+    assertCacheable(res, true);
+  }
+
+  // TODO(dborowitz): Check MIME type.
+  // TODO(dborowitz): Test that JS is not gzipped.
+  // TODO(dborowitz): Test ?e parameter.
+  // TODO(dborowitz): Test If-None-Match behavior.
+  // TODO(dborowitz): Test If-Modified-Since behavior.
+
+  private void writeFile(String path, String content) throws Exception {
+    Files.write(fs.getPath(path), content.getBytes(UTF_8));
+    Files.setLastModifiedTime(
+        fs.getPath(path), FileTime.fromMillis(ts.getAndIncrement()));
+  }
+
+  private static void assertCacheHits(Cache<?, ?> cache, int hits, int misses) {
+    assertThat(cache.stats().hitCount()).named("hits").isEqualTo(hits);
+    assertThat(cache.stats().missCount()).named("misses").isEqualTo(misses);
+  }
+
+  private static void assertCacheable(FakeHttpServletResponse res,
+      boolean revalidate) {
+    String header = res.getHeader("Cache-Control").toLowerCase();
+    assertThat(header).contains("public");
+    if (revalidate) {
+      assertThat(header).contains("must-revalidate");
+    } else {
+      assertThat(header).doesNotContain("must-revalidate");
+    }
+  }
+
+  private static void assertHasLastModified(FakeHttpServletResponse res) {
+    assertThat(res.getHeader("Last-Modified")).isNotNull();
+    assertThat(res.getHeader("ETag")).isNull();
+  }
+
+  private static void assertHasETag(FakeHttpServletResponse res) {
+    assertThat(res.getHeader("ETag")).isNotNull();
+    assertThat(res.getHeader("Last-Modified")).isNull();
+  }
+
+  private static void assertNotCacheable(FakeHttpServletResponse res) {
+    assertThat(res.getHeader("Cache-Control")).contains("no-cache");
+    assertThat(res.getHeader("ETag")).isNull();
+    assertThat(res.getHeader("Last-Modified")).isNull();
+  }
+
+  private static FakeHttpServletRequest request(String path) {
+    return new FakeHttpServletRequest().setPathInfo(path);
+  }
+
+  private static String gunzip(byte[] data) throws Exception {
+    try (InputStream in = new GZIPInputStream(new ByteArrayInputStream(data))) {
+      return new String(ByteStreams.toByteArray(in), UTF_8);
+    }
+  }
+}
diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK
index 6281a1c..687e02f 100644
--- a/gerrit-launcher/BUCK
+++ b/gerrit-launcher/BUCK
@@ -5,6 +5,7 @@
   srcs = ['src/main/java/com/google/gerrit/launcher/GerritLauncher.java'],
   visibility = [
     '//gerrit-acceptance-tests/...',
+    '//gerrit-httpd:',
     '//gerrit-main:main_lib',
     '//gerrit-pgm:',
   ],
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 4ee9676..fb54bcf 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -27,10 +27,16 @@
 import java.lang.reflect.Modifier;
 import java.net.JarURLConnection;
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.CodeSource;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.SortedMap;
@@ -46,11 +52,11 @@
   private static final String pkg = "com.google.gerrit.pgm";
   public static final String NOT_ARCHIVED = "NOT_ARCHIVED";
 
-  public static void main(final String argv[]) throws Exception {
+  public static void main(final String[] argv) throws Exception {
     System.exit(mainImpl(argv));
   }
 
-  public static int mainImpl(final String argv[]) throws Exception {
+  public static int mainImpl(final String[] argv) throws Exception {
     if (argv.length == 0) {
       File me;
       try {
@@ -64,14 +70,15 @@
       System.err.println("usage: java -jar " + jar + " command [ARG ...]");
       System.err.println();
       System.err.println("The most commonly used commands are:");
-      System.err.println("  init           Initialize a Gerrit installation");
-      System.err.println("  reindex        Rebuild the secondary index");
-      System.err.println("  daemon         Run the Gerrit network daemons");
-      System.err.println("  gsql           Run the interactive query console");
-      System.err.println("  version        Display the build version number");
+      System.err.println("  init            Initialize a Gerrit installation");
+      System.err.println("  rebuild-notedb  Rebuild the review notes database");
+      System.err.println("  reindex         Rebuild the secondary index");
+      System.err.println("  daemon          Run the Gerrit network daemons");
+      System.err.println("  gsql            Run the interactive query console");
+      System.err.println("  version         Display the build version number");
       System.err.println();
-      System.err.println("  ls             List files available for cat");
-      System.err.println("  cat FILE       Display a file from the archive");
+      System.err.println("  ls              List files available for cat");
+      System.err.println("  cat FILE        Display a file from the archive");
       System.err.println();
       return 1;
     }
@@ -88,26 +95,25 @@
 
     // Run the application class
     //
-    final ClassLoader cl = libClassLoader();
+    final ClassLoader cl = libClassLoader(isProlog(programClassName(argv[0])));
     Thread.currentThread().setContextClassLoader(cl);
     return invokeProgram(cl, argv);
   }
 
+  private static boolean isProlog(String cn) {
+    return "PrologShell".equals(cn) || "Rulec".equals(cn);
+  }
+
   private static String getVersion(final File me) {
     if (me == null) {
       return "";
     }
 
-    try {
-      final JarFile jar = new JarFile(me);
-      try {
-        Manifest mf = jar.getManifest();
-        Attributes att = mf.getMainAttributes();
-        String val = att.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
-        return val != null ? val : "";
-      } finally {
-        jar.close();
-      }
+    try (JarFile jar = new JarFile(me)) {
+      Manifest mf = jar.getManifest();
+      Attributes att = mf.getMainAttributes();
+      String val = att.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+      return val != null ? val : "";
     } catch (IOException e) {
       return "";
     }
@@ -122,20 +128,7 @@
     Class<?> clazz;
     try {
       try {
-        String cn = name;
-        if (cn.equals(cn.toLowerCase())) {
-          StringBuilder buf = new StringBuilder();
-          buf.append(Character.toUpperCase(cn.charAt(0)));
-          for (int i = 1; i < cn.length(); i++) {
-            if (cn.charAt(i) == '-' && i + 1 < cn.length()) {
-              i++;
-              buf.append(Character.toUpperCase(cn.charAt(i)));
-            } else {
-              buf.append(cn.charAt(i));
-            }
-          }
-          cn = buf.toString();
-        }
+        String cn = programClassName(name);
         clazz = Class.forName(pkg + "." + cn, true, loader);
       } catch (ClassNotFoundException cnfe) {
         if (name.equals(name.toLowerCase())) {
@@ -181,7 +174,25 @@
     }
   }
 
-  private static ClassLoader libClassLoader() throws IOException {
+  private static String programClassName(String cn) {
+    if (cn.equals(cn.toLowerCase())) {
+      StringBuilder buf = new StringBuilder();
+      buf.append(Character.toUpperCase(cn.charAt(0)));
+      for (int i = 1; i < cn.length(); i++) {
+        if (cn.charAt(i) == '-' && i + 1 < cn.length()) {
+          i++;
+          buf.append(Character.toUpperCase(cn.charAt(i)));
+        } else {
+          buf.append(cn.charAt(i));
+        }
+      }
+      return buf.toString();
+    }
+    return cn;
+  }
+
+  private static ClassLoader libClassLoader(boolean prologCompiler)
+      throws IOException {
     final File path;
     try {
       path = getDistributionArchive();
@@ -193,22 +204,23 @@
     }
 
     final SortedMap<String, URL> jars = new TreeMap<>();
-    try {
-      final ZipFile zf = new ZipFile(path);
-      try {
-        final Enumeration<? extends ZipEntry> e = zf.entries();
-        while (e.hasMoreElements()) {
-          final ZipEntry ze = e.nextElement();
-          if (ze.isDirectory()) {
-            continue;
-          } else if (ze.getName().startsWith("WEB-INF/lib/")) {
-            extractJar(zf, ze, jars);
-          } else if (ze.getName().startsWith("WEB-INF/pgm-lib/")) {
+    try (ZipFile zf = new ZipFile(path)) {
+      final Enumeration<? extends ZipEntry> e = zf.entries();
+      while (e.hasMoreElements()) {
+        final ZipEntry ze = e.nextElement();
+        if (ze.isDirectory()) {
+          continue;
+        }
+
+        String name = ze.getName();
+        if (name.startsWith("WEB-INF/lib/")) {
+          extractJar(zf, ze, jars);
+        } else if (name.startsWith("WEB-INF/pgm-lib/")) {
+          // Some Prolog tools are restricted.
+          if (prologCompiler || !name.startsWith("WEB-INF/pgm-lib/prolog-")) {
             extractJar(zf, ze, jars);
           }
         }
-      } finally {
-        zf.close();
       }
     } catch (IOException e) {
       throw new IOException("Cannot obtain libraries from " + path, e);
@@ -242,20 +254,13 @@
   private static void extractJar(ZipFile zf, ZipEntry ze,
       SortedMap<String, URL> jars) throws IOException {
     File tmp = createTempFile(safeName(ze), ".jar");
-    FileOutputStream out = new FileOutputStream(tmp);
-    try {
-      InputStream in = zf.getInputStream(ze);
-      try {
-        byte[] buf = new byte[4096];
-        int n;
-        while ((n = in.read(buf, 0, buf.length)) > 0) {
-          out.write(buf, 0, n);
-        }
-      } finally {
-        in.close();
+    try (FileOutputStream out = new FileOutputStream(tmp);
+        InputStream in = zf.getInputStream(ze)) {
+      byte[] buf = new byte[4096];
+      int n;
+      while ((n = in.read(buf, 0, buf.length)) > 0) {
+        out.write(buf, 0, n);
       }
-    } finally {
-      out.close();
     }
 
     String name = ze.getName();
@@ -295,6 +300,7 @@
   }
 
   private static volatile File myArchive;
+  private static volatile FileSystem myArchiveFs;
   private static volatile File myHome;
 
   /**
@@ -303,11 +309,29 @@
    * @return local path of the Gerrit WAR file.
    * @throws FileNotFoundException if the code cannot guess the location.
    */
-  public static File getDistributionArchive() throws FileNotFoundException {
-    if (myArchive == null) {
-      myArchive = locateMyArchive();
+  public static File getDistributionArchive()
+      throws FileNotFoundException, IOException {
+    File result = myArchive;
+    if (result == null) {
+      synchronized (GerritLauncher.class) {
+        result = myArchive;
+        if (result != null) {
+          return result;
+        }
+        result = locateMyArchive();
+        myArchiveFs = FileSystems.newFileSystem(
+            URI.create("jar:" + result.toPath().toUri()),
+            Collections.<String, String> emptyMap());
+        myArchive = result;
+      }
     }
-    return myArchive;
+    return result;
+  }
+
+  public static FileSystem getDistributionArchiveFileSystem()
+      throws FileNotFoundException, IOException {
+    getDistributionArchive();
+    return myArchiveFs;
   }
 
   private static File locateMyArchive() throws FileNotFoundException {
@@ -348,24 +372,16 @@
     final CodeSource src =
         GerritLauncher.class.getProtectionDomain().getCodeSource();
     if (src != null) {
-      try {
-        final InputStream in = src.getLocation().openStream();
-        try {
-          final File tmp = createTempFile("gerrit_", ".zip");
-          final FileOutputStream out = new FileOutputStream(tmp);
-          try {
-            final byte[] buf = new byte[4096];
-            int n;
-            while ((n = in.read(buf, 0, buf.length)) > 0) {
-              out.write(buf, 0, n);
-            }
-          } finally {
-            out.close();
+      try (InputStream in = src.getLocation().openStream()) {
+        final File tmp = createTempFile("gerrit_", ".zip");
+        try (FileOutputStream out = new FileOutputStream(tmp)) {
+          final byte[] buf = new byte[4096];
+          int n;
+          while ((n = in.read(buf, 0, buf.length)) > 0) {
+            out.write(buf, 0, n);
           }
-          return tmp;
-        } finally {
-          in.close();
         }
+        return tmp;
       } catch (IOException e) {
         // Nope, that didn't work.
         //
@@ -550,7 +566,7 @@
    *
    * @throws FileNotFoundException if the directory cannot be found.
    */
-  public static File getDeveloperBuckOut() throws FileNotFoundException {
+  public static Path getDeveloperBuckOut() throws FileNotFoundException {
     // Find ourselves in the CLASSPATH, we should be a loose class file.
     Class<GerritLauncher> self = GerritLauncher.class;
     URL u = self.getResource(self.getSimpleName() + ".class");
@@ -561,39 +577,43 @@
     }
 
     // Pop up to the top level classes folder that contains us.
-    File dir = new File(u.getPath());
+    Path dir = Paths.get(u.getPath());
     String myName = self.getName();
     for (;;) {
       int dot = myName.lastIndexOf('.');
       if (dot < 0) {
-        dir = dir.getParentFile();
+        dir = dir.getParent();
         break;
       }
       myName = myName.substring(0, dot);
-      dir = dir.getParentFile();
+      dir = dir.getParent();
     }
 
     dir = popdir(u, dir, "classes");
     dir = popdir(u, dir, "eclipse");
-    if ("buck-out".equals(dir.getName())) {
+    if (last(dir).equals("buck-out")) {
       return dir;
     }
     throw new FileNotFoundException("Cannot find buck-out from " + u);
   }
 
-  private static File popdir(URL u, File dir, String name)
+  private static String last(Path dir) {
+    return dir.getName(dir.getNameCount() - 1).toString();
+  }
+
+  private static Path popdir(URL u, Path dir, String name)
       throws FileNotFoundException {
-    if (dir.getName().equals(name)) {
-      return dir.getParentFile();
+    if (last(dir).equals(name)) {
+      return dir.getParent();
     }
     throw new FileNotFoundException("Cannot find buck-out from " + u);
   }
 
   private static ClassLoader useDevClasspath()
       throws MalformedURLException, FileNotFoundException {
-    File out = getDeveloperBuckOut();
+    Path out = getDeveloperBuckOut();
     List<URL> dirs = new ArrayList<>();
-    dirs.add(new File(new File(out, "eclipse"), "classes").toURI().toURL());
+    dirs.add(out.resolve("eclipse").resolve("classes").toUri().toURL());
     ClassLoader cl = GerritLauncher.class.getClassLoader();
     for (URL u : ((URLClassLoader) cl).getURLs()) {
       if (includeJar(u)) {
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 2b45d2b..8ba7479 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -11,7 +11,7 @@
     '//gerrit-server:server',
     '//lib:gwtorm',
     '//lib:guava',
-    '//lib/lucene:core',
+    '//lib/lucene:core-and-backward-codecs',
   ],
   visibility = ['PUBLIC'],
 )
@@ -34,7 +34,8 @@
     '//lib/jgit:jgit',
     '//lib/log:api',
     '//lib/lucene:analyzers-common',
-    '//lib/lucene:core',
+    '//lib/lucene:core-and-backward-codecs',
+    '//lib/lucene:misc',
   ],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
index e0c13ae..4e47bca 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.lucene;
 
-import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
@@ -26,13 +25,28 @@
 import java.io.IOException;
 
 /** Writer that optionally flushes/commits after every write. */
-class AutoCommitWriter extends IndexWriter {
+public class AutoCommitWriter extends IndexWriter {
   private boolean autoCommit;
 
+  AutoCommitWriter(Directory dir, IndexWriterConfig config)
+      throws IOException {
+    this(dir, config, false);
+  }
+
   AutoCommitWriter(Directory dir, IndexWriterConfig config, boolean autoCommit)
       throws IOException {
     super(dir, config);
-    this.autoCommit = autoCommit;
+    setAutoCommit(autoCommit);
+  }
+
+  /**
+   * This method will override Gerrit configuration index.name.commitWithin
+   * until next Gerrit restart (or reconfiguration through this method).
+   *
+   * @param enable auto commit
+   */
+  public void setAutoCommit(boolean enable) {
+    this.autoCommit = enable;
   }
 
   @Override
@@ -43,13 +57,6 @@
   }
 
   @Override
-  public void addDocument(Iterable<? extends IndexableField> doc,
-      Analyzer analyzer) throws IOException {
-    super.addDocument(doc, analyzer);
-    autoFlush();
-  }
-
-  @Override
   public void addDocuments(
       Iterable<? extends Iterable<? extends IndexableField>> docs)
       throws IOException {
@@ -58,14 +65,6 @@
   }
 
   @Override
-  public void addDocuments(
-      Iterable<? extends Iterable<? extends IndexableField>> docs,
-      Analyzer analyzer) throws IOException {
-    super.addDocuments(docs, analyzer);
-    autoFlush();
-  }
-
-  @Override
   public void updateDocuments(Term delTerm,
       Iterable<? extends Iterable<? extends IndexableField>> docs)
       throws IOException {
@@ -74,14 +73,6 @@
   }
 
   @Override
-  public void updateDocuments(Term delTerm,
-      Iterable<? extends Iterable<? extends IndexableField>> docs,
-      Analyzer analyzer) throws IOException {
-    super.updateDocuments(delTerm, docs, analyzer);
-    autoFlush();
-  }
-
-  @Override
   public void deleteDocuments(Term... term) throws IOException {
     super.deleteDocuments(term);
     autoFlush();
@@ -111,13 +102,6 @@
   }
 
   @Override
-  public void updateDocument(Term term, Iterable<? extends IndexableField> doc,
-      Analyzer analyzer) throws IOException {
-    super.updateDocument(term, doc, analyzer);
-    autoFlush();
-  }
-
-  @Override
   public void deleteAll() throws IOException {
     super.deleteAll();
     autoFlush();
@@ -130,7 +114,7 @@
     }
   }
 
-  private void autoFlush() throws IOException {
+  public void autoFlush() throws IOException {
     if (autoCommit) {
       manualFlush();
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index e8825c5..6af320f 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.lucene;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
-import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
-import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriter.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriter.OPEN_STATUSES;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
@@ -29,8 +32,9 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -38,20 +42,24 @@
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.ChangeField.ChangeProtoField;
 import com.google.gerrit.server.index.ChangeField.PatchSetApprovalProtoField;
+import com.google.gerrit.server.index.ChangeField.PatchSetProtoField;
 import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.IndexRewriter;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.Schema.Values;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
+import com.google.gerrit.server.query.change.QueryOptions;
+import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -64,9 +72,12 @@
 import org.apache.lucene.document.Field.Store;
 import org.apache.lucene.document.IntField;
 import org.apache.lucene.document.LongField;
+import org.apache.lucene.document.NumericDocValuesField;
 import org.apache.lucene.document.StoredField;
 import org.apache.lucene.document.StringField;
 import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
@@ -76,28 +87,34 @@
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.SearcherFactory;
 import org.apache.lucene.search.SearcherManager;
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TopFieldDocs;
 import org.apache.lucene.store.RAMDirectory;
+import org.apache.lucene.uninverting.UninvertingReader;
 import org.apache.lucene.util.BytesRef;
-import org.apache.lucene.util.Version;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 
 /**
  * Secondary index implementation using Apache Lucene.
@@ -118,55 +135,21 @@
   private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName();
   private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
   private static final String DELETED_FIELD = ChangeField.DELETED.getName();
-  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+  private static final String ID_FIELD = ChangeField.LEGACY_ID2.getName();
   private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
+  private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
+  private static final String REVIEWEDBY_FIELD =
+      ChangeField.REVIEWEDBY.getName();
+  private static final String UPDATED_SORT_FIELD =
+      sortFieldName(ChangeField.UPDATED);
+
   private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
       ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD,
-      MERGEABLE_FIELD);
+      MERGEABLE_FIELD, PATCH_SET_FIELD, REVIEWEDBY_FIELD);
+
   private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
       "_", " ", ".", " ");
 
-  private static final Map<Schema<ChangeData>, Version> LUCENE_VERSIONS;
-  static {
-    ImmutableMap.Builder<Schema<ChangeData>, Version> versions =
-        ImmutableMap.builder();
-    @SuppressWarnings("deprecation")
-    Version lucene43 = Version.LUCENE_43;
-    @SuppressWarnings("deprecation")
-    Version lucene44 = Version.LUCENE_44;
-    @SuppressWarnings("deprecation")
-    Version lucene46 = Version.LUCENE_46;
-    @SuppressWarnings("deprecation")
-    Version lucene47 = Version.LUCENE_47;
-    @SuppressWarnings("deprecation")
-    Version lucene48 = Version.LUCENE_48;
-    @SuppressWarnings("deprecation")
-    Version lucene410 = Version.LUCENE_4_10_0;
-    // We are using 4.10.2 but there is no difference in the index
-    // format since 4.10.1, so we reuse the version here.
-    @SuppressWarnings("deprecation")
-    Version lucene4101 = Version.LUCENE_4_10_1;
-    for (Map.Entry<Integer, Schema<ChangeData>> e
-        : ChangeSchemas.ALL.entrySet()) {
-      if (e.getKey() <= 3) {
-        versions.put(e.getValue(), lucene43);
-      } else if (e.getKey() <= 5) {
-        versions.put(e.getValue(), lucene44);
-      } else if (e.getKey() <= 8) {
-        versions.put(e.getValue(), lucene46);
-      } else if (e.getKey() <= 10) {
-        versions.put(e.getValue(), lucene47);
-      } else if (e.getKey() <= 11) {
-        versions.put(e.getValue(), lucene48);
-      } else if (e.getKey() <= 13) {
-        versions.put(e.getValue(), lucene410);
-      } else {
-        versions.put(e.getValue(), lucene4101);
-      }
-    }
-    LUCENE_VERSIONS = versions.build();
-  }
-
   public static void setReady(SitePaths sitePaths, int version, boolean ready)
       throws IOException {
     try {
@@ -179,6 +162,10 @@
     }
   }
 
+  private static String sortFieldName(FieldDef<?, ?> f) {
+    return f.getName() + "_SORT";
+  }
+
   static interface Factory {
     LuceneChangeIndex create(Schema<ChangeData> schema, String base);
   }
@@ -187,12 +174,13 @@
     private final IndexWriterConfig luceneConfig;
     private long commitWithinMs;
 
-    private GerritIndexWriterConfig(Version version, Config cfg, String name) {
+    private GerritIndexWriterConfig(Config cfg, String name) {
       CustomMappingAnalyzer analyzer =
           new CustomMappingAnalyzer(new StandardAnalyzer(
               CharArraySet.EMPTY_SET), CUSTOM_CHAR_MAPPING);
-      luceneConfig = new IndexWriterConfig(version, analyzer);
-      luceneConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
+      luceneConfig = new IndexWriterConfig(analyzer)
+          .setOpenMode(OpenMode.CREATE_OR_APPEND)
+          .setCommitOnClose(true);
       double m = 1 << 20;
       luceneConfig.setRAMBufferSizeMB(cfg.getLong(
           "index", name, "ramBufferSize",
@@ -223,11 +211,22 @@
   private final ListeningExecutorService executor;
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
-  private final File dir;
   private final Schema<ChangeData> schema;
   private final QueryBuilder queryBuilder;
   private final SubIndex openIndex;
   private final SubIndex closedIndex;
+  private final String idSortField;
+
+  /**
+   * Whether to use DocValues for range/sorted numeric fields.
+   * <p>
+   * Lucene 5 removed support for sorting based on normal numeric fields, so we
+   * use the newer API for more strongly typed numeric fields in newer schema
+   * versions. These fields also are not stored, so we need to store auxiliary
+   * stored-only field for them as well.
+   */
+  // TODO(dborowitz): Delete when we delete support for pre-Lucene-5.0 schemas.
+  private final boolean useDocValuesForSorting;
 
   @AssistedInject
   LuceneChangeIndex(
@@ -245,37 +244,58 @@
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.schema = schema;
+    this.useDocValuesForSorting = schema.getVersion() >= 15;
+    this.idSortField = sortFieldName(LegacyChangeIdPredicate.idField(schema));
 
-    if (base == null) {
-      dir = LuceneVersionManager.getDir(sitePaths, schema);
-    } else {
-      dir = new File(base);
-    }
-    Version luceneVersion = checkNotNull(
-        LUCENE_VERSIONS.get(schema),
-        "unknown Lucene version for index schema: %s", schema);
     CustomMappingAnalyzer analyzer =
         new CustomMappingAnalyzer(new StandardAnalyzer(CharArraySet.EMPTY_SET),
             CUSTOM_CHAR_MAPPING);
     queryBuilder = new QueryBuilder(analyzer);
 
-    BooleanQuery.setMaxClauseCount(cfg.getInt("index", "defaultMaxClauseCount",
+    BooleanQuery.setMaxClauseCount(cfg.getInt("index", "maxTerms",
         BooleanQuery.getMaxClauseCount()));
 
     GerritIndexWriterConfig openConfig =
-        new GerritIndexWriterConfig(luceneVersion, cfg, "changes_open");
+        new GerritIndexWriterConfig(cfg, "changes_open");
     GerritIndexWriterConfig closedConfig =
-        new GerritIndexWriterConfig(luceneVersion, cfg, "changes_closed");
+        new GerritIndexWriterConfig(cfg, "changes_closed");
 
+    SearcherFactory searcherFactory = newSearcherFactory();
     if (cfg.getBoolean("index", "lucene", "testInmemory", false)) {
-      openIndex = new SubIndex(new RAMDirectory(), "ramOpen", openConfig);
-      closedIndex = new SubIndex(new RAMDirectory(), "ramClosed", closedConfig);
+      openIndex = new SubIndex(new RAMDirectory(), "ramOpen", openConfig,
+          searcherFactory);
+      closedIndex = new SubIndex(new RAMDirectory(), "ramClosed", closedConfig,
+          searcherFactory);
     } else {
-      openIndex = new SubIndex(new File(dir, CHANGES_OPEN), openConfig);
-      closedIndex = new SubIndex(new File(dir, CHANGES_CLOSED), closedConfig);
+      Path dir = base != null ? Paths.get(base)
+          : LuceneVersionManager.getDir(sitePaths, schema);
+      openIndex = new SubIndex(dir.resolve(CHANGES_OPEN), openConfig,
+          searcherFactory);
+      closedIndex = new SubIndex(dir.resolve(CHANGES_CLOSED), closedConfig,
+          searcherFactory);
     }
   }
 
+  private SearcherFactory newSearcherFactory() {
+    if (useDocValuesForSorting) {
+      return new SearcherFactory();
+    }
+    @SuppressWarnings("deprecation")
+    final Map<String, UninvertingReader.Type> mapping = ImmutableMap.of(
+        ChangeField.LEGACY_ID.getName(), UninvertingReader.Type.INTEGER,
+        ChangeField.UPDATED.getName(), UninvertingReader.Type.LONG);
+    return new SearcherFactory() {
+      @Override
+      public IndexSearcher newSearcher(IndexReader reader, IndexReader previousReader)
+          throws IOException {
+        checkState(reader instanceof DirectoryReader,
+            "expected DirectoryReader, found %s", reader.getClass().getName());
+        return new IndexSearcher(
+            UninvertingReader.wrap((DirectoryReader) reader, mapping));
+      }
+    };
+  }
+
   @Override
   public void close() {
     List<ListenableFuture<?>> closeFutures = Lists.newArrayListWithCapacity(2);
@@ -299,10 +319,9 @@
     return schema;
   }
 
-  @SuppressWarnings("unchecked")
   @Override
   public void replace(ChangeData cd) throws IOException {
-    Term id = QueryBuilder.idTerm(cd);
+    Term id = QueryBuilder.idTerm(schema, cd);
     Document doc = toDocument(cd);
     try {
       if (cd.change().getStatus().isOpen()) {
@@ -319,10 +338,9 @@
     }
   }
 
-  @SuppressWarnings("unchecked")
   @Override
   public void delete(Change.Id id) throws IOException {
-    Term idTerm = QueryBuilder.idTerm(id);
+    Term idTerm = QueryBuilder.idTerm(schema, id);
     try {
       Futures.allAsList(
           openIndex.delete(idTerm),
@@ -339,9 +357,9 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException {
-    Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException {
+    Set<Change.Status> statuses = IndexRewriter.getPossibleStatus(p);
     List<SubIndex> indexes = Lists.newArrayListWithCapacity(2);
     if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
       indexes.add(openIndex);
@@ -349,8 +367,7 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
-        getSort());
+    return new QuerySource(indexes, p, opts, getSort());
   }
 
   @Override
@@ -358,27 +375,44 @@
     setReady(sitePaths, schema.getVersion(), ready);
   }
 
-  private static Sort getSort() {
-    return new Sort(
-        new SortField(
-          ChangeField.UPDATED.getName(), SortField.Type.LONG, true),
-        new SortField(
-          ChangeField.LEGACY_ID.getName(), SortField.Type.INT, true));
+  @SuppressWarnings("deprecation")
+  private Sort getSort() {
+    if (useDocValuesForSorting) {
+      return new Sort(
+          new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
+          new SortField(idSortField, SortField.Type.LONG, true));
+    } else {
+      return new Sort(
+          new SortField(
+            ChangeField.UPDATED.getName(), SortField.Type.LONG, true),
+          new SortField(
+            ChangeField.LEGACY_ID.getName(), SortField.Type.INT, true));
+    }
+  }
+
+  public SubIndex getOpenChangesIndex() {
+    return openIndex;
+  }
+
+  public SubIndex getClosedChangesIndex() {
+    return closedIndex;
   }
 
   private class QuerySource implements ChangeDataSource {
     private final List<SubIndex> indexes;
+    private final Predicate<ChangeData> predicate;
     private final Query query;
-    private final int start;
-    private final int limit;
+    private final QueryOptions opts;
     private final Sort sort;
 
-    private QuerySource(List<SubIndex> indexes, Query query, int start,
-        int limit, Sort sort) {
+
+    private QuerySource(List<SubIndex> indexes, Predicate<ChangeData> predicate,
+        QueryOptions opts, Sort sort) throws QueryParseException {
       this.indexes = indexes;
-      this.query = query;
-      this.start = start;
-      this.limit = limit;
+      this.predicate = predicate;
+      this.query = checkNotNull(queryBuilder.toQuery(predicate),
+          "null query from Lucene");
+      this.opts = opts;
       this.sort = sort;
     }
 
@@ -394,46 +428,51 @@
 
     @Override
     public String toString() {
-      return query.toString();
+      return predicate.toString();
     }
 
     @Override
     public ResultSet<ChangeData> read() throws OrmException {
+      if (Thread.interrupted()) {
+        Thread.currentThread().interrupt();
+        throw new OrmException("interupted");
+      }
+      return new ChangeDataResults(
+          executor.submit(new Callable<List<Document>>() {
+            @Override
+            public List<Document> call() throws OrmException {
+              return doRead();
+            }
+
+            @Override
+            public String toString() {
+              return predicate.toString();
+            }
+          }));
+    }
+
+    private List<Document> doRead() throws OrmException {
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
       try {
-        int realLimit = start + limit;
-        TopDocs[] hits = new TopDocs[indexes.size()];
+        int realLimit = opts.start() + opts.limit();
+        if (Integer.MAX_VALUE - opts.limit() < opts.start()) {
+          realLimit = Integer.MAX_VALUE;
+        }
+        TopFieldDocs[] hits = new TopFieldDocs[indexes.size()];
         for (int i = 0; i < indexes.size(); i++) {
           searchers[i] = indexes.get(i).acquire();
           hits[i] = searchers[i].search(query, realLimit, sort);
         }
         TopDocs docs = TopDocs.merge(sort, realLimit, hits);
 
-        List<ChangeData> result =
+        List<Document> result =
             Lists.newArrayListWithCapacity(docs.scoreDocs.length);
-        for (int i = start; i < docs.scoreDocs.length; i++) {
+        for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
           ScoreDoc sd = docs.scoreDocs[i];
           Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
-          result.add(toChangeData(doc));
+          result.add(doc);
         }
-
-        final List<ChangeData> r = Collections.unmodifiableList(result);
-        return new ResultSet<ChangeData>() {
-          @Override
-          public Iterator<ChangeData> iterator() {
-            return r.iterator();
-          }
-
-          @Override
-          public List<ChangeData> toList() {
-            return r;
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        };
+        return result;
       } catch (IOException e) {
         throw new OrmException(e);
       } finally {
@@ -450,6 +489,41 @@
     }
   }
 
+  private class ChangeDataResults implements ResultSet<ChangeData> {
+    private final Future<List<Document>> future;
+
+    ChangeDataResults(Future<List<Document>> future) {
+      this.future = future;
+    }
+
+    @Override
+    public Iterator<ChangeData> iterator() {
+      return toList().iterator();
+    }
+
+    @Override
+    public List<ChangeData> toList() {
+      try {
+        List<Document> docs = future.get();
+        List<ChangeData> result = new ArrayList<>(docs.size());
+        for (Document doc : docs) {
+          result.add(toChangeData(doc));
+        }
+        return result;
+      } catch (InterruptedException e) {
+        close();
+        throw new OrmRuntimeException(e);
+      } catch (ExecutionException e) {
+        Throwables.propagateIfPossible(e.getCause());
+        throw new OrmRuntimeException(e.getCause());
+      }
+    }
+
+    @Override
+    public void close() {
+      future.cancel(false /* do not interrupt Lucene */);
+    }
+  }
   private ChangeData toChangeData(Document doc) {
     BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
     if (cb == null) {
@@ -462,18 +536,19 @@
         cb.bytes, cb.offset, cb.length);
     ChangeData cd = changeDataFactory.create(db.get(), change);
 
-    // Approvals.
-    BytesRef[] approvalsBytes = doc.getBinaryValues(APPROVAL_FIELD);
-    if (approvalsBytes != null) {
-      List<PatchSetApproval> approvals =
-          Lists.newArrayListWithCapacity(approvalsBytes.length);
-      for (BytesRef ab : approvalsBytes) {
-        approvals.add(PatchSetApprovalProtoField.CODEC.decode(
-            ab.bytes, ab.offset, ab.length));
-      }
-      cd.setCurrentApprovals(approvals);
+    // Patch sets.
+    List<PatchSet> patchSets =
+        decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoField.CODEC);
+    if (!patchSets.isEmpty()) {
+      // Will be an empty list for schemas prior to when this field was stored;
+      // this cannot be valid since a change needs at least one patch set.
+      cd.setPatchSets(patchSets);
     }
 
+    // Approvals.
+    cd.setCurrentApprovals(
+        decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoField.CODEC));
+
     // Changed lines.
     IndexableField added = doc.getField(ADDED_FIELD);
     IndexableField deleted = doc.getField(DELETED_FIELD);
@@ -491,9 +566,37 @@
       cd.setMergeable(false);
     }
 
+    // Reviewed-by.
+    IndexableField[] reviewedBy = doc.getFields(REVIEWEDBY_FIELD);
+    if (reviewedBy.length > 0) {
+      Set<Account.Id> accounts =
+          Sets.newHashSetWithExpectedSize(reviewedBy.length);
+      for (IndexableField r : reviewedBy) {
+        int id = r.numericValue().intValue();
+        if (reviewedBy.length == 1 && id == ChangeField.NOT_REVIEWED) {
+          break;
+        }
+        accounts.add(new Account.Id(id));
+      }
+      cd.setReviewedBy(accounts);
+    }
+
     return cd;
   }
 
+  private static <T> List<T> decodeProtos(Document doc, String fieldName,
+      ProtobufCodec<T> codec) {
+    BytesRef[] bytesRefs = doc.getBinaryValues(fieldName);
+    if (bytesRefs.length == 0) {
+      return Collections.emptyList();
+    }
+    List<T> result = new ArrayList<>(bytesRefs.length);
+    for (BytesRef r : bytesRefs) {
+      result.add(codec.decode(r.bytes, r.offset, r.length));
+    }
+    return result;
+  }
+
   private Document toDocument(ChangeData cd) {
     Document result = new Document();
     for (Values<ChangeData> vs : schema.buildFields(cd, fillArgs)) {
@@ -504,11 +607,23 @@
     return result;
   }
 
+  @SuppressWarnings("deprecation")
   private void add(Document doc, Values<ChangeData> values) {
     String name = values.getField().getName();
     FieldType<?> type = values.getField().getType();
     Store store = store(values.getField());
 
+    if (useDocValuesForSorting) {
+      FieldDef<ChangeData, ?> f = values.getField();
+      if (f == ChangeField.LEGACY_ID || f == ChangeField.LEGACY_ID2) {
+        int v = (Integer) getOnlyElement(values.getValues());
+        doc.add(new NumericDocValuesField(sortFieldName(f), v));
+      } else if (f == ChangeField.UPDATED) {
+        long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
+        doc.add(new NumericDocValuesField(UPDATED_SORT_FIELD, t));
+      }
+    }
+
     if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
       for (Object value : values.getValues()) {
         doc.add(new IntField(name, (Integer) value, store));
@@ -535,7 +650,7 @@
         doc.add(new StoredField(name, (byte[]) value));
       }
     } else {
-      throw QueryBuilder.badFieldType(type);
+      throw FieldType.badFieldType(type);
     }
   }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 3b56fc0..5af7cc5 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
@@ -26,6 +27,8 @@
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
 public class LuceneIndexModule extends LifecycleModule {
   private final Integer singleVersion;
   private final int threads;
@@ -44,7 +47,6 @@
 
   @Override
   protected void configure() {
-    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     factory(LuceneChangeIndex.Factory.class);
     factory(OnlineReindexer.Factory.class);
     install(new IndexModule(threads));
@@ -55,7 +57,13 @@
     }
   }
 
-  private class MultiVersionModule extends LifecycleModule {
+  @Provides
+  @Singleton
+  IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+    return IndexConfig.fromConfig(cfg);
+  }
+
+  private static class MultiVersionModule extends LifecycleModule {
     @Override
     public void configure() {
       listener().to(LuceneVersionManager.class);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 381e4ef..407f5a8 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexCollection;
@@ -36,8 +37,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
 import java.util.TreeMap;
@@ -65,15 +68,16 @@
     }
   }
 
-  static File getDir(SitePaths sitePaths, Schema<ChangeData> schema) {
-    return new File(sitePaths.index_dir, String.format("%s%04d",
+  static Path getDir(SitePaths sitePaths, Schema<ChangeData> schema) {
+    return sitePaths.index_dir.resolve(String.format("%s%04d",
         CHANGES_PREFIX, schema.getVersion()));
   }
 
   static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths)
       throws ConfigInvalidException, IOException {
     FileBasedConfig cfg = new FileBasedConfig(
-        new File(sitePaths.index_dir, "gerrit_index.config"), FS.detect());
+        sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
+        FS.detect());
     cfg.load();
     return cfg;
   }
@@ -90,10 +94,12 @@
   private final LuceneChangeIndex.Factory indexFactory;
   private final IndexCollection indexes;
   private final OnlineReindexer.Factory reindexerFactory;
+  private final boolean onlineUpgrade;
   private OnlineReindexer reindexer;
 
   @Inject
   LuceneVersionManager(
+      @GerritServerConfig Config cfg,
       SitePaths sitePaths,
       LuceneChangeIndex.Factory indexFactory,
       IndexCollection indexes,
@@ -102,6 +108,7 @@
     this.indexFactory = indexFactory;
     this.indexes = indexes;
     this.reindexerFactory = reindexerFactory;
+    this.onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
   }
 
   @Override
@@ -109,16 +116,14 @@
     FileBasedConfig cfg;
     try {
       cfg = loadGerritIndexConfig(sitePaths);
-    } catch (ConfigInvalidException e) {
-      throw fail(e);
-    } catch (IOException e) {
+    } catch (ConfigInvalidException | IOException e) {
       throw fail(e);
     }
 
-    if (!sitePaths.index_dir.exists()) {
+    if (!Files.exists(sitePaths.index_dir)) {
       throw new ProvisionException("No index versions ready; run Reindex");
-    } else if (!sitePaths.index_dir.isDirectory()) {
-      log.warn("Not a directory: %s", sitePaths.index_dir.getAbsolutePath());
+    } else if (!Files.exists(sitePaths.index_dir)) {
+      log.warn("Not a directory: %s", sitePaths.index_dir.toAbsolutePath());
       throw new ProvisionException("No index versions ready; run Reindex");
     }
 
@@ -131,7 +136,7 @@
       if (v.schema == null) {
         continue;
       }
-      if (write.isEmpty()) {
+      if (write.isEmpty() && onlineUpgrade) {
         write.add(v);
       }
       if (v.ready) {
@@ -160,7 +165,7 @@
     }
 
     int latest = write.get(0).version;
-    if (latest != search.version) {
+    if (onlineUpgrade && latest != search.version) {
       reindexer = reindexerFactory.create(latest);
       reindexer.start();
     }
@@ -214,29 +219,35 @@
   private TreeMap<Integer, Version> scanVersions(Config cfg) {
     TreeMap<Integer, Version> versions = Maps.newTreeMap();
     for (Schema<ChangeData> schema : ChangeSchemas.ALL.values()) {
-      File f = getDir(sitePaths, schema);
-      boolean exists = f.exists() && f.isDirectory();
-      if (f.exists() && !f.isDirectory()) {
-        log.warn("Not a directory: %s", f.getAbsolutePath());
+      Path p = getDir(sitePaths, schema);
+      boolean isDir = Files.isDirectory(p);
+      if (Files.exists(p) && !isDir) {
+        log.warn("Not a directory: %s", p.toAbsolutePath());
       }
       int v = schema.getVersion();
-      versions.put(v, new Version(schema, v, exists, getReady(cfg, v)));
+      versions.put(v, new Version(schema, v, isDir, getReady(cfg, v)));
     }
 
-    for (File f : sitePaths.index_dir.listFiles()) {
-      if (!f.getName().startsWith(CHANGES_PREFIX)) {
-        continue;
+    try (DirectoryStream<Path> paths =
+        Files.newDirectoryStream(sitePaths.index_dir)) {
+      for (Path p : paths) {
+        String n = p.getFileName().toString();
+        if (!n.startsWith(CHANGES_PREFIX)) {
+          continue;
+        }
+        String versionStr = n.substring(CHANGES_PREFIX.length());
+        Integer v = Ints.tryParse(versionStr);
+        if (v == null || versionStr.length() != 4) {
+          log.warn("Unrecognized version in index directory: {}",
+              p.toAbsolutePath());
+          continue;
+        }
+        if (!versions.containsKey(v)) {
+          versions.put(v, new Version(null, v, true, getReady(cfg, v)));
+        }
       }
-      String versionStr = f.getName().substring(CHANGES_PREFIX.length());
-      Integer v = Ints.tryParse(versionStr);
-      if (v == null || versionStr.length() != 4) {
-        log.warn("Unrecognized version in index directory: {}",
-            f.getAbsolutePath());
-        continue;
-      }
-      if (!versions.containsKey(v)) {
-        versions.put(v, new Version(null, v, true, getReady(cfg, v)));
-      }
+    } catch (IOException e) {
+      log.error("Error scanning index directory: " + sitePaths.index_dir, e);
     }
     return versions;
   }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index 28af057..3af0713 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.lucene;
 
+import static com.google.gerrit.server.query.change.LegacyChangeIdPredicate.idField;
 import static org.apache.lucene.search.BooleanClause.Occur.MUST;
 import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
 import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.IntegerRangePredicate;
 import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.NotPredicate;
@@ -49,14 +50,13 @@
 import java.util.List;
 
 public class QueryBuilder {
-  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
 
-  public static Term idTerm(ChangeData cd) {
-    return intTerm(ID_FIELD, cd.getId().get());
+  public static Term idTerm(Schema<ChangeData> schema, ChangeData cd) {
+    return intTerm(idField(schema).getName(), cd.getId().get());
   }
 
-  public static Term idTerm(Change.Id id) {
-    return intTerm(ID_FIELD, id.get());
+  public static Term idTerm(Schema<ChangeData> schema, Change.Id id) {
+    return intTerm(idField(schema).getName(), id.get());
   }
 
   private final org.apache.lucene.util.QueryBuilder queryBuilder;
@@ -82,11 +82,11 @@
   private Query or(Predicate<ChangeData> p)
       throws QueryParseException {
     try {
-      BooleanQuery q = new BooleanQuery();
+      BooleanQuery.Builder q = new BooleanQuery.Builder();
       for (int i = 0; i < p.getChildCount(); i++) {
         q.add(toQuery(p.getChild(i)), SHOULD);
       }
-      return q;
+      return q.build();
     } catch (BooleanQuery.TooManyClauses e) {
       throw new QueryParseException("cannot create query for index: " + p, e);
     }
@@ -95,7 +95,7 @@
   private Query and(Predicate<ChangeData> p)
       throws QueryParseException {
     try {
-      BooleanQuery b = new BooleanQuery();
+      BooleanQuery.Builder b = new BooleanQuery.Builder();
       List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
       for (int i = 0; i < p.getChildCount(); i++) {
         Predicate<ChangeData> c = p.getChild(i);
@@ -113,7 +113,7 @@
       for (Query q : not) {
         b.add(q, MUST_NOT);
       }
-      return b;
+      return b.build();
     } catch (BooleanQuery.TooManyClauses e) {
       throw new QueryParseException("cannot create query for index: " + p, e);
     }
@@ -127,10 +127,10 @@
     }
 
     // Lucene does not support negation, start with all and subtract.
-    BooleanQuery q = new BooleanQuery();
-    q.add(new MatchAllDocsQuery(), MUST);
-    q.add(toQuery(n), MUST_NOT);
-    return q;
+    return new BooleanQuery.Builder()
+      .add(new MatchAllDocsQuery(), MUST)
+      .add(toQuery(n), MUST_NOT)
+      .build();
   }
 
   private Query fieldQuery(IndexPredicate<ChangeData> p)
@@ -148,7 +148,7 @@
     } else if (p.getType() == FieldType.FULL_TEXT) {
       return fullTextQuery(p);
     } else {
-      throw badFieldType(p.getType());
+      throw FieldType.badFieldType(p.getType());
     }
   }
 
@@ -164,8 +164,8 @@
     try {
       // Can't use IntPredicate because it and IndexPredicate are different
       // subclasses of OperatorPredicate.
-      value = Integer.valueOf(p.getValue());
-    } catch (IllegalArgumentException e) {
+      value = Integer.parseInt(p.getValue());
+    } catch (NumberFormatException e) {
       throw new QueryParseException("not an integer: " + p.getValue());
     }
     return new TermQuery(intTerm(p.getField().getName(), value));
@@ -242,15 +242,22 @@
     return new PrefixQuery(new Term(p.getField().getName(), p.getValue()));
   }
 
-  private Query fullTextQuery(IndexPredicate<ChangeData> p) {
-    return queryBuilder.createPhraseQuery(p.getField().getName(), p.getValue());
+  private Query fullTextQuery(IndexPredicate<ChangeData> p)
+      throws QueryParseException {
+    String value = p.getValue();
+    if (value == null) {
+      throw new QueryParseException(
+          "Full-text search over empty string not supported");
+    }
+    Query query = queryBuilder.createPhraseQuery(p.getField().getName(), value);
+    if (query == null) {
+      throw new QueryParseException(
+          "Cannot create full-text query with value: " + value);
+    }
+    return query;
   }
 
   public int toIndexTimeInMinutes(Date ts) {
     return (int) (ts.getTime() / 60000);
   }
-
-  public static IllegalArgumentException badFieldType(FieldType<?> t) {
-    return new IllegalArgumentException("unknown index field type " + t);
-  }
 }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
index e024f76..84a7bda 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
@@ -18,7 +18,11 @@
 
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.lucene.LuceneChangeIndex.GerritIndexWriterConfig;
 
@@ -28,59 +32,68 @@
 import org.apache.lucene.index.TrackingIndexWriter;
 import org.apache.lucene.search.ControlledRealTimeReopenThread;
 import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.ReferenceManager;
 import org.apache.lucene.search.ReferenceManager.RefreshListener;
 import org.apache.lucene.search.SearcherFactory;
-import org.apache.lucene.search.SearcherManager;
 import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.store.FSDirectory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 /** Piece of the change index that is implemented as a separate Lucene index. */
-class SubIndex {
+public class SubIndex {
   private static final Logger log = LoggerFactory.getLogger(SubIndex.class);
 
   private final Directory dir;
+  private final String dirName;
   private final TrackingIndexWriter writer;
-  private final SearcherManager searcherManager;
+  private final ListeningExecutorService writerThread;
+  private final ReferenceManager<IndexSearcher> searcherManager;
   private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
   private final Set<NrtFuture> notDoneNrtFutures;
+  private ScheduledThreadPoolExecutor autoCommitExecutor;
 
-  SubIndex(File file, GerritIndexWriterConfig writerConfig) throws IOException {
-    this(FSDirectory.open(file), file.getName(), writerConfig);
+  SubIndex(Path path, GerritIndexWriterConfig writerConfig,
+      SearcherFactory searcherFactory) throws IOException {
+    this(FSDirectory.open(path), path.getFileName().toString(), writerConfig,
+        searcherFactory);
   }
 
   SubIndex(Directory dir, final String dirName,
-      GerritIndexWriterConfig writerConfig) throws IOException {
+      GerritIndexWriterConfig writerConfig,
+      SearcherFactory searcherFactory) throws IOException {
     this.dir = dir;
+    this.dirName = dirName;
     IndexWriter delegateWriter;
     long commitPeriod = writerConfig.getCommitWithinMs();
 
     if (commitPeriod < 0) {
-      delegateWriter = new IndexWriter(dir, writerConfig.getLuceneConfig());
+      delegateWriter = new AutoCommitWriter(dir, writerConfig.getLuceneConfig());
     } else if (commitPeriod == 0) {
       delegateWriter =
           new AutoCommitWriter(dir, writerConfig.getLuceneConfig(), true);
     } else {
       final AutoCommitWriter autoCommitWriter =
-          new AutoCommitWriter(dir, writerConfig.getLuceneConfig(), false);
+          new AutoCommitWriter(dir, writerConfig.getLuceneConfig());
       delegateWriter = autoCommitWriter;
 
-      new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
-          .setNameFormat("Commit-%d " + dirName)
-          .setDaemon(true)
-          .build())
-          .scheduleAtFixedRate(new Runnable() {
+      autoCommitExecutor =
+          new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
+              .setNameFormat("Commit-%d " + dirName)
+              .setDaemon(true).build());
+      autoCommitExecutor.scheduleAtFixedRate(new Runnable() {
             @Override
             public void run() {
               try {
@@ -103,11 +116,18 @@
           }, commitPeriod, commitPeriod, MILLISECONDS);
     }
     writer = new TrackingIndexWriter(delegateWriter);
-    searcherManager = new SearcherManager(
-        writer.getIndexWriter(), true, new SearcherFactory());
+    searcherManager = new WrappableSearcherManager(
+        writer.getIndexWriter(), true, searcherFactory);
 
     notDoneNrtFutures = Sets.newConcurrentHashSet();
 
+    writerThread = MoreExecutors.listeningDecorator(
+        Executors.newFixedThreadPool(1,
+            new ThreadFactoryBuilder()
+              .setNameFormat("Write-%d " + dirName)
+              .setDaemon(true)
+              .build()));
+
     reopenThread = new ControlledRealTimeReopenThread<>(
         writer, searcherManager,
         0.500 /* maximum stale age (seconds) */,
@@ -124,6 +144,8 @@
     // searching generation being up to date when calling
     // reopenThread.waitForGeneration(gen, 0), therefore the reopen thread's
     // internal listener needs to be called first.
+    // TODO(dborowitz): This may have been fixed by
+    // http://issues.apache.org/jira/browse/LUCENE-5461
     searcherManager.addListener(new RefreshListener() {
       @Override
       public void beforeRefresh() throws IOException {
@@ -141,6 +163,20 @@
   }
 
   void close() {
+    if (autoCommitExecutor != null) {
+      autoCommitExecutor.shutdown();
+    }
+
+    writerThread.shutdown();
+    try {
+      if (!writerThread.awaitTermination(5, TimeUnit.SECONDS)) {
+        log.warn(
+            "shutting down {} index with pending Lucene writes", dirName);
+      }
+    } catch (InterruptedException e) {
+      log.warn("interrupted waiting for pending Lucene writes of " + dirName +
+          " index", e);
+    }
     reopenThread.close();
 
     // Closing the reopen thread sets its generation to Long.MAX_VALUE, but we
@@ -157,12 +193,9 @@
     }
 
     try {
-      writer.getIndexWriter().commit();
-      try {
-        writer.getIndexWriter().close();
-      } catch (AlreadyClosedException e) {
-        // Ignore.
-      }
+      writer.getIndexWriter().close();
+    } catch (AlreadyClosedException e) {
+      // Ignore.
     } catch (IOException e) {
       log.warn("error closing Lucene writer", e);
     }
@@ -173,22 +206,55 @@
     }
   }
 
-  ListenableFuture<?> insert(Document doc) throws IOException {
-    return new NrtFuture(writer.addDocument(doc));
+  ListenableFuture<?> insert(final Document doc) {
+    return submit(new Callable<Long>() {
+      @Override
+      public Long call() throws IOException, InterruptedException {
+        return writer.addDocument(doc);
+      }
+    });
   }
 
-  ListenableFuture<?> replace(Term term, Document doc) throws IOException {
-    return new NrtFuture(writer.updateDocument(term, doc));
+  ListenableFuture<?> replace(final Term term, final Document doc) {
+    return submit(new Callable<Long>() {
+      @Override
+      public Long call() throws IOException, InterruptedException {
+        return writer.updateDocument(term, doc);
+      }
+    });
   }
 
-  ListenableFuture<?> delete(Term term) throws IOException {
-    return new NrtFuture(writer.deleteDocuments(term));
+  ListenableFuture<?> delete(final Term term)  {
+    return submit(new Callable<Long>() {
+      @Override
+      public Long call() throws IOException, InterruptedException {
+        return writer.deleteDocuments(term);
+      }
+    });
+  }
+
+  private ListenableFuture<?> submit(Callable<Long> task) {
+    ListenableFuture<Long> future =
+        Futures.nonCancellationPropagating(writerThread.submit(task));
+    return Futures.transformAsync(future, new AsyncFunction<Long, Void>() {
+      @Override
+      public ListenableFuture<Void> apply(Long gen) throws InterruptedException {
+        // Tell the reopen thread a future is waiting on this
+        // generation so it uses the min stale time when refreshing.
+        reopenThread.waitForGeneration(gen, 0);
+        return new NrtFuture(gen);
+      }
+    });
   }
 
   void deleteAll() throws IOException {
     writer.deleteAll();
   }
 
+  public TrackingIndexWriter getWriter() {
+    return writer;
+  }
+
   IndexSearcher acquire() throws IOException {
     return searcherManager.acquire();
   }
@@ -202,9 +268,6 @@
 
     NrtFuture(long gen) {
       this.gen = gen;
-      // Tell the reopen thread we are waiting on this generation so it uses the
-      // min stale time when refreshing.
-      isGenAvailableNowForCurrentSearcher();
     }
 
     @Override
@@ -220,12 +283,10 @@
     public Void get(long timeout, TimeUnit unit) throws InterruptedException,
         TimeoutException, ExecutionException {
       if (!isDone()) {
-        if (reopenThread.waitForGeneration(gen,
-            (int) MILLISECONDS.convert(timeout, unit))) {
-          set(null);
-        } else {
+        if (!reopenThread.waitForGeneration(gen, (int) unit.toMillis(timeout))) {
           throw new TimeoutException();
         }
+        set(null);
       }
       return super.get(timeout, unit);
     }
@@ -237,6 +298,9 @@
       } else if (isGenAvailableNowForCurrentSearcher()) {
         set(null);
         return true;
+      } else if (!reopenThread.isAlive()) {
+        setException(new IllegalStateException("NRT thread is dead"));
+        return true;
       }
       return false;
     }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java
new file mode 100644
index 0000000..7bcd0a6
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -0,0 +1,220 @@
+package com.google.gerrit.lucene;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.FilterDirectoryReader;
+import org.apache.lucene.index.FilterLeafReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.ReferenceManager;
+import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.store.Directory;
+
+import java.io.IOException;
+
+/**
+ * Utility class to safely share {@link IndexSearcher} instances across multiple
+ * threads, while periodically reopening. This class ensures each searcher is
+ * closed only once all threads have finished using it.
+ *
+ * <p>
+ * Use {@link #acquire} to obtain the current searcher, and {@link #release} to
+ * release it, like this:
+ *
+ * <pre class="prettyprint">
+ * IndexSearcher s = manager.acquire();
+ * try {
+ *   // Do searching, doc retrieval, etc. with s
+ * } finally {
+ *   manager.release(s);
+ * }
+ * // Do not use s after this!
+ * s = null;
+ * </pre>
+ *
+ * <p>
+ * In addition you should periodically call {@link #maybeRefresh}. While it's
+ * possible to call this just before running each query, this is discouraged
+ * since it penalizes the unlucky queries that need to refresh. It's better to use
+ * a separate background thread, that periodically calls {@link #maybeRefresh}. Finally,
+ * be sure to call {@link #close} once you are done.
+ *
+ * @see SearcherFactory
+ *
+ * @lucene.experimental
+ */
+// This file was copied from:
+// https://github.com/apache/lucene-solr/blob/lucene_solr_5_0/lucene/core/src/java/org/apache/lucene/search/SearcherManager.java
+// The only change (other than class name and import fixes)
+// is to skip the check in getSearcher that searcherFactory.newSearcher wraps
+// the provided searcher exactly.
+final class WrappableSearcherManager extends ReferenceManager<IndexSearcher> {
+
+  private final SearcherFactory searcherFactory;
+
+  /**
+   * Creates and returns a new SearcherManager from the given
+   * {@link IndexWriter}.
+   *
+   * @param writer
+   *          the IndexWriter to open the IndexReader from.
+   * @param applyAllDeletes
+   *          If <code>true</code>, all buffered deletes will be applied (made
+   *          visible) in the {@link IndexSearcher} / {@link DirectoryReader}.
+   *          If <code>false</code>, the deletes may or may not be applied, but
+   *          remain buffered (in IndexWriter) so that they will be applied in
+   *          the future. Applying deletes can be costly, so if your app can
+   *          tolerate deleted documents being returned you might gain some
+   *          performance by passing <code>false</code>. See
+   *          {@link DirectoryReader#openIfChanged(DirectoryReader, IndexWriter, boolean)}.
+   * @param searcherFactory
+   *          An optional {@link SearcherFactory}. Pass <code>null</code> if you
+   *          don't require the searcher to be warmed before going live or other
+   *          custom behavior.
+   *
+   * @throws IOException if there is a low-level I/O error
+   */
+  public WrappableSearcherManager(IndexWriter writer, boolean applyAllDeletes, SearcherFactory searcherFactory) throws IOException {
+    if (searcherFactory == null) {
+      searcherFactory = new SearcherFactory();
+    }
+    this.searcherFactory = searcherFactory;
+    current = getSearcher(searcherFactory, DirectoryReader.open(writer, applyAllDeletes));
+  }
+
+  /**
+   * Creates and returns a new SearcherManager from the given {@link Directory}.
+   * @param dir the directory to open the DirectoryReader on.
+   * @param searcherFactory An optional {@link SearcherFactory}. Pass
+   *        <code>null</code> if you don't require the searcher to be warmed
+   *        before going live or other custom behavior.
+   *
+   * @throws IOException if there is a low-level I/O error
+   */
+  public WrappableSearcherManager(Directory dir, SearcherFactory searcherFactory) throws IOException {
+    if (searcherFactory == null) {
+      searcherFactory = new SearcherFactory();
+    }
+    this.searcherFactory = searcherFactory;
+    current = getSearcher(searcherFactory, DirectoryReader.open(dir));
+  }
+
+  /**
+   * Creates and returns a new SearcherManager from an existing {@link DirectoryReader}.  Note that
+   * this steals the incoming reference.
+   *
+   * @param reader the DirectoryReader.
+   * @param searcherFactory An optional {@link SearcherFactory}. Pass
+   *        <code>null</code> if you don't require the searcher to be warmed
+   *        before going live or other custom behavior.
+   *
+   * @throws IOException if there is a low-level I/O error
+   */
+  public WrappableSearcherManager(DirectoryReader reader, SearcherFactory searcherFactory) throws IOException {
+    if (searcherFactory == null) {
+      searcherFactory = new SearcherFactory();
+    }
+    this.searcherFactory = searcherFactory;
+    this.current = getSearcher(searcherFactory, reader);
+  }
+
+  @Override
+  protected void decRef(IndexSearcher reference) throws IOException {
+    reference.getIndexReader().decRef();
+  }
+
+  @Override
+  protected IndexSearcher refreshIfNeeded(IndexSearcher referenceToRefresh) throws IOException {
+    final IndexReader r = referenceToRefresh.getIndexReader();
+    assert r instanceof DirectoryReader: "searcher's IndexReader should be a DirectoryReader, but got " + r;
+    final IndexReader newReader = DirectoryReader.openIfChanged((DirectoryReader) r);
+    if (newReader == null) {
+      return null;
+    } else {
+      return getSearcher(searcherFactory, newReader);
+    }
+  }
+
+  @Override
+  protected boolean tryIncRef(IndexSearcher reference) {
+    return reference.getIndexReader().tryIncRef();
+  }
+
+  @Override
+  protected int getRefCount(IndexSearcher reference) {
+    return reference.getIndexReader().getRefCount();
+  }
+
+  /**
+   * Returns <code>true</code> if no changes have occured since this searcher
+   * ie. reader was opened, otherwise <code>false</code>.
+   * @see DirectoryReader#isCurrent()
+   */
+  public boolean isSearcherCurrent() throws IOException {
+    final IndexSearcher searcher = acquire();
+    try {
+      final IndexReader r = searcher.getIndexReader();
+      assert r instanceof DirectoryReader: "searcher's IndexReader should be a DirectoryReader, but got " + r;
+      return ((DirectoryReader) r).isCurrent();
+    } finally {
+      release(searcher);
+    }
+  }
+
+  /** Expert: creates a searcher from the provided {@link
+   *  IndexReader} using the provided {@link
+   *  SearcherFactory}.  NOTE: this decRefs incoming reader
+   * on throwing an exception. */
+  @SuppressWarnings("resource")
+  public static IndexSearcher getSearcher(SearcherFactory searcherFactory, IndexReader reader) throws IOException {
+    boolean success = false;
+    final IndexSearcher searcher;
+    try {
+      searcher = searcherFactory.newSearcher(reader, null);
+      // Modification for Gerrit: Allow searcherFactory to transitively wrap the
+      // provided reader.
+      IndexReader unwrapped = searcher.getIndexReader();
+      while (true) {
+        if (unwrapped == reader) {
+          break;
+        } else if (unwrapped instanceof FilterDirectoryReader) {
+          unwrapped = ((FilterDirectoryReader) unwrapped).getDelegate();
+        } else if (unwrapped instanceof FilterLeafReader) {
+          unwrapped = ((FilterLeafReader) unwrapped).getDelegate();
+        } else {
+          break;
+        }
+      }
+
+      if (unwrapped != reader) {
+        throw new IllegalStateException(
+            "SearcherFactory must wrap the provided reader (got " +
+            searcher.getIndexReader() +
+            " but expected " + reader + ")");
+      }
+      success = true;
+    } finally {
+      if (!success) {
+        reader.decRef();
+      }
+    }
+    return searcher;
+  }
+}
diff --git a/gerrit-main/src/main/java/Main.java b/gerrit-main/src/main/java/Main.java
index d3c9fdd..54cf20e 100644
--- a/gerrit-main/src/main/java/Main.java
+++ b/gerrit-main/src/main/java/Main.java
@@ -20,7 +20,7 @@
   // to jump into the real main code.
   //
 
-  public static void main(final String argv[]) throws Exception {
+  public static void main(final String[] argv) throws Exception {
     if (onSupportedJavaVersion()) {
       com.google.gerrit.launcher.GerritLauncher.main(argv);
 
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
index 2d73634..333af15 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.oauth;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
@@ -32,7 +34,6 @@
 import org.w3c.dom.Element;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
@@ -183,7 +184,7 @@
     byte[] bin = HtmlDomUtil.toUTF8(doc);
     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     res.setContentType("text/html");
-    res.setCharacterEncoding(StandardCharsets.UTF_8.name());
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(bin.length);
     try (ServletOutputStream out = res.getOutputStream()) {
       out.write(bin);
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index b8080c9..8b05c72 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
@@ -164,11 +166,14 @@
       mode = SignInMode.SIGN_IN;
     }
 
+    log.debug("mode \"{}\"", mode);
     OAuthServiceProvider oauthProvider = lookupOAuthServiceProvider(id);
 
     if (oauthProvider == null) {
+      log.debug("OpenId provider \"{}\"", id);
       discover(req, res, link, id, remember, token, mode);
     } else {
+      log.debug("OAuth provider \"{}\"", id);
       OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
       if (!currentUserProvider.get().isIdentifiedUser()
           && oauthSession.isLoggedIn()) {
@@ -317,13 +322,10 @@
     byte[] bin = HtmlDomUtil.toUTF8(doc);
     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     res.setContentType("text/html");
-    res.setCharacterEncoding("UTF-8");
+    res.setCharacterEncoding(UTF_8.name());
     res.setContentLength(bin.length);
-    ServletOutputStream out = res.getOutputStream();
-    try {
+    try (ServletOutputStream out = res.getOutputStream()) {
       out.write(bin);
-    } finally {
-      out.close();
     }
   }
 
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index ba7ce87..33c6e34 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -147,6 +147,7 @@
         if (actualId != null) {
           if (claimedId.equals(actualId)) {
             // Both link to the same account, that's what we expected.
+            log.debug("Both link to the same account. All is fine.");
           } else {
             // This is (for now) a fatal error. There are two records
             // for what might be the same user. The admin would have to
@@ -160,7 +161,7 @@
           }
         } else {
           // Claimed account already exists: link to it.
-          //
+          log.debug("Claimed account already exists: link to it.");
           try {
             accountManager.link(claimedId, areq);
           } catch (OrmException e) {
@@ -173,11 +174,14 @@
         }
       } else if (linkMode) {
         // Use case 2: link mode activated from the UI
+        Account.Id accountId = identifiedUser.get().getAccountId();
         try {
-          accountManager.link(identifiedUser.get().getAccountId(), areq);
+          log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(),
+              accountId);
+          accountManager.link(accountId, areq);
         } catch (OrmException e) {
           log.error("Cannot link: " + user.getExternalId()
-              + " to user identity: " + identifiedUser.get().getAccountId());
+              + " to user identity: " + accountId);
           rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
           return;
         } finally {
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
index 4719a84..b48d3ed 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -29,7 +31,6 @@
 @Singleton
 class XrdsServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private static final String ENC = "UTF-8";
   static final String LOCATION = "OpenID.XRDS";
 
   private final Provider<String> url;
@@ -43,7 +44,8 @@
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     final StringBuilder r = new StringBuilder();
-    r.append("<?xml version=\"1.0\" encoding=\"").append(ENC).append("\"?>");
+    r.append("<?xml version=\"1.0\" encoding=\"")
+     .append(UTF_8.name()).append("\"?>");
     r.append("<xrds:XRDS");
     r.append(" xmlns:xrds=\"xri://$xrds\"");
     r.append(" xmlns:openid=\"http://openid.net/xmlns/1.0\"");
@@ -58,16 +60,13 @@
     r.append("</xrds:XRDS>");
     r.append("\n");
 
-    final byte[] raw = r.toString().getBytes(ENC);
+    final byte[] raw = r.toString().getBytes(UTF_8);
     rsp.setContentLength(raw.length);
     rsp.setContentType("application/xrds+xml");
-    rsp.setCharacterEncoding(ENC);
+    rsp.setCharacterEncoding(UTF_8.name());
 
-    final ServletOutputStream out = rsp.getOutputStream();
-    try {
+    try (ServletOutputStream out = rsp.getOutputStream()) {
       out.write(raw);
-    } finally {
-      out.close();
     }
   }
 }
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index bb8296e..d24d179 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -14,6 +14,8 @@
 
 package org.apache.commons.net.smtp;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 
 import org.apache.commons.codec.binary.Base64;
@@ -32,11 +34,11 @@
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 
 public class AuthSMTPClient extends SMTPClient {
-  private static final String UTF_8 = "UTF-8";
-
   private String authTypes;
 
   public AuthSMTPClient(final String charset) {
@@ -55,6 +57,12 @@
 
     _socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true);
 
+    if (verify) {
+      SSLParameters sslParams = new SSLParameters();
+      sslParams.setEndpointIdentificationAlgorithm("HTTPS");
+      ((SSLSocket)_socket_).setSSLParameters(sslParams);
+    }
+
     // XXX: Can't call _connectAction_() because SMTP server doesn't
     // give banner information again after STARTTLS, thus SMTP._connectAction_()
     // will wait on __getReply() forever, see source code of commons-net-2.2.
@@ -65,11 +73,9 @@
     _input_ = _socket_.getInputStream();
     _output_ = _socket_.getOutputStream();
     _reader =
-        new BufferedReader(new InputStreamReader(_input_,
-                      UTF_8));
+        new BufferedReader(new InputStreamReader(_input_, UTF_8));
     _writer =
-        new BufferedWriter(new OutputStreamWriter(_output_,
-                      UTF_8));
+        new BufferedWriter(new OutputStreamWriter(_output_, UTF_8));
     return true;
   }
 
@@ -145,9 +151,7 @@
       Mac mac = Mac.getInstance(macName);
       mac.init(new SecretKeySpec(smtpPass.getBytes(UTF_8), macName));
       sec = toHex(mac.doFinal(nonce));
-    } catch (NoSuchAlgorithmException e) {
-      throw new IOException("Cannot use CRAM-" + alg, e);
-    } catch (InvalidKeyException e) {
+    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
       throw new IOException("Cannot use CRAM-" + alg, e);
     }
 
@@ -192,7 +196,7 @@
     return SMTPReply.isPositiveCompletion(sendCommand("AUTH", cmd));
   }
 
-  private static String encodeBase64(final byte[] data) throws UnsupportedEncodingException {
+  private static String encodeBase64(final byte[] data) {
     return new String(Base64.encodeBase64(data), UTF_8);
   }
 }
diff --git a/gerrit-patch-jgit/BUCK b/gerrit-patch-jgit/BUCK
index e621722..b54499f 100644
--- a/gerrit-patch-jgit/BUCK
+++ b/gerrit-patch-jgit/BUCK
@@ -9,10 +9,9 @@
   gwt_xml = SRC + 'JGit.gwt.xml',
   deps = [
     '//lib:gwtjsonrpc',
-    '//lib/gwt:user',
-    '//lib/jgit:jgit',
     '//lib/jgit:Edit',
   ],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 9a317cd..c57ec52 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -18,7 +18,8 @@
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/log:api',
-    '//lib/log:log4j',
+    '//lib/log:jsonevent-layout',
+    '//lib/log:log4j'
 ]
 
 java_library(
@@ -44,6 +45,7 @@
     '//gerrit-common:annotations',
     '//gerrit-lucene:lucene',
     '//lib:args4j',
+    '//lib:derby',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
     '//lib:h2',
@@ -52,21 +54,24 @@
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
+    '//gerrit-acceptance-framework/...',
     '//gerrit-acceptance-tests/...',
     '//gerrit-war:',
   ],
 )
 
+REST_UTIL_DEPS = [
+  '//gerrit-cache-h2:cache-h2',
+  '//gerrit-util-cli:cli',
+  '//lib:args4j',
+  '//lib:gwtorm',
+  '//lib/commons:dbcp',
+]
+
 java_library(
   name = 'util',
-  srcs = glob([SRCS + 'util/*.java']),
-  deps = DEPS + [
-    '//gerrit-cache-h2:cache-h2',
-    '//gerrit-util-cli:cli',
-    '//lib:args4j',
-    '//lib:gwtorm',
-    '//lib/commons:dbcp',
-  ],
+  deps = DEPS + REST_UTIL_DEPS,
+  exported_deps = [':util-nodep'],
   visibility = [
     '//gerrit-acceptance-tests/...',
     '//gerrit-gwtdebug:gwtdebug',
@@ -75,6 +80,15 @@
 )
 
 java_library(
+  name = 'util-nodep',
+  srcs = glob([SRCS + 'util/*.java']),
+  provided_deps = DEPS + REST_UTIL_DEPS,
+  visibility = [
+    '//gerrit-acceptance-framework/...',
+  ],
+)
+
+java_library(
   name = 'http',
   srcs = glob([SRCS + 'http/**/*.java']),
   deps = DEPS + [
@@ -89,26 +103,32 @@
   visibility = ['//gerrit-war:'],
 )
 
+REST_PGM_DEPS = [
+  ':http',
+  ':init',
+  ':init-api',
+  ':util',
+  '//gerrit-cache-h2:cache-h2',
+  '//gerrit-gpg:gpg',
+  '//gerrit-lucene:lucene',
+  '//gerrit-oauth:oauth',
+  '//gerrit-openid:openid',
+  '//lib:args4j',
+  '//lib:gwtorm',
+  '//lib:protobuf',
+  '//lib:servlet-api-3_1',
+  '//lib/auto:auto-value',
+  '//lib/prolog:cafeteria',
+  '//lib/prolog:compiler',
+  '//lib/prolog:runtime',
+]
+
 java_library(
   name = 'pgm',
-  srcs = glob([SRCS + '*.java']),
   resources = glob([RSRCS + '*']),
-  deps = DEPS + [
-    ':http',
-    ':init',
-    ':init-api',
-    ':util',
-    '//gerrit-cache-h2:cache-h2',
-    '//gerrit-lucene:lucene',
-    '//gerrit-oauth:oauth',
-    '//gerrit-openid:openid',
-    '//gerrit-solr:solr',
-    '//lib:args4j',
-    '//lib:gwtorm',
-    '//lib:servlet-api-3_1',
-    '//lib/prolog:prolog-cafe',
+  deps = DEPS + REST_PGM_DEPS + [
+    ':daemon',
   ],
-  provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
@@ -118,6 +138,21 @@
   ],
 )
 
+# no transitive deps, used for gerrit-acceptance-framework
+java_library(
+  name = 'daemon',
+  srcs = glob([SRCS + '*.java', SRCS + 'rules/*.java']),
+  resources = glob([RSRCS + '*']),
+  deps = ['//lib/auto:auto-value'],
+  provided_deps = DEPS + REST_PGM_DEPS + [
+    '//gerrit-launcher:launcher',
+  ],
+  visibility = [
+    '//gerrit-acceptance-framework/...',
+    '//gerrit-gwtdebug:gwtdebug',
+  ],
+)
+
 java_test(
   name = 'pgm_tests',
   srcs = glob(['src/test/java/**/*.java']),
@@ -125,7 +160,9 @@
     ':init',
     ':init-api',
     ':pgm',
+    '//gerrit-common:server',
     '//gerrit-server:server',
+    '//lib:guava',
     '//lib:junit',
     '//lib/easymock:easymock',
     '//lib/guice:guice',
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Cat.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Cat.java
index da6a42e..2214587 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Cat.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Cat.java
@@ -40,13 +40,12 @@
       name = "WEB-INF/" + fileName;
     }
 
-    final InputStream in = open(name);
-    if (in == null) {
-      System.err.println("error: no such file " + fileName);
-      return 1;
-    }
+    try (InputStream in = open(name)) {
+      if (in == null) {
+        System.err.println("error: no such file " + fileName);
+        return 1;
+      }
 
-    try {
       try {
         final byte[] buf = new byte[4096];
         int n;
@@ -56,8 +55,6 @@
       } finally {
         System.out.flush();
       }
-    } finally {
-      in.close();
     }
     return 0;
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index feb07e1..ee1b111 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.pgm;
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.GerritOptions;
 import com.google.gerrit.httpd.GetUserFilter;
@@ -43,6 +45,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.change.ChangeCleanupRunner;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -50,10 +53,7 @@
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.RestCacheAdminModule;
-import com.google.gerrit.server.contact.ContactStoreModule;
-import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.ChangeCacheImplModule;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
@@ -76,11 +76,11 @@
 import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
-import com.google.gerrit.solr.SolrIndexModule;
 import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
+import com.google.gerrit.sshd.commands.IndexCommandsModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -94,10 +94,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.lang.Thread.UncaughtExceptionHandler;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -145,15 +145,18 @@
   private final LifecycleManager manager = new LifecycleManager();
   private Injector dbInjector;
   private Injector cfgInjector;
+  private Config config;
   private Injector sysInjector;
   private Injector sshInjector;
   private Injector webInjector;
   private Injector httpdInjector;
-  private File runFile;
+  private Path runFile;
   private boolean test;
   private AbstractModule luceneModule;
+  private Module emailModule;
 
   private Runnable serverStarted;
+  private IndexType indexType;
 
   public Daemon() {
   }
@@ -185,7 +188,7 @@
     });
 
     if (runId != null) {
-      runFile = new File(new File(getSitePath(), "logs"), "gerrit.run");
+      runFile = getSitePath().resolve("logs").resolve("gerrit.run");
     }
 
     if (httpd == null) {
@@ -196,11 +199,6 @@
       throw die("No services enabled, nothing to do");
     }
 
-    if (consoleLog) {
-    } else {
-      manager.add(ErrorLogFile.start(getSitePath()));
-    }
-
     try {
       start();
       RuntimeShutdown.add(new Runnable() {
@@ -208,7 +206,11 @@
         public void run() {
           log.info("caught shutdown, cleaning up");
           if (runId != null) {
-            runFile.delete();
+            try {
+              Files.delete(runFile);
+            } catch (IOException err) {
+              log.warn("failed to delete " + runFile, err);
+            }
           }
           manager.stop();
         }
@@ -217,15 +219,8 @@
       log.info("Gerrit Code Review " + myVersion() + " ready");
       if (runId != null) {
         try {
-          runFile.createNewFile();
-          runFile.setReadable(true, false);
-
-          FileOutputStream out = new FileOutputStream(runFile);
-          try {
-            out.write((runId + "\n").getBytes("UTF-8"));
-          } finally {
-            out.close();
-          }
+          Files.write(runFile, (runId + "\n").getBytes(UTF_8));
+          runFile.toFile().setReadable(true, false);
         } catch (IOException err) {
           log.warn("Cannot write --run-id to " + runFile, err);
         }
@@ -265,22 +260,36 @@
   }
 
   @VisibleForTesting
+  public void setEmailModuleForTesting(Module module) {
+    emailModule = module;
+  }
+
+  @VisibleForTesting
   public void setLuceneModule(LuceneIndexModule m) {
     luceneModule = m;
     test = true;
   }
 
   @VisibleForTesting
-  public void start() {
+  public void start() throws IOException {
     if (dbInjector == null) {
       dbInjector = createDbInjector(MULTI_USER);
     }
     cfgInjector = createCfgInjector();
+    config = cfgInjector.getInstance(
+        Key.get(Config.class, GerritServerConfig.class));
+    if (!slave) {
+      initIndexType();
+    }
     sysInjector = createSysInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class)
       .setDbCfgInjector(dbInjector, cfgInjector);
     manager.add(dbInjector, cfgInjector, sysInjector);
 
+    if (!consoleLog) {
+      manager.add(ErrorLogFile.start(getSitePath(), config));
+    }
+
     sshd &= !sshdOff();
     if (sshd) {
       initSshd();
@@ -299,8 +308,7 @@
   }
 
   private boolean sshdOff() {
-    Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    return new SshAddressesModule().getListenAddresses(cfg).isEmpty();
+    return new SshAddressesModule().getListenAddresses(config).isEmpty();
   }
 
   private String myVersion() {
@@ -326,10 +334,15 @@
     modules.add(new ChangeCacheImplModule(slave));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
-    modules.add(new SmtpEmailSender.Module());
+    if (emailModule != null) {
+      modules.add(emailModule);
+    } else {
+      modules.add(new SmtpEmailSender.Module());
+    }
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginRestApiModule());
     modules.add(new RestCacheAdminModule());
+    modules.add(new GpgModule(config));
     modules.add(createIndexModule());
     if (MoreObjects.firstNonNull(httpd, true)) {
       modules.add(new CanonicalWebUrlModule() {
@@ -351,9 +364,6 @@
     } else {
       modules.add(NoSshKeyCache.module());
     }
-    if (!slave) {
-      modules.add(new MasterNodeStartup());
-    }
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
@@ -366,6 +376,9 @@
       }
     });
     modules.add(new GarbageCollectionModule());
+    if (!slave) {
+      modules.add(new ChangeCleanupRunner.Module());
+    }
     return cfgInjector.createChildInjector(modules);
   }
 
@@ -373,12 +386,19 @@
     if (slave) {
       return new DummyIndexModule();
     }
-    IndexType indexType = IndexModule.getIndexType(cfgInjector);
     switch (indexType) {
       case LUCENE:
         return luceneModule != null ? luceneModule : new LuceneIndexModule();
-      case SOLR:
-        return new SolrIndexModule();
+      default:
+        throw new IllegalStateException("unsupported index.type = " + indexType);
+    }
+  }
+
+  private void initIndexType() {
+    indexType = IndexModule.getIndexType(cfgInjector);
+    switch (indexType) {
+      case LUCENE:
+        break;
       default:
         throw new IllegalStateException("unsupported index.type = " + indexType);
     }
@@ -399,6 +419,9 @@
     }
     modules.add(new DefaultCommandModule(slave,
         sysInjector.getInstance(DownloadConfig.class)));
+    if (!slave && indexType == IndexType.LUCENE) {
+      modules.add(new IndexCommandsModule());
+    }
     return sysInjector.createChildInjector(modules);
   }
 
@@ -424,11 +447,9 @@
     modules.add(RequestContextFilter.module());
     modules.add(AllRequestFilter.module());
     modules.add(H2CacheBasedWebSession.module());
-    modules.add(HttpContactStoreConnection.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
     modules.add(new HttpPluginModule());
-    modules.add(new ContactStoreModule());
     if (sshd) {
       modules.add(sshInjector.getInstance(WebSshGlueModule.class));
     } else {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
index a5ce908..8e09578 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
@@ -16,10 +16,10 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.sshd.commands.QueryShell;
 import com.google.gerrit.sshd.commands.QueryShell.Factory;
 import com.google.inject.Injector;
@@ -52,6 +52,7 @@
         try {
           System.in.close();
         } catch (IOException e) {
+          // Ignored
         }
         manager.stop();
       }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 38b1824..e08b7ac 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -37,8 +37,8 @@
 
 import org.kohsuke.args4j.Option;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -63,6 +63,10 @@
       usage = "Path to jar providing SecureStore implementation class")
   private String secureStoreLib;
 
+  @Option(name = "--dev",
+      usage = "Setup site with default options suitable for developers")
+  private boolean dev;
+
   @Inject
   Browser browser;
 
@@ -70,7 +74,7 @@
     super(new WarDistribution(), null);
   }
 
-  public Init(File sitePath) {
+  public Init(Path sitePath) {
     super(sitePath, true, true, new WarDistribution(), null);
     batchMode = true;
     noAutoStart = true;
@@ -106,7 +110,7 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(File.class).annotatedWith(SitePath.class).toInstance(getSitePath());
+        bind(Path.class).annotatedWith(SitePath.class).toInstance(getSitePath());
         bind(Browser.class);
         bind(String.class).annotatedWith(SecureStoreClassName.class)
             .toProvider(Providers.of(getConfiguredSecureStoreClass()));
@@ -138,6 +142,11 @@
   }
 
   @Override
+  protected boolean isDev() {
+    return dev;
+  }
+
+  @Override
   protected String getSecureStoreLib() {
     return secureStoreLib;
   }
@@ -157,8 +166,8 @@
   }
 
   void startDaemon(SiteRun run) {
-    final String[] argv = {run.site.gerrit_sh.getAbsolutePath(), "start"};
-    final Process proc;
+    String[] argv = {run.site.gerrit_sh.toAbsolutePath().toString(), "start"};
+    Process proc;
     try {
       System.err.println("Executing " + argv[0] + " " + argv[1]);
       proc = Runtime.getRuntime().exec(argv);
@@ -170,6 +179,7 @@
     try {
       proc.getOutputStream().close();
     } catch (IOException e) {
+      // Ignored
     }
 
     IoUtil.copyWithThread(proc.getInputStream(), System.err);
@@ -177,7 +187,7 @@
 
     for (;;) {
       try {
-        final int rc = proc.waitFor();
+        int rc = proc.waitFor();
         if (rc != 0) {
           System.err.println("error: cannot start Gerrit: exit status " + rc);
         }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
index ff157ce..d3643f3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
@@ -80,9 +80,7 @@
     try {
       shell = console.newInstance();
       log.info("Jython shell instance created.");
-    } catch (InstantiationException e) {
-      throw noInterpreter(e);
-    } catch (IllegalAccessException e) {
+    } catch (InstantiationException | IllegalAccessException e) {
       throw noInterpreter(e);
     }
     injectedVariables = new ArrayList<>();
@@ -96,13 +94,8 @@
       Method m;
       m = klazz.getMethod(name, sig);
       return m.invoke(instance, args);
-    } catch (NoSuchMethodException e) {
-      throw cannotStart(e);
-    } catch (SecurityException e) {
-      throw cannotStart(e);
-    } catch (IllegalArgumentException e) {
-      throw cannotStart(e);
-    } catch (IllegalAccessException e) {
+    } catch (NoSuchMethodException | IllegalAccessException
+        | IllegalArgumentException | SecurityException e) {
       throw cannotStart(e);
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index d208a3c..224c75c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -60,14 +60,11 @@
     manager.start();
     dbInjector.injectMembers(this);
 
-    final ReviewDb db = database.open();
-    try {
+    try (ReviewDb db = database.open()) {
       todo = db.accountExternalIds().all().toList();
       synchronized (monitor) {
-        monitor.beginTask("Converting local username", todo.size());
+        monitor.beginTask("Converting local usernames", todo.size());
       }
-    } finally {
-      db.close();
     }
 
     final List<Worker> workers = new ArrayList<>(threads);
@@ -119,14 +116,7 @@
   private class Worker extends Thread {
     @Override
     public void run() {
-      final ReviewDb db;
-      try {
-        db = database.open();
-      } catch (OrmException e) {
-        e.printStackTrace();
-        return;
-      }
-      try {
+      try (ReviewDb db = database.open()){
         for (;;) {
           final AccountExternalId extId = next();
           if (extId == null) {
@@ -137,8 +127,8 @@
             monitor.update(1);
           }
         }
-      } finally {
-        db.close();
+      } catch (OrmException e) {
+        e.printStackTrace();
       }
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Ls.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Ls.java
index 7e7b602..9f6436b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Ls.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Ls.java
@@ -26,8 +26,7 @@
 public class Ls extends AbstractProgram {
   @Override
   public int run() throws IOException {
-    final ZipFile zf = new ZipFile(GerritLauncher.getDistributionArchive());
-    try {
+    try (ZipFile zf = new ZipFile(GerritLauncher.getDistributionArchive())) {
       final Enumeration<? extends ZipEntry> e = zf.entries();
       while (e.hasMoreElements()) {
         final ZipEntry ze = e.nextElement();
@@ -39,6 +38,7 @@
         show &= !ze.isDirectory();
         show &= !name.startsWith("WEB-INF/classes/");
         show &= !name.startsWith("WEB-INF/lib/");
+        show &= !name.startsWith("WEB-INF/pgm-lib/");
         show &= !name.equals("WEB-INF/web.xml");
         if (show) {
           if (name.startsWith("WEB-INF/")) {
@@ -47,8 +47,6 @@
           System.out.println(name);
         }
       }
-    } finally {
-      zf.close();
     }
     return 0;
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
index fa434a6..58162a1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
@@ -16,11 +16,10 @@
 
 import com.google.gerrit.pgm.util.AbstractProgram;
 
+import com.googlecode.prolog_cafe.exceptions.HaltException;
 import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
-import com.googlecode.prolog_cafe.lang.HaltException;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.PrologClassLoader;
-import com.googlecode.prolog_cafe.lang.PrologMain;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 
 import org.kohsuke.args4j.Option;
@@ -28,7 +27,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.EnumSet;
 import java.util.List;
 
 public class PrologShell extends AbstractProgram {
@@ -41,14 +39,10 @@
 
     BufferingPrologControl pcl = new BufferingPrologControl();
     pcl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
-    pcl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
     pcl.setEnabled(Prolog.Feature.IO, true);
-    pcl.setEnabled(Prolog.Feature.STATISTICS_RUNTIME, true);
-
+    pcl.setEnabled(Prolog.Feature.STATISTICS, true);
+    pcl.configureUserIO(System.in, System.out, System.err);
     pcl.initialize(Prolog.BUILTIN);
-    pcl.execute(Prolog.BUILTIN, "set_prolog_flag",
-        SymbolTerm.intern("print_stack_trace"),
-        SymbolTerm.intern("on"));
 
     for (String file : fileName) {
       String path;
@@ -76,8 +70,6 @@
     System.err.format("Gerrit Code Review %s - Interactive Prolog Shell",
         com.google.gerrit.common.Version.getVersion());
     System.err.println();
-    System.err.println("based on " + PrologMain.VERSION);
-    System.err.println("         " + PrologMain.COPYRIGHT);
     System.err.println("(type Ctrl-D or \"halt.\" to exit,"
         + " \"['path/to/file.pl'].\" to load a file)");
     System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
index 12e1e99..7dba8ed 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.pgm;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.pgm.util.AbstractProgram;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.schema.java.JavaSchemaModel;
 
 import org.eclipse.jgit.internal.storage.file.LockFile;
-import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.kohsuke.args4j.Option;
 
@@ -37,7 +38,7 @@
 
   @Override
   public int run() throws Exception {
-    LockFile lock = new LockFile(file.getAbsoluteFile(), FS.DETECTED);
+    LockFile lock = new LockFile(file.getAbsoluteFile());
     if (!lock.lock()) {
       throw die("Cannot lock " + file);
     }
@@ -45,16 +46,13 @@
       JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
       try (OutputStream o = lock.getOutputStream();
           PrintWriter out = new PrintWriter(
-              new BufferedWriter(new OutputStreamWriter(o, "UTF-8")))) {
+              new BufferedWriter(new OutputStreamWriter(o, UTF_8)))) {
         String header;
-        InputStream in = getClass().getResourceAsStream("ProtoGenHeader.txt");
-        try {
+        try (InputStream in = getClass().getResourceAsStream("ProtoGenHeader.txt")) {
           ByteBuffer buf = IO.readWholeStream(in, 1024);
           int ptr = buf.arrayOffset() + buf.position();
           int len = buf.remaining();
-          header = new String(buf.array(), ptr, len, "UTF-8");
-        } finally {
-          in.close();
+          header = new String(buf.array(), ptr, len, UTF_8);
         }
 
         String version = com.google.gerrit.common.Version.getVersion();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
new file mode 100644
index 0000000..948182e
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2015 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.pgm;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.RuntimeShutdown;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.schema.RelationModel;
+import com.google.gwtorm.schema.java.JavaSchemaModel;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Parser;
+import com.google.protobuf.UnknownFieldSet;
+
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Import data from a protocol buffer dump into the database.
+ * <p>
+ * Takes as input a file containing protocol buffers concatenated together with
+ * varint length encoding, as in {@link Parser#parseDelimitedFrom(InputStream)}.
+ * Each message contains a single field with a tag corresponding to the relation
+ * ID in the {@link com.google.gwtorm.server.Relation} annotation.
+ * <p>
+ * <strong>Warning</strong>: This method blindly upserts data into the database.
+ * It should only be used to restore a protobuf-formatted backup into a new,
+ * empty site.
+ */
+public class ProtobufImport extends SiteProgram {
+  @Option(name = "--file", aliases = {"-f"}, required = true, metaVar = "FILE",
+      usage = "File to import from")
+  private File file;
+
+  private final LifecycleManager manager = new LifecycleManager();
+  private final Map<Integer, Relation> relations = new HashMap<>();
+
+  @Inject
+  private SchemaFactory<ReviewDb> schemaFactory;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+
+    Injector dbInjector = createDbInjector(SINGLE_USER);
+    manager.add(dbInjector);
+    manager.start();
+    RuntimeShutdown.add(new Runnable() {
+      @Override
+      public void run() {
+        manager.stop();
+      }
+    });
+    dbInjector.injectMembers(this);
+
+    ProgressMonitor progress = new TextProgressMonitor();
+    progress.beginTask("Importing entities", ProgressMonitor.UNKNOWN);
+    try (ReviewDb db = schemaFactory.open()) {
+      for (RelationModel model
+          : new JavaSchemaModel(ReviewDb.class).getRelations()) {
+        relations.put(model.getRelationID(), Relation.create(model, db));
+      }
+
+      Parser<UnknownFieldSet> parser =
+          UnknownFieldSet.getDefaultInstance().getParserForType();
+      try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
+        UnknownFieldSet msg;
+        while ((msg = parser.parseDelimitedFrom(in)) != null) {
+          Map.Entry<Integer, UnknownFieldSet.Field> e =
+              Iterables.getOnlyElement(msg.asMap().entrySet());
+          Relation rel = checkNotNull(relations.get(e.getKey()),
+              "unknown relation ID %s in message: %s", e.getKey(), msg);
+          List<ByteString> values = e.getValue().getLengthDelimitedList();
+          checkState(values.size() == 1,
+            "expected one string field in message: %s", msg);
+          upsert(rel, values.get(0));
+          progress.update(1);
+        }
+      }
+      progress.endTask();
+    }
+
+    return 0;
+  }
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  private static void upsert(Relation rel, ByteString s)
+      throws OrmException {
+    Collection ents = Collections.singleton(rel.codec().decode(s));
+    try {
+      // Not all relations support update; fall back manually.
+      rel.access().insert(ents);
+    } catch (OrmDuplicateKeyException e) {
+      rel.access().delete(ents);
+      rel.access().insert(ents);
+    }
+  }
+
+  @AutoValue
+  abstract static class Relation {
+    private static Relation create(RelationModel model, ReviewDb db)
+        throws IllegalAccessException, InvocationTargetException,
+        NoSuchMethodException, ClassNotFoundException {
+      Method m = db.getClass().getMethod(model.getMethodName());
+      Class<?> clazz = Class.forName(model.getEntityTypeClassName());
+      return new AutoValue_ProtobufImport_Relation(
+          (Access<?, ?>) m.invoke(db),
+          CodecFactory.encoder(clazz));
+    }
+
+    abstract Access<?, ?> access();
+    abstract ProtobufCodec<?> codec();
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
index ab20f53..dd5fe0c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
@@ -25,6 +25,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -76,7 +77,8 @@
   private static final Logger log =
       LoggerFactory.getLogger(RebuildNotedb.class);
 
-  @Option(name = "--threads", usage = "Number of threads to use for indexing")
+  @Option(name = "--threads",
+      usage = "Number of threads to use for rebuilding NoteDb")
   private int threads = Runtime.getRuntime().availableProcessors();
 
   private Injector dbInjector;
@@ -113,22 +115,20 @@
         sysInjector.getInstance(GitRepositoryManager.class);
     final Project.NameKey allUsersName =
         sysInjector.getInstance(AllUsersName.class);
-    final Repository allUsersRepo =
-        repoManager.openMetadataRepository(allUsersName);
-    try {
+    try (Repository allUsersRepo =
+        repoManager.openMetadataRepository(allUsersName)) {
       deleteDraftRefs(allUsersRepo);
       for (final Project.NameKey project : changesByProject.keySet()) {
-        final Repository repo = repoManager.openMetadataRepository(project);
-        try {
+        try (Repository repo = repoManager.openMetadataRepository(project)) {
           final BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
           final BatchRefUpdate bruForDrafts =
               allUsersRepo.getRefDatabase().newBatchUpdate();
           List<ListenableFuture<?>> futures = Lists.newArrayList();
 
-          // Here, we truncate the project name to 50 characters to ensure that
+          // Here, we elide the project name to 50 characters to ensure that
           // the whole monitor line for a project fits on one line (<80 chars).
           final MultiProgressMonitor mpm = new MultiProgressMonitor(System.out,
-              truncateProjectName(project.get()));
+              FormatUtil.elide(project.get(), 50));
           final Task doneTask =
               mpm.beginSubTask("done", changesByProject.get(project).size());
           final Task failedTask =
@@ -143,7 +143,7 @@
                 MoreExecutors.directExecutor());
           }
 
-          mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
+          mpm.waitFor(Futures.transformAsync(Futures.successfulAsList(futures),
               new AsyncFunction<List<?>, Void>() {
                   @Override
                 public ListenableFuture<Void> apply(List<?> input)
@@ -158,12 +158,8 @@
           log.error("Error rebuilding notedb", e);
           ok.set(false);
           break;
-        } finally {
-          repo.close();
         }
       }
-    } finally {
-      allUsersRepo.close();
     }
 
     double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
@@ -172,17 +168,6 @@
     return ok.get() ? 0 : 1;
   }
 
-  private static String truncateProjectName(String projectName) {
-    int monitorStringMaxLength = 50;
-    String monitorString = (projectName.length() > monitorStringMaxLength)
-        ? projectName.substring(0, monitorStringMaxLength)
-        : projectName;
-    if (projectName.length() > monitorString.length()) {
-      monitorString = monitorString + "...";
-    }
-    return monitorString;
-  }
-
   private static void execute(BatchRefUpdate bru, Repository repo)
       throws IOException {
     try (RevWalk rw = new RevWalk(repo)) {
@@ -231,20 +216,17 @@
     // rebuilder threads to use the full connection pool.
     SchemaFactory<ReviewDb> schemaFactory = sysInjector.getInstance(Key.get(
         new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
-    ReviewDb db = schemaFactory.open();
     Multimap<Project.NameKey, Change> changesByProject =
         ArrayListMultimap.create();
-    try {
+    try (ReviewDb db = schemaFactory.open()) {
       for (Change c : db.changes().all()) {
         changesByProject.put(c.getProject(), c);
       }
       return changesByProject;
-    } finally {
-      db.close();
     }
   }
 
-  private class RebuildListener implements Runnable {
+  private static class RebuildListener implements Runnable {
     private Change.Id changeId;
     private ListenableFuture<?> future;
     private AtomicBoolean ok;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 44f80f2..00914d2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.SiteIndexer;
-import com.google.gerrit.solr.SolrIndexModule;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
@@ -64,9 +63,6 @@
   @Option(name = "--verbose", usage = "Output debug information for each change")
   private boolean verbose;
 
-  @Option(name = "--dry-run", usage = "Dry run: don't write anything to index")
-  private boolean dryRun;
-
   private Injector dbInjector;
   private Injector sysInjector;
   private Config globalConfig;
@@ -122,9 +118,6 @@
       case LUCENE:
         changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
         break;
-      case SOLR:
-        changeIndexModule = new SolrIndexModule(false, threads, outputBase);
-        break;
       default:
         throw new IllegalStateException("unsupported index.type");
     }
@@ -149,21 +142,18 @@
   }
 
   private int indexAll() throws Exception {
-    ReviewDb db = sysInjector.getInstance(ReviewDb.class);
     ProgressMonitor pm = new TextProgressMonitor();
     pm.start(1);
     pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
     Set<Project.NameKey> projects = Sets.newTreeSet();
     int changeCount = 0;
-    try {
+    try (ReviewDb db = sysInjector.getInstance(ReviewDb.class)) {
       for (Change change : db.changes().all()) {
         changeCount++;
         if (projects.add(change.getProject())) {
           pm.update(1);
         }
       }
-    } finally {
-      db.close();
     }
     pm.endTask();
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index 17a54d4..7134f49 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -16,16 +16,16 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.rules.PrologCompiler;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.rules.PrologCompiler;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
 
 import org.eclipse.jgit.lib.Repository;
 import org.kohsuke.args4j.Argument;
@@ -81,8 +81,7 @@
 
     boolean error = false;
     for (Project.NameKey project : names) {
-      Repository git = gitManager.openRepository(project);
-      try {
+      try (Repository git = gitManager.openRepository(project)) {
         switch (jarFactory.create(git).call()) {
           case NO_RULES:
             if (!all || projectNames.contains(project.get())) {
@@ -105,8 +104,6 @@
           System.err.println("fatal: " + err.getMessage());
         }
         error = true;
-      } finally {
-        git.close();
       }
     }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java
index f2feae1..ac84e82 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -19,7 +19,6 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
-import com.google.common.io.Files;
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.common.SiteLibraryLoaderUtil;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -37,8 +36,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import java.util.jar.JarFile;
@@ -47,7 +48,7 @@
 public class SwitchSecureStore extends SiteProgram {
   private static String getSecureStoreClassFromGerritConfig(SitePaths sitePaths) {
     FileBasedConfig cfg =
-        new FileBasedConfig(sitePaths.gerrit_config, FS.DETECTED);
+        new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
     try {
       cfg.load();
     } catch (IOException | ConfigInvalidException e) {
@@ -67,14 +68,14 @@
   @Override
   public int run() throws Exception {
     SitePaths sitePaths = new SitePaths(getSitePath());
-    File newSecureStoreFile = new File(newSecureStoreLib);
-    if (!newSecureStoreFile.exists()) {
-      log.error(String.format("File %s doesn't exists",
-          newSecureStoreFile.getAbsolutePath()));
+    Path newSecureStorePath = Paths.get(newSecureStoreLib);
+    if (!Files.exists(newSecureStorePath)) {
+      log.error(String.format("File %s doesn't exist",
+          newSecureStorePath.toAbsolutePath()));
       return -1;
     }
 
-    String newSecureStore = getNewSecureStoreClassName(newSecureStoreFile);
+    String newSecureStore = getNewSecureStoreClassName(newSecureStorePath);
     String currentSecureStoreName = getCurrentSecureStoreClassName(sitePaths);
 
     if (currentSecureStoreName.equals(newSecureStore)) {
@@ -83,7 +84,7 @@
       return -1;
     }
 
-    IoUtil.loadJARs(newSecureStoreFile);
+    IoUtil.loadJARs(newSecureStorePath);
     SiteLibraryLoaderUtil.loadSiteLib(sitePaths.lib_dir);
 
     log.info("Current secureStoreClass property ({}) will be replaced with {}",
@@ -96,7 +97,7 @@
     migrateProperties(currentStore, newStore);
 
     removeOldLib(sitePaths, currentSecureStoreName);
-    copyNewLib(sitePaths, newSecureStoreFile);
+    copyNewLib(sitePaths, newSecureStorePath);
 
     updateGerritConfig(sitePaths, newSecureStore);
 
@@ -123,14 +124,17 @@
     }
   }
 
-  private void removeOldLib(SitePaths sitePaths, String currentSecureStoreName) {
-    File oldSecureStore =
+  private void removeOldLib(SitePaths sitePaths, String currentSecureStoreName)
+      throws IOException {
+    Path oldSecureStore =
         findJarWithSecureStore(sitePaths, currentSecureStoreName);
     if (oldSecureStore != null) {
       log.info("Removing old SecureStore ({}) from lib/ directory",
-          oldSecureStore.getName());
-      if (!oldSecureStore.delete()) {
-        log.error("Cannot remove {}", oldSecureStore.getAbsolutePath());
+          oldSecureStore.getFileName());
+      try {
+        Files.delete(oldSecureStore);
+      } catch (IOException e) {
+        log.error("Cannot remove {}", oldSecureStore.toAbsolutePath(), e);
       }
     } else {
       log.info("Cannot find jar with old SecureStore ({}) in lib/ directory",
@@ -138,12 +142,12 @@
     }
   }
 
-  private void copyNewLib(SitePaths sitePaths, File newSecureStoreFile)
+  private void copyNewLib(SitePaths sitePaths, Path newSecureStorePath)
       throws IOException {
     log.info("Copy new SecureStore ({}) into lib/ directory",
-        newSecureStoreFile.getName());
-    Files.copy(newSecureStoreFile, new File(sitePaths.lib_dir,
-        newSecureStoreFile.getName()));
+        newSecureStorePath.getFileName());
+    Files.copy(newSecureStorePath,
+        sitePaths.lib_dir.resolve(newSecureStorePath.getFileName()));
   }
 
   private void updateGerritConfig(SitePaths sitePaths, String newSecureStore)
@@ -151,13 +155,13 @@
     log.info("Set gerrit.secureStoreClass property of gerrit.config to {}",
         newSecureStore);
     FileBasedConfig config =
-        new FileBasedConfig(sitePaths.gerrit_config, FS.DETECTED);
+        new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
     config.load();
     config.setString("gerrit", null, "secureStoreClass", newSecureStore);
     config.save();
   }
 
-  private String getNewSecureStoreClassName(File secureStore)
+  private String getNewSecureStoreClassName(Path secureStore)
       throws IOException {
     JarScanner scanner = new JarScanner(secureStore);
     List<String> newSecureStores =
@@ -165,12 +169,12 @@
     if (newSecureStores.isEmpty()) {
       throw new RuntimeException(String.format(
           "Cannot find implementation of SecureStore interface in %s",
-          secureStore.getAbsolutePath()));
+          secureStore.toAbsolutePath()));
     }
     if (newSecureStores.size() > 1) {
       throw new RuntimeException(String.format(
           "Found too many implementations of SecureStore:\n%s\nin %s", Joiner
-              .on("\n").join(newSecureStores), secureStore.getAbsolutePath()));
+              .on("\n").join(newSecureStores), secureStore.toAbsolutePath()));
     }
     return Iterables.getOnlyElement(newSecureStores);
   }
@@ -195,15 +199,12 @@
     }
   }
 
-  private File findJarWithSecureStore(SitePaths sitePaths,
-      String secureStoreClass) {
-    File[] jars = SiteLibraryLoaderUtil.listJars(sitePaths.lib_dir);
-    if (jars == null || jars.length == 0) {
-      return null;
-    }
+  private Path findJarWithSecureStore(SitePaths sitePaths,
+      String secureStoreClass) throws IOException {
+    List<Path> jars = SiteLibraryLoaderUtil.listJars(sitePaths.lib_dir);
     String secureStoreClassPath = secureStoreClass.replace('.', '/') + ".class";
-    for (File jar : jars) {
-      try (JarFile jarFile = new JarFile(jar)) {
+    for (Path jar : jars) {
+      try (JarFile jarFile = new JarFile(jar.toFile())) {
         ZipEntry entry = jarFile.getEntry(secureStoreClassPath);
         if (entry != null) {
           return jar;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
index 3b0a590..1b663ae 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.pgm.http.jetty;
 
-import com.google.common.base.Charsets;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
 import com.google.common.base.Strings;
 import com.google.gwtexpui.server.CacheHeaders;
 
@@ -56,11 +57,8 @@
     try {
       CacheHeaders.setNotCacheable(res);
     } finally {
-      ServletOutputStream out = res.getOutputStream();
-      try {
+      try (ServletOutputStream out = res.getOutputStream()) {
         out.write(msg);
-      } finally {
-        out.close();
       }
     }
   }
@@ -71,7 +69,7 @@
       msg = HttpStatus.getMessage(conn.getHttpChannel()
           .getResponse().getStatus());
     }
-    return msg.getBytes(Charsets.ISO_8859_1);
+    return msg.getBytes(ISO_8859_1);
   }
 
   private static void log(HttpServletRequest req) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 907624d..0684650 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -17,21 +17,12 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.common.base.Charsets;
-import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
-import com.google.common.escape.Escaper;
-import com.google.common.html.HtmlEscapers;
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtexpui.linker.server.UserAgentRule;
-import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
@@ -60,46 +51,26 @@
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.BlockingArrayQueue;
 import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.resource.Resource;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.util.thread.ThreadPool;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.io.PrintWriter;
 import java.lang.management.ManagementFactory;
-import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.EnumSet;
-import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Properties;
 import java.util.Set;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 
 import javax.servlet.DispatcherType;
 import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 
 @Singleton
 public class JettyServer {
@@ -155,13 +126,9 @@
 
   private boolean reverseProxy;
 
-  /** Location on disk where our WAR file was unpacked to. */
-  private Resource baseResource;
-
   @Inject
   JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
-      final JettyEnv env, final HttpLogFactory httpLogFactory)
-      throws MalformedURLException, IOException {
+      final JettyEnv env, final HttpLogFactory httpLogFactory) {
     this.site = site;
 
     httpd = new Server(threadPool(cfg));
@@ -220,22 +187,22 @@
 
       } else if ("https".equals(u.getScheme())) {
         SslContextFactory ssl = new SslContextFactory();
-        final File keystore = getFile(cfg, "sslkeystore", "etc/keystore");
+        final Path keystore = getFile(cfg, "sslkeystore", "etc/keystore");
         String password = cfg.getString("httpd", null, "sslkeypassword");
         if (password == null) {
           password = "gerrit";
         }
-        ssl.setKeyStorePath(keystore.getAbsolutePath());
-        ssl.setTrustStorePath(keystore.getAbsolutePath());
+        ssl.setKeyStorePath(keystore.toAbsolutePath().toString());
+        ssl.setTrustStorePath(keystore.toAbsolutePath().toString());
         ssl.setKeyStorePassword(password);
         ssl.setTrustStorePassword(password);
 
         if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
           ssl.setNeedClientAuth(true);
 
-          File crl = getFile(cfg, "sslcrl", "etc/crl.pem");
-          if (crl.exists()) {
-            ssl.setCrlPath(crl.getAbsolutePath());
+          Path crl = getFile(cfg, "sslcrl", "etc/crl.pem");
+          if (Files.exists(crl)) {
+            ssl.setCrlPath(crl.toAbsolutePath().toString());
             ssl.setValidatePeerCerts(true);
           }
         }
@@ -340,7 +307,7 @@
     return r;
   }
 
-  private File getFile(final Config cfg, final String name, final String def) {
+  private Path getFile(Config cfg, String name, String def) {
     String path = cfg.getString("httpd", null, name);
     if (path == null || path.length() == 0) {
       path = def;
@@ -351,7 +318,7 @@
   private ThreadPool threadPool(Config cfg) {
     int maxThreads = cfg.getInt("httpd", null, "maxthreads", 25);
     int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
-    int maxQueued = cfg.getInt("httpd", null, "maxqueued", 50);
+    int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
     int idleTimeout = (int)MILLISECONDS.convert(60, SECONDS);
     int maxCapacity = maxQueued == 0
         ? Integer.MAX_VALUE
@@ -369,8 +336,7 @@
     return pool;
   }
 
-  private Handler makeContext(final JettyEnv env, final Config cfg)
-      throws MalformedURLException, IOException {
+  private Handler makeContext(final JettyEnv env, final Config cfg) {
     final Set<String> paths = new HashSet<>();
     for (URI u : listenURLs(cfg)) {
       String p = u.getPath();
@@ -405,7 +371,7 @@
   }
 
   private ContextHandler makeContext(final String contextPath,
-      final JettyEnv env, final Config cfg) throws MalformedURLException, IOException {
+      final JettyEnv env, final Config cfg) {
     final ServletContextHandler app = new ServletContextHandler();
 
     // This enables the use of sessions in Jetty, feature available
@@ -418,12 +384,6 @@
     //
     app.setContextPath(contextPath);
 
-    // Serve static resources directly from our JAR. This way we don't
-    // need to unpack them into yet another temporary directory prior to
-    // serving to clients.
-    //
-    app.setBaseResource(getBaseResource(app));
-
     // HTTP front-end filter to be used as surrogate of Apache HTTP
     // reverse-proxy filtering.
     // It is meant to be used as simpler tiny deployment of custom-made
@@ -475,234 +435,4 @@
     app.setWelcomeFiles(new String[0]);
     return app;
   }
-
-  private Resource getBaseResource(ServletContextHandler app)
-      throws IOException {
-    if (baseResource == null) {
-      try {
-        baseResource = unpackWar(GerritLauncher.getDistributionArchive());
-      } catch (FileNotFoundException err) {
-        if (GerritLauncher.NOT_ARCHIVED.equals(err.getMessage())) {
-          baseResource = useDeveloperBuild(app);
-        } else {
-          throw err;
-        }
-      }
-    }
-    return baseResource;
-  }
-
-  private static Resource unpackWar(File srcwar) throws IOException {
-    File dstwar = makeWarTempDir();
-    unpack(srcwar, dstwar);
-    return Resource.newResource(dstwar.toURI());
-  }
-
-  private static File makeWarTempDir() throws IOException {
-    // Obtain our local temporary directory, but it comes back as a file
-    // so we have to switch it to be a directory post creation.
-    //
-    File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
-    if (!dstwar.delete() || !dstwar.mkdir()) {
-      throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
-    }
-
-    // Jetty normally refuses to serve out of a symlinked directory, as
-    // a security feature. Try to resolve out any symlinks in the path.
-    //
-    try {
-      return dstwar.getCanonicalFile();
-    } catch (IOException e) {
-      return dstwar.getAbsoluteFile();
-    }
-  }
-
-  private static void unpack(File srcwar, File dstwar) throws IOException {
-    final ZipFile zf = new ZipFile(srcwar);
-    try {
-      final Enumeration<? extends ZipEntry> e = zf.entries();
-      while (e.hasMoreElements()) {
-        final ZipEntry ze = e.nextElement();
-        final String name = ze.getName();
-
-        if (ze.isDirectory()) continue;
-        if (name.startsWith("WEB-INF/")) continue;
-        if (name.startsWith("META-INF/")) continue;
-        if (name.startsWith("com/google/gerrit/launcher/")) continue;
-        if (name.equals("Main.class")) continue;
-
-        final File rawtmp = new File(dstwar, name);
-        mkdir(rawtmp.getParentFile());
-        rawtmp.deleteOnExit();
-
-        final FileOutputStream rawout = new FileOutputStream(rawtmp);
-        try {
-          final InputStream in = zf.getInputStream(ze);
-          try {
-            final byte[] buf = new byte[4096];
-            int n;
-            while ((n = in.read(buf, 0, buf.length)) > 0) {
-              rawout.write(buf, 0, n);
-            }
-          } finally {
-            in.close();
-          }
-        } finally {
-          rawout.close();
-        }
-      }
-    } finally {
-      zf.close();
-    }
-  }
-
-  private static void mkdir(File dir) throws IOException {
-    if (!dir.isDirectory()) {
-      mkdir(dir.getParentFile());
-      if (!dir.mkdir())
-        throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
-      dir.deleteOnExit();
-    }
-  }
-
-  private Resource useDeveloperBuild(ServletContextHandler app)
-      throws IOException {
-    final File dir = GerritLauncher.getDeveloperBuckOut();
-    final File gen = new File(dir, "gen");
-    final File root = dir.getParentFile();
-    final File dstwar = makeWarTempDir();
-    File ui = new File(dstwar, "gerrit_ui");
-    File p = new File(ui, "permutations");
-    mkdir(ui);
-    p.createNewFile();
-    p.deleteOnExit();
-
-    app.addFilter(new FilterHolder(new Filter() {
-      private final boolean gwtuiRecompile =
-          System.getProperty("gerrit.disable-gwtui-recompile") == null;
-      private final UserAgentRule rule = new UserAgentRule();
-      private final Set<String> uaInitialized = new HashSet<>();
-      private String lastTarget;
-      private long lastTime;
-
-      @Override
-      public void doFilter(ServletRequest request, ServletResponse res,
-          FilterChain chain) throws IOException, ServletException {
-        String pkg = "gerrit-gwtui";
-        String target = "ui_" + rule.select((HttpServletRequest) request);
-        if (gwtuiRecompile || !uaInitialized.contains(target)) {
-          String rule = "//" + pkg + ":" + target;
-          // TODO(davido): instead of assuming specific Buck's internal
-          // target directory for gwt_binary() artifacts, ask Buck for
-          // the location of user agent permutation GWT zip, e. g.:
-          // $ buck targets --show_output //gerrit-gwtui:ui_safari \
-          //    | awk '{print $2}'
-          String child = String.format("%s/__gwt_binary_%s__", pkg, target);
-          File zip = new File(new File(gen, child), target + ".zip");
-
-          synchronized (this) {
-            try {
-              build(root, gen, rule);
-            } catch (BuildFailureException e) {
-              displayFailure(rule, e.why, (HttpServletResponse) res);
-              return;
-            }
-
-            if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
-              lastTarget = target;
-              lastTime = zip.lastModified();
-              unpack(zip, dstwar);
-            }
-          }
-          uaInitialized.add(target);
-        }
-        chain.doFilter(request, res);
-      }
-
-      private void displayFailure(String rule, byte[] why, HttpServletResponse res)
-          throws IOException {
-        res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        res.setContentType("text/html");
-        res.setCharacterEncoding(Charsets.UTF_8.name());
-        CacheHeaders.setNotCacheable(res);
-
-        Escaper html = HtmlEscapers.htmlEscaper();
-        PrintWriter w = res.getWriter();
-        w.write("<html><title>BUILD FAILED</title><body>");
-        w.format("<h1>%s FAILED</h1>", html.escape(rule));
-        w.write("<pre>");
-        w.write(html.escape(RawParseUtils.decode(why)));
-        w.write("</pre>");
-        w.write("</body></html>");
-        w.close();
-      }
-
-      @Override
-      public void init(FilterConfig config) {
-      }
-
-      @Override
-      public void destroy() {
-      }
-    }), "/", EnumSet.of(DispatcherType.REQUEST));
-    return Resource.newResource(dstwar.toURI());
-  }
-
-  private static void build(File root, File gen, String target)
-      throws IOException, BuildFailureException {
-    log.info("buck build " + target);
-    Properties properties = loadBuckProperties(gen);
-    String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
-    ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
-        .directory(root)
-        .redirectErrorStream(true);
-    if (properties.containsKey("PATH")) {
-      proc.environment().put("PATH", properties.getProperty("PATH"));
-    }
-    long start = TimeUtil.nowMs();
-    Process rebuild = proc.start();
-    byte[] out;
-    InputStream in = rebuild.getInputStream();
-    try {
-      out = ByteStreams.toByteArray(in);
-    } finally {
-      rebuild.getOutputStream().close();
-      in.close();
-    }
-
-    int status;
-    try {
-      status = rebuild.waitFor();
-    } catch (InterruptedException e) {
-      throw new InterruptedIOException("interrupted waiting for " + buck);
-    }
-    if (status != 0) {
-      throw new BuildFailureException(out);
-    }
-
-    long time = TimeUtil.nowMs() - start;
-    log.info(String.format("UPDATED    %s in %.3fs", target, time / 1000.0));
-  }
-
-  private static Properties loadBuckProperties(File gen)
-      throws FileNotFoundException, IOException {
-    Properties properties = new Properties();
-    InputStream in = new FileInputStream(
-        new File(new File(gen, "tools"), "buck.properties"));
-    try {
-      properties.load(in);
-    } finally {
-      in.close();
-    }
-    return properties;
-  }
-
-  @SuppressWarnings("serial")
-  private static class BuildFailureException extends Exception {
-    final byte[] why;
-
-    BuildFailureException(byte[] why) {
-      this.why = why;
-    }
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index 7730fa5..0f75a09 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -20,7 +20,6 @@
 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
@@ -84,17 +83,17 @@
     }
   }
 
-  private final Provider<CurrentUser> userProvider;
+  private final Provider<CurrentUser> user;
   private final QueueProvider queue;
 
   private final ServletContext context;
   private final long maxWait;
 
   @Inject
-  ProjectQoSFilter(final Provider<CurrentUser> userProvider,
+  ProjectQoSFilter(final Provider<CurrentUser> user,
       QueueProvider queue, final ServletContext context,
       @GerritServerConfig final Config cfg) {
-    this.userProvider = userProvider;
+    this.user = user;
     this.queue = queue;
     this.context = context;
     this.maxWait = MINUTES.toMillis(getTimeUnit(cfg, "httpd", null, "maxwait", 5, MINUTES));
@@ -142,7 +141,7 @@
   }
 
   private WorkQueue.Executor getExecutor() {
-    return queue.getQueue(userProvider.get().getCapabilities().getQueueType());
+    return queue.getQueue(user.get().getCapabilities().getQueueType());
   }
 
   @Override
@@ -226,9 +225,9 @@
     private String generateName(HttpServletRequest req) {
       String userName = "";
 
-      CurrentUser who = userProvider.get();
+      CurrentUser who = user.get();
       if (who.isIdentifiedUser()) {
-        String name = ((IdentifiedUser) who).getUserName();
+        String name = who.asIdentifiedUser().getUserName();
         if (name != null && !name.isEmpty()) {
           userName = " (" + name + ")";
         }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 57c6b9d..b200ed5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -57,9 +57,14 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -87,12 +92,12 @@
     this.pluginsToInstall = pluginsToInstall;
   }
 
-  public BaseInit(File sitePath, boolean standalone, boolean initDb,
+  public BaseInit(Path sitePath, boolean standalone, boolean initDb,
       PluginsDistribution pluginsDistribution, List<String> pluginsToInstall) {
     this(sitePath, null, standalone, initDb, pluginsDistribution, pluginsToInstall);
   }
 
-  public BaseInit(File sitePath, final Provider<DataSource> dsProvider,
+  public BaseInit(Path sitePath, final Provider<DataSource> dsProvider,
       boolean standalone, boolean initDb,
       PluginsDistribution pluginsDistribution, List<String> pluginsToInstall) {
     super(sitePath, dsProvider);
@@ -110,6 +115,7 @@
     }
 
     init.flags.autoStart = getAutoStart() && init.site.isNew;
+    init.flags.dev = isDev() && init.site.isNew;
     init.flags.skipPlugins = skipPlugins();
 
     final SiteRun run;
@@ -121,19 +127,14 @@
       run.upgradeSchema();
 
       init.initializer.postRun(createSysInjector(init));
-    } catch (Exception failure) {
-      if (init.flags.deleteOnFailure) {
-        recursiveDelete(getSitePath());
-      }
-      throw failure;
-    } catch (Error failure) {
+    } catch (Exception | Error failure) {
       if (init.flags.deleteOnFailure) {
         recursiveDelete(getSitePath());
       }
       throw failure;
     }
 
-    System.err.println("Initialized " + getSitePath().getCanonicalPath());
+    System.err.println("Initialized " + getSitePath().toRealPath().normalize());
     afterInit(run);
     return 0;
   }
@@ -209,19 +210,18 @@
 
   private SiteInit createSiteInit() {
     final ConsoleUI ui = getConsoleUI();
-    final File sitePath = getSitePath();
+    final Path sitePath = getSitePath();
     final List<Module> m = new ArrayList<>();
     final SecureStoreInitData secureStoreInitData = discoverSecureStoreClass();
     final String currentSecureStoreClassName = getConfiguredSecureStoreClass();
 
     if (secureStoreInitData != null && currentSecureStoreClassName != null
         && !currentSecureStoreClassName.equals(secureStoreInitData.className)) {
-      String err =
-          String.format(
-              "Different secure store was previously configured: %s. "
-              + "Use SwitchSecureStore program to switch between implementations.",
-              currentSecureStoreClassName);
-      die(err, new RuntimeException("secure store mismatch"));
+      String err = String.format(
+          "Different secure store was previously configured: %s. "
+          + "Use SwitchSecureStore program to switch between implementations.",
+          currentSecureStoreClassName);
+      throw die(err);
     }
 
     m.add(new GerritServerConfigModule());
@@ -230,7 +230,7 @@
       @Override
       protected void configure() {
         bind(ConsoleUI.class).toInstance(ui);
-        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
         List<String> plugins =
             MoreObjects.firstNonNull(
                 getInstallPlugins(), Lists.<String> newArrayList());
@@ -289,8 +289,8 @@
     }
 
     try {
-      File secureStoreLib = new File(secureStore);
-      if (!secureStoreLib.exists()) {
+      Path secureStoreLib = Paths.get(secureStore);
+      if (!Files.exists(secureStoreLib)) {
         throw new InvalidSecureStoreException(String.format(
             "File %s doesn't exist", secureStore));
       }
@@ -410,15 +410,45 @@
     return sysInjector;
   }
 
-  private static void recursiveDelete(File path) {
-    File[] entries = path.listFiles();
-    if (entries != null) {
-      for (File e : entries) {
-        recursiveDelete(e);
-      }
+  private static void recursiveDelete(Path path) {
+    final String msg = "warn: Cannot remove ";
+    try {
+      Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+        @Override
+        public FileVisitResult visitFile(Path f, BasicFileAttributes attrs)
+            throws IOException {
+          try {
+            Files.delete(f);
+          } catch (IOException e) {
+            System.err.println(msg + f);
+          }
+          return FileVisitResult.CONTINUE;
+        }
+
+        @Override
+        public FileVisitResult postVisitDirectory(Path dir, IOException err) {
+          try {
+            // Previously warned if err was not null; if dir is not empty as a
+            // result, will cause an error that will be logged below.
+            Files.delete(dir);
+          } catch (IOException e) {
+            System.err.println(msg + dir);
+          }
+          return FileVisitResult.CONTINUE;
+        }
+
+        @Override
+        public FileVisitResult visitFileFailed(Path f, IOException e) {
+          System.err.println(msg + f);
+          return FileVisitResult.CONTINUE;
+        }
+      });
+    } catch (IOException e) {
+      System.err.println(msg + path);
     }
-    if (!path.delete() && path.exists()) {
-      System.err.println("warn: Cannot remove " + path);
-    }
+  }
+
+  protected boolean isDev() {
+    return false;
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
index 5a1eab2..41cb87e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
@@ -31,7 +31,7 @@
   private final Config cfg;
 
   @Inject
-  Browser(final @GerritServerConfig Config cfg) {
+  Browser(@GerritServerConfig final Config cfg) {
     this.cfg = cfg;
   }
 
@@ -75,6 +75,7 @@
         try {
           Thread.sleep(100);
         } catch (InterruptedException ie) {
+          // Ignored
         }
         continue;
       }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DB2Initializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DB2Initializer.java
new file mode 100644
index 0000000..3f6abcf
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DB2Initializer.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2015 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.pgm.init;
+
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
+
+import com.google.gerrit.pgm.init.api.Section;
+
+
+public class DB2Initializer implements DatabaseConfigInitializer {
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    final String defPort = "50001";
+    databaseSection.string("Server hostname", "hostname", "localhost");
+    databaseSection.string("Server port", "port", defPort, false);
+    databaseSection.string("Database name", "database", "gerrit");
+    databaseSection.string("Database username", "username", username());
+    databaseSection.password("username", "password");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
index 607d6b4..a11f56f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
@@ -30,6 +30,10 @@
   protected void configure() {
     bind(SitePaths.class).toInstance(site);
     bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("db2")).to(DB2Initializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("derby")).to(DerbyInitializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("h2")).to(H2Initializer.class);
     bind(DatabaseConfigInitializer.class).annotatedWith(
         Names.named("jdbc")).to(JDBCInitializer.class);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java
new file mode 100644
index 0000000..4d710f1
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 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.pgm.init;
+
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+
+import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import java.nio.file.Path;
+
+class DerbyInitializer implements DatabaseConfigInitializer {
+
+  private final SitePaths site;
+
+  @Inject
+  DerbyInitializer(final SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    String path = databaseSection.get("database");
+    Path db;
+    if (path == null) {
+      db = site.resolve("db").resolve("ReviewDB");
+      databaseSection.set("database", db.toString());
+    } else {
+      db = site.resolve(path);
+    }
+    if (db == null) {
+      throw die("database.database must be supplied for Derby");
+    }
+    db = db.getParent();
+    FileUtil.mkdirsOrDie(db, "cannot create database.database");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
index e20346a..6d60ad1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.gerrit.pgm.init.api.InitUtil;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
-import java.io.File;
+import java.nio.file.Path;
 
 class H2Initializer implements DatabaseConfigInitializer {
 
@@ -33,18 +35,17 @@
   @Override
   public void initConfig(Section databaseSection) {
     String path = databaseSection.get("database");
+    Path db;
     if (path == null) {
-      path = "db/ReviewDB";
-      databaseSection.set("database", path);
+      db = site.resolve("db").resolve("ReviewDB");
+      databaseSection.set("database", db.toString());
+    } else {
+      db = site.resolve(path);
     }
-    File db = site.resolve(path);
     if (db == null) {
-      throw InitUtil.die("database.database must be supplied for H2");
+      throw die("database.database must be supplied for H2");
     }
-    db = db.getParentFile();
-    if (!db.exists() && !db.mkdirs()) {
-      throw InitUtil.die("cannot create database.database "
-          + db.getAbsolutePath());
-    }
+    db = db.getParent();
+    FileUtil.mkdirsOrDie(db, "cannot create database.database");
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 3a4d0f2..40a07b4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -24,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
@@ -31,6 +34,10 @@
 
 import org.apache.commons.validator.routines.EmailValidator;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Collections;
 
 public class InitAdminUser implements InitStep {
@@ -63,16 +70,16 @@
       return;
     }
 
-    ReviewDb db = dbFactory.open();
-    try {
+    try (ReviewDb db = dbFactory.open()) {
       if (db.accounts().anyAccounts().toList().isEmpty()) {
         ui.header("Gerrit Administrator");
         if (ui.yesno(true, "Create administrator user")) {
           Account.Id id = new Account.Id(db.nextAccountId());
           String username = ui.readString("admin", "username");
           String name = ui.readString("Administrator", "name");
-          String email = readEmail();
           String httpPassword = ui.readString("secret", "HTTP password");
+          AccountSshKey sshKey = readSshKey(id);
+          String email = readEmail(sshKey);
 
           AccountExternalId extUser =
               new AccountExternalId(id, new AccountExternalId.Key(
@@ -101,19 +108,56 @@
               new AccountGroupMember(new AccountGroupMember.Key(id,
                   adminGroup.getId()));
           db.accountGroupMembers().insert(Collections.singleton(m));
+
+          if (sshKey != null) {
+            db.accountSshKeys().insert(Collections.singleton(sshKey));
+          }
         }
       }
-    } finally {
-      db.close();
     }
   }
 
-  private String readEmail() {
-    String email = ui.readString("admin@example.com", "email");
+  private String readEmail(AccountSshKey sshKey) {
+    String defaultEmail = "admin@example.com";
+    if (sshKey != null && sshKey.getComment() != null) {
+     String c = sshKey.getComment().trim();
+     if (EmailValidator.getInstance().isValid(c)) {
+       defaultEmail = c;
+     }
+    }
+    return readEmail(defaultEmail);
+  }
+
+  private String readEmail(String defaultEmail) {
+    String email = ui.readString(defaultEmail, "email");
     if (email != null && !EmailValidator.getInstance().isValid(email)) {
       ui.message("error: invalid email address\n");
-      return readEmail();
+      return readEmail(defaultEmail);
     }
     return email;
   }
+
+  private AccountSshKey readSshKey(Account.Id id) throws IOException {
+    String defaultPublicSshKeyFile = "";
+    Path defaultPublicSshKeyPath =
+        Paths.get(System.getProperty("user.home"), ".ssh", "id_rsa.pub");
+    if (Files.exists(defaultPublicSshKeyPath)) {
+      defaultPublicSshKeyFile = defaultPublicSshKeyPath.toString();
+    }
+    String publicSshKeyFile =
+        ui.readString(defaultPublicSshKeyFile, "public SSH key file");
+    return !Strings.isNullOrEmpty(publicSshKeyFile)
+        ? createSshKey(id, publicSshKeyFile) : null;
+  }
+
+  private AccountSshKey createSshKey(Account.Id id, String keyFile)
+      throws IOException {
+    Path p = Paths.get(keyFile);
+    if (!Files.exists(p)) {
+      throw new IOException(String.format(
+          "Cannot add public SSH key: %s is not a file", keyFile));
+    }
+    String content = new String(Files.readAllBytes(p), UTF_8);
+    return new AccountSshKey(new AccountSshKey.Id(id, 0), content);
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index 5c716ef..6b30f80 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.pgm.init.api.InitUtil.dnOf;
 
 import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -27,25 +28,45 @@
 /** Initialize the {@code auth} configuration section. */
 @Singleton
 class InitAuth implements InitStep {
+  private static final String RECEIVE = "receive";
+  private static final String ENABLE_SIGNED_PUSH = "enableSignedPush";
+
   private final ConsoleUI ui;
   private final Section auth;
   private final Section ldap;
+  private final Section receive;
+  private final Libraries libraries;
+  private final InitFlags flags;
 
   @Inject
-  InitAuth(final ConsoleUI ui, final Section.Factory sections) {
+  InitAuth(InitFlags flags,
+      ConsoleUI ui,
+      Libraries libraries,
+      Section.Factory sections) {
+    this.flags = flags;
     this.ui = ui;
     this.auth = sections.get("auth", null);
     this.ldap = sections.get("ldap", null);
+    this.receive = sections.get(RECEIVE, null);
+    this.libraries = libraries;
   }
 
   @Override
   public void run() {
     ui.header("User Authentication");
 
-    final AuthType auth_type =
-        auth.select("Authentication method", "type", AuthType.OPENID);
+    initAuthType();
+    if (auth.getSecure("registerEmailPrivateKey") == null) {
+      auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey());
+    }
 
-    switch (auth_type) {
+    initSignedPush();
+  }
+
+  private void initAuthType() {
+    AuthType authType = auth.select("Authentication method", "type",
+        flags.dev ? AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT : AuthType.OPENID);
+    switch (authType) {
       case HTTP:
       case HTTP_LDAP: {
         String hdr = auth.get("httpHeader");
@@ -69,7 +90,7 @@
         break;
     }
 
-    switch (auth_type) {
+    switch (authType) {
       case LDAP:
       case LDAP_BIND:
       case HTTP_LDAP: {
@@ -103,13 +124,15 @@
       case OPENID_SSO:
         break;
     }
+  }
 
-    if (auth.getSecure("registerEmailPrivateKey") == null) {
-      auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey());
-    }
-
-    if (auth.getSecure("restTokenPrivateKey") == null) {
-      auth.setSecure("restTokenPrivateKey", SignedToken.generateRandomKey());
+  private void initSignedPush() {
+    boolean def = flags.cfg.getBoolean(RECEIVE, ENABLE_SIGNED_PUSH, false);
+    boolean enable = ui.yesno(def, "Enable signed push support");
+    receive.set("enableSignedPush", Boolean.toString(enable));
+    if (enable) {
+      libraries.bouncyCastleProvider.downloadRequired();
+      libraries.bouncyCastlePGP.downloadRequired();
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
index 8da4a03..4e5d044 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
@@ -14,15 +14,14 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.api.InitUtil.die;
-
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import java.io.File;
+import java.nio.file.Path;
 
 /** Initialize the {@code cache} configuration section. */
 @Singleton
@@ -52,10 +51,8 @@
       cache.set("directory", path);
     }
 
-    final File loc = site.resolve(path);
-    if (!loc.exists() && !loc.mkdirs()) {
-      throw die("cannot create cache.directory " + loc.getAbsolutePath());
-    }
+    Path loc = site.resolve(path);
+    FileUtil.mkdirsOrDie(loc, "cannot create cache.directory");
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
index f830854..36754a1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.pgm.init.api.InitUtil.die;
 import static com.google.gerrit.pgm.init.api.InitUtil.username;
 
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -26,13 +27,13 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.internal.storage.file.LockFile;
-import org.eclipse.jgit.util.FS;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /** Initialize the {@code container} configuration section. */
 @Singleton
@@ -56,9 +57,9 @@
     container.string("Run as", "user", username());
     container.string("Java runtime", "javaHome", javaHome());
 
-    File myWar;
+    Path myWar;
     try {
-      myWar = GerritLauncher.getDistributionArchive();
+      myWar = GerritLauncher.getDistributionArchive().toPath();
     } catch (FileNotFoundException e) {
       System.err.println("warn: Cannot find distribution archive (e.g. gerrit.war)");
       myWar = null;
@@ -66,53 +67,41 @@
 
     String path = container.get("war");
     if (path != null) {
-      path = container.string("Gerrit runtime", "war", //
-          myWar != null ? myWar.getAbsolutePath() : null);
+      path = container.string("Gerrit runtime", "war",
+          myWar != null ? myWar.toAbsolutePath().toString() : null);
       if (path == null || path.isEmpty()) {
         throw die("container.war is required");
       }
 
     } else if (myWar != null) {
       final boolean copy;
-      final File siteWar = site.gerrit_war;
-      if (siteWar.exists()) {
-        copy = ui.yesno(true, "Upgrade %s", siteWar.getPath());
+      final Path siteWar = site.gerrit_war;
+      if (Files.exists(siteWar)) {
+        copy = ui.yesno(true, "Upgrade %s", siteWar);
       } else {
-        copy = ui.yesno(true, "Copy %s to %s", myWar.getName(), siteWar.getPath());
+        copy = ui.yesno(true, "Copy %s to %s", myWar.getFileName(), siteWar);
         if (copy) {
           container.unset("war");
         } else {
-          container.set("war", myWar.getAbsolutePath());
+          container.set("war", myWar.toAbsolutePath().toString());
         }
       }
       if (copy) {
         if (!ui.isBatch()) {
-          System.err.format("Copying %s to %s", myWar.getName(), siteWar.getPath());
+          System.err.format("Copying %s to %s", myWar.getFileName(), siteWar);
           System.err.println();
         }
 
-        FileInputStream in = new FileInputStream(myWar);
-        try {
-          siteWar.getParentFile().mkdirs();
+        try (InputStream in = Files.newInputStream(myWar)) {
+          Files.createDirectories(siteWar.getParent());
 
-          LockFile lf = new LockFile(siteWar, FS.DETECTED);
+          LockFile lf = new LockFile(siteWar.toFile());
           if (!lf.lock()) {
             throw new IOException("Cannot lock " + siteWar);
           }
-
           try {
-            final OutputStream out = lf.getOutputStream();
-            try {
-              final byte[] tmp = new byte[4096];
-              for (;;) {
-                int n = in.read(tmp);
-                if (n < 0) {
-                  break;
-                }
-                out.write(tmp, 0, n);
-              }
-            } finally {
-              out.close();
+            try (OutputStream out = lf.getOutputStream()) {
+              ByteStreams.copy(in, out);
             }
             if (!lf.commit()) {
               throw new IOException("Cannot commit " + siteWar);
@@ -120,8 +109,6 @@
           } finally {
             lf.unlock();
           }
-        } finally {
-          in.close();
         }
       }
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index 3fcc911..abea521 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -84,6 +84,8 @@
       libraries.mysqlDriver.downloadRequired();
     } else if (dci instanceof OracleInitializer) {
       libraries.oracleDriver.downloadRequired();
+    } else if (dci instanceof DB2Initializer) {
+      libraries.db2Driver.downloadRequired();
     }
 
     dci.initConfig(database);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
index 067b103..d8fd509 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
@@ -16,13 +16,14 @@
 
 import static com.google.gerrit.pgm.init.api.InitUtil.die;
 
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import java.io.File;
+import java.nio.file.Path;
 
 /** Initialize the GitRepositoryManager configuration section. */
 @Singleton
@@ -40,13 +41,11 @@
   public void run() {
     ui.header("Git Repositories");
 
-    File d = gerrit.path("Location of Git repositories", "basePath", "git");
+    Path d = gerrit.path("Location of Git repositories", "basePath", "git");
     if (d == null) {
       throw die("gerrit.basePath is required");
     }
-    if (!d.exists() && !d.mkdirs()) {
-      throw die("Cannot create " + d);
-    }
+    FileUtil.mkdirsOrDie(d, "Cannot create");
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
index c8f1cd7..a907d46 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
@@ -29,10 +29,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import java.io.File;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /** Initialize the {@code httpd} configuration section. */
 @Singleton
@@ -57,7 +58,8 @@
   public void run() throws IOException, InterruptedException {
     ui.header("HTTP Daemon");
 
-    boolean proxy = false, ssl = false;
+    boolean proxy = false;
+    boolean ssl = false;
     String address = "*";
     int port = -1;
     String context = "/";
@@ -149,8 +151,9 @@
       return;
     }
 
-    final File store = site.ssl_keystore;
-    if (!ui.yesno(!store.exists(), "Create new self-signed SSL certificate")) {
+    Path store = site.ssl_keystore;
+    if (!ui.yesno(!Files.exists(store),
+        "Create new self-signed SSL certificate")) {
       return;
     }
 
@@ -167,15 +170,17 @@
     final String dname =
         "CN=" + hostname + ",OU=Gerrit Code Review,O=" + domainOf(hostname);
 
-    final File tmpdir = new File(site.etc_dir, "tmp.sslcertgen");
-    if (!tmpdir.mkdir()) {
-      throw die("Cannot create directory " + tmpdir);
+    Path tmpdir = site.etc_dir.resolve("tmp.sslcertgen");
+    try {
+      Files.createDirectory(tmpdir);
+    } catch (IOException e) {
+      throw die("Cannot create directory " + tmpdir, e);
     }
     chmod(0600, tmpdir);
 
-    final File tmpstore = new File(tmpdir, "keystore");
+    Path tmpstore = tmpdir.resolve("keystore");
     Runtime.getRuntime().exec(new String[] {"keytool", //
-        "-keystore", tmpstore.getAbsolutePath(), //
+        "-keystore", tmpstore.toAbsolutePath().toString(), //
         "-storepass", ssl_pass, //
         "-genkeypair", //
         "-alias", hostname, //
@@ -186,11 +191,15 @@
     }).waitFor();
     chmod(0600, tmpstore);
 
-    if (!tmpstore.renameTo(store)) {
-      throw die("Cannot rename " + tmpstore + " to " + store);
+    try {
+      Files.move(tmpstore, store);
+    } catch (IOException e) {
+      throw die("Cannot rename " + tmpstore + " to " + store, e);
     }
-    if (!tmpdir.delete()) {
-      throw die("Cannot delete " + tmpdir);
+    try {
+      Files.delete(tmpdir);
+    } catch (IOException e) {
+      throw die("Cannot delete " + tmpdir, e);
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index acb8a6b..a177fe7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -51,9 +51,6 @@
     ui.header("Index");
 
     IndexType type = index.select("Type", "type", IndexType.LUCENE);
-    if (type == IndexType.SOLR) {
-      index.string("Solr Index URL", "url", "localhost:9983");
-    }
     if (site.isNew && type == IndexType.LUCENE) {
       LuceneChangeIndex.setReady(
           site, ChangeSchemas.getLatest().getVersion(), true);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java
index 8fb05ca..b6b4cc1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java
@@ -26,6 +26,8 @@
 
 @Singleton
 public class InitLabels implements InitStep {
+  private static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE =
+      "copyAllScoresIfNoCodeChange";
   private static final String KEY_LABEL = "label";
   private static final String KEY_FUNCTION = "function";
   private static final String KEY_VALUE = "value";
@@ -58,6 +60,7 @@
       cfg.setString(KEY_LABEL, LABEL_VERIFIED, KEY_FUNCTION, "MaxWithBlock");
       cfg.setStringList(KEY_LABEL, LABEL_VERIFIED, KEY_VALUE,
           Arrays.asList(new String[] {"-1 Fails", " 0 No score", "+1 Verified"}));
+      cfg.setBoolean(KEY_LABEL, LABEL_VERIFIED, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE, true);
       allProjectsConfig.save("Configure 'Verified' label");
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index 8d08520..57a1a30 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.internal.UniqueAnnotations;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index fa9f710..3476de5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -26,23 +27,22 @@
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
-import java.io.File;
-import java.io.FileFilter;
 import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 
 @Singleton
 public class InitPluginStepsLoader {
-  private final File pluginsDir;
+  private final Path pluginsDir;
   private final Injector initInjector;
   final ConsoleUI ui;
 
@@ -55,10 +55,10 @@
   }
 
   public Collection<InitStep> getInitSteps() {
-    List<File> jars = scanJarsInPluginsDirectory();
+    List<Path> jars = scanJarsInPluginsDirectory();
     ArrayList<InitStep> pluginsInitSteps = new ArrayList<>();
 
-    for (File jar : jars) {
+    for (Path jar : jars) {
       InitStep init = loadInitStep(jar);
       if (init != null) {
         pluginsInitSteps.add(init);
@@ -68,12 +68,12 @@
   }
 
   @SuppressWarnings("resource")
-  private InitStep loadInitStep(File jar) {
+  private InitStep loadInitStep(Path jar) {
     try {
       URLClassLoader pluginLoader =
-          new URLClassLoader(new URL[] {jar.toURI().toURL()},
+          new URLClassLoader(new URL[] {jar.toUri().toURL()},
              InitPluginStepsLoader.class.getClassLoader());
-      try (JarFile jarFile = new JarFile(jar)) {
+      try (JarFile jarFile = new JarFile(jar.toFile())) {
         Attributes jarFileAttributes = jarFile.getManifest().getMainAttributes();
         String initClassName = jarFileAttributes.getValue("Gerrit-InitStep");
         if (initClassName == null) {
@@ -86,12 +86,12 @@
       } catch (ClassCastException e) {
         ui.message(
             "WARN: InitStep from plugin %s does not implement %s (Exception: %s)\n",
-            jar.getName(), InitStep.class.getName(), e.getMessage());
+            jar.getFileName(), InitStep.class.getName(), e.getMessage());
         return null;
       } catch (NoClassDefFoundError e) {
         ui.message(
             "WARN: Failed to run InitStep from plugin %s (Missing class: %s)\n",
-            jar.getName(), e.getMessage());
+            jar.getFileName(), e.getMessage());
         return null;
       }
     } catch (Exception e) {
@@ -102,11 +102,10 @@
     }
   }
 
-  private Injector getPluginInjector(final File jarFile) throws IOException {
-    final String pluginName =
-        MoreObjects.firstNonNull(
-            JarPluginProvider.getJarPluginName(jarFile),
-            PluginLoader.nameOf(jarFile));
+  private Injector getPluginInjector(Path jarPath) throws IOException {
+    final String pluginName = MoreObjects.firstNonNull(
+        JarPluginProvider.getJarPluginName(jarPath),
+        PluginLoader.nameOf(jarPath));
     return initInjector.createChildInjector(new AbstractModule() {
       @Override
       protected void configure() {
@@ -116,27 +115,24 @@
     });
   }
 
-  private List<File> scanJarsInPluginsDirectory() {
-    if (pluginsDir == null || !pluginsDir.exists()) {
+  private List<Path> scanJarsInPluginsDirectory() {
+    if (pluginsDir == null || !Files.isDirectory(pluginsDir)) {
       return Collections.emptyList();
     }
-    File[] matches = pluginsDir.listFiles(new FileFilter() {
+    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
       @Override
-      public boolean accept(File pathname) {
-        String n = pathname.getName();
-        return (n.endsWith(".jar") && pathname.isFile());
+      public boolean accept(Path entry) throws IOException {
+        return entry.getFileName().toString().endsWith(".jar")
+            && Files.isRegularFile(entry);
       }
-    });
-    if (matches == null) {
-      ui.message("WARN: Cannot list %s", pluginsDir.getAbsolutePath());
+    };
+    try (DirectoryStream<Path> paths =
+        Files.newDirectoryStream(pluginsDir, filter)) {
+      return Ordering.natural().sortedCopy(paths);
+    } catch (IOException e) {
+      ui.message("WARN: Cannot list %s: %s", pluginsDir.toAbsolutePath(),
+          e.getMessage());
       return Collections.emptyList();
     }
-    Arrays.sort(matches, new Comparator<File>() {
-      @Override
-      public int compare(File o1, File o2) {
-        return o1.getName().compareTo(o2.getName());
-      }
-    });
-    return Arrays.asList(matches);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index 722c4a1..6a3d7cb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -25,9 +25,10 @@
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
 import java.util.jar.Attributes;
@@ -56,10 +57,10 @@
     pluginsDistribution.foreach(new PluginsDistribution.Processor() {
       @Override
       public void process(String pluginName, InputStream in) throws IOException {
-        File tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
+        Path tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
         String pluginVersion = getVersion(tmpPlugin);
         if (deleteTempPluginFile) {
-          tmpPlugin.delete();
+          Files.delete(tmpPlugin);
         }
         result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
       }
@@ -110,13 +111,13 @@
     for (PluginData plugin : plugins) {
       String pluginName = plugin.name;
       try {
-        final File tmpPlugin = plugin.pluginFile;
-        File p = new File(site.plugins_dir, plugin.name + ".jar");
-        boolean upgrade = p.exists();
+        final Path tmpPlugin = plugin.pluginPath;
+        Path p = site.plugins_dir.resolve(plugin.name + ".jar");
+        boolean upgrade = Files.exists(p);
 
         if (!(initFlags.installPlugins.contains(pluginName) || ui.yesno(upgrade,
             "Install plugin %s version %s", pluginName, plugin.version))) {
-          tmpPlugin.delete();
+          Files.deleteIfExists(tmpPlugin);
           continue;
         }
 
@@ -125,23 +126,25 @@
           if (!ui.yesno(upgrade,
               "version %s is already installed, overwrite it",
               installedPluginVersion)) {
-            tmpPlugin.delete();
+            Files.deleteIfExists(tmpPlugin);
             continue;
           }
-          if (!p.delete()) {
+          try {
+            Files.delete(p);
+          } catch (IOException e) {
             throw new IOException("Failed to delete plugin " + pluginName
-                + ": " + p.getAbsolutePath());
+                + ": " + p.toAbsolutePath(), e);
           }
         }
-        if (!tmpPlugin.renameTo(p)) {
+        try {
+          Files.move(tmpPlugin, p);
+        } catch (IOException e) {
           throw new IOException("Failed to install plugin " + pluginName
-              + ": " + tmpPlugin.getAbsolutePath() + " -> "
-              + p.getAbsolutePath());
+              + ": " + tmpPlugin.toAbsolutePath() + " -> "
+              + p.toAbsolutePath(), e);
         }
       } finally {
-        if (plugin.pluginFile.exists()) {
-          plugin.pluginFile.delete();
-        }
+        Files.deleteIfExists(plugin.pluginPath);
       }
     }
     if (plugins.isEmpty()) {
@@ -168,14 +171,11 @@
     }
   }
 
-  private static String getVersion(final File plugin) throws IOException {
-    final JarFile jarFile = new JarFile(plugin);
-    try {
-      final Manifest manifest = jarFile.getManifest();
-      final Attributes main = manifest.getMainAttributes();
+  private static String getVersion(Path plugin) throws IOException {
+    try (JarFile jarFile = new JarFile(plugin.toFile())) {
+      Manifest manifest = jarFile.getManifest();
+      Attributes main = manifest.getMainAttributes();
       return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
-    } finally {
-      jarFile.close();
     }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
index 51eaa22..5c7eefd 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
@@ -25,6 +25,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.nio.file.Files;
+
 /** Initialize the {@code sendemail} configuration section. */
 @Singleton
 class InitSendEmail implements InitStep {
@@ -54,7 +56,7 @@
             true);
 
     String username = null;
-    if (site.gerrit_config.exists()) {
+    if (Files.exists(site.gerrit_config)) {
       username = sendemail.get("smtpUser");
     } else if ((enc != null && enc != Encryption.NONE) || !isLocal(hostname)) {
       username = username();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index ed18d73..c654c8d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.common.FileUtil.chmod;
 import static com.google.gerrit.pgm.init.api.InitUtil.die;
 import static com.google.gerrit.pgm.init.api.InitUtil.hostname;
+import static java.nio.file.Files.exists;
 
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -29,9 +30,10 @@
 import org.apache.sshd.common.util.SecurityUtils;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 
-import java.io.File;
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /** Initialize the {@code sshd} configuration section. */
 @Singleton
@@ -74,9 +76,9 @@
     port = ui.readInt(port, "Listen on port");
     sshd.set("listenAddress", SocketUtil.format(hostname, port));
 
-    if (site.ssh_rsa.exists() || site.ssh_dsa.exists()) {
+    if (exists(site.ssh_rsa) || exists(site.ssh_dsa)) {
       libraries.bouncyCastleSSL.downloadRequired();
-    } else if (!site.ssh_key.exists()) {
+    } else if (!exists(site.ssh_key)) {
       libraries.bouncyCastleSSL.downloadOptional();
     }
 
@@ -90,9 +92,9 @@
   }
 
   private void generateSshHostKeys() throws InterruptedException, IOException {
-    if (!site.ssh_key.exists() //
-        && !site.ssh_rsa.exists() //
-        && !site.ssh_dsa.exists()) {
+    if (!exists(site.ssh_key) //
+        && !exists(site.ssh_rsa) //
+        && !exists(site.ssh_dsa)) {
       System.err.print("Generating SSH host key ...");
       System.err.flush();
 
@@ -108,7 +110,7 @@
             "-t", "rsa", //
             "-P", "", //
             "-C", comment, //
-            "-f", site.ssh_rsa.getAbsolutePath() //
+            "-f", site.ssh_rsa.toAbsolutePath().toString() //
             }).waitFor();
 
         System.err.print(" dsa...");
@@ -118,7 +120,7 @@
             "-t", "dsa", //
             "-P", "", //
             "-C", comment, //
-            "-f", site.ssh_dsa.getAbsolutePath() //
+            "-f", site.ssh_dsa.toAbsolutePath().toString() //
             }).waitFor();
 
       } else {
@@ -128,28 +130,34 @@
         // short period of time. We try to reduce that risk by creating
         // the key within a temporary directory.
         //
-        final File tmpdir = new File(site.etc_dir, "tmp.sshkeygen");
-        if (!tmpdir.mkdir()) {
-          throw die("Cannot create directory " + tmpdir);
+        Path tmpdir = site.etc_dir.resolve("tmp.sshkeygen");
+        try {
+          Files.createDirectory(tmpdir);
+        } catch (IOException e) {
+          throw die("Cannot create directory " + tmpdir, e);
         }
         chmod(0600, tmpdir);
 
-        final File tmpkey = new File(tmpdir, site.ssh_key.getName());
-        final SimpleGeneratorHostKeyProvider p;
+        Path tmpkey = tmpdir.resolve(site.ssh_key.getFileName().toString());
+        SimpleGeneratorHostKeyProvider p;
 
         System.err.print(" rsa(simple)...");
         System.err.flush();
         p = new SimpleGeneratorHostKeyProvider();
-        p.setPath(tmpkey.getAbsolutePath());
+        p.setPath(tmpkey.toAbsolutePath().toString());
         p.setAlgorithm("RSA");
         p.loadKeys(); // forces the key to generate.
         chmod(0600, tmpkey);
 
-        if (!tmpkey.renameTo(site.ssh_key)) {
-          throw die("Cannot rename " + tmpkey + " to " + site.ssh_key);
+        try {
+          Files.move(tmpkey, site.ssh_key);
+        } catch (IOException e) {
+          throw die("Cannot rename " + tmpkey + " to " + site.ssh_key, e);
         }
-        if (!tmpdir.delete()) {
-          throw die("Cannot delete " + tmpdir);
+        try {
+          Files.delete(tmpdir);
+        } catch (IOException e) {
+          throw die("Cannot delete " + tmpdir, e);
         }
       }
       System.err.println(" done");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
index dfd6171..4659ee3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
@@ -33,7 +33,9 @@
   private void guessDriver(Section database) {
     String url = Strings.emptyToNull(database.get("url"));
     if (url != null && Strings.isNullOrEmpty(database.get("driver"))) {
-      if (url.startsWith("jdbc:h2:")) {
+      if (url.startsWith("jdbc:derby:")) {
+        database.set("driver", "org.apache.derby.jdbc.EmbeddedDriver");
+      } else if (url.startsWith("jdbc:h2:")) {
         database.set("driver", "org.h2.Driver");
       } else if (url.startsWith("jdbc:mysql:")) {
         database.set("driver", "com.mysql.jdbc.Driver");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index ea8f0f1..7cc8f10 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -37,8 +39,11 @@
 
   private final Provider<LibraryDownloader> downloadProvider;
 
+  /* final */LibraryDownloader bouncyCastlePGP;
   /* final */LibraryDownloader bouncyCastleProvider;
   /* final */LibraryDownloader bouncyCastleSSL;
+  /* final */LibraryDownloader db2Driver;
+  /* final */LibraryDownloader db2DriverLicense;
   /* final */LibraryDownloader mysqlDriver;
   /* final */LibraryDownloader oracleDriver;
 
@@ -53,9 +58,7 @@
     final Config cfg = new Config();
     try {
       cfg.fromText(read(RESOURCE_FILE));
-    } catch (IOException e) {
-      throw new RuntimeException(e.getMessage(), e);
-    } catch (ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException e) {
       throw new RuntimeException(e.getMessage(), e);
     }
 
@@ -89,16 +92,25 @@
     LibraryDownloader dl = (LibraryDownloader) field.get(this);
     dl.setName(get(cfg, n, "name"));
     dl.setJarUrl(get(cfg, n, "url"));
-    dl.setSHA1(get(cfg, n, "sha1"));
+    dl.setSHA1(getOptional(cfg, n, "sha1"));
     dl.setRemove(get(cfg, n, "remove"));
     for (String d : cfg.getStringList("library", n, "needs")) {
       dl.addNeeds((LibraryDownloader) getClass().getDeclaredField(d).get(this));
     }
   }
 
+  private static String getOptional(Config cfg, String name, String key) {
+    return doGet(cfg, name, key, false);
+  }
+
   private static String get(Config cfg, String name, String key) {
+    return doGet(cfg, name, key, true);
+  }
+
+  private static final String doGet(Config cfg, String name, String key,
+      boolean required) {
     String val = cfg.getString("library", name, key);
-    if (val == null || val.isEmpty()) {
+    if ((val == null || val.isEmpty()) && required) {
       throw new IllegalStateException("Variable library." + name + "." + key
           + " is required within " + RESOURCE_FILE);
     }
@@ -110,7 +122,7 @@
       if (in == null) {
         throw new FileNotFoundException("Cannot load resource " + p);
       }
-      try (Reader r = new InputStreamReader(in, "UTF-8")) {
+      try (Reader r = new InputStreamReader(in, UTF_8)) {
         final StringBuilder buf = new StringBuilder();
         final char[] tmp = new char[512];
         int n;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index 4bf1c88..e4cc305 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -15,21 +15,19 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.base.Strings;
-import com.google.common.io.Files;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.util.HttpSupport;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -38,15 +36,17 @@
 import java.net.ProxySelector;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 
 /** Get optional or required 3rd party library files into $site_path/lib. */
 class LibraryDownloader {
   private final ConsoleUI ui;
-  private final File lib_dir;
+  private final Path lib_dir;
 
   private boolean required;
   private String name;
@@ -55,7 +55,7 @@
   private String remove;
   private List<LibraryDownloader> needs;
   private LibraryDownloader neededBy;
-  private File dst;
+  private Path dst;
   private boolean download; // download or copy
   private boolean exists;
 
@@ -118,8 +118,8 @@
       name = jarName;
     }
 
-    dst = new File(lib_dir, jarName);
-    if (dst.exists()) {
+    dst = lib_dir.resolve(jarName);
+    if (Files.exists(dst)) {
       exists = true;
     } else if (shouldGet()) {
       doGet();
@@ -158,8 +158,12 @@
   }
 
   private void doGet() {
-    if (!lib_dir.exists() && !lib_dir.mkdirs()) {
-      throw new Die("Cannot create " + lib_dir);
+    if (!Files.exists(lib_dir)) {
+      try {
+        Files.createDirectories(lib_dir);
+      } catch (IOException e) {
+        throw new Die("Cannot create " + lib_dir, e);
+      }
     }
 
     try {
@@ -171,7 +175,11 @@
       }
       verifyFileChecksum();
     } catch (IOException err) {
-      dst.delete();
+      try {
+        Files.delete(dst);
+      } catch (IOException e) {
+        // Delete failed; leave alone.
+      }
 
       if (ui.isBatch()) {
         throw new Die("error: Cannot get " + jarUrl, err);
@@ -186,13 +194,13 @@
       System.err.println();
       System.err.println("and save as:");
       System.err.println();
-      System.err.println("  " + dst.getAbsolutePath());
+      System.err.println("  " + dst.toAbsolutePath());
       System.err.println();
       System.err.flush();
 
       ui.waitForUser();
 
-      if (dst.exists()) {
+      if (Files.exists(dst)) {
         verifyFileChecksum();
 
       } else if (!ui.yesno(!required, "Continue without this library")) {
@@ -200,7 +208,7 @@
       }
     }
 
-    if (dst.exists()) {
+    if (Files.exists(dst)) {
       exists = true;
       IoUtil.loadJARs(dst);
     }
@@ -208,131 +216,122 @@
 
   private void removeStaleVersions() {
     if (!Strings.isNullOrEmpty(remove)) {
-      String[] names = lib_dir.list(new FilenameFilter() {
+      DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
         @Override
-        public boolean accept(File dir, String name) {
-          return name.matches("^" + remove + "$");
+        public boolean accept(Path entry) {
+          return entry.getFileName().toString()
+              .matches("^" + remove + "$");
         }
-      });
-      if (names != null) {
-        for (String old : names) {
+      };
+      try (DirectoryStream<Path> paths =
+          Files.newDirectoryStream(lib_dir, filter)) {
+        for (Path p : paths) {
+          String old = p.getFileName().toString();
           String bak = "." + old + ".backup";
-          ui.message("Renaming %s to %s", old, bak);
-          if (!new File(lib_dir, old).renameTo(new File(lib_dir, bak))) {
-            throw new Die("cannot rename " + old);
+          ui.message("Renaming %s to %s\n", old, bak);
+          try {
+            Files.move(p, p.resolveSibling(bak));
+          } catch (IOException e) {
+            throw new Die("cannot rename " + old, e);
           }
         }
+      } catch (IOException e) {
+        throw new Die("cannot remove stale library versions", e);
       }
     }
   }
 
   private void doGetByLocalCopy() throws IOException {
     System.err.print("Copying " + jarUrl + " ...");
-    File f = url2file(jarUrl);
-    if (!f.exists()) {
+    Path p = url2file(jarUrl);
+    if (!Files.exists(p)) {
       StringBuilder msg = new StringBuilder()
           .append("\n")
           .append("Can not find the %s at this location: %s\n")
           .append("Please provide alternative URL");
-      f = url2file(ui.readString(null, msg.toString(), name, jarUrl));
+      p = url2file(ui.readString(null, msg.toString(), name, jarUrl));
     }
-    Files.copy(f, dst);
+    Files.copy(p, dst);
   }
 
-  private static File url2file(final String urlString) throws IOException {
+  private static Path url2file(final String urlString) throws IOException {
     final URL url = new URL(urlString);
     try {
-      return new File(url.toURI());
+      return Paths.get(url.toURI());
     } catch (URISyntaxException e) {
-      return new File(url.getPath());
+      return Paths.get(url.getPath());
     }
   }
 
   private void doGetByHttp() throws IOException {
     System.err.print("Downloading " + jarUrl + " ...");
     System.err.flush();
-    try {
-      final ProxySelector proxySelector = ProxySelector.getDefault();
-      final URL url = new URL(jarUrl);
-      final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
-      final HttpURLConnection c = (HttpURLConnection) url.openConnection(proxy);
-      final InputStream in;
-
-      switch (HttpSupport.response(c)) {
-        case HttpURLConnection.HTTP_OK:
-          in = c.getInputStream();
-          break;
-
-        case HttpURLConnection.HTTP_NOT_FOUND:
-          throw new FileNotFoundException(url.toString());
-
-        default:
-          throw new IOException(url.toString() + ": " + HttpSupport.response(c)
-              + " " + c.getResponseMessage());
-      }
-
-      try {
-        final OutputStream out = new FileOutputStream(dst);
-        try {
-          final byte[] buf = new byte[8192];
-          int n;
-          while ((n = in.read(buf)) > 0) {
-            out.write(buf, 0, n);
-          }
-        } finally {
-          out.close();
-        }
-      } finally {
-        in.close();
-      }
+    try (InputStream in = openHttpStream(jarUrl);
+        OutputStream out = Files.newOutputStream(dst)) {
+      ByteStreams.copy(in, out);
       System.err.println(" OK");
       System.err.flush();
     } catch (IOException err) {
-      dst.delete();
+      deleteDst();
       System.err.println(" !! FAIL !!");
       System.err.flush();
       throw err;
     }
   }
 
+  private static InputStream openHttpStream(String urlStr) throws IOException {
+    ProxySelector proxySelector = ProxySelector.getDefault();
+    URL url = new URL(urlStr);
+    Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
+    HttpURLConnection c = (HttpURLConnection) url.openConnection(proxy);
+
+    switch (HttpSupport.response(c)) {
+      case HttpURLConnection.HTTP_OK:
+        return c.getInputStream();
+
+      case HttpURLConnection.HTTP_NOT_FOUND:
+        throw new FileNotFoundException(url.toString());
+
+      default:
+        throw new IOException(url.toString() + ": " + HttpSupport.response(c)
+            + " " + c.getResponseMessage());
+    }
+  }
+
   private void verifyFileChecksum() {
-    if (sha1 != null) {
-      try {
-        final MessageDigest md = MessageDigest.getInstance("SHA-1");
-        final FileInputStream in = new FileInputStream(dst);
-        try {
-          final byte[] buf = new byte[8192];
-          int n;
-          while ((n = in.read(buf)) > 0) {
-            md.update(buf, 0, n);
-          }
-        } finally {
-          in.close();
-        }
+    if (sha1 == null) {
+      System.err.println();
+      System.err.flush();
+      return;
+    }
+    Hasher h = Hashing.sha1().newHasher();
+    try (InputStream in = Files.newInputStream(dst);
+        OutputStream out = Funnels.asOutputStream(h)) {
+      ByteStreams.copy(in, out);
+    } catch (IOException e) {
+      deleteDst();
+      throw new Die("cannot checksum " + dst, e);
+    }
+    if (sha1.equals(h.hash().toString())) {
+      System.err.println("Checksum " + dst.getFileName() + " OK");
+      System.err.flush();
+    } else if (ui.isBatch()) {
+      deleteDst();
+      throw new Die(dst + " SHA-1 checksum does not match");
 
-        if (sha1.equals(ObjectId.fromRaw(md.digest()).name())) {
-          System.err.println("Checksum " + dst.getName() + " OK");
-          System.err.flush();
+    } else if (!ui.yesno(null /* force an answer */,
+        "error: SHA-1 checksum does not match\n" + "Use %s anyway",//
+        dst.getFileName())) {
+      deleteDst();
+      throw new Die("aborted by user");
+    }
+  }
 
-        } else if (ui.isBatch()) {
-          dst.delete();
-          throw new Die(dst + " SHA-1 checksum does not match");
-
-        } else if (!ui.yesno(null /* force an answer */,
-            "error: SHA-1 checksum does not match\n" + "Use %s anyway",//
-            dst.getName())) {
-          dst.delete();
-          throw new Die("aborted by user");
-        }
-
-      } catch (IOException checksumError) {
-        dst.delete();
-        throw new Die("cannot checksum " + dst, checksumError);
-
-      } catch (NoSuchAlgorithmException checksumError) {
-        dst.delete();
-        throw new Die("cannot checksum " + dst, checksumError);
-      }
+  private void deleteDst() {
+    try {
+      Files.delete(dst);
+    } catch (IOException e) {
+      System.err.println(" Failed to clean up lib: " + dst);
     }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java
index 8926759..49877dc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.pgm.init;
 
-import java.io.File;
+import java.nio.file.Path;
 
 class SecureStoreInitData {
-  final File jarFile;
+  final Path jarFile;
   final String className;
 
-  SecureStoreInitData(File jar, String className) {
+  SecureStoreInitData(Path jar, String className) {
     this.className = className;
     this.jarFile = jar;
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 10c9bad..6270a15 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -21,7 +21,7 @@
 import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
 import static com.google.gerrit.pgm.init.api.InitUtil.version;
 
-import com.google.common.io.Files;
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -35,8 +35,9 @@
 import com.google.inject.Injector;
 import com.google.inject.TypeLiteral;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -53,7 +54,7 @@
   public SitePathInitializer(final Injector injector, final ConsoleUI ui,
       final InitFlags flags, final SitePaths site,
       final Section.Factory sectionFactory,
-      final @Nullable SecureStoreInitData secureStoreInitData) {
+      @Nullable final SecureStoreInitData secureStoreInitData) {
     this.ui = ui;
     this.flags = flags;
     this.site = site;
@@ -66,12 +67,10 @@
     ui.header("Gerrit Code Review %s", version());
 
     if (site.isNew) {
-      if (!ui.yesno(true, "Create '%s'", site.site_path.getCanonicalPath())) {
+      if (!ui.yesno(true, "Create '%s'", site.site_path.toAbsolutePath())) {
         throw die("aborted by user");
       }
-      if (!site.site_path.isDirectory() && !site.site_path.mkdirs()) {
-        throw die("Cannot make directory " + site.site_path);
-      }
+      FileUtil.mkdirsOrDie(site.site_path, "Cannot make directory");
       flags.deleteOnFailure = true;
     }
 
@@ -101,6 +100,7 @@
     chmod(0700, site.tmp_dir);
 
     extractMailExample("Abandoned.vm");
+    extractMailExample("AddKey.vm");
     extractMailExample("ChangeFooter.vm");
     extractMailExample("ChangeSubject.vm");
     extractMailExample("Comment.vm");
@@ -132,7 +132,8 @@
 
   private void saveSecureStore() throws IOException {
     if (secureStoreInitData != null) {
-      File dst = new File(site.lib_dir, secureStoreInitData.jarFile.getName());
+      Path dst =
+          site.lib_dir.resolve(secureStoreInitData.jarFile.getFileName());
       Files.copy(secureStoreInitData.jarFile, dst);
       Section gerritSection = sectionFactory.get("gerrit", null);
       gerritSection.set("secureStoreClass", secureStoreInitData.className);
@@ -140,7 +141,7 @@
   }
 
   private void extractMailExample(String orig) throws Exception {
-    File ex = new File(site.mail_dir, orig + ".example");
+    Path ex = site.mail_dir.resolve(orig + ".example");
     extract(ex, OutgoingEmail.class, orig);
     chmod(0444, ex);
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
index 8c13540..3c91241 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.pgm.init.api.InitUtil.die;
 import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -30,13 +31,14 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.InetSocketAddress;
 import java.net.URLDecoder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Properties;
@@ -64,8 +66,8 @@
 
   private final FileBasedConfig cfg;
   private final SecureStore sec;
-  private final File site_path;
-  private final File etc_dir;
+  private final Path site_path;
+  private final Path etc_dir;
   private final Section.Factory sections;
 
   @Inject
@@ -82,7 +84,7 @@
 
   boolean isNeedUpgrade() {
     for (String name : etcFiles) {
-      if (new File(site_path, name).exists()) {
+      if (Files.exists(site_path.resolve(name))) {
         return true;
       }
     }
@@ -95,19 +97,21 @@
       return;
     }
 
-    if (!ui.yesno(true, "Upgrade '%s'", site_path.getCanonicalPath())) {
+    if (!ui.yesno(true, "Upgrade '%s'", site_path.toAbsolutePath())) {
       throw die("aborted by user");
     }
 
     for (String name : etcFiles) {
-      final File src = new File(site_path, name);
-      final File dst = new File(etc_dir, name);
-      if (src.exists()) {
-        if (dst.exists()) {
+      Path src = site_path.resolve(name);
+      Path dst = etc_dir.resolve(name);
+      if (Files.exists(src)) {
+        if (Files.exists(dst)) {
           throw die("File " + src + " would overwrite " + dst);
         }
-        if (!src.renameTo(dst)) {
-          throw die("Cannot rename " + src + " to " + dst);
+        try {
+          Files.move(src, dst);
+        } catch (IOException e) {
+          throw die("Cannot rename " + src + " to " + dst, e);
         }
       }
     }
@@ -169,8 +173,8 @@
           return false;
         }
 
-        String n = URLDecoder.decode(pair.substring(0, eq), "UTF-8");
-        String v = URLDecoder.decode(pair.substring(eq + 1), "UTF-8");
+        String n = URLDecoder.decode(pair.substring(0, eq), UTF_8.name());
+        String v = URLDecoder.decode(pair.substring(eq + 1), UTF_8.name());
 
         if ("user".equals(n) || "username".equals(n)) {
           username = v;
@@ -256,23 +260,18 @@
   private Properties readGerritServerProperties() throws IOException {
     final Properties srvprop = new Properties();
     final String name = System.getProperty("GerritServer");
-    File path;
+    Path path;
     if (name != null) {
-      path = new File(name);
+      path = Paths.get(name);
     } else {
-      path = new File(site_path, "GerritServer.properties");
-      if (!path.exists()) {
-        path = new File("GerritServer.properties");
+      path = site_path.resolve("GerritServer.properties");
+      if (!Files.exists(path)) {
+        path = Paths.get("GerritServer.properties");
       }
     }
-    if (path.exists()) {
-      try {
-        final InputStream in = new FileInputStream(path);
-        try {
-          srvprop.load(in);
-        } finally {
-          in.close();
-        }
+    if (Files.exists(path)) {
+      try (InputStream in = Files.newInputStream(path)) {
+        srvprop.load(in);
       } catch (IOException e) {
         throw new IOException("Cannot read " + name, e);
       }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index f45a970..aea438c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GroupList;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.inject.Inject;
 
@@ -40,6 +39,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 public class AllProjectsConfig extends VersionedMetaData {
 
@@ -68,21 +68,18 @@
   }
 
   private File getPath() {
-    File basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
+    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
     if (basePath == null) {
       throw new IllegalStateException("gerrit.basePath must be configured");
     }
-    return FileKey.resolve(new File(basePath, project), FS.DETECTED);
+    return FileKey.resolve(basePath.resolve(project).toFile(), FS.DETECTED);
   }
 
   public AllProjectsConfig load() throws IOException, ConfigInvalidException {
     File path = getPath();
     if (path != null) {
-      Repository repo = new FileRepository(path);
-      try {
+      try (Repository repo = new FileRepository(path)) {
         load(repo);
-      } finally {
-        repo.close();
       }
     }
     return this;
@@ -104,15 +101,8 @@
   }
 
   private GroupList readGroupList() throws IOException {
-    ValidationError.Sink errors = new ValidationError.Sink() {
-      @Override
-      public void error(ValidationError error) {
-        log.error("Error parsing file " + GroupList.FILE_NAME + ": " + error.getMessage());
-      }
-    };
-    String text = readUTF8(GroupList.FILE_NAME);
-
-    return GroupList.parse(text, errors);
+    return GroupList.parse(readUTF8(GroupList.FILE_NAME),
+        GroupList.createLoggerSink(GroupList.FILE_NAME, log));
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index fb0ef28..e210d5b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm.init.api;
 
-import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
-
 import com.google.gerrit.common.Die;
 
 import java.io.Console;
@@ -45,15 +43,9 @@
   protected static <T extends Enum<?>> T[] all(final T value) {
     try {
       return (T[]) value.getClass().getMethod("values").invoke(null);
-    } catch (IllegalArgumentException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (SecurityException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (IllegalAccessException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (InvocationTargetException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (NoSuchMethodException e) {
+    } catch (IllegalArgumentException | NoSuchMethodException
+        | InvocationTargetException | IllegalAccessException
+        | SecurityException e) {
       throw new IllegalArgumentException("Cannot obtain enumeration values", e);
     }
   }
@@ -224,7 +216,7 @@
           return def;
         }
         for (final T e : options) {
-          if (equalsIgnoreCase(e.toString(), r)) {
+          if (e.toString().equalsIgnoreCase(r)) {
             return e;
           }
         }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
index 2a8155e..bdd8b86 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
@@ -39,6 +39,9 @@
   /** Skip plugins */
   public boolean skipPlugins;
 
+  /** Dev mode */
+  public boolean dev;
+
   public final FileBasedConfig cfg;
   public final SecureStore sec;
   public final List<String> installPlugins;
@@ -47,11 +50,11 @@
   @Inject
   public InitFlags(final SitePaths site,
       final SecureStore secureStore,
-      final @InstallPlugins List<String> installPlugins) throws IOException,
+      @InstallPlugins final List<String> installPlugins) throws IOException,
       ConfigInvalidException {
     sec = secureStore;
     this.installPlugins = installPlugins;
-    cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    cfg = new FileBasedConfig(site.gerrit_config.toFile(), FS.DETECTED);
 
     cfg.load();
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
index 881208d..1e1ddd7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -16,14 +16,14 @@
 
 import static com.google.gerrit.common.FileUtil.modified;
 
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.Die;
 
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.SystemReader;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -33,7 +33,10 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Arrays;
 
 /** Utility functions to help initialize a site. */
 public class InitUtil {
@@ -51,9 +54,18 @@
     }
   }
 
-  public static void mkdir(final File path) {
-    if (!path.isDirectory() && !path.mkdir()) {
-      throw die("Cannot make directory " + path);
+  public static void mkdir(File file) {
+    mkdir(file.toPath());
+  }
+
+  public static void mkdir(Path path) {
+    if (Files.isDirectory(path)) {
+      return;
+    }
+    try {
+      Files.createDirectory(path);
+    } catch (IOException e) {
+      throw die("Cannot make directory " + path, e);
     }
   }
 
@@ -109,12 +121,11 @@
     return name;
   }
 
-  public static void extract(final File dst, final Class<?> sibling,
-      final String name) throws IOException {
+  public static void extract(Path dst, Class<?> sibling, String name)
+      throws IOException {
     try (InputStream in = open(sibling, name)) {
       if (in != null) {
-        ByteBuffer buf = IO.readWholeStream(in, 8192);
-        copy(dst, buf);
+        copy(dst, ByteStreams.toByteArray(in));
       }
     }
   }
@@ -136,35 +147,28 @@
     return in;
   }
 
-  public static void copy(final File dst, final ByteBuffer buf)
+  public static void copy(Path dst, byte[] buf)
       throws FileNotFoundException, IOException {
     // If the file already has the content we want to put there,
     // don't attempt to overwrite the file.
     //
-    try {
-      if (buf.equals(ByteBuffer.wrap(IO.readFully(dst)))) {
+    try (InputStream in = Files.newInputStream(dst)) {
+      if (Arrays.equals(buf, ByteStreams.toByteArray(in))) {
         return;
       }
-    } catch (FileNotFoundException notFound) {
+    } catch (NoSuchFileException notFound) {
       // Fall through and write the file.
     }
 
-    dst.getParentFile().mkdirs();
-    LockFile lf = new LockFile(dst, FS.DETECTED);
+    Files.createDirectories(dst.getParent());
+    LockFile lf = new LockFile(dst.toFile());
     if (!lf.lock()) {
       throw new IOException("Cannot lock " + dst);
     }
     try {
-      final OutputStream out = lf.getOutputStream();
-      try {
-        final byte[] tmp = new byte[4096];
-        while (0 < buf.remaining()) {
-          int n = Math.min(buf.remaining(), tmp.length);
-          buf.get(tmp, 0, n);
-          out.write(tmp, 0, n);
-        }
-      } finally {
-        out.close();
+      try (InputStream in = new ByteArrayInputStream(buf);
+          OutputStream out = lf.getOutputStream()) {
+        ByteStreams.copy(in, out);
       }
       if (!lf.commit()) {
         throw new IOException("Cannot commit " + dst);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
index fbd8ecd..52b0daa 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
@@ -20,7 +20,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Set;
@@ -68,10 +68,9 @@
         flags.cfg.setStringList(section, subsection, name, all);
       }
 
-    } else if (all.size() == 0) {
     } else if (all.size() == 1) {
       flags.cfg.unset(section, subsection, name);
-    } else {
+    } else if (all.size() != 0) {
       all.remove(0);
       flags.cfg.setStringList(section, subsection, name, all);
     }
@@ -106,7 +105,7 @@
     return nv;
   }
 
-  public File path(final String title, final String name, final String defValue) {
+  public Path path(final String title, final String name, final String defValue) {
     return site.resolve(string(title, name, defValue));
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
similarity index 73%
rename from gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 4724bc2..064cc19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.rules;
+package com.google.gerrit.pgm.rules;
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
@@ -22,8 +22,8 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
 import com.googlecode.prolog_cafe.compiler.Compiler;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.Config;
@@ -35,8 +35,11 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -67,14 +70,14 @@
     NO_RULES, COMPILED
   }
 
-  private final File ruleDir;
+  private final Path ruleDir;
   private final Repository git;
 
   @Inject
   PrologCompiler(@GerritServerConfig Config config, SitePaths site,
       @Assisted Repository gitRepository) {
-    File cacheDir = site.resolve(config.getString("cache", null, "directory"));
-    ruleDir = cacheDir != null ? new File(cacheDir, "rules") : null;
+    Path cacheDir = site.resolve(config.getString("cache", null, "directory"));
+    ruleDir = cacheDir != null ? cacheDir.resolve("rules") : null;
     git = gitRepository;
   }
 
@@ -93,9 +96,7 @@
     if (ruleDir == null) {
       throw new CompileException("Caching not enabled");
     }
-    if (!ruleDir.isDirectory() && !ruleDir.mkdir()) {
-      throw new IOException("Cannot create " + ruleDir);
-    }
+    Files.createDirectories(ruleDir);
 
     File tempDir = File.createTempFile("GerritCodeReview_", ".rulec");
     if (!tempDir.delete() || !tempDir.mkdir()) {
@@ -111,9 +112,9 @@
       compileProlog(rulesId, tempDir);
       compileJava(tempDir);
 
-      File jarFile = new File(ruleDir, "rules-" + rulesId.getName() + ".jar");
+      Path jarPath = ruleDir.resolve("rules-" + rulesId.getName() + ".jar");
       List<String> classFiles = getRelativePaths(tempDir, ".class");
-      createJar(jarFile, classFiles, tempDir, metaConfig, rulesId);
+      createJar(jarPath, classFiles, tempDir, metaConfig, rulesId);
 
       return Status.COMPILED;
     } finally {
@@ -138,11 +139,8 @@
     // Any leak of tmp caused by this method failing will be cleaned
     // up by our caller when tempDir is recursively deleted.
     File tmp = File.createTempFile("rules", ".pl", tempDir);
-    FileOutputStream out = new FileOutputStream(tmp);
-    try {
+    try (FileOutputStream out = new FileOutputStream(tmp)) {
       git.open(blobId).copyTo(out);
-    } finally {
-      out.close();
     }
     return tmp;
   }
@@ -156,9 +154,8 @@
 
     DiagnosticCollector<JavaFileObject> diagnostics =
         new DiagnosticCollector<>();
-    StandardJavaFileManager fileManager =
-        compiler.getStandardFileManager(diagnostics, null, null);
-    try {
+    try (StandardJavaFileManager fileManager =
+        compiler.getStandardFileManager(diagnostics, null, null)) {
       Iterable<? extends JavaFileObject> compilationUnits = fileManager
         .getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java"));
       ArrayList<String> options = new ArrayList<>();
@@ -194,8 +191,6 @@
         }
         throw new CompileException(msg.toString());
       }
-    } finally {
-      fileManager.close();
     }
   }
 
@@ -222,62 +217,61 @@
   }
 
   /** Takes compiled prolog .class files, puts them into the jar file. */
-  private void createJar(File archiveFile, List<String> toBeJared,
+  private void createJar(Path archiveFile, List<String> toBeJared,
       File tempDir, ObjectId metaConfig, ObjectId rulesId) throws IOException {
     long now = TimeUtil.nowMs();
-    File tmpjar = File.createTempFile(".rulec_", ".jar", archiveFile.getParentFile());
-    try {
-      Manifest mf = new Manifest();
-      mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
-      mf.getMainAttributes().putValue("Built-by", "Gerrit Code Review " + Version.getVersion());
-      if (git.getDirectory() != null) {
-        mf.getMainAttributes().putValue("Source-Repository", git.getDirectory().getPath());
-      }
-      mf.getMainAttributes().putValue("Source-Commit", metaConfig.name());
-      mf.getMainAttributes().putValue("Source-Blob", rulesId.name());
+    Manifest mf = new Manifest();
+    mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+    mf.getMainAttributes().putValue("Built-by", "Gerrit Code Review " + Version.getVersion());
+    if (git.getDirectory() != null) {
+      mf.getMainAttributes().putValue("Source-Repository", git.getDirectory().getPath());
+    }
+    mf.getMainAttributes().putValue("Source-Commit", metaConfig.name());
+    mf.getMainAttributes().putValue("Source-Blob", rulesId.name());
 
-      try (FileOutputStream stream = new FileOutputStream(tmpjar);
-          JarOutputStream out = new JarOutputStream(stream, mf)) {
-        byte buffer[] = new byte[10240];
-        for (String path : toBeJared) {
-          JarEntry jarAdd = new JarEntry(path);
-          File f = new File(tempDir, path);
-          jarAdd.setTime(now);
-          out.putNextEntry(jarAdd);
-          if (f.isFile()) {
-            FileInputStream in = new FileInputStream(f);
-            try {
-              while (true) {
-                int nRead = in.read(buffer, 0, buffer.length);
-                if (nRead <= 0) {
-                  break;
-                }
-                out.write(buffer, 0, nRead);
+    Path tmpjar =
+        Files.createTempFile(archiveFile.getParent(), ".rulec_", ".jar");
+    try (OutputStream stream = Files.newOutputStream(tmpjar);
+        JarOutputStream out = new JarOutputStream(stream, mf)) {
+      byte[] buffer = new byte[10240];
+      // TODO: fixify this loop
+      for (String path : toBeJared) {
+        JarEntry jarAdd = new JarEntry(path);
+        File f = new File(tempDir, path);
+        jarAdd.setTime(now);
+        out.putNextEntry(jarAdd);
+        if (f.isFile()) {
+          try (FileInputStream in = new FileInputStream(f)) {
+            while (true) {
+              int nRead = in.read(buffer, 0, buffer.length);
+              if (nRead <= 0) {
+                break;
               }
-            } finally {
-              in.close();
+              out.write(buffer, 0, nRead);
             }
           }
-          out.closeEntry();
         }
+        out.closeEntry();
       }
+    }
 
-      if (!tmpjar.renameTo(archiveFile)) {
-        throw new IOException("Cannot replace " + archiveFile);
-      }
-    } finally {
-      tmpjar.delete();
+    try {
+      Files.move(tmpjar, archiveFile);
+    } catch (IOException e) {
+      throw new IOException("Cannot replace " + archiveFile, e);
     }
   }
 
-  private List<File> getAllFiles(File dir, String extension) {
+  private List<File> getAllFiles(File dir, String extension)
+      throws IOException {
     ArrayList<File> fileList = new ArrayList<>();
     getAllFiles(dir, extension, fileList);
     return fileList;
   }
 
-  private void getAllFiles(File dir, String extension, List<File> fileList) {
-    for (File f : dir.listFiles()) {
+  private void getAllFiles(File dir, String extension, List<File> fileList)
+      throws IOException {
+    for (File f : listFiles(dir)) {
       if (f.getName().endsWith(extension)) {
         fileList.add(f);
       }
@@ -287,14 +281,16 @@
     }
   }
 
-  private List<String> getRelativePaths(File dir, String extension) {
+  private List<String> getRelativePaths(File dir, String extension)
+      throws IOException {
     ArrayList<String> pathList = new ArrayList<>();
     getRelativePaths(dir, extension, "", pathList);
     return pathList;
   }
 
-  private void getRelativePaths(File dir, String extension, String path, List<String> pathList) {
-    for (File f : dir.listFiles()) {
+  private static void getRelativePaths(File dir, String extension, String path,
+      List<String> pathList) throws IOException {
+    for (File f : listFiles(dir)) {
       if (f.getName().endsWith(extension)) {
         pathList.add(path + f.getName());
       }
@@ -304,8 +300,8 @@
     }
   }
 
-  private void deleteAllFiles(File dir) {
-    for (File f : dir.listFiles()) {
+  private static void deleteAllFiles(File dir) throws IOException {
+    for (File f : listFiles(dir)) {
       if (f.isDirectory()) {
         deleteAllFiles(f);
       } else {
@@ -314,4 +310,12 @@
     }
     dir.delete();
   }
+
+  private static File[] listFiles(File dir) throws IOException {
+    File[] files = dir.listFiles();
+    if (files == null) {
+      throw new IOException("Failed to list directory: " + dir);
+    }
+    return files;
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
index 11ab073..be573e6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.DisabledChangeHooks;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidators;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 23983b7..b6539f1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -17,6 +17,7 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -35,13 +36,14 @@
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.group.GroupModule;
@@ -87,6 +89,7 @@
     install(reviewDbModule);
     install(new DiffExecutorModule());
     install(PatchListCacheImpl.module());
+
     // Plugins are not loaded and we're just running through each change
     // once, so don't worry about cache removal.
     bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
@@ -105,8 +108,10 @@
     bind(ReplacePatchSetSender.Factory.class).toProvider(
         Providers.<ReplacePatchSetSender.Factory>of(null));
     bind(CurrentUser.class).to(IdentifiedUser.class);
+    factory(BatchUpdate.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchSetInserter.Factory.class);
+    factory(RebaseChangeOp.Factory.class);
 
     bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
       .annotatedWith(GitUploadPackGroups.class)
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index a766d1e..95caf25 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -14,22 +14,26 @@
 
 package com.google.gerrit.pgm.util;
 
-import com.google.gerrit.common.Die;
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.SystemLog;
 
+import net.logstash.log4j.JSONEventLayoutV1;
+
 import org.apache.log4j.ConsoleAppender;
 import org.apache.log4j.Level;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
+import org.eclipse.jgit.lib.Config;
 
-import java.io.File;
-import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Path;
 
 public class ErrorLogFile {
   static final String LOG_NAME = "error_log";
+  static final String JSON_SUFFIX = ".json";
 
   public static void errorOnlyConsole() {
     LogManager.resetConfiguration();
@@ -41,20 +45,19 @@
     dst.setLayout(layout);
     dst.setTarget("System.err");
     dst.setThreshold(Level.ERROR);
+    dst.activateOptions();
 
     final Logger root = LogManager.getRootLogger();
     root.removeAllAppenders();
     root.addAppender(dst);
   }
 
-  public static LifecycleListener start(final File sitePath)
-      throws FileNotFoundException {
-    final File logdir = new SitePaths(sitePath).logs_dir;
-    if (!logdir.exists() && !logdir.mkdirs()) {
-      throw new Die("Cannot create log directory: " + logdir);
-    }
+  public static LifecycleListener start(final Path sitePath, final Config config)
+      throws IOException {
+    Path logdir = FileUtil.mkdirsOrDie(new SitePaths(sitePath).logs_dir,
+        "Cannot create log directory");
     if (SystemLog.shouldConfigure()) {
-      initLogSystem(logdir);
+      initLogSystem(logdir, config);
     }
 
     return new LifecycleListener() {
@@ -69,10 +72,21 @@
     };
   }
 
-  private static void initLogSystem(final File logdir) {
+  private static void initLogSystem(Path logdir, Config config) {
     final Logger root = LogManager.getRootLogger();
     root.removeAllAppenders();
-    root.addAppender(SystemLog.createAppender(logdir, LOG_NAME,
-        new PatternLayout("[%d] %-5p %c %x: %m%n")));
+
+    boolean json = config.getBoolean("log", "jsonLogging", false);
+    boolean text = config.getBoolean("log", "textLogging", true) || !json;
+
+    if (text) {
+      root.addAppender(SystemLog.createAppender(logdir, LOG_NAME,
+          new PatternLayout("[%d] [%t] %-5p %c %x: %m%n")));
+    }
+
+    if (json) {
+      root.addAppender(SystemLog.createAppender(logdir, LOG_NAME + JSON_SUFFIX,
+          new JSONEventLayoutV1()));
+    }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index db74ac3..1107208 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.SitePaths;
@@ -25,12 +26,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.zip.GZIPOutputStream;
 
 /** Compresses the old error logs. */
@@ -65,76 +66,78 @@
     }
   }
 
-  private final File logs_dir;
+  private final Path logs_dir;
 
   @Inject
   LogFileCompressor(final SitePaths site) {
     logs_dir = resolve(site.logs_dir);
   }
 
-  private static File resolve(final File logs_dir) {
+  private static Path resolve(Path p) {
     try {
-      return logs_dir.getCanonicalFile();
+      return p.toRealPath().normalize();
     } catch (IOException e) {
-      return logs_dir.getAbsoluteFile();
+      return p.toAbsolutePath().normalize();
     }
   }
 
   @Override
   public void run() {
-    final File[] list = logs_dir.listFiles();
-    if (list == null) {
+    if (!Files.isDirectory(logs_dir)) {
       return;
     }
-
-    for (final File entry : list) {
-      if (!isLive(entry) && !isCompressed(entry) && isLogFile(entry)) {
-        compress(entry);
+    try (DirectoryStream<Path> list = Files.newDirectoryStream(logs_dir)) {
+      for (Path entry : list) {
+        if (!isLive(entry) && !isCompressed(entry) && isLogFile(entry)) {
+          compress(entry);
+        }
       }
+    } catch (IOException e) {
+      log.error("Error listing logs to compress in " + logs_dir, e);
     }
   }
 
-  private boolean isLive(final File entry) {
-    final String name = entry.getName();
+  private boolean isLive(Path entry) {
+    String name = entry.getFileName().toString();
     return name.endsWith("_log")
         || name.endsWith(".log")
         || name.endsWith(".run")
         || name.endsWith(".pid");
   }
 
-  private boolean isCompressed(final File entry) {
-    final String name = entry.getName();
+  private boolean isCompressed(Path entry) {
+    String name = entry.getFileName().toString();
     return name.endsWith(".gz") //
         || name.endsWith(".zip") //
         || name.endsWith(".bz2");
   }
 
-  private boolean isLogFile(final File entry) {
-    return entry.isFile();
+  private boolean isLogFile(Path entry) {
+    return Files.isRegularFile(entry);
   }
 
-  private void compress(final File src) {
-    final File dir = src.getParentFile();
-    final File dst = new File(dir, src.getName() + ".gz");
-    final File tmp = new File(dir, ".tmp." + src.getName());
+  private void compress(Path src) {
+    Path dst = src.resolveSibling(src.getFileName() + ".gz");
+    Path tmp = src.resolveSibling(".tmp." + src.getFileName());
     try {
-      try (InputStream in = new FileInputStream(src);
-          FileOutputStream fo = new FileOutputStream(tmp);
-          OutputStream out = new GZIPOutputStream(fo)) {
-        final byte[] buf = new byte[2048];
-        int n;
-        while (0 < (n = in.read(buf))) {
-          out.write(buf, 0, n);
-        }
-        tmp.setReadOnly();
+      try (InputStream in = Files.newInputStream(src);
+          OutputStream out = new GZIPOutputStream(Files.newOutputStream(tmp))) {
+        ByteStreams.copy(in, out);
       }
-      if (!tmp.renameTo(dst)) {
-        throw new IOException("Cannot rename " + tmp + " to " + dst);
+      tmp.toFile().setReadOnly();
+      try {
+        Files.move(tmp, dst);
+      } catch (IOException e) {
+        throw new IOException("Cannot rename " + tmp + " to " + dst, e);
       }
-      src.delete();
+      Files.delete(src);
     } catch (IOException e) {
       log.error("Cannot compress " + src, e);
-      tmp.delete();
+      try {
+        Files.deleteIfExists(tmp);
+      } catch (IOException e2) {
+        log.warn("Failed to delete temporary log file " + tmp, e2);
+      }
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
index c713b79..048c2ee 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
@@ -24,14 +24,14 @@
 
 import org.eclipse.jgit.lib.Config;
 
-import java.io.File;
+import java.nio.file.Path;
 
 import javax.sql.DataSource;
 
 /** Loads the site library if not yet loaded. */
 @Singleton
 public class SiteLibraryBasedDataSourceProvider extends DataSourceProvider {
-  private final File libdir;
+  private final Path libdir;
   private boolean init;
 
   @Inject
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 9763b1a..a6f1f93 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -50,8 +50,10 @@
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
-import java.io.File;
 import java.lang.annotation.Annotation;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.ArrayList;
@@ -61,30 +63,30 @@
 
 public abstract class SiteProgram extends AbstractProgram {
   @Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
-  private File sitePath = new File(".");
+  private void setSitePath(String path) {
+    sitePath = Paths.get(path);
+  }
 
   protected Provider<DataSource> dsProvider;
 
+  private Path sitePath = Paths.get(".");
+
   protected SiteProgram() {
   }
 
-  protected SiteProgram(File sitePath, final Provider<DataSource> dsProvider) {
+  protected SiteProgram(Path sitePath, final Provider<DataSource> dsProvider) {
     this.sitePath = sitePath;
     this.dsProvider = dsProvider;
   }
 
   /** @return the site path specified on the command line. */
-  protected File getSitePath() {
-    File path = sitePath.getAbsoluteFile();
-    if (".".equals(path.getName())) {
-      path = path.getParentFile();
-    }
-    return path;
+  protected Path getSitePath() {
+    return sitePath;
   }
 
   /** Ensures we are running inside of a valid site, otherwise throws a Die. */
   protected void mustHaveValidSite() throws Die {
-    if (!new File(new File(getSitePath(), "etc"), "gerrit.config").exists()) {
+    if (!Files.exists(sitePath.resolve("etc").resolve("gerrit.config"))) {
       throw die("not a Gerrit site: '" + getSitePath() + "'\n"
           + "Perhaps you need to run init first?");
     }
@@ -92,13 +94,13 @@
 
   /** @return provides database connectivity and site path. */
   protected Injector createDbInjector(final DataSourceProvider.Context context) {
-    final File sitePath = getSitePath();
+    final Path sitePath = getSitePath();
     final List<Module> modules = new ArrayList<>();
 
     Module sitePathModule = new AbstractModule() {
       @Override
       protected void configure() {
-        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
         bind(String.class).annotatedWith(SecureStoreClassName.class)
             .toProvider(Providers.of(getConfiguredSecureStoreClass()));
       }
@@ -143,7 +145,8 @@
       @Override
       protected void configure() {
         bind(DataSourceType.class).toInstance(dst);
-      }});
+      }
+    });
     modules.add(new DatabaseModule());
     modules.add(new SchemaModule());
     modules.add(new LocalDiskRepositoryManager.Module());
@@ -198,7 +201,7 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(Path.class).annotatedWith(SitePath.class).toInstance(getSitePath());
       }
     });
     modules.add(new GerritServerConfigModule());
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
index 6cc73fe..6614b2b 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -15,19 +15,27 @@
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleProvider"]
-  name = Bouncy Castle Crypto Provider v151
-  url = http://repo2.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.51/bcprov-jdk15on-1.51.jar
-  sha1 = 9ab8afcc2842d5ef06eb775a0a2b12783b99aa80
+  name = Bouncy Castle Crypto Provider v152
+  url = http://repo2.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.52/bcprov-jdk15on-1.52.jar
+  sha1 = 88a941faf9819d371e3174b5ed56a3f3f7d73269
   remove = bcprov-.*[.]jar
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleSSL"]
-  name = Bouncy Castle Crypto SSL v151
-  url = http://repo2.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.51/bcpkix-jdk15on-1.51.jar
-  sha1 = 6c8c1f61bf27a09f9b1a8abc201523669bba9597
+  name = Bouncy Castle Crypto SSL v152
+  url = http://repo2.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.52/bcpkix-jdk15on-1.52.jar
+  sha1 = b8ffac2bbc6626f86909589c8cc63637cc936504
   needs = bouncyCastleProvider
   remove = bcpkix-.*[.]jar
 
+# Version should match lib/bouncycastle/BUCK
+[library "bouncyCastlePGP"]
+  name = Bouncy Castle Crypto OpenPGP v152
+  url = http://repo2.maven.org/maven2/org/bouncycastle/bcpg-jdk15on/1.52/bcpg-jdk15on-1.52.jar
+  sha1 = ff4665a4b5633ff6894209d5dd10b7e612291858
+  needs = bouncyCastleProvider
+  remove = bcpg-.*[.]jar
+
 [library "mysqlDriver"]
   name = MySQL Connector/J 5.1.21
   url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
@@ -39,3 +47,17 @@
   url = file:///u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
   sha1 = 2f89cd9176772c3a6c261ce6a8e3d0d4425f5679
   remove = ojdbc6.jar
+
+[library "db2Driver"]
+  name = DB2 Type 4 JDBC driver (10.5)
+  url = file:///opt/ibm/db2/V10.5/java/db2jcc4.jar
+  sha1 = 9344d4fd41d6511f2d1d1deb7759056495b3a39b
+  needs = db2DriverLicense
+  remove = db2jcc4.jar
+
+# Omit SHA-1 for license JAR as it's not stable and depends on the product
+# the customer has purchased.
+[library "db2DriverLicense"]
+  name = DB2 Type 4 JDBC driver license (10.5)
+  url = file:///opt/ibm/db2/V10.5/java/db2jcc_license_cu.jar
+  remove = db2jcc_license_cu.jar
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java
index 4d7370b..150309e 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java
@@ -16,11 +16,11 @@
 
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 public abstract class InitTestCase extends LocalDiskRepositoryTestCase {
-  protected File newSitePath() throws IOException {
-    return new File(createWorkRepository().getWorkTree(), "test_site");
+  protected Path newSitePath() throws IOException {
+    return createWorkRepository().getWorkTree().toPath().resolve("test_site");
   }
 }
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
index a37c97d..2198788 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -25,13 +25,12 @@
 
 import org.junit.Test;
 
-import java.io.File;
-import java.io.FileNotFoundException;
+import java.nio.file.Paths;
 
 public class LibrariesTest {
   @Test
-  public void testCreate() throws FileNotFoundException {
-    final SitePaths site = new SitePaths(new File("."));
+  public void testCreate() throws Exception {
+    final SitePaths site = new SitePaths(Paths.get("."));
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
 
     replay(ui);
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index 720d108..dc7ce59 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
@@ -24,6 +25,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.Section;
@@ -34,13 +37,12 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.IO;
 import org.junit.Test;
 
-import java.io.File;
-import java.io.FileWriter;
 import java.io.IOException;
-import java.io.Writer;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 
@@ -49,23 +51,17 @@
 
   @Test
   public void testUpgrade() throws IOException, ConfigInvalidException {
-    final File p = newSitePath();
+    final Path p = newSitePath();
     final SitePaths site = new SitePaths(p);
     assertTrue(site.isNew);
-    assertTrue(site.site_path.mkdir());
-    assertTrue(site.etc_dir.mkdir());
+    FileUtil.mkdirsOrDie(site.etc_dir, "Failed to create");
 
     for (String n : UpgradeFrom2_0_x.etcFiles) {
-      Writer w = new FileWriter(new File(p, n));
-      try {
-        w.write("# " + n + "\n");
-      } finally {
-        w.close();
-      }
+      Files.write(p.resolve(n), ("# " + n + "\n").getBytes(UTF_8));
     }
 
     FileBasedConfig old =
-        new FileBasedConfig(new File(p, "gerrit.config"), FS.DETECTED);
+        new FileBasedConfig(p.resolve("gerrit.config").toFile(), FS.DETECTED);
 
     old.setString("ldap", null, "username", "ldap.user");
     old.setString("ldap", null, "password", "ldap.s3kr3t");
@@ -85,8 +81,11 @@
       }
     };
 
-    expect(ui.yesno(eq(true), eq("Upgrade '%s'"), eq(p.getCanonicalPath())))
-        .andReturn(true);
+    expect(ui.yesno(
+        eq(true),
+        eq("Upgrade '%s'"),
+        eq(p.toAbsolutePath().normalize())))
+      .andReturn(true);
     replay(ui);
 
     UpgradeFrom2_0_x u = new UpgradeFrom2_0_x(site, flags, ui, sections);
@@ -96,13 +95,17 @@
     verify(ui);
 
     for (String n : UpgradeFrom2_0_x.etcFiles) {
-      if ("gerrit.config".equals(n)) continue;
-      if ("secure.config".equals(n)) continue;
-      assertEquals("# " + n + "\n",//
-          new String(IO.readFully(new File(site.etc_dir, n)), "UTF-8"));
+      if ("gerrit.config".equals(n) || "secure.config".equals(n)) {
+        continue;
+      }
+      try (InputStream in = Files.newInputStream(site.etc_dir.resolve(n))) {
+        assertEquals("# " + n + "\n",
+            new String(ByteStreams.toByteArray(in), UTF_8));
+      }
     }
 
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    FileBasedConfig cfg =
+        new FileBasedConfig(site.gerrit_config.toFile(), FS.DETECTED);
     cfg.load();
 
     assertEquals("email.user", cfg.getString("sendemail", null, "smtpUser"));
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 80ab9a3..abcacf9 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -33,12 +33,13 @@
     '//lib:jsch',
     '//lib:mime-util',
     '//lib:servlet-api-3_1',
-    '//lib/commons:io',
+    '//lib:velocity',
     '//lib/commons:lang',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
+    '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/mina:sshd',
   ],
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index d13f32f..0417910 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,11 +2,11 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <licenses>
     <license>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 9ef2148..db150bd 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,10 +20,10 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Plugins</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <properties>
     <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index 681af10..270e15c 100644
--- a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -32,7 +32,7 @@
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
     <requiredProperty key="Implementation-Url">
-      <defaultValue>http://code.google.com/p/gerrit/</defaultValue>
+      <defaultValue>https://www.gerritcodereview.com/</defaultValue>
     </requiredProperty>
 
     <requiredProperty key="gerritApiType">
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
index 8667cfd..5a0ad22 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
@@ -1,3 +1,2 @@
-#Tue Sep 02 16:59:24 PDT 2008
 eclipse.preferences.version=1
 line.separator=\n
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
index d4218a5..7397758 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
@@ -1,10 +1,9 @@
-#Wed Jul 29 11:31:38 PDT 2009
 eclipse.preferences.version=1
 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
 formatter_profile=_Google Format
 formatter_settings_version=11
 org.eclipse.jdt.ui.ignorelowercasenames=true
-org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax;
 org.eclipse.jdt.ui.ondemandthreshold=99
 org.eclipse.jdt.ui.staticondemandthreshold=99
 org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
index 17c9722..a6103b1 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2014 The Android Open Source Project
+Copyright (C) 2015 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.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
index d7955a6..a0fed9e 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
index dfe0c4f..39ce59b 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
index bf9a531..1ef7cc8 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
index 80f0627d..e4e944a 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
@@ -31,6 +31,11 @@
   cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
 ```
 
+Add link to the .watchmanconfig file:
+```
+  cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig
+```
+
 To build the plugin, issue the following command:
 
 
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 5a79752..47a7095 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,10 +20,10 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <properties>
     <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index fa86ab4..3c3508c 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -22,7 +22,7 @@
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
     <requiredProperty key="Implementation-Url">
-      <defaultValue>http://code.google.com/p/gerrit/</defaultValue>
+      <defaultValue>https://www.gerritcodereview.com/</defaultValue>
     </requiredProperty>
     <requiredProperty key="Gwt-Version">
       <defaultValue>2.7.0</defaultValue>
@@ -59,7 +59,12 @@
       <include>lib/gerrit/BUCK</include>
       <include>lib/gwt/BUCK</include>
       <excludes>
+        <exclude>**/client/</exclude>
+        <exclude>**/public/</exclude>
+        <exclude>**/*.css</exclude>
+        <exclude>**/*.png</exclude>
         <exclude>**/*.java</exclude>
+        <exclude>**/*.gwt.xml</exclude>
       </excludes>
     </fileSet>
 
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
index 8667cfd..5a0ad22 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
@@ -1,3 +1,2 @@
-#Tue Sep 02 16:59:24 PDT 2008
 eclipse.preferences.version=1
 line.separator=\n
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
index d4218a5..7397758 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
@@ -1,10 +1,9 @@
-#Wed Jul 29 11:31:38 PDT 2009
 eclipse.preferences.version=1
 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
 formatter_profile=_Google Format
 formatter_settings_version=11
 org.eclipse.jdt.ui.ignorelowercasenames=true
-org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax;
 org.eclipse.jdt.ui.ondemandthreshold=99
 org.eclipse.jdt.ui.staticondemandthreshold=99
 org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
index b19312c..b224bf6 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
@@ -10,7 +10,6 @@
     'Gerrit-ApiType: plugin',
     'Gerrit-ApiVersion: ${gerritApiVersion}',
     'Gerrit-Module: ${package}.Module',
-    'Gerrit-SshModule: ${package}.SshModule',
     'Gerrit-HttpModule: ${package}.HttpModule',
   ],
 )
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/ow2/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/ow2/BUCK
new file mode 100644
index 0000000..db6c76c
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/ow2/BUCK
@@ -0,0 +1,32 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VERSION = '5.0.3'
+
+maven_jar(
+  name = 'ow2-asm',
+  id = 'org.ow2.asm:asm:' + VERSION,
+  sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-analysis',
+  id = 'org.ow2.asm:asm-analysis:' + VERSION,
+  sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-tree',
+  id = 'org.ow2.asm:asm-tree:' + VERSION,
+  sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
+  license = 'ow2',
+)
+
+maven_jar(
+  name = 'ow2-asm-util',
+  id = 'org.ow2.asm:asm-util:' + VERSION,
+  sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
+  license = 'ow2',
+)
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
index 098d071..d67c7cb 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2014 The Android Open Source Project
+Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java
index f3c8dea..d2d9d80 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml
index a05527d..1f6f81e 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
- Copyright (C) 2014 The Android Open Source Project
+ Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
index c3fbfbe..4f043d0 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
index c5320d2..c734bb7 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java
index 62e87ff..4a7e149 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java
index d067a96..09b8b92 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
index 4c56ed6..e225bab 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
@@ -30,8 +30,12 @@
   cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
 ```
 
-To build the plugin, issue the following command:
+Add link to the .watchmanconfig file:
+```
+  cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig
+```
 
+To build the plugin, issue the following command:
 
 ```
   buck build plugin
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index 0f4c064..2ee0e19 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -9,7 +9,6 @@
   name = 'gwtui-api',
   deps = [
     ':gwtui-api-lib',
-    '//gerrit-extension-api:client-lib',
     '//gerrit-gwtui-common:client-lib',
   ],
   visibility = ['PUBLIC'],
@@ -17,22 +16,9 @@
 
 java_library(
   name = 'gwtui-api-lib',
-  exported_deps = [':gwtui-api-lib2'],
-  visibility = ['PUBLIC'],
-)
-
-java_library(
-  name = 'gwtui-api-lib2',
   srcs = SRCS,
   resources = glob(['src/main/**/*']),
-  exported_deps = [
-    '//gerrit-extension-api:client-lib',
-    '//gerrit-gwtexpui:Clippy',
-    '//gerrit-gwtexpui:GlobalKey',
-    '//gerrit-gwtexpui:SafeHtml',
-    '//gerrit-gwtexpui:UserAgent',
-    '//gerrit-gwtui-common:client-lib2',
-  ],
+  exported_deps = ['//gerrit-gwtui-common:client-lib'],
   provided_deps = DEPS + ['//lib/gwt:dev'],
   visibility = ['PUBLIC'],
 )
@@ -67,8 +53,12 @@
   paths = COMMON + GWTEXPUI,
   srcs = SRCS,
   deps = DEPS + [
-    '//lib/gwt:dev__jar',
-    '//gerrit-gwtui-common:client-lib2',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm_client',
+    '//lib/gwt:dev',
+    '//gerrit-gwtui-common:client-lib',
+    '//gerrit-common:client',
+    '//gerrit-reviewdb:client',
   ],
   visibility = ['PUBLIC'],
   do_it_wrong = True,
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index e9870e6..01d38ef 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,11 +2,11 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin GWT UI</name>
   <description>Common Classes for Gerrit GWT UI Plugins</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <licenses>
     <license>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/FormatUtil.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/FormatUtil.java
new file mode 100644
index 0000000..e5e8c62
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/FormatUtil.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2015 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.plugin.client;
+
+import com.google.gerrit.client.AccountFormatter;
+import com.google.gerrit.client.DateFormatter;
+import com.google.gerrit.client.RelativeDateFormatter;
+import com.google.gerrit.client.info.AccountInfo;
+
+import java.util.Date;
+
+public class FormatUtil {
+  private static final AccountFormatter accountFormatter =
+      new AccountFormatter(Plugin.get().getServerInfo().user()
+          .anonymousCowardName());
+
+  /** Format a date using a really short format. */
+  public static String shortFormat(Date dt) {
+    return createDateFormatter().shortFormat(dt);
+  }
+
+  /** Format a date using a really short format. */
+  public static String shortFormatDayTime(Date dt) {
+    return createDateFormatter().shortFormatDayTime(dt);
+  }
+
+  /** Format a date using the locale's medium length format. */
+  public static String mediumFormat(Date dt) {
+    return createDateFormatter().mediumFormat(dt);
+  }
+
+  private static DateFormatter createDateFormatter() {
+    return new DateFormatter(Plugin.get().getUserPreferences());
+  }
+
+  /** Format a date using git log's relative date format. */
+  public static String relativeFormat(Date dt) {
+    return RelativeDateFormatter.format(dt);
+  }
+
+  /**
+   * Formats an account as a name and an email address.
+   * <p>
+   * Example output:
+   * <ul>
+   * <li>{@code A U. Thor &lt;author@example.com&gt;}: full populated</li>
+   * <li>{@code A U. Thor (12)}: missing email address</li>
+   * <li>{@code Anonymous Coward &lt;author@example.com&gt;}: missing name</li>
+   * <li>{@code Anonymous Coward (12)}: missing name and email address</li>
+   * </ul>
+   */
+  public static String nameEmail(AccountInfo info) {
+    return accountFormatter.nameEmail(info);
+  }
+
+  /**
+   * Formats an account name.
+   * <p>
+   * If the account has a full name, it returns only the full name. Otherwise it
+   * returns a longer form that includes the email address.
+   */
+  public static String name(AccountInfo info) {
+    return accountFormatter.name(info);
+  }
+}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
index bf19352..4354072 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.plugin.client;
 
+import com.google.gerrit.client.GerritUiExtensionPoint;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AccountPreferencesInfo;
+import com.google.gerrit.client.info.ServerInfo;
+import com.google.gerrit.plugin.client.extension.Panel;
 import com.google.gerrit.plugin.client.screen.Screen;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -54,6 +59,22 @@
   public final native void refreshMenuBar()
   /*-{ return this.refreshMenuBar() }-*/;
 
+  /** @return the preferences of the currently signed in user, the default preferences if not signed in */
+  public final native AccountPreferencesInfo getUserPreferences()
+  /*-{ return this.getUserPreferences() }-*/;
+
+  /** Refresh the user preferences of the current user. */
+  public final native void refreshUserPreferences()
+  /*-{ return this.refreshUserPreferences() }-*/;
+
+  /** @return the server info */
+  public final native ServerInfo getServerInfo()
+  /*-{ return this.getServerInfo() }-*/;
+
+  /** @return the current user */
+  public final native AccountInfo getCurrentUser()
+  /*-{ return this.getCurrentUser() }-*/;
+
   /** Check if user is signed in. */
   public final native boolean isSignedIn()
   /*-{ return this.isSignedIn() }-*/;
@@ -91,6 +112,33 @@
   private final native void screenRegex(String p, JavaScriptObject e)
   /*-{ this.screen(new $wnd.RegExp(p), e) }-*/;
 
+  /**
+   * Register a settings screen displayed at {@code /#/settings/x/plugin/token}.
+   *
+   * @param token literal anchor token appearing after the plugin name.
+   * @param entry callback function invoked to create the settings screen widgets.
+   */
+  public final void settingsScreen(String token, String menu, Screen.EntryPoint entry) {
+    settingsScreen(token, menu, wrap(entry));
+  }
+
+  private final native void settingsScreen(String t, String m, JavaScriptObject e)
+  /*-{ this.settingsScreen(t, m, e) }-*/;
+
+  /**
+   * Register a panel for a UI extension point.
+   *
+   * @param extensionPoint the UI extension point for which the panel should be
+   *        registered.
+   * @param entry callback function invoked to create the panel widgets.
+   */
+  public final void panel(GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry) {
+    panel(extensionPoint.name(), wrap(entry));
+  }
+
+  private final native void panel(String i, JavaScriptObject e)
+  /*-{ this.panel(i, e) }-*/;
+
   protected Plugin() {
   }
 
@@ -105,4 +153,11 @@
         @com.google.gerrit.plugin.client.screen.Screen::new(Lcom/google/gerrit/plugin/client/screen/Screen$Context;)(c));
     });
   }-*/;
+
+  private static final native JavaScriptObject wrap(Panel.EntryPoint b) /*-{
+    return $entry(function(c){
+      b.@com.google.gerrit.plugin.client.extension.Panel.EntryPoint::onLoad(Lcom/google/gerrit/plugin/client/extension/Panel;)(
+        @com.google.gerrit.plugin.client.extension.Panel::new(Lcom/google/gerrit/plugin/client/extension/Panel$Context;)(c));
+    });
+  }-*/;
 }
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
new file mode 100644
index 0000000..6d4e719
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2015 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.plugin.client.extension;
+
+import com.google.gerrit.client.GerritUiExtensionPoint;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+/**
+ * Panel that extends a Gerrit core screen contributed by this plugin.
+ *
+ * Panel should be registered early at module load:
+ *
+ * <pre>
+ * &#064;Override
+ * public void onModuleLoad() {
+ *   Plugin.get().panel(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+ *       new Panel.EntryPoint() {
+ *         &#064;Override
+ *         public void onLoad(Panel panel) {
+ *           panel.setWidget(new Label(&quot;World&quot;));
+ *         }
+ *       });
+ * }
+ * </pre>
+ */
+public class Panel extends SimplePanel {
+  /** Initializes a panel for display. */
+  public interface EntryPoint {
+    /**
+     * Invoked when the panel has been created.
+     * <p>
+     * The implementation should create a single widget to define the content of
+     * this panel and add it to the passed panel instance.
+     * <p>
+     * To use multiple widgets, compose them in panels such as {@code FlowPanel}
+     * and add only the top level widget to the panel.
+     * <p>
+     * The panel is already attached to the browser DOM.
+     * Any widgets added to the screen will immediately receive {@code onLoad()}.
+     * GWT will fire {@code onUnload()} when the panel is removed from the UI,
+     * generally caused by the user navigating to another screen.
+     *
+     * @param panel panel that will contain the panel widget.
+     */
+    public void onLoad(Panel panel);
+  }
+
+  static final class Context extends JavaScriptObject {
+    final native Element body() /*-{ return this.body }-*/;
+
+    final native String get(String k) /*-{ return this.p[k]; }-*/;
+    final native int getInt(String k, int d) /*-{
+      return this.p.hasOwnProperty(k) ? this.p[k] : d
+    }-*/;
+    final native int getBoolean(String k, boolean d) /*-{
+      return this.p.hasOwnProperty(k) ? this.p[k] : d
+    }-*/;
+    final native JavaScriptObject getObject(String k)
+    /*-{ return this.p[k]; }-*/;
+
+
+    final native void detach(Panel p) /*-{
+      this.onUnload($entry(function(){
+        p.@com.google.gwt.user.client.ui.Widget::onDetach()();
+      }));
+    }-*/;
+
+    protected Context() {
+    }
+  }
+
+  private final Context ctx;
+
+  Panel(Context ctx) {
+    super(ctx.body());
+    this.ctx = ctx;
+    onAttach();
+    ctx.detach(this);
+  }
+
+  public String get(GerritUiExtensionPoint.Key key) {
+    return ctx.get(key.name());
+  }
+
+  public int getInt(GerritUiExtensionPoint.Key key, int defaultValue) {
+    return ctx.getInt(key.name(), defaultValue);
+  }
+
+  public int getBoolean(GerritUiExtensionPoint.Key key, boolean defaultValue) {
+    return ctx.getBoolean(key.name(), defaultValue);
+  }
+
+  public JavaScriptObject getObject(GerritUiExtensionPoint.Key key) {
+    return ctx.getObject(key.name());
+  }
+}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
new file mode 100644
index 0000000..528b07a
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2015 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.plugin.client.ui;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.ui.HighlightSuggestion;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A {@code SuggestOracle} for groups. */
+public class GroupSuggestOracle extends SuggestOracle {
+
+  private final int chars;
+
+  /**
+   * @param chars minimum chars to start suggesting.
+   */
+  public GroupSuggestOracle(int chars) {
+    this.chars = chars;
+  }
+
+  @Override
+  public boolean isDisplayStringHTML() {
+    return true;
+  }
+
+  @Override
+  public void requestSuggestions(final Request req, final Callback done) {
+    if (req.getQuery().length() < chars) {
+      responseEmptySuggestion(req, done);
+      return;
+    }
+    RestApi rest = new RestApi("/groups/").addParameter("suggest", req.getQuery());
+    if (req.getLimit() > 0) {
+      rest.addParameter("n", req.getLimit());
+    }
+    rest.get(new AsyncCallback<NativeMap<JavaScriptObject>>() {
+      @Override
+      public void onSuccess(NativeMap<JavaScriptObject> result) {
+        List<String> keys = result.sortedKeys();
+        List<Suggestion> suggestions = new ArrayList<>(keys.size());
+        for (String g : keys) {
+          suggestions.add(new HighlightSuggestion(req.getQuery(), g));
+        }
+        done.onSuggestionsReady(req, new Response(suggestions));
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        responseEmptySuggestion(req, done);
+      }
+    });
+  }
+
+  private static void responseEmptySuggestion(Request req, Callback done) {
+    List<Suggestion> empty = Collections.emptyList();
+    done.onSuggestionsReady(req, new Response(empty));
+  }
+}
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index 8e09f0e..c495998 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,10 +20,10 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <properties>
     <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index e4978dd..fbf1e46 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -22,7 +22,7 @@
       <defaultValue>Gerrit Code Review</defaultValue>
     </requiredProperty>
     <requiredProperty key="Implementation-Url">
-      <defaultValue>http://code.google.com/p/gerrit/</defaultValue>
+      <defaultValue>https://gerrit.googlesource.com/</defaultValue>
     </requiredProperty>
 
     <requiredProperty key="gerritApiType">
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
index 8667cfd..5a0ad22 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs
@@ -1,3 +1,2 @@
-#Tue Sep 02 16:59:24 PDT 2008
 eclipse.preferences.version=1
 line.separator=\n
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
index d4218a5..7397758 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs
@@ -1,10 +1,9 @@
-#Wed Jul 29 11:31:38 PDT 2009
 eclipse.preferences.version=1
 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
 formatter_profile=_Google Format
 formatter_settings_version=11
 org.eclipse.jdt.ui.ignorelowercasenames=true
-org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax;
 org.eclipse.jdt.ui.ondemandthreshold=99
 org.eclipse.jdt.ui.staticondemandthreshold=99
 org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
index ac692ff..f24d81e 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2014 The Android Open Source Project
+Copyright (C) 2015 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.
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java
index bb4e0c5..39d06e3 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
diff --git a/gerrit-prettify/BUCK b/gerrit-prettify/BUCK
index 22b363f..9a0136e 100644
--- a/gerrit-prettify/BUCK
+++ b/gerrit-prettify/BUCK
@@ -12,14 +12,17 @@
   ]),
   deps = [
     ':google-code-prettify',
+    '//gerrit-gwtexpui:SafeHtml',
+  ],
+  exported_deps = [
+    '//gerrit-extension-api:client',
     '//gerrit-patch-jgit:client',
     '//gerrit-reviewdb:client',
-    '//gerrit-gwtexpui:SafeHtml',
-    '//lib:guava',
     '//lib:gwtjsonrpc',
-    '//lib/gwt:user',
-    '//lib/jgit:jgit',
+    '//lib:gwtjsonrpc_src',
+    '//lib/jgit:Edit',
   ],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
index 48591f8..fd88f6c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/PrettyFormatter.gwt.xml
@@ -14,10 +14,9 @@
  limitations under the License.
 -->
 <module>
-  <replace-with class='com.google.gerrit.prettify.client.PrivateScopeImplIE6'>
+  <replace-with class='com.google.gerrit.prettify.client.PrivateScopeImplIE8'>
     <when-type-is class='com.google.gerrit.prettify.client.PrivateScopeImpl'/>
     <any>
-      <when-property-is name="user.agent" value="ie6" />
       <when-property-is name="user.agent" value="ie8" />
     </any>
   </replace-with>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index cdf800c..49dc2fc 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.prettify.client;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -73,7 +73,7 @@
   protected SparseFileContent content;
   protected EditFilter side;
   protected List<Edit> edits;
-  protected AccountDiffPreference diffPrefs;
+  protected DiffPreferencesInfo diffPrefs;
   protected String fileName;
   protected Set<Integer> trailingEdits;
 
@@ -110,7 +110,7 @@
     edits = all;
   }
 
-  public void setDiffPrefs(AccountDiffPreference how) {
+  public void setDiffPrefs(DiffPreferencesInfo how) {
     diffPrefs = how;
   }
 
@@ -132,7 +132,7 @@
     String html = toHTML(src);
 
     html = expandTabs(html);
-    if (diffPrefs.isSyntaxHighlighting() && getFileType() != null
+    if (diffPrefs.syntaxHighlighting && getFileType() != null
         && src.isWholeFile()) {
       // The prettify parsers don't like &#39; as an entity for the
       // single quote character. Replace them all out so we don't
@@ -233,7 +233,7 @@
       cleanText(txt, pos, start);
       pos = txt.indexOf(';', start + 1) + 1;
 
-      if (diffPrefs.getLineLength() <= col) {
+      if (diffPrefs.lineLength <= col) {
         buf.append("<br />");
         col = 0;
       }
@@ -247,14 +247,14 @@
 
   private void cleanText(String txt, int pos, int end) {
     while (pos < end) {
-      int free = diffPrefs.getLineLength() - col;
+      int free = diffPrefs.lineLength - col;
       if (free <= 0) {
         // The current line is full. Throw an explicit line break
         // onto the end, and we'll continue on the next line.
         //
         buf.append("<br />");
         col = 0;
-        free = diffPrefs.getLineLength();
+        free = diffPrefs.lineLength;
       }
 
       int n = Math.min(end - pos, free);
@@ -326,7 +326,7 @@
   private String toHTML(SparseFileContent src) {
     SafeHtml html;
 
-    if (diffPrefs.isIntralineDifference()) {
+    if (diffPrefs.intralineDifference) {
       html = colorLineEdits(src);
     } else {
       SafeHtmlBuilder b = new SafeHtmlBuilder();
@@ -342,7 +342,7 @@
       html = html.replaceAll("\r([^\n])", r);
     }
 
-    if (diffPrefs.isShowWhitespaceErrors()) {
+    if (diffPrefs.showWhitespaceErrors) {
       // We need to do whitespace errors before showing tabs, because
       // these patterns rely on \t as a literal, before it expands.
       //
@@ -350,12 +350,12 @@
       html = showTrailingWhitespace(html);
     }
 
-    if (diffPrefs.isShowLineEndings()){
+    if (diffPrefs.showLineEndings){
       html = showLineEndings(html);
     }
 
-    if (diffPrefs.isShowTabs()) {
-      String t = 1 < diffPrefs.getTabSize() ? "\t" : "";
+    if (diffPrefs.showTabs) {
+      String t = 1 < diffPrefs.tabSize ? "\t" : "";
       html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
     }
 
@@ -528,10 +528,10 @@
   private String expandTabs(String html) {
     StringBuilder tmp = new StringBuilder();
     int i = 0;
-    if (diffPrefs.isShowTabs()) {
+    if (diffPrefs.showTabs) {
       i = 1;
     }
-    for (; i < diffPrefs.getTabSize(); i++) {
+    for (; i < diffPrefs.tabSize; i++) {
       tmp.append("&nbsp;");
     }
     return html.replaceAll("\t", tmp.toString());
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE6.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE8.java
similarity index 90%
rename from gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE6.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE8.java
index abb4e15..0496d91 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE6.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrivateScopeImplIE8.java
@@ -16,8 +16,8 @@
 
 import com.google.gwt.core.client.JavaScriptObject;
 
-/** IE6 requires us to initialize the document before we can use it. */
-public class PrivateScopeImplIE6 extends PrivateScopeImpl {
+/** MSIE requires us to initialize the document before we can use it. */
+public class PrivateScopeImplIE8 extends PrivateScopeImpl {
   private JavaScriptObject context;
 
   @Override
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
index 6a42a8e..ec5c78d8 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
@@ -68,8 +68,9 @@
 
   private int findCombinedEnd(final int i) {
     int end = i + 1;
-    while (end < edits.size() && (combineA(end) || combineB(end)))
+    while (end < edits.size() && (combineA(end) || combineB(end))) {
       end++;
+    }
     return end - 1;
   }
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index b6e3bf9..a57146f 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -130,10 +130,11 @@
         return size();
       }
 
-      if (idx < cur.base)
+      if (idx < cur.base) {
         high = mid;
-      else
+      } else {
         low = mid + 1;
+      }
     } while (low < high);
 
     return size();
@@ -183,10 +184,11 @@
         currentRangeIdx = mid;
         return cur.get(idx);
       }
-      if (idx < cur.base)
+      if (idx < cur.base) {
         high = mid;
-      else
+      } else {
         low = mid + 1;
+      }
     } while (low < high);
     return null;
   }
diff --git a/gerrit-reviewdb/BUCK b/gerrit-reviewdb/BUCK
index ca2c18c..7bed0f3 100644
--- a/gerrit-reviewdb/BUCK
+++ b/gerrit-reviewdb/BUCK
@@ -7,8 +7,8 @@
   gwt_xml = SRC + 'ReviewDB.gwt.xml',
   deps = [
     '//gerrit-extension-api:client',
-    '//lib:gwtorm',
-    '//lib:gwtorm_src'
+    '//lib:gwtorm_client',
+    '//lib:gwtorm_client_src'
   ],
   visibility = ['PUBLIC'],
 )
@@ -29,9 +29,7 @@
   srcs = glob([TESTS + 'client/**/*.java']),
   deps = [
     ':client',
-    '//lib:guava',
     '//lib:gwtorm',
-    '//lib:junit',
     '//lib:truth',
   ],
   source_under_test = [':client'],
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index d4fb311..295239f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_USER;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
@@ -51,7 +52,7 @@
  * notifications of updates on that change, or just book-marking it for faster
  * future reference. One record per starred change.</li>
  *
- * <li>{@link AccountDiffPreference}: user's preferences for rendering side-to-side
+ * <li>{@link DiffPreferencesInfo}: user's preferences for rendering side-to-side
  * and unified diff</li>
  *
  * </ul>
@@ -111,8 +112,8 @@
       if (name == null) {
         return null;
       }
-      if (name.startsWith(REFS_USER)) {
-        return fromRefPart(name.substring(REFS_USER.length()));
+      if (name.startsWith(REFS_USERS)) {
+        return fromRefPart(name.substring(REFS_USERS.length()));
       }
       return null;
     }
@@ -182,9 +183,7 @@
   @Column(id = 4, notNull = false)
   protected String preferredEmail;
 
-  /** When did the user last give us contact information? Null if never. */
-  @Column(id = 5, notNull = false)
-  protected Timestamp contactFiledOn;
+  // DELETED: id = 5 (contactFiledOn)
 
   /** This user's preferences */
   @Column(id = 6, name = Column.NONE)
@@ -257,18 +256,6 @@
     generalPreferences = p;
   }
 
-  public boolean isContactFiled() {
-    return contactFiledOn != null;
-  }
-
-  public Timestamp getContactFiledOn() {
-    return contactFiledOn;
-  }
-
-  public void setContactFiled(Timestamp ts) {
-    contactFiledOn = ts;
-  }
-
   public boolean isActive() {
     return ! inactive;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
deleted file mode 100644
index 7948080..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
+++ /dev/null
@@ -1,338 +0,0 @@
-// Copyright (C) 2010 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.reviewdb.client;
-
-import com.google.gerrit.extensions.client.Theme;
-import com.google.gwtorm.client.Column;
-
-/** Diff formatting preferences of an account */
-public class AccountDiffPreference {
-
-  /** Default number of lines of context. */
-  public static final short DEFAULT_CONTEXT = 10;
-
-  /** Context setting to display the entire file. */
-  public static final short WHOLE_FILE_CONTEXT = -1;
-
-  /** Typical valid choices for the default context setting. */
-  public static final short[] CONTEXT_CHOICES =
-      {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
-
-  public static enum Whitespace implements CodedEnum {
-    IGNORE_NONE('N'), //
-    IGNORE_SPACE_AT_EOL('E'), //
-    IGNORE_SPACE_CHANGE('S'), //
-    IGNORE_ALL_SPACE('A');
-
-    private final char code;
-
-    private Whitespace(final char c) {
-      code = c;
-    }
-
-    @Override
-    public char getCode() {
-      return code;
-    }
-
-    public static Whitespace forCode(final char c) {
-      for (final Whitespace s : Whitespace.values()) {
-        if (s.code == c) {
-          return s;
-        }
-      }
-      return null;
-    }
-  }
-
-  public static AccountDiffPreference createDefault(Account.Id accountId) {
-    AccountDiffPreference p = new AccountDiffPreference(accountId);
-    p.setIgnoreWhitespace(Whitespace.IGNORE_NONE);
-    p.setTheme(Theme.DEFAULT);
-    p.setTabSize(8);
-    p.setLineLength(100);
-    p.setSyntaxHighlighting(true);
-    p.setShowWhitespaceErrors(true);
-    p.setShowLineEndings(true);
-    p.setIntralineDifference(true);
-    p.setShowTabs(true);
-    p.setContext(DEFAULT_CONTEXT);
-    p.setManualReview(false);
-    p.setHideEmptyPane(false);
-    p.setAutoHideDiffTableHeader(true);
-    return p;
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Account.Id accountId;
-
-  @Column(id = 2)
-  protected char ignoreWhitespace;
-
-  @Column(id = 3)
-  protected int tabSize;
-
-  @Column(id = 4)
-  protected int lineLength;
-
-  @Column(id = 5)
-  protected boolean syntaxHighlighting;
-
-  @Column(id = 6)
-  protected boolean showWhitespaceErrors;
-
-  @Column(id = 7)
-  protected boolean intralineDifference;
-
-  @Column(id = 8)
-  protected boolean showTabs;
-
-  /** Number of lines of context when viewing a patch. */
-  @Column(id = 9)
-  protected short context;
-
-  @Column(id = 10)
-  protected boolean skipDeleted;
-
-  @Column(id = 11)
-  protected boolean skipUncommented;
-
-  @Column(id = 12)
-  protected boolean expandAllComments;
-
-  @Column(id = 13)
-  protected boolean retainHeader;
-
-  @Column(id = 14)
-  protected boolean manualReview;
-
-  @Column(id = 15)
-  protected boolean showLineEndings;
-
-  @Column(id = 16)
-  protected boolean hideTopMenu;
-
-  @Column(id = 17)
-  protected boolean hideLineNumbers;
-
-  @Column(id = 18)
-  protected boolean renderEntireFile;
-
-  @Column(id = 19, length = 20, notNull = false)
-  protected String theme;
-
-  @Column(id = 20)
-  protected boolean hideEmptyPane;
-
-  @Column(id = 21)
-  protected boolean autoHideDiffTableHeader;
-
-  protected AccountDiffPreference() {
-  }
-
-  public AccountDiffPreference(Account.Id accountId) {
-    this.accountId = accountId;
-  }
-
-  public AccountDiffPreference(AccountDiffPreference p) {
-    this.accountId = p.accountId;
-    this.ignoreWhitespace = p.ignoreWhitespace;
-    this.tabSize = p.tabSize;
-    this.lineLength = p.lineLength;
-    this.syntaxHighlighting = p.syntaxHighlighting;
-    this.showWhitespaceErrors = p.showWhitespaceErrors;
-    this.showLineEndings = p.showLineEndings;
-    this.intralineDifference = p.intralineDifference;
-    this.showTabs = p.showTabs;
-    this.skipDeleted = p.skipDeleted;
-    this.skipUncommented = p.skipUncommented;
-    this.expandAllComments = p.expandAllComments;
-    this.context = p.context;
-    this.retainHeader = p.retainHeader;
-    this.manualReview = p.manualReview;
-    this.hideTopMenu = p.hideTopMenu;
-    this.hideLineNumbers = p.hideLineNumbers;
-    this.renderEntireFile = p.renderEntireFile;
-    this.hideEmptyPane = p.hideEmptyPane;
-    this.autoHideDiffTableHeader = p.autoHideDiffTableHeader;
-  }
-
-  public Account.Id getAccountId() {
-    return accountId;
-  }
-
-  public Whitespace getIgnoreWhitespace() {
-    return Whitespace.forCode(ignoreWhitespace);
-  }
-
-  public void setIgnoreWhitespace(Whitespace ignoreWhitespace) {
-    this.ignoreWhitespace = ignoreWhitespace.getCode();
-  }
-
-  public int getTabSize() {
-    return tabSize;
-  }
-
-  public void setTabSize(int tabSize) {
-    this.tabSize = tabSize;
-  }
-
-  public int getLineLength() {
-    return lineLength;
-  }
-
-  public void setLineLength(int lineLength) {
-    this.lineLength = lineLength;
-  }
-
-  public boolean isSyntaxHighlighting() {
-    return syntaxHighlighting;
-  }
-
-  public void setSyntaxHighlighting(boolean syntaxHighlighting) {
-    this.syntaxHighlighting = syntaxHighlighting;
-  }
-
-  public boolean isShowWhitespaceErrors() {
-    return showWhitespaceErrors;
-  }
-
-  public void setShowWhitespaceErrors(boolean showWhitespaceErrors) {
-    this.showWhitespaceErrors = showWhitespaceErrors;
-  }
-
-  public boolean isShowLineEndings() {
-    return showLineEndings;
-  }
-
-  public void setShowLineEndings(boolean showLineEndings) {
-    this.showLineEndings = showLineEndings;
-  }
-
-  public boolean isIntralineDifference() {
-    return intralineDifference;
-  }
-
-  public void setIntralineDifference(boolean intralineDifference) {
-    this.intralineDifference = intralineDifference;
-  }
-
-  public boolean isShowTabs() {
-    return showTabs;
-  }
-
-  public void setShowTabs(boolean showTabs) {
-    this.showTabs = showTabs;
-  }
-
-  /** Get the number of lines of context when viewing a patch. */
-  public short getContext() {
-    return context;
-  }
-
-  /** Set the number of lines of context when viewing a patch. */
-  public void setContext(final short context) {
-    assert 0 <= context || context == WHOLE_FILE_CONTEXT;
-    this.context = context;
-  }
-
-  public boolean isSkipDeleted() {
-    return skipDeleted;
-  }
-
-  public void setSkipDeleted(boolean skip) {
-    skipDeleted = skip;
-  }
-
-  public boolean isSkipUncommented() {
-    return skipUncommented;
-  }
-
-  public void setSkipUncommented(boolean skip) {
-    skipUncommented = skip;
-  }
-
-  public boolean isExpandAllComments() {
-    return expandAllComments;
-  }
-
-  public void setExpandAllComments(boolean expand) {
-    expandAllComments = expand;
-  }
-
-  public boolean isRetainHeader() {
-    return retainHeader;
-  }
-
-  public void setRetainHeader(boolean retain) {
-    retainHeader = retain;
-  }
-
-  public boolean isManualReview() {
-    return manualReview;
-  }
-
-  public void setManualReview(boolean manual) {
-    manualReview = manual;
-  }
-
-  public boolean isHideTopMenu() {
-    return hideTopMenu;
-  }
-
-  public void setHideTopMenu(boolean hide) {
-    hideTopMenu = hide;
-  }
-
-  public boolean isHideLineNumbers() {
-    return hideLineNumbers;
-  }
-
-  public void setHideLineNumbers(boolean hide) {
-    hideLineNumbers = hide;
-  }
-
-  public boolean isRenderEntireFile() {
-    return renderEntireFile;
-  }
-
-  public void setRenderEntireFile(boolean render) {
-    renderEntireFile = render;
-  }
-
-  public Theme getTheme() {
-    return theme != null ? Theme.valueOf(theme) : null;
-  }
-
-  public void setTheme(Theme theme) {
-    this.theme = theme != null ? theme.name() : null;
-  }
-
-  public boolean isHideEmptyPane() {
-    return hideEmptyPane;
-  }
-
-  public void setHideEmptyPane(boolean hideEmptyPane) {
-    this.hideEmptyPane = hideEmptyPane;
-  }
-
-  public void setAutoHideDiffTableHeader(boolean hide) {
-    autoHideDiffTableHeader = hide;
-  }
-
-  public boolean isAutoHideDiffTableHeader() {
-    return autoHideDiffTableHeader;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
index 8f9c726..41336791 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
@@ -36,6 +36,9 @@
   /** Scheme for the username used to authenticate an account, e.g. over SSH. */
   public static final String SCHEME_USERNAME = "username:";
 
+  /** Scheme used for GPG public keys. */
+  public static final String SCHEME_GPGKEY = "gpgkey:";
+
   /** Scheme for external auth used during authentication, e.g. OAuth Token */
   public static final String SCHEME_EXTERNAL = "external:";
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index 31494ba..2e33575 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -25,14 +25,9 @@
   /** Valid choices for the page size. */
   public static final short[] PAGESIZE_CHOICES = {10, 25, 50, 100};
 
-  /** Preferred scheme type to download a change. */
-  public static enum DownloadScheme {
-    ANON_GIT, ANON_HTTP, HTTP, SSH, REPO_DOWNLOAD, DEFAULT_DOWNLOADS
-  }
-
   /** Preferred method to download a change. */
   public static enum DownloadCommand {
-    REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH, DEFAULT_DOWNLOADS
+    REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH
   }
 
   public static enum DateFormat {
@@ -187,19 +182,49 @@
     useFlashClipboard = b;
   }
 
-  public DownloadScheme getDownloadUrl() {
-    if (downloadUrl == null) {
-      return null;
+  public String getDownloadUrl() {
+    // Translate from legacy enum names to modern display names. (May be removed
+    // if accompanied by a 2-phase schema upgrade.)
+    if (downloadUrl != null) {
+      switch (downloadUrl) {
+        case "ANON_GIT":
+          return CoreDownloadSchemes.ANON_GIT;
+        case "ANON_HTTP":
+          return CoreDownloadSchemes.ANON_HTTP;
+        case "HTTP":
+          return CoreDownloadSchemes.HTTP;
+        case "SSH":
+          return CoreDownloadSchemes.SSH;
+        case "REPO_DOWNLOAD":
+          return CoreDownloadSchemes.REPO_DOWNLOAD;
+      }
     }
-    return DownloadScheme.valueOf(downloadUrl);
+    return downloadUrl;
   }
 
-  public void setDownloadUrl(DownloadScheme url) {
-    if (url != null) {
-      downloadUrl = url.name();
-    } else {
-      downloadUrl = null;
+  public void setDownloadUrl(String url) {
+    // Translate from modern display names to legacy enum names. (May be removed
+    // if accompanied by a 2-phase schema upgrade.)
+    if (downloadUrl != null) {
+      switch (url) {
+        case CoreDownloadSchemes.ANON_GIT:
+          url = "ANON_GIT";
+          break;
+        case CoreDownloadSchemes.ANON_HTTP:
+          url = "ANON_HTTP";
+          break;
+        case CoreDownloadSchemes.HTTP:
+          url = "HTTP";
+          break;
+        case CoreDownloadSchemes.SSH:
+          url = "SSH";
+          break;
+        case CoreDownloadSchemes.REPO_DOWNLOAD:
+          url = "REPO_DOWNLOAD";
+          break;
+      }
     }
+    downloadUrl = url;
   }
 
   public DownloadCommand getDownloadCommand() {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
index 07e7d03..161a66e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -98,4 +98,16 @@
     removedBy = deleter;
     removedOn = when;
   }
+
+  public Account.Id getAddedBy() {
+    return addedBy;
+  }
+
+  public Account.Id getRemovedBy() {
+    return removedBy;
+  }
+
+  public Timestamp getRemovedOn() {
+    return removedOn;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index d3798db..c1b057a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -103,4 +103,16 @@
     removedBy = addedBy;
     removedOn = key.addedOn;
   }
+
+  public Account.Id getAddedBy() {
+    return addedBy;
+  }
+
+  public Account.Id getRemovedBy() {
+    return removedBy;
+  }
+
+  public Timestamp getRemovedOn() {
+    return removedOn;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
index e1a15e5..3f04306 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
@@ -19,9 +19,6 @@
 
 /** Line of development within a {@link Project}. */
 public final class Branch {
-  public static final String R_HEADS = "refs/heads/";
-  public static final String R_REFS = "refs/";
-
   /** Branch name key */
   public static class NameKey extends StringKey<Project.NameKey> {
     private static final long serialVersionUID = 1L;
@@ -36,9 +33,9 @@
       projectName = new Project.NameKey();
     }
 
-    public NameKey(final Project.NameKey proj, final String n) {
+    public NameKey(final Project.NameKey proj, final String branchName) {
       projectName = proj;
-      branchName = n;
+      set(branchName);
     }
 
     @Override
@@ -48,7 +45,7 @@
 
     @Override
     protected void set(String newValue) {
-      branchName = newValue;
+      branchName = RefNames.fullName(newValue);
     }
 
     @Override
@@ -57,15 +54,7 @@
     }
 
     public String getShortName() {
-      final String n = get();
-
-      // Git style branches will tend to start with "refs/heads/".
-      //
-      if (n.startsWith(R_HEADS)) {
-        return n.substring(R_HEADS.length());
-      }
-
-      return n;
+      return RefNames.shortName(get());
     }
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 2b1a7cf..b1816a8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -39,8 +39,6 @@
  *          |
  *          +- {@link PatchSetApproval}: a +/- vote on the change's current state.
  *          |
- *          +- {@link PatchSetAncestor}: parents of this change's commit.
- *          |
  *          +- {@link PatchLineComment}: comment about a specific line
  * </pre>
  * <p>
@@ -51,11 +49,6 @@
  * {@link Account} is usually also listed as the author and committer in the
  * PatchSetInfo.
  * <p>
- * The {@link PatchSetAncestor} entities are a mirror of the Git commit
- * metadata, providing access to the information without needing direct
- * accessing Git. These entities are actually legacy artifacts from Gerrit 1.x
- * and could be removed, replaced by direct RevCommit access.
- * <p>
  * Each PatchSet contains zero or more Patch records, detailing the file paths
  * impacted by the change (otherwise known as, the file paths the author
  * added/deleted/modified). Sometimes a merge commit can contain zero patches,
@@ -125,6 +118,23 @@
       id = newValue;
     }
 
+    public String toRefPrefix() {
+      return refPrefixBuilder().toString();
+    }
+
+    StringBuilder refPrefixBuilder() {
+      StringBuilder r = new StringBuilder(32)
+         .append(REFS_CHANGES);
+      int m = id % 100;
+      if (m < 10) {
+        r.append('0');
+      }
+      return r.append(m)
+          .append('/')
+          .append(id)
+          .append('/');
+    }
+
     /** Parse a Change.Id out of a string representation. */
     public static Id parse(final String str) {
       final Id r = new Id();
@@ -145,6 +155,17 @@
       return null;
     }
 
+    public static Id fromEditRefPart(String ref) {
+      int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) +
+          RefNames.EDIT_PREFIX.length();
+      int endChangeId = nextNonDigit(ref, startChangeId);
+      String id = ref.substring(startChangeId, endChangeId);
+      if (id != null && !id.isEmpty()) {
+        return new Change.Id(Integer.parseInt(id));
+      }
+      return null;
+    }
+
     static int startIndex(String ref) {
       if (ref == null || !ref.startsWith(REFS_CHANGES)) {
         return -1;
@@ -243,8 +264,6 @@
   private static final char MIN_OPEN = 'a';
   /** Database constant for {@link Status#NEW}. */
   public static final char STATUS_NEW = 'n';
-  /** Database constant for {@link Status#SUBMITTED}. */
-  public static final char STATUS_SUBMITTED = 's';
   /** Database constant for {@link Status#DRAFT}. */
   public static final char STATUS_DRAFT = 'd';
   /** Maximum database status constant for an open change. */
@@ -277,40 +296,13 @@
      * <p>
      * Changes in the NEW state can be moved to:
      * <ul>
-     * <li>{@link #SUBMITTED} - when the Submit Patch Set action is used;
+     * <li>{@link #MERGED} - when the Submit Patch Set action is used;
      * <li>{@link #ABANDONED} - when the Abandon action is used.
      * </ul>
      */
     NEW(STATUS_NEW, ChangeStatus.NEW),
 
     /**
-     * Change is open, but has been submitted to the merge queue.
-     *
-     * <p>
-     * A change enters the SUBMITTED state when an authorized user presses the
-     * "submit" action through the web UI, requesting that Gerrit merge the
-     * change's current patch set into the destination branch.
-     *
-     * <p>
-     * Typically a change resides in the SUBMITTED for only a brief sub-second
-     * period while the merge queue fires and the destination branch is updated.
-     * However, if a dependency commit (a {@link PatchSetAncestor}, directly or
-     * transitively) is not yet merged into the branch, the change will hang in
-     * the SUBMITTED state indefinitely.
-     *
-     * <p>
-     * Changes in the SUBMITTED state can be moved to:
-     * <ul>
-     * <li>{@link #NEW} - when a replacement patch set is supplied, OR when a
-     * merge conflict is detected;
-     * <li>{@link #MERGED} - when the change has been successfully merged into
-     * the destination branch;
-     * <li>{@link #ABANDONED} - when the Abandon action is used.
-     * </ul>
-     */
-    SUBMITTED(STATUS_SUBMITTED, ChangeStatus.SUBMITTED),
-
-    /**
      * Change is a draft change that only consists of draft patchsets.
      *
      * <p>
@@ -467,13 +459,20 @@
 
   /**
    * First line of first patch set's commit message.
-   *
+   * <p>
    * Unlike {@link #subject}, this string does not change if future patch sets
    * change the first line.
    */
   @Column(id = 17, notNull = false)
   protected String originalSubject;
 
+  /**
+   * Unique id for the changes submitted together assigned during merging.
+   * Only set if the status is MERGED.
+   */
+  @Column(id = 18, notNull = false)
+  protected String submissionId;
+
   protected Change() {
   }
 
@@ -500,6 +499,7 @@
     currentPatchSetId = other.currentPatchSetId;
     subject = other.subject;
     originalSubject = other.originalSubject;
+    submissionId = other.submissionId;
     topic = other.topic;
   }
 
@@ -583,6 +583,14 @@
     }
   }
 
+  public String getSubmissionId() {
+    return submissionId;
+  }
+
+  public void setSubmissionId(String id) {
+    this.submissionId = id;
+  }
+
   public Status getStatus() {
     return Status.forCode(status);
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContactInformation.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContactInformation.java
deleted file mode 100644
index 195387c..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContactInformation.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-
-/** Non-Internet contact details, such as a postal address and telephone. */
-public final class ContactInformation {
-  @Column(id = 1, length = Integer.MAX_VALUE, notNull = false)
-  protected String address;
-
-  @Column(id = 2, notNull = false, length = 40)
-  protected String country;
-
-  @Column(id = 3, notNull = false, length = 30)
-  protected String phoneNbr;
-
-  @Column(id = 4, notNull = false, length = 30)
-  protected String faxNbr;
-
-  public ContactInformation() {
-  }
-
-  public String getAddress() {
-    return address;
-  }
-
-  public void setAddress(final String a) {
-    address = a;
-  }
-
-  public String getCountry() {
-    return country;
-  }
-
-  public void setCountry(final String c) {
-    country = c;
-  }
-
-  public String getPhoneNumber() {
-    return phoneNbr;
-  }
-
-  public void setPhoneNumber(final String p) {
-    phoneNbr = p;
-  }
-
-  public String getFaxNumber() {
-    return faxNbr;
-  }
-
-  public void setFaxNumber(final String f) {
-    faxNbr = f;
-  }
-
-  public static boolean hasData(final ContactInformation contactInformation) {
-    if (contactInformation == null) {
-      return false;
-    }
-    return hasData(contactInformation.address)
-        || hasData(contactInformation.country)
-        || hasData(contactInformation.phoneNbr)
-        || hasData(contactInformation.faxNbr);
-  }
-
-  public static boolean hasAddress(final ContactInformation contactInformation) {
-    return contactInformation != null && hasData(contactInformation.address);
-  }
-
-  private static boolean hasData(final String s) {
-    return s != null && s.trim().length() > 0;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CoreDownloadSchemes.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CoreDownloadSchemes.java
new file mode 100644
index 0000000..9303373
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CoreDownloadSchemes.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 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.reviewdb.client;
+
+/**
+ * Download scheme string constants supported by the download-commands core
+ * plugin.
+ */
+public class CoreDownloadSchemes {
+  public static final String ANON_GIT = "git";
+  public static final String ANON_HTTP = "anonymous http";
+  public static final String HTTP = "http";
+  public static final String SSH = "ssh";
+  public static final String REPO_DOWNLOAD = "repo";
+
+  private CoreDownloadSchemes() {
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index acf8b45..3ecd539 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -52,7 +52,7 @@
     }
 
     @Override
-    protected void set(String newValue) {
+    public void set(String newValue) {
       uuid = newValue;
     }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 2361b1c..4f2ed31 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -14,20 +14,65 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
-
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
 import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
 
 /** A single revision of a {@link Change}. */
 public final class PatchSet {
   /** Is the reference name a change reference? */
-  public static boolean isRef(String name) {
+  public static boolean isChangeRef(String name) {
     return Id.fromRef(name) != null;
   }
 
+  /**
+   * Is the reference name a change reference?
+   *
+   * @deprecated use isChangeRef instead.
+   **/
+  @Deprecated
+  public static boolean isRef(String name) {
+    return isChangeRef(name);
+  }
+
+  public static String joinGroups(Iterable<String> groups) {
+    if (groups == null) {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    for (String g : groups) {
+      if (!first) {
+        sb.append(',');
+      } else {
+        first = false;
+      }
+      sb.append(g);
+    }
+    return sb.toString();
+  }
+
+  public static List<String> splitGroups(String joinedGroups) {
+    if (joinedGroups == null) {
+      return null;
+    }
+    List<String> groups = new ArrayList<>();
+    int i = 0;
+    while (true) {
+      int idx = joinedGroups.indexOf(',', i);
+      if (idx < 0) {
+        groups.add(joinedGroups.substring(i, joinedGroups.length()));
+        break;
+      }
+      groups.add(joinedGroups.substring(i, idx));
+      i = idx + 1;
+    }
+    return groups;
+  }
+
   public static class Id extends IntKey<Change.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -62,19 +107,9 @@
     }
 
     public String toRefName() {
-      StringBuilder r = new StringBuilder();
-      r.append(REFS_CHANGES);
-      int change = changeId.get();
-      int m = change % 100;
-      if (m < 10) {
-        r.append('0');
-      }
-      r.append(m);
-      r.append('/');
-      r.append(change);
-      r.append('/');
-      r.append(patchSetId);
-      return r.toString();
+      return changeId.refPrefixBuilder()
+          .append(patchSetId)
+          .toString();
     }
 
     /** Parse a PatchSet.Id out of a string representation. */
@@ -140,6 +175,22 @@
   @Column(id = 5)
   protected boolean draft;
 
+  /**
+   * Opaque group identifier, usually assigned during creation.
+   * <p>
+   * This field is actually a comma-separated list of values, as in rare cases
+   * involving merge commits a patch set may belong to multiple groups.
+   * <p>
+   * Changes on the same branch having patch sets with intersecting groups are
+   * considered related, as in the "Related Changes" tab.
+   */
+  @Column(id = 6, notNull = false)
+  protected String groups;
+
+  /** Certificate sent with a push that created this patch set. */
+  @Column(id = 7, notNull = false, length = Integer.MAX_VALUE)
+  protected String pushCertficate;
+
   protected PatchSet() {
   }
 
@@ -187,10 +238,26 @@
     draft = draftStatus;
   }
 
+  public List<String> getGroups() {
+    return splitGroups(groups);
+  }
+
+  public void setGroups(Iterable<String> groups) {
+    this.groups = joinGroups(groups);
+  }
+
   public String getRefName() {
     return id.toRefName();
   }
 
+  public String getPushCertificate() {
+    return pushCertficate;
+  }
+
+  public void setPushCertificate(String cert) {
+    pushCertficate = cert;
+  }
+
   @Override
   public String toString() {
     return "[PatchSet " + getId().toString() + "]";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
deleted file mode 100644
index 8412788..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.IntKey;
-
-/** Ancestors of a {@link PatchSet} that the PatchSet depends upon. */
-public final class PatchSetAncestor {
-  public static class Id extends IntKey<PatchSet.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1, name = Column.NONE)
-    protected PatchSet.Id patchSetId;
-
-    @Column(id = 2)
-    protected int position;
-
-    protected Id() {
-      patchSetId = new PatchSet.Id();
-    }
-
-    public Id(final PatchSet.Id psId, final int pos) {
-      this.patchSetId = psId;
-      this.position = pos;
-    }
-
-    @Override
-    public PatchSet.Id getParentKey() {
-      return patchSetId;
-    }
-
-    @Override
-    public int get() {
-      return position;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      position = newValue;
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Id key;
-
-  @Column(id = 2)
-  protected RevId ancestorRevision;
-
-  protected PatchSetAncestor() {
-  }
-
-  public PatchSetAncestor(final PatchSetAncestor.Id k) {
-    key = k;
-  }
-
-  public PatchSetAncestor.Id getId() {
-    return key;
-  }
-
-  public PatchSet.Id getPatchSet() {
-    return key.patchSetId;
-  }
-
-  public int getPosition() {
-    return key.position;
-  }
-
-  public RevId getAncestorRevision() {
-    return ancestorRevision;
-  }
-
-  public void setAncestorRevision(final RevId id) {
-    ancestorRevision = id;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index 209998a..af9e75c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -96,6 +96,9 @@
 
   protected InheritableBoolean createNewChangeForAllNotInTarget;
 
+  protected InheritableBoolean enableSignedPush;
+  protected InheritableBoolean requireSignedPush;
+
   protected Project() {
   }
 
@@ -108,6 +111,8 @@
     requireChangeID = InheritableBoolean.INHERIT;
     useContentMerge = InheritableBoolean.INHERIT;
     createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
+    enableSignedPush = InheritableBoolean.INHERIT;
+    requireSignedPush = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -171,6 +176,22 @@
     this.createNewChangeForAllNotInTarget = useAllNotInTarget;
   }
 
+  public InheritableBoolean getEnableSignedPush() {
+    return enableSignedPush;
+  }
+
+  public void setEnableSignedPush(InheritableBoolean enable) {
+    enableSignedPush = enable;
+  }
+
+  public InheritableBoolean getRequireSignedPush() {
+    return requireSignedPush;
+  }
+
+  public void setRequireSignedPush(InheritableBoolean require) {
+    requireSignedPush = require;
+  }
+
   public void setMaxObjectSizeLimit(final String limit) {
     maxObjectSizeLimit = limit;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 072982a..0940a7f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -17,6 +17,14 @@
 
 /** Constants and utilities for Gerrit-specific ref names. */
 public class RefNames {
+  public static final String HEAD = "HEAD";
+
+  public static final String REFS = "refs/";
+
+  public static final String REFS_HEADS = "refs/heads/";
+
+  public static final String REFS_TAGS = "refs/tags/";
+
   public static final String REFS_CHANGES = "refs/changes/";
 
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
@@ -26,7 +34,10 @@
   public static final String REFS_CONFIG = "refs/meta/config";
 
   /** Preference settings for a user {@code refs/users} */
-  public static final String REFS_USER = "refs/users/";
+  public static final String REFS_USERS = "refs/users/";
+
+  /** Default user preference settings */
+  public static final String REFS_USERS_DEFAULT = RefNames.REFS_USERS + "default";
 
   /** Configurations of project-specific dashboards (canned search queries). */
   public static final String REFS_DASHBOARDS = "refs/meta/dashboards/";
@@ -48,9 +59,25 @@
   /** Suffix of a meta ref in the notedb. */
   public static final String META_SUFFIX = "/meta";
 
+  public static final String EDIT_PREFIX = "edit-";
+
+  public static String fullName(String ref) {
+    return (ref.startsWith(REFS) || ref.equals(HEAD)) ?
+        ref : REFS_HEADS + ref;
+  }
+
+  public static final String shortName(String ref) {
+    if (ref.startsWith(REFS_HEADS)) {
+      return ref.substring(REFS_HEADS.length());
+    } else if (ref.startsWith(REFS_TAGS)) {
+      return ref.substring(REFS_TAGS.length());
+    }
+    return ref;
+  }
+
   public static String refsUsers(Account.Id accountId) {
     StringBuilder r = new StringBuilder();
-    r.append(REFS_USER);
+    r.append(REFS_USERS);
     int account = accountId.get();
     int m = account % 100;
     if (m < 10) {
@@ -102,9 +129,10 @@
    */
   public static String refsEditPrefix(Account.Id accountId, Change.Id changeId) {
     return new StringBuilder(refsUsers(accountId))
-      .append("/edit-")
+      .append('/')
+      .append(EDIT_PREFIX)
       .append(changeId.get())
-      .append("/")
+      .append('/')
       .toString();
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
index 1a64ee7..a73fe5d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -64,4 +64,9 @@
   public boolean equals(Object o) {
     return (o instanceof RevId) && id.equals(((RevId) o).id);
   }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "{" + id + "}";
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
deleted file mode 100644
index 499cb77..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2010 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-
-public interface AccountDiffPreferenceAccess extends Access<AccountDiffPreference, Account.Id> {
-
-  @Override
-  @PrimaryKey("accountId")
-  AccountDiffPreference get(Account.Id key) throws OrmException;
-
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
index d16d286..480e8e4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
@@ -32,4 +32,8 @@
   @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
   ResultSet<AccountGroupByIdAud> byGroupInclude(AccountGroup.Id groupId,
       AccountGroup.UUID incGroupUUID) throws OrmException;
+
+  @Query("WHERE key.groupId = ?")
+  ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId)
+      throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
index 236d1c1..041254a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
@@ -33,4 +33,8 @@
   @Query("WHERE key.groupId = ? AND key.accountId = ?")
   ResultSet<AccountGroupMemberAudit> byGroupAccount(AccountGroup.Id groupId,
       Account.Id accountId) throws OrmException;
+
+  @Query("WHERE key.groupId = ?")
+  ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId)
+      throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
deleted file mode 100644
index 02459d9..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface PatchSetAncestorAccess extends
-    Access<PatchSetAncestor, PatchSetAncestor.Id> {
-  @Override
-  @PrimaryKey("key")
-  PatchSetAncestor get(PatchSetAncestor.Id key) throws OrmException;
-
-  @Query("WHERE key.patchSetId = ? ORDER BY key.position")
-  ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
-
-  @Query("WHERE key.patchSetId.changeId = ?")
-  ResultSet<PatchSetAncestor> byChange(Change.Id id) throws OrmException;
-
-  @Query("WHERE key.patchSetId = ?")
-  ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;
-
-  @Query("WHERE ancestorRevision = ?")
-  ResultSet<PatchSetAncestor> descendantsOf(RevId revision)
-      throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 85f2b26..a63e638 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -68,8 +68,7 @@
   @Relation(id = 13)
   AccountGroupMemberAuditAccess accountGroupMembersAudit();
 
-  @Relation(id = 17)
-  AccountDiffPreferenceAccess accountDiffPreferences();
+  //Deleted @Relation(id = 17)
 
   @Relation(id = 18)
   StarredChangeAccess starredChanges();
@@ -92,8 +91,7 @@
   @Relation(id = 24)
   PatchSetAccess patchSets();
 
-  @Relation(id = 25)
-  PatchSetAncestorAccess patchSetAncestors();
+  // Deleted @Relation(id = 25)
 
   @Relation(id = 26)
   PatchLineCommentAccess patchComments();
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index a62c762..1162a5f 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -88,14 +88,6 @@
 ON patch_sets (revision);
 
 -- *********************************************************************
--- PatchSetAncestorAccess
---    @PrimaryKey covers: ancestorsOf
---    covers:             descendantsOf
-CREATE INDEX patch_set_ancestors_desc
-ON patch_set_ancestors (ancestor_revision);
-
-
--- *********************************************************************
 -- StarredChangeAccess
 --    @PrimaryKey covers: byAccount
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index f88c169..258b7be 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -97,15 +97,6 @@
 #
 
 -- *********************************************************************
--- PatchSetAncestorAccess
---    @PrimaryKey covers: ancestorsOf
---    covers:             descendantsOf
-CREATE INDEX patch_set_ancestors_desc
-ON patch_set_ancestors (ancestor_revision)
-#
-
-
--- *********************************************************************
 -- StarredChangeAccess
 --    @PrimaryKey covers: byAccount
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index a6b21ee..1fe7dce 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -137,13 +137,6 @@
 ON patch_sets (revision);
 
 -- *********************************************************************
--- PatchSetAncestorAccess
---    @PrimaryKey covers: ancestorsOf
---    covers:             descendantsOf
-CREATE INDEX patch_set_ancestors_desc
-ON patch_set_ancestors (ancestor_revision);
-
--- *********************************************************************
 -- StarredChangeAccess
 --    @PrimaryKey covers: byAccount
 
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
index 218d04f..47f409a 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -72,11 +71,19 @@
     assertNotRef("refs/changes/01/1/1/meta");
   }
 
+  @Test
+  public void toRefPrefix() {
+    assertThat(new Change.Id(1).toRefPrefix())
+        .isEqualTo("refs/changes/01/1/");
+    assertThat(new Change.Id(1234).toRefPrefix())
+        .isEqualTo("refs/changes/34/1234/");
+  }
+
   private static void assertRef(int changeId, String refName) {
-    assertEquals(new Change.Id(changeId), Change.Id.fromRef(refName));
+    assertThat(Change.Id.fromRef(refName)).isEqualTo(new Change.Id(changeId));
   }
 
   private static void assertNotRef(String refName) {
-    assertNull(Change.Id.fromRef(refName));
+    assertThat(Change.Id.fromRef(refName)).isNull();
   }
 }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
index 33da24a..7a6be87 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.reviewdb.client.PatchSet.joinGroups;
+import static com.google.gerrit.reviewdb.client.PatchSet.splitGroups;
+
+import com.google.common.collect.ImmutableList;
 
 import org.junit.Test;
 
@@ -62,14 +63,42 @@
     assertNotRef("refs/changes/34/1234foo");
   }
 
+  @Test
+  public void testSplitGroups() {
+    assertThat(splitGroups(null)).isNull();
+    assertThat(splitGroups("")).containsExactly("");
+    assertThat(splitGroups("abcd")).containsExactly("abcd");
+    assertThat(splitGroups("ab,cd")).containsExactly("ab", "cd").inOrder();
+    assertThat(splitGroups("ab,")).containsExactly("ab", "").inOrder();
+    assertThat(splitGroups(",cd")).containsExactly("", "cd").inOrder();
+  }
+
+  @Test
+  public void testJoinGroups() {
+    assertThat(joinGroups(null)).isNull();
+    assertThat(joinGroups(ImmutableList.of(""))).isEqualTo("");
+    assertThat(joinGroups(ImmutableList.of("abcd"))).isEqualTo("abcd");
+    assertThat(joinGroups(ImmutableList.of("ab", "cd"))).isEqualTo("ab,cd");
+    assertThat(joinGroups(ImmutableList.of("ab", ""))).isEqualTo("ab,");
+    assertThat(joinGroups(ImmutableList.of("", "cd"))).isEqualTo(",cd");
+  }
+
+  @Test
+  public void testToRefName() {
+    assertThat(new PatchSet.Id(new Change.Id(1), 23).toRefName())
+        .isEqualTo("refs/changes/01/1/23");
+    assertThat(new PatchSet.Id(new Change.Id(1234), 5).toRefName())
+        .isEqualTo("refs/changes/34/1234/5");
+  }
+
   private static void assertRef(int changeId, int psId, String refName) {
-    assertTrue(PatchSet.isRef(refName));
-    assertEquals(new PatchSet.Id(new Change.Id(changeId), psId),
-        PatchSet.Id.fromRef(refName));
+    assertThat(PatchSet.isChangeRef(refName)).isTrue();
+    assertThat(PatchSet.Id.fromRef(refName))
+        .isEqualTo(new PatchSet.Id(new Change.Id(changeId), psId));
   }
 
   private static void assertNotRef(String refName) {
-    assertFalse(PatchSet.isRef(refName));
-    assertNull(PatchSet.Id.fromRef(refName));
+    assertThat(PatchSet.isChangeRef(refName)).isFalse();
+    assertThat(PatchSet.Id.fromRef(refName)).isNull();
   }
 }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
new file mode 100644
index 0000000..bd8b8e0
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 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.reviewdb.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class RefNamesTest {
+  private final Account.Id accountId = new Account.Id(1011123);
+  private final Change.Id changeId = new Change.Id(67473);
+  private final PatchSet.Id psId = new PatchSet.Id(changeId, 42);
+
+  @Test
+  public void fullName() throws Exception {
+    assertThat(RefNames.fullName("refs/meta/config")).isEqualTo("refs/meta/config");
+    assertThat(RefNames.fullName("refs/heads/master")).isEqualTo("refs/heads/master");
+    assertThat(RefNames.fullName("master")).isEqualTo("refs/heads/master");
+    assertThat(RefNames.fullName("refs/tags/v1.0")).isEqualTo("refs/tags/v1.0");
+    assertThat(RefNames.fullName("HEAD")).isEqualTo("HEAD");
+  }
+
+  @Test
+  public void refsUsers() throws Exception {
+    assertThat(RefNames.refsUsers(accountId)).isEqualTo("refs/users/23/1011123");
+  }
+
+  @Test
+  public void refsDraftComments() throws Exception {
+    assertThat(RefNames.refsDraftComments(accountId, changeId))
+      .isEqualTo("refs/draft-comments/23/1011123-67473");
+  }
+
+  @Test
+  public void refsEdit() throws Exception {
+    assertThat(RefNames.refsEdit(accountId, changeId, psId))
+      .isEqualTo("refs/users/23/1011123/edit-67473/42");
+  }
+}
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index a2b76f0..cfe116a 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -48,6 +48,7 @@
     '//lib/antlr:java_runtime',
     '//lib/auto:auto-value',
     '//lib/commons:codec',
+    '//lib/commons:compress',
     '//lib/commons:dbcp',
     '//lib/commons:lang',
     '//lib/commons:net',
@@ -59,20 +60,18 @@
     '//lib/jgit:jgit-archive',
     '//lib/joda:joda-time',
     '//lib/log:api',
+    '//lib/log:jsonevent-layout',
     '//lib/log:log4j',
     '//lib/lucene:analyzers-common',
-    '//lib/lucene:core',
-    '//lib/lucene:query-parser',
+    '//lib/lucene:core-and-backward-codecs',
+    '//lib/lucene:queryparser',
     '//lib/ow2:ow2-asm',
     '//lib/ow2:ow2-asm-tree',
     '//lib/ow2:ow2-asm-util',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
   provided_deps = [
     '//lib:servlet-api-3_1',
-    '//lib/bouncycastle:bcprov',
-    '//lib/bouncycastle:bcpg',
-    '//lib/bouncycastle:bcpkix',
   ],
   visibility = ['PUBLIC'],
 )
@@ -83,6 +82,27 @@
   visibility = ['PUBLIC'],
 )
 
+TESTUTIL_DEPS = [
+  ':server',
+  '//gerrit-common:server',
+  '//gerrit-cache-h2:cache-h2',
+  '//gerrit-extension-api:api',
+  '//gerrit-gpg:gpg',
+  '//gerrit-lucene:lucene',
+  '//gerrit-reviewdb:server',
+  '//lib:gwtorm',
+  '//lib:h2',
+  '//lib:truth',
+  '//lib/guice:guice',
+  '//lib/guice:guice-servlet',
+  '//lib/jgit:jgit',
+  '//lib/jgit:junit',
+  '//lib/joda:joda-time',
+  '//lib/log:api',
+  '//lib/log:impl_log4j',
+  '//lib/log:log4j',
+]
+
 TESTUTIL = glob([
   'src/test/java/com/google/gerrit/testutil/**/*.java',
   'src/test/java/com/google/gerrit/server/project/Util.java',
@@ -91,22 +111,9 @@
   name = 'testutil',
   srcs = TESTUTIL,
   deps = [
-    ':server',
-    '//gerrit-common:server',
-    '//gerrit-cache-h2:cache-h2',
-    '//gerrit-lucene:lucene',
-    '//gerrit-reviewdb:server',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:h2',
-    '//lib:junit',
-    '//lib/guice:guice',
-    '//lib/guice:guice-servlet',
-    '//lib/jgit:jgit',
-    '//lib/jgit:junit',
-    '//lib/log:impl_log4j',
-    '//lib/log:log4j',
+    '//lib/auto:auto-value',
   ],
+  provided_deps = TESTUTIL_DEPS,
   exported_deps = [
     '//lib/easymock:easymock',
     '//lib/powermock:powermock-api-easymock',
@@ -132,9 +139,12 @@
   deps = [
     ':server',
     '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//lib:guava',
     '//lib:junit',
+    '//lib:truth',
     '//lib/guice:guice',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
 )
 
@@ -142,20 +152,11 @@
   name = 'prolog_tests',
   srcs = PROLOG_TESTS,
   resources = glob(['src/test/resources/com/google/gerrit/rules/**/*']),
-  deps = [
+  deps = TESTUTIL_DEPS + [
     ':prolog_test_case',
-    ':server',
     ':testutil',
-    '//gerrit-common:server',
-    '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:junit',
-    '//lib:truth',
-    '//lib/jgit:jgit',
-    '//lib/guice:guice',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
 )
 
@@ -166,25 +167,13 @@
 java_test(
   name = 'query_tests',
   srcs = QUERY_TESTS,
-  deps = [
-    ':server',
+  deps = TESTUTIL_DEPS + [
     ':testutil',
     '//gerrit-antlr:query_exception',
     '//gerrit-antlr:query_parser',
     '//gerrit-common:annotations',
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
-    '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:junit',
-    '//lib:truth',
     '//lib/antlr:java_runtime',
-    '//lib/guice:guice',
-    '//lib/jgit:jgit',
-    '//lib/jgit:junit',
-    '//lib/joda:joda-time',
   ],
   source_under_test = [':server'],
 )
@@ -196,27 +185,18 @@
     ['src/test/java/**/*.java'],
     excludes = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
   ),
-  deps = [
-    ':server',
+  resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']),
+  deps = TESTUTIL_DEPS + [
     ':testutil',
     '//gerrit-antlr:query_exception',
     '//gerrit-common:annotations',
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
-    '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
     '//lib:args4j',
     '//lib:grappa',
     '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:junit',
-    '//lib:truth',
-    '//lib/guice:guice',
+    '//lib/commons:validator',
     '//lib/guice:guice-assistedinject',
-    '//lib/jgit:jgit',
-    '//lib/jgit:junit',
-    '//lib/joda:joda-time',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
   source_under_test = [':server'],
   visibility = ['//tools/eclipse:classpath'],
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
index e25b7cb..14f12b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -79,9 +79,15 @@
 
   @Override
   public boolean equals(Object obj) {
-    if (this == obj) return true;
-    if (obj == null) return false;
-    if (getClass() != obj.getClass()) return false;
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null) {
+      return false;
+    }
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
 
     AuditEvent other = (AuditEvent) obj;
     return this.uuid.equals(other.uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java
new file mode 100644
index 0000000..78fe38c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.audit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Extended audit event. Adds request, resource and view data to HttpAuditEvent.
+ */
+public class ExtendedHttpAuditEvent extends HttpAuditEvent {
+  public final HttpServletRequest httpRequest;
+  public final RestResource resource;
+  public final RestView<? extends RestResource> view;
+
+  /**
+   * Creates a new audit event with results
+   *
+   * @param sessionId session id the event belongs to
+   * @param who principal that has generated the event
+   * @param httpRequest the HttpServletRequest
+   * @param when time-stamp of when the event started
+   * @param params parameters of the event
+   * @param result result of the event
+   * @param resource REST resource data
+   * @param view view rendering object
+   */
+  public ExtendedHttpAuditEvent(String sessionId, CurrentUser who,
+      HttpServletRequest httpRequest, long when, Multimap<String, ?> params,
+      Object input, int status, Object result, RestResource resource,
+      RestView<RestResource> view) {
+    super(sessionId, who, httpRequest.getRequestURI(), when, params, httpRequest.getMethod(),
+        input, status, result);
+    this.httpRequest = Preconditions.checkNotNull(httpRequest);
+    this.resource = resource;
+    this.view = view;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 8bd082d..889f008 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.common;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Account;
@@ -36,16 +36,17 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.DraftPublishedEvent;
-import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.HashtagsChangedEvent;
 import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
 import com.google.gerrit.server.events.TopicChangedEvent;
@@ -62,16 +63,19 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.BufferedReader;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -88,7 +92,7 @@
 /** Spawns local executables when a hook action occurs. */
 @Singleton
 public class ChangeHookRunner implements ChangeHooks, EventDispatcher,
-  EventSource, LifecycleListener {
+  EventSource, LifecycleListener, NewProjectCreatedListener {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
@@ -99,18 +103,19 @@
         bind(ChangeHooks.class).to(ChangeHookRunner.class);
         bind(EventDispatcher.class).to(ChangeHookRunner.class);
         bind(EventSource.class).to(ChangeHookRunner.class);
+        DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(ChangeHookRunner.class);
         listener().to(ChangeHookRunner.class);
       }
     }
 
     private static class EventListenerHolder {
-        final EventListener listener;
-        final CurrentUser user;
+      final EventListener listener;
+      final CurrentUser user;
 
-        EventListenerHolder(EventListener l, CurrentUser u) {
-            listener = l;
-            user = u;
-        }
+      EventListenerHolder(EventListener l, CurrentUser u) {
+        listener = l;
+        user = u;
+      }
     }
 
     /** Container class used to hold the return code and output of script hook execution */
@@ -169,44 +174,47 @@
     /** Listeners to receive all changes as they happen. */
     private final DynamicSet<EventListener> unrestrictedListeners;
 
-    /** Filename of the new patchset hook. */
-    private final File patchsetCreatedHook;
+    /** Path of the new patchset hook. */
+    private final Path patchsetCreatedHook;
 
-    /** Filename of the draft published hook. */
-    private final File draftPublishedHook;
+    /** Path of the draft published hook. */
+    private final Path draftPublishedHook;
 
-    /** Filename of the new comments hook. */
-    private final File commentAddedHook;
+    /** Path of the new comments hook. */
+    private final Path commentAddedHook;
 
-    /** Filename of the change merged hook. */
-    private final File changeMergedHook;
+    /** Path of the change merged hook. */
+    private final Path changeMergedHook;
 
-    /** Filename of the merge failed hook. */
-    private final File mergeFailedHook;
+    /** Path of the merge failed hook. */
+    private final Path mergeFailedHook;
 
-    /** Filename of the change abandoned hook. */
-    private final File changeAbandonedHook;
+    /** Path of the change abandoned hook. */
+    private final Path changeAbandonedHook;
 
-    /** Filename of the change restored hook. */
-    private final File changeRestoredHook;
+    /** Path of the change restored hook. */
+    private final Path changeRestoredHook;
 
-    /** Filename of the ref updated hook. */
-    private final File refUpdatedHook;
+    /** Path of the ref updated hook. */
+    private final Path refUpdatedHook;
 
-    /** Filename of the reviewer added hook. */
-    private final File reviewerAddedHook;
+    /** Path of the reviewer added hook. */
+    private final Path reviewerAddedHook;
 
-    /** Filename of the topic changed hook. */
-    private final File topicChangedHook;
+    /** Path of the topic changed hook. */
+    private final Path topicChangedHook;
 
-    /** Filename of the cla signed hook. */
-    private final File claSignedHook;
+    /** Path of the cla signed hook. */
+    private final Path claSignedHook;
 
-    /** Filename of the update hook. */
-    private final File refUpdateHook;
+    /** Path of the update hook. */
+    private final Path refUpdateHook;
 
-    /** Filename of the hashtags changed hook */
-    private final File hashtagsChangedHook;
+    /** Path of the hashtags changed hook */
+    private final Path hashtagsChangedHook;
+
+    /** Path of the project created hook. */
+    private final Path projectCreatedHook;
 
     private final String anonymousCowardName;
 
@@ -240,15 +248,15 @@
      * @param projectCache the project cache instance for the server.
      */
     @Inject
-    public ChangeHookRunner(final WorkQueue queue,
-      final GitRepositoryManager repoManager,
-      final @GerritServerConfig Config config,
-      final @AnonymousCowardName String anonymousCowardName,
-      final SitePaths sitePath,
-      final ProjectCache projectCache,
-      final AccountCache accountCache,
-      final EventFactory eventFactory,
-      final DynamicSet<EventListener> unrestrictedListeners) {
+    public ChangeHookRunner(WorkQueue queue,
+      GitRepositoryManager repoManager,
+      @GerritServerConfig Config config,
+      @AnonymousCowardName String anonymousCowardName,
+      SitePaths sitePath,
+      ProjectCache projectCache,
+      AccountCache accountCache,
+      EventFactory eventFactory,
+      DynamicSet<EventListener> unrestrictedListeners) {
         this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
@@ -258,21 +266,31 @@
         this.sitePaths = sitePath;
         this.unrestrictedListeners = unrestrictedListeners;
 
-        final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
+        Path hooksPath;
+        String hooksPathConfig = config.getString("hooks", null, "path");
+        if (hooksPathConfig != null) {
+          hooksPath = Paths.get(hooksPathConfig);
+        } else {
+          hooksPath = sitePath.hooks_dir;
+        }
 
-        patchsetCreatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "patchsetCreatedHook", "patchset-created")).getPath());
-        draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath());
-        commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath());
-        changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
-        mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailedHook", "merge-failed")).getPath());
-        changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
-        changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath());
-        refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath());
-        reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
-        topicChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "topicChangedHook", "topic-changed")).getPath());
-        claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
-        refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
-        hashtagsChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "hashtagsChangedHook", "hashtags-changed")).getPath());
+        // When adding a new hook, make sure to check that the setting name
+        // canonicalizes correctly in hook() below.
+        patchsetCreatedHook = hook(config, hooksPath, "patchset-created");
+        draftPublishedHook = hook(config, hooksPath, "draft-published");
+        commentAddedHook = hook(config, hooksPath, "comment-added");
+        changeMergedHook = hook(config, hooksPath, "change-merged");
+        mergeFailedHook = hook(config, hooksPath, "merge-failed");
+        changeAbandonedHook = hook(config, hooksPath, "change-abandoned");
+        changeRestoredHook = hook(config, hooksPath, "change-restored");
+        refUpdatedHook = hook(config, hooksPath, "ref-updated");
+        reviewerAddedHook = hook(config, hooksPath, "reviewer-added");
+        topicChangedHook = hook(config, hooksPath, "topic-changed");
+        claSignedHook = hook(config, hooksPath, "cla-signed");
+        refUpdateHook = hook(config, hooksPath, "ref-update");
+        hashtagsChangedHook = hook(config, hooksPath, "hashtags-changed");
+        projectCreatedHook = hook(config, hooksPath, "project-created");
+
         syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
         syncHookThreadPool = Executors.newCachedThreadPool(
             new ThreadFactoryBuilder()
@@ -280,28 +298,20 @@
               .build());
     }
 
+    private static Path hook(Config config, Path path, String name) {
+      String setting = name.replace("-", "") + "hook";
+      String value = config.getString("hooks", null, setting);
+      return path.resolve(value != null ? value : name);
+    }
+
     @Override
     public void addEventListener(EventListener listener, CurrentUser user) {
-        listeners.put(listener, new EventListenerHolder(listener, user));
+      listeners.put(listener, new EventListenerHolder(listener, user));
     }
 
     @Override
     public void removeEventListener(EventListener listener) {
-        listeners.remove(listener);
-    }
-
-    /**
-     * Helper Method for getting values from the config.
-     *
-     * @param config Config file to get value from.
-     * @param section Section to look in.
-     * @param setting Setting to get.
-     * @param fallback Fallback value.
-     * @return Setting value if found, else fallback.
-     */
-    private String getValue(final Config config, final String section, final String setting, final String fallback) {
-        final String result = config.getString(section, null, setting);
-        return Strings.isNullOrEmpty(result) ? fallback : result;
+      listeners.remove(listener);
     }
 
     /**
@@ -310,20 +320,30 @@
      * @param name Project to get repo for,
      * @return Repository or null.
      */
-    private Repository openRepository(final Project.NameKey name) {
-        try {
-            return repoManager.openRepository(name);
-        } catch (IOException err) {
-            log.warn("Cannot open repository " + name.get(), err);
-            return null;
-        }
+    private Repository openRepository(Project.NameKey name) {
+      try {
+        return repoManager.openRepository(name);
+      } catch (IOException err) {
+        log.warn("Cannot open repository " + name.get(), err);
+        return null;
+      }
     }
 
     private void addArg(List<String> args, String name, String value) {
-        if (value != null) {
-            args.add(name);
-            args.add(value);
-        }
+      if (value != null) {
+        args.add(name);
+        args.add(value);
+      }
+    }
+
+    private PatchSetAttribute asPatchSetAttribute(Change change,
+        PatchSet patchSet, ReviewDb db) throws OrmException {
+      try (Repository repo = repoManager.openRepository(change.getProject());
+          RevWalk revWalk = new RevWalk(repo)) {
+        return eventFactory.asPatchSetAttribute(db, revWalk, patchSet);
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
     }
 
     /**
@@ -331,10 +351,10 @@
      *
      */
     @Override
-    public HookResult doRefUpdateHook(final Project project, final String refname,
-        final Account uploader, final ObjectId oldId, final ObjectId newId) {
+    public HookResult doRefUpdateHook(Project project, String refname,
+        Account uploader, ObjectId oldId, ObjectId newId) {
 
-      final List<String> args = new ArrayList<>();
+      List<String> args = new ArrayList<>();
       addArg(args, "--project", project.getName());
       addArg(args, "--refname", refname);
       addArg(args, "--uploader", getDisplayName(uploader));
@@ -344,6 +364,20 @@
       return runSyncHook(project.getNameKey(), refUpdateHook, args);
     }
 
+    @Override
+    public void doProjectCreatedHook(Project.NameKey project, String headName) {
+      ProjectCreatedEvent event = new ProjectCreatedEvent();
+      event.projectName = project.get();
+      event.headName = headName;
+      fireEvent(project, event);
+
+      List<String> args = new ArrayList<>();
+      addArg(args, "--project", project.get());
+      addArg(args, "--head", headName);
+
+      runHook(project, projectCreatedHook, args);
+    }
+
     /**
      * Fire the Patchset Created Hook.
      *
@@ -352,219 +386,222 @@
      * @throws OrmException
      */
     @Override
-    public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet,
-          final ReviewDb db) throws OrmException {
-        final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
-        final AccountState uploader = accountCache.get(patchSet.getUploader());
-        final AccountState owner = accountCache.get(change.getOwner());
+    public void doPatchsetCreatedHook(Change change, PatchSet patchSet,
+          ReviewDb db) throws OrmException {
+      PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+      AccountState uploader = accountCache.get(patchSet.getUploader());
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
-        fireEvent(change, event, db);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
+      fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--is-draft", String.valueOf(patchSet.isDraft()));
-        addArg(args, "--kind", String.valueOf(event.patchSet.kind));
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--patchset", event.patchSet.number);
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--is-draft", String.valueOf(patchSet.isDraft()));
+      addArg(args, "--kind", String.valueOf(event.patchSet.kind));
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--patchset", event.patchSet.number);
 
-        runHook(change.getProject(), patchsetCreatedHook, args);
+      runHook(change.getProject(), patchsetCreatedHook, args);
     }
 
     @Override
-    public void doDraftPublishedHook(final Change change, final PatchSet patchSet,
-          final ReviewDb db) throws OrmException {
-        final DraftPublishedEvent event = new DraftPublishedEvent();
-        final AccountState uploader = accountCache.get(patchSet.getUploader());
-        final AccountState owner = accountCache.get(change.getOwner());
+    public void doDraftPublishedHook(Change change, PatchSet patchSet,
+          ReviewDb db) throws OrmException {
+      DraftPublishedEvent event = new DraftPublishedEvent();
+      AccountState uploader = accountCache.get(patchSet.getUploader());
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
-        fireEvent(change, event, db);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
+      fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--patchset", event.patchSet.number);
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--patchset", event.patchSet.number);
 
-        runHook(change.getProject(), draftPublishedHook, args);
+      runHook(change.getProject(), draftPublishedHook, args);
     }
 
     @Override
-    public void doCommentAddedHook(final Change change, final Account account,
-          final PatchSet patchSet, final String comment, final Map<String, Short> approvals,
-          final ReviewDb db) throws OrmException {
-        final CommentAddedEvent event = new CommentAddedEvent();
-        final AccountState owner = accountCache.get(change.getOwner());
+    public void doCommentAddedHook(Change change, Account account,
+          PatchSet patchSet, String comment, Map<String, Short> approvals,
+          ReviewDb db) throws OrmException {
+      CommentAddedEvent event = new CommentAddedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.author =  eventFactory.asAccountAttribute(account);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.comment = comment;
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.author =  eventFactory.asAccountAttribute(account);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.comment = comment;
 
-        LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes();
-        if (approvals.size() > 0) {
-            event.approvals = new ApprovalAttribute[approvals.size()];
-            int i = 0;
-            for (Map.Entry<String, Short> approval : approvals.entrySet()) {
-                event.approvals[i++] = getApprovalAttribute(labelTypes, approval);
-            }
-        }
-
-        fireEvent(change, event, db);
-
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--author", getDisplayName(account));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--comment", comment == null ? "" : comment);
+      LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes();
+      if (approvals.size() > 0) {
+        event.approvals = new ApprovalAttribute[approvals.size()];
+        int i = 0;
         for (Map.Entry<String, Short> approval : approvals.entrySet()) {
-          LabelType lt = labelTypes.byLabel(approval.getKey());
-          if (lt != null) {
-            addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
-          }
+          event.approvals[i++] = getApprovalAttribute(labelTypes, approval);
         }
+      }
 
-        runHook(change.getProject(), commentAddedHook, args);
+      fireEvent(change, event, db);
+
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--author", getDisplayName(account));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--comment", comment == null ? "" : comment);
+      for (Map.Entry<String, Short> approval : approvals.entrySet()) {
+        LabelType lt = labelTypes.byLabel(approval.getKey());
+        if (lt != null) {
+          addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
+        }
+      }
+
+      runHook(change.getProject(), commentAddedHook, args);
     }
 
     @Override
-  public void doChangeMergedHook(final Change change, final Account account,
-      final PatchSet patchSet, final ReviewDb db, String mergeResultRev)
-      throws OrmException {
-        final ChangeMergedEvent event = new ChangeMergedEvent();
-        final AccountState owner = accountCache.get(change.getOwner());
+    public void doChangeMergedHook(Change change, Account account,
+        PatchSet patchSet, ReviewDb db, String mergeResultRev)
+        throws OrmException {
+      ChangeMergedEvent event = new ChangeMergedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.submitter = eventFactory.asAccountAttribute(account);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.newRev = mergeResultRev;
-        fireEvent(change, event, db);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.submitter = eventFactory.asAccountAttribute(account);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.newRev = mergeResultRev;
+      fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--submitter", getDisplayName(account));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--newrev", mergeResultRev);
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--submitter", getDisplayName(account));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--newrev", mergeResultRev);
 
-        runHook(change.getProject(), changeMergedHook, args);
+      runHook(change.getProject(), changeMergedHook, args);
     }
 
     @Override
-    public void doMergeFailedHook(final Change change, final Account account,
-          final PatchSet patchSet, final String reason,
-          final ReviewDb db) throws OrmException {
-        final MergeFailedEvent event = new MergeFailedEvent();
-        final AccountState owner = accountCache.get(change.getOwner());
+    public void doMergeFailedHook(Change change, Account account,
+          PatchSet patchSet, String reason,
+          ReviewDb db) throws OrmException {
+      MergeFailedEvent event = new MergeFailedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.submitter = eventFactory.asAccountAttribute(account);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.reason = reason;
-        fireEvent(change, event, db);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.submitter = eventFactory.asAccountAttribute(account);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.reason = reason;
+      fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--submitter", getDisplayName(account));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--reason",  reason == null ? "" : reason);
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--submitter", getDisplayName(account));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--reason",  reason == null ? "" : reason);
 
-        runHook(change.getProject(), mergeFailedHook, args);
+      runHook(change.getProject(), mergeFailedHook, args);
     }
 
     @Override
-    public void doChangeAbandonedHook(final Change change, final Account account,
-          final PatchSet patchSet, final String reason, final ReviewDb db)
+    public void doChangeAbandonedHook(Change change, Account account,
+          PatchSet patchSet, String reason, ReviewDb db)
           throws OrmException {
-        final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
-        final AccountState owner = accountCache.get(change.getOwner());
+      ChangeAbandonedEvent event = new ChangeAbandonedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.abandoner = eventFactory.asAccountAttribute(account);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.reason = reason;
-        fireEvent(change, event, db);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.abandoner = eventFactory.asAccountAttribute(account);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.reason = reason;
+      fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--abandoner", getDisplayName(account));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--reason", reason == null ? "" : reason);
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--abandoner", getDisplayName(account));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--reason", reason == null ? "" : reason);
 
-        runHook(change.getProject(), changeAbandonedHook, args);
+      runHook(change.getProject(), changeAbandonedHook, args);
     }
 
     @Override
-    public void doChangeRestoredHook(final Change change, final Account account,
-          final PatchSet patchSet, final String reason, final ReviewDb db)
+    public void doChangeRestoredHook(Change change, Account account,
+          PatchSet patchSet, String reason, ReviewDb db)
           throws OrmException {
-        final ChangeRestoredEvent event = new ChangeRestoredEvent();
-        final AccountState owner = accountCache.get(change.getOwner());
+      ChangeRestoredEvent event = new ChangeRestoredEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-        event.change = eventFactory.asChangeAttribute(change);
-        event.restorer = eventFactory.asAccountAttribute(account);
-        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.reason = reason;
-        fireEvent(change, event, db);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.restorer = eventFactory.asAccountAttribute(account);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
+      event.reason = reason;
+      fireEvent(change, event, db);
 
-        final List<String> args = new ArrayList<>();
-        addArg(args, "--change", event.change.id);
-        addArg(args, "--change-url", event.change.url);
-        addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
-        addArg(args, "--project", event.change.project);
-        addArg(args, "--branch", event.change.branch);
-        addArg(args, "--topic", event.change.topic);
-        addArg(args, "--restorer", getDisplayName(account));
-        addArg(args, "--commit", event.patchSet.revision);
-        addArg(args, "--reason", reason == null ? "" : reason);
+      List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--topic", event.change.topic);
+      addArg(args, "--restorer", getDisplayName(account));
+      addArg(args, "--commit", event.patchSet.revision);
+      addArg(args, "--reason", reason == null ? "" : reason);
 
-        runHook(change.getProject(), changeRestoredHook, args);
+      runHook(change.getProject(), changeRestoredHook, args);
     }
 
     @Override
-    public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) {
-      doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account);
+    public void doRefUpdatedHook(Branch.NameKey refName, RefUpdate refUpdate,
+        Account account) {
+      doRefUpdatedHook(refName, refUpdate.getOldObjectId(),
+          refUpdate.getNewObjectId(), account);
     }
 
     @Override
-    public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) {
-      final RefUpdatedEvent event = new RefUpdatedEvent();
+    public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
+        ObjectId newId, Account account) {
+      RefUpdatedEvent event = new RefUpdatedEvent();
 
       if (account != null) {
         event.submitter = eventFactory.asAccountAttribute(account);
@@ -572,7 +609,7 @@
       event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName);
       fireEvent(refName, event);
 
-      final List<String> args = new ArrayList<>();
+      List<String> args = new ArrayList<>();
       addArg(args, "--oldrev", event.refUpdate.oldRev);
       addArg(args, "--newrev", event.refUpdate.newRev);
       addArg(args, "--refname", event.refUpdate.refName);
@@ -585,17 +622,17 @@
     }
 
     @Override
-    public void doReviewerAddedHook(final Change change, final Account account,
-        final PatchSet patchSet, final ReviewDb db) throws OrmException {
-      final ReviewerAddedEvent event = new ReviewerAddedEvent();
-      final AccountState owner = accountCache.get(change.getOwner());
+    public void doReviewerAddedHook(Change change, Account account,
+        PatchSet patchSet, ReviewDb db) throws OrmException {
+      ReviewerAddedEvent event = new ReviewerAddedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.reviewer = eventFactory.asAccountAttribute(account);
       fireEvent(change, event, db);
 
-      final List<String> args = new ArrayList<>();
+      List<String> args = new ArrayList<>();
       addArg(args, "--change", event.change.id);
       addArg(args, "--change-url", event.change.url);
       addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
@@ -607,18 +644,18 @@
     }
 
     @Override
-    public void doTopicChangedHook(final Change change, final Account account,
-        final String oldTopic, final ReviewDb db)
+    public void doTopicChangedHook(Change change, Account account,
+        String oldTopic, ReviewDb db)
             throws OrmException {
-      final TopicChangedEvent event = new TopicChangedEvent();
-      final AccountState owner = accountCache.get(change.getOwner());
+      TopicChangedEvent event = new TopicChangedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.changer = eventFactory.asAccountAttribute(account);
       event.oldTopic = oldTopic;
       fireEvent(change, event, db);
 
-      final List<String> args = new ArrayList<>();
+      List<String> args = new ArrayList<>();
       addArg(args, "--change", event.change.id);
       addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
       addArg(args, "--project", event.change.project);
@@ -645,7 +682,7 @@
       HashtagsChangedEvent event = new HashtagsChangedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.editor = eventFactory.asAccountAttribute(account);
       event.hashtags = hashtagArray(hashtags);
       event.added = hashtagArray(added);
@@ -653,7 +690,7 @@
 
       fireEvent(change, event, db);
 
-      final List<String> args = new ArrayList<>();
+      List<String> args = new ArrayList<>();
       addArg(args, "--change", event.change.id);
       addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
       addArg(args, "--project", event.change.project);
@@ -680,7 +717,7 @@
     @Override
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
-        final List<String> args = new ArrayList<>();
+        List<String> args = new ArrayList<>();
         addArg(args, "--submitter", getDisplayName(account));
         addArg(args, "--user-id", account.getId().toString());
         addArg(args, "--cla-name", cla.getName());
@@ -690,60 +727,85 @@
     }
 
     @Override
-    public void postEvent(final Change change, final Event event,
-        final ReviewDb db) throws OrmException {
+    public void postEvent(Change change, com.google.gerrit.server.events.Event event,
+        ReviewDb db) throws OrmException {
       fireEvent(change, event, db);
     }
 
     @Override
-    public void postEvent(final Branch.NameKey branchName,
-        final Event event) {
+    public void postEvent(Branch.NameKey branchName, com.google.gerrit.server.events.Event event) {
       fireEvent(branchName, event);
     }
 
-    private void fireEventForUnrestrictedListeners(final Event event) {
+    private void fireEventForUnrestrictedListeners(com.google.gerrit.server.events.Event event) {
       for (EventListener listener : unrestrictedListeners) {
-          listener.onEvent(event);
+        listener.onEvent(event);
       }
     }
 
-    private void fireEvent(final Change change, final Event event,
-        final ReviewDb db) throws OrmException {
+    private void fireEvent(Change change, com.google.gerrit.server.events.Event event,
+        ReviewDb db) throws OrmException {
       for (EventListenerHolder holder : listeners.values()) {
-          if (isVisibleTo(change, holder.user, db)) {
-              holder.listener.onEvent(event);
-          }
-      }
-
-      fireEventForUnrestrictedListeners( event );
-    }
-
-    private void fireEvent(Branch.NameKey branchName, final Event event) {
-      for (EventListenerHolder holder : listeners.values()) {
-          if (isVisibleTo(branchName, holder.user)) {
-              holder.listener.onEvent(event);
-          }
-      }
-
-      fireEventForUnrestrictedListeners( event );
-    }
-
-    private boolean isVisibleTo(Change change, CurrentUser user, ReviewDb db) throws OrmException {
-        final ProjectState pe = projectCache.get(change.getProject());
-        if (pe == null) {
-          return false;
+        if (isVisibleTo(change, holder.user, db)) {
+          holder.listener.onEvent(event);
         }
-        final ProjectControl pc = pe.controlFor(user);
-        return pc.controlFor(change).isVisible(db);
+      }
+
+      fireEventForUnrestrictedListeners( event );
+    }
+
+    private void fireEvent(Project.NameKey project, ProjectCreatedEvent event) {
+      for (EventListenerHolder holder : listeners.values()) {
+        if (isVisibleTo(project, event, holder.user)) {
+          holder.listener.onEvent(event);
+        }
+      }
+
+      fireEventForUnrestrictedListeners(event);
+    }
+
+    private void fireEventForUnrestrictedListeners(ProjectCreatedEvent event) {
+      for (EventListener listener : unrestrictedListeners) {
+        listener.onEvent(event);
+      }
+    }
+
+    private boolean isVisibleTo(Project.NameKey project, ProjectCreatedEvent event, CurrentUser user) {
+      ProjectState pe = projectCache.get(project);
+      if (pe == null) {
+        return false;
+      }
+      ProjectControl pc = pe.controlFor(user);
+      return pc.controlForRef(event.getHeadName()).isVisible();
+    }
+
+    private void fireEvent(Branch.NameKey branchName, com.google.gerrit.server.events.Event event) {
+      for (EventListenerHolder holder : listeners.values()) {
+        if (isVisibleTo(branchName, holder.user)) {
+          holder.listener.onEvent(event);
+        }
+      }
+
+      fireEventForUnrestrictedListeners(event);
+    }
+
+    private boolean isVisibleTo(Change change, CurrentUser user, ReviewDb db)
+        throws OrmException {
+      ProjectState pe = projectCache.get(change.getProject());
+      if (pe == null) {
+        return false;
+      }
+      ProjectControl pc = pe.controlFor(user);
+      return pc.controlFor(change).isVisible(db);
     }
 
     private boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) {
-        final ProjectState pe = projectCache.get(branchName.getParentKey());
-        if (pe == null) {
-          return false;
-        }
-        final ProjectControl pc = pe.controlFor(user);
-        return pc.controlForRef(branchName).isVisible();
+      ProjectState pe = projectCache.get(branchName.getParentKey());
+      if (pe == null) {
+        return false;
+      }
+      ProjectControl pc = pe.controlFor(user);
+      return pc.controlForRef(branchName).isVisible();
     }
 
     /**
@@ -753,14 +815,14 @@
      */
     private ApprovalAttribute getApprovalAttribute(LabelTypes labelTypes,
             Entry<String, Short> approval) {
-        ApprovalAttribute a = new ApprovalAttribute();
-        a.type = approval.getKey();
-        LabelType lt = labelTypes.byLabel(approval.getKey());
-        if (lt != null) {
-          a.description = lt.getName();
-        }
-        a.value = Short.toString(approval.getValue());
-        return a;
+      ApprovalAttribute a = new ApprovalAttribute();
+      a.type = approval.getKey();
+      LabelType lt = labelTypes.byLabel(approval.getKey());
+      if (lt != null) {
+        a.description = lt.getName();
+      }
+      a.value = Short.toString(approval.getValue());
+      return a;
     }
 
     /**
@@ -769,16 +831,18 @@
      * @param account Account to get name for.
      * @return Name for this account.
      */
-    private String getDisplayName(final Account account) {
-        if (account != null) {
-            String result = (account.getFullName() == null) ? anonymousCowardName : account.getFullName();
-            if (account.getPreferredEmail() != null) {
-                result += " (" + account.getPreferredEmail() + ")";
-            }
-            return result;
+    private String getDisplayName(Account account) {
+      if (account != null) {
+        String result = (account.getFullName() == null)
+            ? anonymousCowardName
+            : account.getFullName();
+        if (account.getPreferredEmail() != null) {
+          result += " (" + account.getPreferredEmail() + ")";
         }
+        return result;
+      }
 
-        return anonymousCowardName;
+      return anonymousCowardName;
     }
 
   /**
@@ -788,23 +852,23 @@
    * @param hook the hook to execute.
    * @param args Arguments to use to run the hook.
    */
-  private synchronized void runHook(Project.NameKey project, File hook,
+  private synchronized void runHook(Project.NameKey project, Path hook,
       List<String> args) {
-    if (project != null && hook.exists()) {
+    if (project != null && Files.exists(hook)) {
       hookQueue.execute(new AsyncHookTask(project, hook, args));
     }
   }
 
-  private synchronized void runHook(File hook, List<String> args) {
-    if (hook.exists()) {
+  private synchronized void runHook(Path hook, List<String> args) {
+    if (Files.exists(hook)) {
       hookQueue.execute(new AsyncHookTask(null, hook, args));
     }
   }
 
   private HookResult runSyncHook(Project.NameKey project,
-      File hook, List<String> args) {
+      Path hook, List<String> args) {
 
-    if (!hook.exists()) {
+    if (!Files.exists(hook)) {
       return null;
     }
 
@@ -818,10 +882,10 @@
     try {
       return task.get(syncHookTimeout, TimeUnit.SECONDS);
     } catch (TimeoutException e) {
-      message = "Synchronous hook timed out "  + hook.getAbsolutePath();
+      message = "Synchronous hook timed out "  + hook.toAbsolutePath();
       log.error(message);
     } catch (Exception e) {
-      message = "Error running hook " + hook.getAbsolutePath();
+      message = "Error running hook " + hook.toAbsolutePath();
       log.error(message, e);
     }
 
@@ -849,12 +913,12 @@
 
   private class HookTask {
     private final Project.NameKey project;
-    private final File hook;
+    private final Path hook;
     private final List<String> args;
     private StringWriter output;
     private Process ps;
 
-    protected HookTask(Project.NameKey project, File hook, List<String> args) {
+    protected HookTask(Project.NameKey project, Path hook, List<String> args) {
       this.project = project;
       this.hook = hook;
       this.args = args;
@@ -869,19 +933,19 @@
       HookResult result = null;
       try {
 
-        final List<String> argv = new ArrayList<>(1 + args.size());
-        argv.add(hook.getAbsolutePath());
+        List<String> argv = new ArrayList<>(1 + args.size());
+        argv.add(hook.toAbsolutePath().toString());
         argv.addAll(args);
 
-        final ProcessBuilder pb = new ProcessBuilder(argv);
+        ProcessBuilder pb = new ProcessBuilder(argv);
         pb.redirectErrorStream(true);
 
         if (project != null) {
           repo = openRepository(project);
         }
 
-        final Map<String, String> env = pb.environment();
-        env.put("GERRIT_SITE", sitePaths.site_path.getAbsolutePath());
+        Map<String, String> env = pb.environment();
+        env.put("GERRIT_SITE", sitePaths.site_path.toAbsolutePath().toString());
 
         if (repo != null) {
           pb.directory(repo.getDirectory());
@@ -891,22 +955,17 @@
 
         ps = pb.start();
         ps.getOutputStream().close();
-        InputStream is = ps.getInputStream();
         String output = null;
-        try {
+        try (InputStream is = ps.getInputStream()) {
           output = readOutput(is);
         } finally {
-          try {
-            is.close();
-          } catch (IOException closeErr) {
-          }
           ps.waitFor();
           result = new HookResult(ps.exitValue(), output);
         }
       } catch (InterruptedException iex) {
         // InterruptedExeception - timeout or cancel
       } catch (Throwable err) {
-        log.error("Error running hook " + hook.getAbsolutePath(), err);
+        log.error("Error running hook " + hook.toAbsolutePath(), err);
       } finally {
         if (repo != null) {
           repo.close();
@@ -914,7 +973,7 @@
       }
 
       if (result != null) {
-        final int exitValue = result.getExitValue();
+        int exitValue = result.getExitValue();
         if (exitValue == 0) {
           log.debug("hook[" + getName() + "] exitValue:" + exitValue);
         } else {
@@ -949,12 +1008,12 @@
     }
 
     protected String getName() {
-      return hook.getName();
+      return hook.getFileName().toString();
     }
 
     @Override
     public String toString() {
-      return "hook " + hook.getName();
+      return "hook " + hook.getFileName();
     }
 
     public void cancel() {
@@ -966,7 +1025,7 @@
   private final class SyncHookTask extends HookTask
       implements Callable<HookResult> {
 
-    private SyncHookTask(Project.NameKey project, File hook, List<String> args) {
+    private SyncHookTask(Project.NameKey project, Path hook, List<String> args) {
       super(project, hook, args);
     }
 
@@ -979,7 +1038,7 @@
   /** Runnable type used to run asynchronous hooks */
   private final class AsyncHookTask extends HookTask implements Runnable {
 
-    private AsyncHookTask(Project.NameKey project, File hook, List<String> args) {
+    private AsyncHookTask(Project.NameKey project, Path hook, List<String> args) {
       super(project, hook, args);
     }
 
@@ -988,4 +1047,10 @@
       super.runHook();
     }
   }
+
+  @Override
+  public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
+    Project.NameKey project = new Project.NameKey(event.getProjectName());
+    doProjectCreatedHook(project, event.getHeadName());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 7f7e8b2..b16a8a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -181,4 +181,12 @@
   public void doHashtagsChangedHook(Change change, Account account,
       Set<String>added, Set<String> removed, Set<String> hashtags,
       ReviewDb db) throws OrmException;
+
+  /**
+   * Fire the project created hook
+   *
+   * @param project The project that was created
+   * @param headName The head name of the created project
+   */
+  public void doProjectCreatedHook(Project.NameKey project, String headName);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 156672e..bed77a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -114,6 +114,10 @@
   }
 
   @Override
+  public void doProjectCreatedHook(Project.NameKey project, String headName) {
+  }
+
+  @Override
   public void postEvent(Change change, Event event, ReviewDb db) {
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/Version.java b/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
index 641ba03..57a2946 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.common;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,7 +41,7 @@
       if (in == null) {
         return "(dev)";
       }
-      try (BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"))) {
+      try (BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8))) {
         String vs = r.readLine();
         if (vs != null && vs.startsWith("v")) {
           vs = vs.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
index a22e6da..24df965 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -1,7 +1,7 @@
 package com.google.gerrit.lifecycle;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.inject.Singleton;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.internal.UniqueAnnotations;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 2afdffc..26c0fd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -28,6 +28,7 @@
 
 import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
 import com.googlecode.prolog_cafe.lang.Predicate;
+import com.googlecode.prolog_cafe.lang.PredicateEncoder;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
 
@@ -50,11 +51,8 @@
  * A single copy of the Prolog interpreter, for the current thread.
  */
 public class PrologEnvironment extends BufferingPrologControl {
-
   private static final Logger log =
-    LoggerFactory.getLogger(PrologEnvironment.class);
-
-  static final int MAX_ARITY = 8;
+      LoggerFactory.getLogger(PrologEnvironment.class);
 
   public static interface Factory {
     /**
@@ -68,19 +66,14 @@
 
   private final Args args;
   private final Map<StoredValue<Object>, Object> storedValues;
-  private int reductionLimit;
-  private int reductionsRemaining;
   private List<Runnable> cleanup;
 
   @Inject
   PrologEnvironment(Args a, @Assisted PrologMachineCopy src) {
     super(src);
-    setMaxArity(MAX_ARITY);
     setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
     args = a;
     storedValues = new HashMap<>();
-    reductionLimit = a.reductionLimit;
-    reductionsRemaining = reductionLimit;
     cleanup = new LinkedList<>();
   }
 
@@ -89,25 +82,9 @@
   }
 
   @Override
-  public boolean isEngineStopped() {
-    if (super.isEngineStopped()) {
-      return true;
-    } else if (--reductionsRemaining <= 0) {
-      throw new ReductionLimitException(reductionLimit);
-    }
-    return false;
-  }
-
-  @Override
   public void setPredicate(Predicate goal) {
     super.setPredicate(goal);
-    reductionLimit = args.reductionLimit(goal);
-    reductionsRemaining = reductionLimit;
-  }
-
-  /** @return number of reductions during execution. */
-  public int getReductions() {
-    return reductionLimit - reductionsRemaining;
+    setReductionLimit(args.reductionLimit(goal));
   }
 
   /**
@@ -177,6 +154,19 @@
 
   @Singleton
   public static class Args {
+    private static final Class<Predicate> CONSULT_STREAM_2;
+    static {
+      try {
+        @SuppressWarnings("unchecked")
+        Class<Predicate> c = (Class<Predicate>) Class.forName(
+            PredicateEncoder.encode(Prolog.BUILTIN, "consult_stream", 2),
+            false, RulesCache.class.getClassLoader());
+        CONSULT_STREAM_2 = c;
+      } catch (ClassNotFoundException e) {
+        throw new LinkageError("cannot find predicate consult_stream", e);
+      }
+    }
+
     private final ProjectCache projectCache;
     private final GitRepositoryManager repositoryManager;
     private final PatchListCache patchListCache;
@@ -210,8 +200,7 @@
     }
 
     private int reductionLimit(Predicate goal) {
-      if ("com.googlecode.prolog_cafe.builtin.PRED_consult_stream_2"
-          .equals(goal.getClass().getName())) {
+      if (goal.getClass() == CONSULT_STREAM_2) {
         return compileLimit;
       }
       return reductionLimit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
index 74a3928..7ed048b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.rules;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.config.FactoryModule;
 
 public class PrologModule extends FactoryModule {
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index 5dea6a2..5dd26f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -16,6 +16,7 @@
 
 import static com.googlecode.prolog_cafe.lang.PrologMachineCopy.save;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -27,16 +28,20 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
+import com.googlecode.prolog_cafe.exceptions.SyntaxException;
+import com.googlecode.prolog_cafe.exceptions.TermException;
 import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.PrologClassLoader;
 import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
 
 import org.eclipse.jgit.errors.LargeObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -44,10 +49,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.RawParseUtils;
 
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.PushbackReader;
 import java.io.Reader;
 import java.io.StringReader;
@@ -57,6 +59,8 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
@@ -96,8 +100,8 @@
   }
 
   private final boolean enableProjectRules;
-  private final File cacheDir;
-  private final File rulesDir;
+  private final Path cacheDir;
+  private final Path rulesDir;
   private final GitRepositoryManager gitMgr;
   private final DynamicSet<PredicateProvider> predicateProviders;
   private final ClassLoader systemLoader;
@@ -108,7 +112,7 @@
       GitRepositoryManager gm, DynamicSet<PredicateProvider> predicateProviders) {
     enableProjectRules = config.getBoolean("rules", null, "enable", true);
     cacheDir = site.resolve(config.getString("cache", null, "directory"));
-    rulesDir = cacheDir != null ? new File(cacheDir, "rules") : null;
+    rulesDir = cacheDir != null ? cacheDir.resolve("rules") : null;
     gitMgr = gm;
     this.predicateProviders = predicateProviders;
 
@@ -153,9 +157,9 @@
     return pcm;
   }
 
-  public PrologMachineCopy loadMachine(String name, InputStream in)
+  public PrologMachineCopy loadMachine(String name, Reader in)
       throws CompileException {
-    PrologMachineCopy pmc = consultRules(name, new InputStreamReader(in));
+    PrologMachineCopy pmc = consultRules(name, in);
     if (pmc == null) {
       throw new CompileException("Cannot consult rules from the stream " + name);
     }
@@ -178,9 +182,9 @@
     // that over dynamic consult as the bytecode will be faster.
     //
     if (rulesDir != null) {
-      File jarFile = new File(rulesDir, "rules-" + rulesId.getName() + ".jar");
-      if (jarFile.isFile()) {
-        URL[] cp = new URL[] {toURL(jarFile)};
+      Path jarPath = rulesDir.resolve("rules-" + rulesId.getName() + ".jar");
+      if (Files.isRegularFile(jarPath)) {
+        URL[] cp = new URL[] {toURL(jarPath)};
         return save(newEmptyMachine(new URLClassLoader(cp, systemLoader)));
       }
     }
@@ -204,40 +208,79 @@
           SymbolTerm.intern(name), new JavaObjectTerm(in))) {
         return null;
       }
+    } catch (SyntaxException e) {
+      throw new CompileException(e.toString(), e);
+    } catch (TermException e) {
+      Term m = e.getMessageTerm();
+      if (m instanceof StructureTerm && "syntax_error".equals(m.name())
+          && m.arity() >= 1) {
+        StringBuilder msg = new StringBuilder();
+        if (m.arg(0) instanceof ListTerm) {
+          msg.append(Joiner.on(' ').join(((ListTerm) m.arg(0)).toJava()));
+        } else {
+          msg.append(m.arg(0).toString());
+        }
+        if (m.arity() == 2 && m.arg(1) instanceof StructureTerm
+            && "at".equals(m.arg(1).name())) {
+          Term at = m.arg(1).arg(0).dereference();
+          if (at instanceof ListTerm) {
+            msg.append(" at: ");
+            msg.append(prettyProlog(at));
+          }
+        }
+        throw new CompileException(msg.toString(), e);
+      }
+      throw new CompileException("Error while consulting rules from " + name, e);
     } catch (RuntimeException e) {
       throw new CompileException("Error while consulting rules from " + name, e);
     }
     return save(ctl);
   }
 
+  private static String prettyProlog(Term at) {
+    StringBuilder b = new StringBuilder();
+    for (Object o : ((ListTerm) at).toJava()) {
+      if (o instanceof Term) {
+        Term t = (Term) o;
+        if (!(t instanceof StructureTerm)) {
+          b.append(t.toString()).append(' ');
+          continue;
+        }
+        switch (t.name()) {
+          case "atom":
+            SymbolTerm atom = (SymbolTerm) t.arg(0);
+            b.append(atom.toString());
+            break;
+          case "var":
+            b.append(t.arg(0).toString());
+            break;
+        }
+      } else {
+        b.append(o);
+      }
+    }
+    return b.toString().trim();
+  }
+
   private String read(Project.NameKey project, ObjectId rulesId)
       throws CompileException {
-    Repository git;
-    try {
-      git = gitMgr.openRepository(project);
-    } catch (RepositoryNotFoundException e) {
-      throw new CompileException("Cannot open repository " + project, e);
+    try (Repository git = gitMgr.openRepository(project)) {
+      try {
+        ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
+        byte[] raw = ldr.getCachedBytes(SRC_LIMIT);
+        return RawParseUtils.decode(raw);
+      } catch (LargeObjectException e) {
+        throw new CompileException("rules of " + project + " are too large", e);
+      } catch (RuntimeException | IOException e) {
+        throw new CompileException("Cannot load rules of " + project, e);
+      }
     } catch (IOException e) {
       throw new CompileException("Cannot open repository " + project, e);
     }
-    try {
-      ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
-      byte[] raw = ldr.getCachedBytes(SRC_LIMIT);
-      return RawParseUtils.decode(raw);
-    } catch (LargeObjectException e) {
-      throw new CompileException("rules of " + project + " are too large", e);
-    } catch (RuntimeException e) {
-      throw new CompileException("Cannot load rules of " + project, e);
-    } catch (IOException e) {
-      throw new CompileException("Cannot load rules of " + project, e);
-    } finally {
-      git.close();
-    }
   }
 
   private BufferingPrologControl newEmptyMachine(ClassLoader cl) {
     BufferingPrologControl ctl = new BufferingPrologControl();
-    ctl.setMaxArity(PrologEnvironment.MAX_ARITY);
     ctl.setMaxDatabaseSize(DB_MAX);
     ctl.setPrologClassLoader(new PrologClassLoader(new PredicateClassLoader(
         predicateProviders, cl)));
@@ -254,11 +297,11 @@
     return ctl;
   }
 
-  private static URL toURL(File jarFile) throws CompileException {
+  private static URL toURL(Path jarPath) throws CompileException {
     try {
-      return jarFile.toURI().toURL();
+      return jarPath.toUri().toURL();
     } catch (MalformedURLException e) {
-      throw new CompileException("Cannot create URL for " + jarFile, e);
+      throw new CompileException("Cannot create URL for " + jarPath, e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
index d454e40..27f15e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.rules;
 
+import com.googlecode.prolog_cafe.exceptions.SystemException;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.SystemException;
 
 /**
  * Defines a value cached in a {@link PrologEnvironment}.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 6136742..44c1f01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -17,8 +17,8 @@
 import static com.google.gerrit.rules.StoredValue.create;
 
 import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
@@ -38,8 +38,8 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 
+import com.googlecode.prolog_cafe.exceptions.SystemException;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.SystemException;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -99,14 +99,14 @@
       PatchSet ps = getPatchSet(engine);
       PatchListCache plCache = env.getArgs().getPatchListCache();
       Change change = getChange(engine);
-      Project.NameKey projectKey = change.getProject();
+      Project.NameKey project = change.getProject();
       ObjectId a = null;
       ObjectId b = ObjectId.fromString(ps.getRevision().get());
       Whitespace ws = Whitespace.IGNORE_NONE;
-      PatchListKey plKey = new PatchListKey(projectKey, a, b, ws);
+      PatchListKey plKey = new PatchListKey(a, b, ws);
       PatchList patchList;
       try {
-        patchList = plCache.get(plKey);
+        patchList = plCache.get(plKey, project);
       } catch (PatchListNotAvailableException e) {
         throw new SystemException("Cannot create " + plKey);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index bd175a9..1d1f571 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.change.ChangeKind.NO_CHANGE;
 import static com.google.gerrit.server.change.ChangeKind.NO_CODE_CHANGE;
 import static com.google.gerrit.server.change.ChangeKind.TRIVIAL_REBASE;
@@ -45,6 +46,7 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.NavigableSet;
 import java.util.Objects;
@@ -86,16 +88,22 @@
 
   Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId) throws OrmException {
-    return getForPatchSet(db, ctl, db.patchSets().get(psId));
+    PatchSet ps = db.patchSets().get(psId);
+    if (ps == null) {
+      return Collections.emptyList();
+    }
+    return getForPatchSet(db, ctl, ps);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet ps) throws OrmException {
+    checkNotNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(db, ctl);
     try {
       ProjectState project =
           projectCache.checkedGet(cd.change().getDest().getParentKey());
       ListMultimap<PatchSet.Id, PatchSetApproval> all = cd.approvals();
+      checkNotNull(all, "all should not be null");
 
       Table<String, Account.Id, PatchSetApproval> byUser =
           HashBasedTable.create();
@@ -106,9 +114,8 @@
       TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
       NavigableSet<Integer> allPsIds = patchSets.navigableKeySet();
 
-      Repository repo =
-          repoManager.openRepository(project.getProject().getNameKey());
-      try {
+      try (Repository repo =
+          repoManager.openRepository(project.getProject().getNameKey())) {
         // Walk patch sets strictly less than current in descending order.
         Collection<PatchSet> allPrior = patchSets.descendingMap()
             .tailMap(ps.getId().get(), false)
@@ -132,8 +139,6 @@
           }
         }
         return labelNormalizer.normalize(ctl, byUser.values()).getNormalized();
-      } finally {
-        repo.close();
       }
     } catch (IOException e) {
       throw new OrmException(e);
@@ -142,7 +147,7 @@
 
   private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd)
       throws OrmException {
-    Collection<PatchSet> patchSets = cd.patches();
+    Collection<PatchSet> patchSets = cd.patchSets();
     TreeMap<Integer, PatchSet> result = Maps.newTreeMap();
     for (PatchSet ps : patchSets) {
       result.put(ps.getId().get(), ps);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 6a44219..f31a65b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -58,10 +58,10 @@
     }
   }
 
-  public List<ChangeMessage> byPatchSet(ReviewDb db, ChangeNotes notes,
+  public Iterable<ChangeMessage> byPatchSet(ReviewDb db, ChangeNotes notes,
       PatchSet.Id psId) throws OrmException {
     if (!migration.readChanges()) {
-      return sortChangeMessages(db.changeMessages().byPatchSet(psId));
+      return db.changeMessages().byPatchSet(psId);
     }
     return notes.load().getChangeMessages().get(psId);
   }
@@ -71,4 +71,4 @@
     update.setChangeMessage(changeMessage.getMessage());
     db.changeMessages().insert(Collections.singleton(changeMessage));
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 2128cc2..6c80d26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,40 +14,37 @@
 
 package com.google.gerrit.server;
 
-import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.GERRIT;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.change.ChangeTriplet;
-import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.RevertedSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.server.util.MagicBranch;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -75,12 +72,8 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.text.MessageFormat;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -98,6 +91,17 @@
   private static final Logger log =
       LoggerFactory.getLogger(ChangeUtil.class);
 
+  public static final Function<PatchSet, Integer> TO_PS_ID =
+      new Function<PatchSet, Integer>() {
+        @Override
+        public Integer apply(PatchSet in) {
+          return in.getId().get();
+        }
+      };
+
+  public static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
+    .onResultOf(TO_PS_ID);
+
   /**
    * Generate a new unique identifier for change message entities.
    *
@@ -107,7 +111,8 @@
    * @throws OrmException the database couldn't be incremented.
    */
   public static String messageUUID(ReviewDb db) throws OrmException {
-    int p, s;
+    int p;
+    int s;
     synchronized (uuidLock) {
       if (uuidSeq == 0) {
         uuidPrefix = db.nextChangeMessageId();
@@ -144,19 +149,6 @@
     c.setLastUpdatedOn(TimeUtil.nowTs());
   }
 
-  public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
-      throws OrmException {
-    int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
-    for (int p = 0; p < cnt; p++) {
-      PatchSetAncestor a =
-          new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
-      a.setAncestorRevision(new RevId(src.getParent(p).getId().getName()));
-      toInsert.add(a);
-    }
-    db.patchSetAncestors().insert(toInsert);
-  }
-
   public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
       PatchSet.Id id) {
     PatchSet.Id next = nextPatchSetId(id);
@@ -185,45 +177,48 @@
     return subject;
   }
 
-  private final Provider<CurrentUser> userProvider;
-  private final CommitValidators.Factory commitValidatorsFactory;
+  private final Provider<IdentifiedUser> user;
   private final Provider<ReviewDb> db;
   private final Provider<InternalChangeQuery> queryProvider;
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeInserter.Factory changeInserterFactory;
-  private final PatchSetInserter.Factory patchSetInserterFactory;
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final ChangeIndexer indexer;
+  private final BatchUpdate.Factory updateFactory;
+  private final ChangeMessagesUtil changeMessagesUtil;
+  private final ChangeUpdate.Factory changeUpdateFactory;
 
   @Inject
-  ChangeUtil(Provider<CurrentUser> userProvider,
-      CommitValidators.Factory commitValidatorsFactory,
+  ChangeUtil(Provider<IdentifiedUser> user,
       Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
       RevertedSender.Factory revertedSenderFactory,
       ChangeInserter.Factory changeInserterFactory,
-      PatchSetInserter.Factory patchSetInserterFactory,
       GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated,
-      ChangeIndexer indexer) {
-    this.userProvider = userProvider;
-    this.commitValidatorsFactory = commitValidatorsFactory;
+      ChangeIndexer indexer,
+      BatchUpdate.Factory updateFactory,
+      ChangeMessagesUtil changeMessagesUtil,
+      ChangeUpdate.Factory changeUpdateFactory) {
+    this.user = user;
     this.db = db;
     this.queryProvider = queryProvider;
     this.revertedSenderFactory = revertedSenderFactory;
     this.changeInserterFactory = changeInserterFactory;
-    this.patchSetInserterFactory = patchSetInserterFactory;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
     this.indexer = indexer;
+    this.updateFactory = updateFactory;
+    this.changeMessagesUtil = changeMessagesUtil;
+    this.changeUpdateFactory = changeUpdateFactory;
   }
 
   public Change.Id revert(ChangeControl ctl, PatchSet.Id patchSetId,
-      String message, PersonIdent myIdent, SshInfo sshInfo)
+      String message, PersonIdent myIdent)
       throws NoSuchChangeException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException {
+      RestApiException, UpdateException {
     Change.Id changeId = patchSetId.getParentKey();
     PatchSet patch = db.get().patchSets().get(patchSetId);
     if (patch == null) {
@@ -237,8 +232,12 @@
       RevCommit commitToRevert =
           revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
-      PersonIdent authorIdent =
-          user().newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
+      PersonIdent authorIdent = user.get()
+          .newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
+
+      if (commitToRevert.getParentCount() == 0) {
+        throw new ResourceConflictException("Cannot revert initial commit");
+      }
 
       RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
       revWalk.parseHeaders(parentToCommitToRevert);
@@ -262,137 +261,59 @@
           ChangeIdUtil.insertId(message, computedChangeId, true));
 
       RevCommit revertCommit;
+      ChangeInserter ins;
       try (ObjectInserter oi = git.newObjectInserter()) {
         ObjectId id = oi.insert(revertCommitBuilder);
         oi.flush();
         revertCommit = revWalk.parseCommit(id);
+
+        RefControl refControl = ctl.getRefControl();
+        Change change = new Change(
+            new Change.Key("I" + computedChangeId.name()),
+            new Change.Id(db.get().nextChangeId()),
+            user.get().getAccountId(),
+            changeToRevert.getDest(),
+            TimeUtil.nowTs());
+        change.setTopic(changeToRevert.getTopic());
+        ins = changeInserterFactory.create(
+              refControl, change, revertCommit)
+            .setValidatePolicy(CommitValidators.Policy.GERRIT);
+
+        ChangeMessage changeMessage = new ChangeMessage(
+            new ChangeMessage.Key(
+                patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
+                user.get().getAccountId(), TimeUtil.nowTs(), patchSetId);
+        StringBuilder msgBuf = new StringBuilder();
+        msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
+        msgBuf.append("\n\n");
+        msgBuf.append("This patchset was reverted in change: ")
+              .append(change.getKey().get());
+        changeMessage.setMessage(msgBuf.toString());
+        ChangeUpdate update = changeUpdateFactory.create(ctl, TimeUtil.nowTs());
+        changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+        update.commit();
+
+        ins.setMessage("Uploaded patch set 1.");
+        try (BatchUpdate bu = updateFactory.create(
+            db.get(), change.getProject(), refControl.getUser(),
+            change.getCreatedOn())) {
+          bu.setRepository(git, revWalk, oi);
+          bu.insertChange(ins);
+          bu.execute();
+        }
       }
 
-      RefControl refControl = ctl.getRefControl();
-      Change change = new Change(
-          new Change.Key("I" + computedChangeId.name()),
-          new Change.Id(db.get().nextChangeId()),
-          user().getAccountId(),
-          changeToRevert.getDest(),
-          TimeUtil.nowTs());
-      change.setTopic(changeToRevert.getTopic());
-      ChangeInserter ins =
-          changeInserterFactory.create(refControl.getProjectControl(),
-              change, revertCommit);
-      PatchSet ps = ins.getPatchSet();
-
-      String ref = refControl.getRefName();
-      String cmdRef = MagicBranch.NEW_PUBLISH_CHANGE
-          + ref.substring(ref.lastIndexOf('/') + 1);
-      CommitReceivedEvent commitReceivedEvent = new CommitReceivedEvent(
-          new ReceiveCommand(ObjectId.zeroId(), revertCommit.getId(), cmdRef),
-          refControl.getProjectControl().getProject(),
-          refControl.getRefName(), revertCommit, user());
-
+      Change.Id id = ins.getChange().getId();
       try {
-        commitValidatorsFactory.create(refControl, sshInfo, git)
-            .validateForGerritCommits(commitReceivedEvent);
-      } catch (CommitValidationException e) {
-        throw new InvalidChangeOperationException(e.getMessage());
-      }
-
-      RefUpdate ru = git.updateRef(ps.getRefName());
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(revertCommit);
-      ru.disableRefLog();
-      if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-        throw new IOException(String.format(
-            "Failed to create ref %s in %s: %s", ps.getRefName(),
-            change.getDest().getParentKey().get(), ru.getResult()));
-      }
-
-      ChangeMessage cmsg = new ChangeMessage(
-          new ChangeMessage.Key(changeId, messageUUID(db.get())),
-          user().getAccountId(), TimeUtil.nowTs(), patchSetId);
-      StringBuilder msgBuf = new StringBuilder();
-      msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
-      msgBuf.append("\n\n");
-      msgBuf.append("This patchset was reverted in change: ")
-            .append(change.getKey().get());
-      cmsg.setMessage(msgBuf.toString());
-
-      ins.setMessage(cmsg).insert();
-
-      try {
-        RevertedSender cm = revertedSenderFactory.create(change);
-        cm.setFrom(user().getAccountId());
-        cm.setChangeMessage(cmsg);
+        RevertedSender cm = revertedSenderFactory.create(id);
+        cm.setFrom(user.get().getAccountId());
+        cm.setChangeMessage(ins.getChangeMessage());
         cm.send();
       } catch (Exception err) {
-        log.error("Cannot send email for revert change " + change.getId(),
-            err);
+        log.error("Cannot send email for revert change " + id, err);
       }
 
-      return change.getId();
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeId, e);
-    }
-  }
-
-  public Change.Id editCommitMessage(ChangeControl ctl, PatchSet ps,
-      String message, PersonIdent myIdent) throws NoSuchChangeException,
-      OrmException, MissingObjectException, IncorrectObjectTypeException,
-      IOException, InvalidChangeOperationException {
-    Change change = ctl.getChange();
-    Change.Id changeId = change.getId();
-
-    if (Strings.isNullOrEmpty(message)) {
-      throw new InvalidChangeOperationException(
-          "The commit message cannot be empty");
-    }
-
-    Project.NameKey project = ctl.getChange().getProject();
-    try (Repository git = gitManager.openRepository(project);
-        RevWalk revWalk = new RevWalk(git)) {
-      RevCommit commit =
-          revWalk.parseCommit(ObjectId.fromString(ps.getRevision()
-              .get()));
-      if (commit.getFullMessage().equals(message)) {
-        throw new InvalidChangeOperationException(
-            "New commit message cannot be same as existing commit message");
-      }
-
-      Date now = myIdent.getWhen();
-      PersonIdent authorIdent =
-          user().newCommitterIdent(now, myIdent.getTimeZone());
-
-      CommitBuilder commitBuilder = new CommitBuilder();
-      commitBuilder.setTreeId(commit.getTree());
-      commitBuilder.setParentIds(commit.getParents());
-      commitBuilder.setAuthor(commit.getAuthorIdent());
-      commitBuilder.setCommitter(authorIdent);
-      commitBuilder.setMessage(message);
-
-      RevCommit newCommit;
-      try (ObjectInserter oi = git.newObjectInserter()) {
-        ObjectId id = oi.insert(commitBuilder);
-        oi.flush();
-        newCommit = revWalk.parseCommit(id);
-      }
-
-      PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId());
-      PatchSet newPatchSet = new PatchSet(id);
-      newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
-      newPatchSet.setUploader(user().getAccountId());
-      newPatchSet.setRevision(new RevId(newCommit.name()));
-
-      String msg = "Patch Set " + newPatchSet.getPatchSetId()
-          + ": Commit message was updated";
-
-      change = patchSetInserterFactory
-          .create(git, revWalk, ctl, newCommit)
-          .setPatchSet(newPatchSet)
-          .setMessage(msg)
-          .setValidatePolicy(GERRIT)
-          .setDraft(ps.isDraft())
-          .insert();
-
-      return change.getId();
+      return id;
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
     }
@@ -427,28 +348,39 @@
     ReviewDb db = this.db.get();
     db.changes().beginTransaction(change.getId());
     try {
-      Map<RevId, String> refsToDelete = new HashMap<>();
-      for (PatchSet ps : db.patchSets().byChange(changeId)) {
-        // These should all be draft patch sets.
-        deleteOnlyDraftPatchSetPreserveRef(db, ps);
-        refsToDelete.put(ps.getRevision(), ps.getRefName());
+      List<PatchSet> patchSets = db.patchSets().byChange(changeId).toList();
+      for (PatchSet ps : patchSets) {
+        if (!ps.isDraft()) {
+          throw new NoSuchChangeException(changeId);
+        }
+        db.accountPatchReviews().delete(
+            db.accountPatchReviews().byPatchSet(ps.getId()));
       }
+
+      // No need to delete from notedb; draft patch sets will be filtered out.
+      db.patchComments().delete(db.patchComments().byChange(changeId));
+
+      db.patchSetApprovals().delete(db.patchSetApprovals().byChange(changeId));
+      db.patchSets().delete(patchSets);
       db.changeMessages().delete(db.changeMessages().byChange(changeId));
       db.starredChanges().delete(db.starredChanges().byChange(changeId));
       db.changes().delete(Collections.singleton(change));
 
-      // Delete all refs at once
+      // Delete all refs at once.
       try (Repository repo = gitManager.openRepository(change.getProject());
           RevWalk rw = new RevWalk(repo)) {
+        String prefix = new PatchSet.Id(changeId, 1).toRefName();
+        prefix = prefix.substring(0, prefix.length() - 1);
         BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate();
-        for (Map.Entry<RevId, String> e : refsToDelete.entrySet()) {
-          ru.addCommand(new ReceiveCommand(ObjectId.fromString(e.getKey().get()),
-              ObjectId.zeroId(), e.getValue()));
+        for (Ref ref : repo.getRefDatabase().getRefs(prefix).values()) {
+          ru.addCommand(
+              new ReceiveCommand(
+                ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
         }
         ru.execute(rw, NullProgressMonitor.INSTANCE);
         for (ReceiveCommand cmd : ru.getCommands()) {
           if (cmd.getResult() != ReceiveCommand.Result.OK) {
-            throw new IOException("failed: " + cmd);
+            throw new IOException("failed: " + cmd + ": " + cmd.getResult());
           }
         }
       }
@@ -467,8 +399,7 @@
       throw new NoSuchChangeException(patchSetId.getParentKey());
     }
 
-    Repository repo = gitManager.openRepository(change.getProject());
-    try {
+    try (Repository repo = gitManager.openRepository(change.getProject())) {
       RefUpdate update = repo.updateRef(patch.getRefName());
       update.setForceUpdate(true);
       update.disableRefLog();
@@ -483,9 +414,7 @@
           throw new IOException("Failed to delete ref " + patch.getRefName() +
               " in " + repo.getDirectory() + ": " + update.getResult());
       }
-      gitRefUpdated.fire(change.getProject(), update);
-    } finally {
-      repo.close();
+      gitRefUpdated.fire(change.getProject(), update, ReceiveCommand.Type.DELETE);
     }
 
     deleteOnlyDraftPatchSetPreserveRef(this.db.get(), patch);
@@ -526,10 +455,6 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private IdentifiedUser user() {
-    return (IdentifiedUser) userProvider.get();
-  }
-
   private static void deleteOnlyDraftPatchSetPreserveRef(ReviewDb db,
       PatchSet patch) throws NoSuchChangeException, OrmException {
     PatchSet.Id patchSetId = patch.getId();
@@ -542,7 +467,6 @@
     // No need to delete from notedb; draft patch sets will be filtered out.
     db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
     db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId));
-    db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(patchSetId));
 
     db.patchSets().delete(Collections.singleton(patch));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
index 956a0d1..282d51e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.args4j.ProjectControlHandler;
 import com.google.gerrit.server.args4j.SocketAddressHandler;
 import com.google.gerrit.server.args4j.TimestampHandler;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index ad34f9c..6a8600f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
@@ -101,6 +102,18 @@
     return false;
   }
 
+  /** Cast to IdentifiedUser if possible. */
+  public IdentifiedUser asIdentifiedUser() {
+    throw new UnsupportedOperationException(
+        getClass().getSimpleName() + " is not an IdentifiedUser");
+  }
+
+  /** Return account ID if {@link #isIdentifiedUser} is true. */
+  public Account.Id getAccountId() {
+    throw new UnsupportedOperationException(
+        getClass().getSimpleName() + " is not an IdentifiedUser");
+  }
+
   /** Check if the CurrentUser is an InternalUser. */
   public boolean isInternalUser() {
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/EnableSignedPush.java
similarity index 73%
rename from gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/EnableSignedPush.java
index 876c51f..13942a67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/EnableSignedPush.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2015 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,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@@ -21,10 +21,9 @@
 import java.lang.annotation.Retention;
 
 /**
- * Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups which
- * should become owners of a created project.
+ * Marker on a boolean indicating whether signed push is enabled on the server.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
-public @interface ProjectOwnerGroups {
-}
\ No newline at end of file
+public @interface EnableSignedPush {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java b/gerrit-server/src/main/java/com/google/gerrit/server/GpgException.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/GpgException.java
index 2c27240..5ed27b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/GpgException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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,13 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.rules;
+package com.google.gerrit.server;
 
-/** Thrown by {@link PrologEnvironment} if a script runs too long. */
-public class ReductionLimitException extends RuntimeException {
+/** Generic exception type for GPG-related exceptions. */
+public class GpgException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  ReductionLimitException(int limit) {
-    super(String.format("exceeded reduction limit of %d", limit));
+  public GpgException(String message) {
+    super(message);
+  }
+
+  public GpgException(Throwable cause) {
+    super(cause);
+  }
+
+  public GpgException(String message, Throwable cause) {
+    super(message, cause);
   }
 }
+
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index be5e000..2768733 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.StarredChange;
@@ -108,14 +109,16 @@
     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, Providers.of(remotePeer), null, id, null);
+          disableReverseDnsLookup, Providers.of(remotePeer), null,
+          id, null);
     }
 
     public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
         @Nullable CurrentUser caller) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, Providers.of(remotePeer), null, id, caller);
+          disableReverseDnsLookup, Providers.of(remotePeer), null,
+          id, caller);
     }
   }
 
@@ -144,13 +147,12 @@
         CapabilityControl.Factory capabilityControlFactory,
         final AuthConfig authConfig,
         Realm realm,
-        final @AnonymousCowardName String anonymousCowardName,
-        final @CanonicalWebUrl Provider<String> canonicalUrl,
+        @AnonymousCowardName final String anonymousCowardName,
+        @CanonicalWebUrl final Provider<String> canonicalUrl,
         final AccountCache accountCache,
         final GroupBackend groupBackend,
-        final @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
-
-        final @RemotePeer Provider<SocketAddress> remotePeerProvider,
+        @DisableReverseDnsLookup final Boolean disableReverseDnsLookup,
+        @RemotePeer final Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
@@ -160,7 +162,6 @@
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
       this.disableReverseDnsLookup = disableReverseDnsLookup;
-
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
     }
@@ -168,13 +169,15 @@
     public IdentifiedUser create(Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, null);
+          disableReverseDnsLookup, remotePeerProvider, dbProvider,
+          id, null);
     }
 
     public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, caller);
+          disableReverseDnsLookup, remotePeerProvider, dbProvider,
+          id, caller);
     }
   }
 
@@ -193,7 +196,8 @@
   private final GroupBackend groupBackend;
   private final String anonymousCowardName;
   private final Boolean disableReverseDnsLookup;
-  private final Set<String> validEmails = Sets.newHashSetWithExpectedSize(4);
+  private final Set<String> validEmails =
+      Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -252,7 +256,12 @@
     return state;
   }
 
-  /** The account identity for the user. */
+  @Override
+  public IdentifiedUser asIdentifiedUser() {
+    return this;
+  }
+
+  @Override
   public Account.Id getAccountId() {
     return accountId;
   }
@@ -267,20 +276,6 @@
     return state().getAccount();
   }
 
-  public AccountDiffPreference getAccountDiffPreference() {
-    AccountDiffPreference diffPref;
-    try {
-      diffPref = dbProvider.get().accountDiffPreferences().get(getAccountId());
-      if (diffPref == null) {
-        diffPref = AccountDiffPreference.createDefault(getAccountId());
-      }
-    } catch (OrmException e) {
-      log.warn("Cannot query account diff preferences", e);
-      diffPref = AccountDiffPreference.createDefault(getAccountId());
-    }
-    return diffPref;
-  }
-
   public boolean hasEmailAddress(String email) {
     if (validEmails.contains(email)) {
       return true;
@@ -290,7 +285,7 @@
       validEmails.add(email);
       return true;
     } else if (invalidEmails == null) {
-      invalidEmails = Sets.newHashSetWithExpectedSize(4);
+      invalidEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
     }
     invalidEmails.add(email);
     return false;
@@ -327,35 +322,31 @@
   @Override
   public Set<Change.Id> getStarredChanges() {
     if (starredChanges == null) {
-      if (dbProvider == null) {
-        throw new OutOfScopeException("Not in request scoped user");
-      }
-      Set<Change.Id> h = Sets.newHashSet();
+      checkRequestScope();
       try {
-        if (starredQuery != null) {
-          for (StarredChange sc : starredQuery) {
-            h.add(sc.getChangeId());
-          }
-          starredQuery = null;
-        } else {
-          for (StarredChange sc : dbProvider.get().starredChanges()
-              .byAccount(getAccountId())) {
-            h.add(sc.getChangeId());
-          }
-        }
-      } catch (OrmException e) {
-        log.warn("Cannot query starred by user changes", e);
+        starredChanges = starredChangeIds(
+            starredQuery != null ? starredQuery : starredQuery());
+      } catch (OrmException | RuntimeException e) {
+        log.warn("Cannot query starred changes", e);
+        starredChanges = Collections.emptySet();
+      } finally {
+        starredQuery = null;
       }
-      starredChanges = Collections.unmodifiableSet(h);
     }
     return starredChanges;
   }
 
+  public void clearStarredChanges() {
+    // Async query may have started before an update that the caller expects
+    // to see the results of, so we can't trust it.
+    abortStarredChanges();
+    starredChanges = null;
+  }
+
   public void asyncStarredChanges() {
     if (starredChanges == null && dbProvider != null) {
       try {
-        starredQuery =
-            dbProvider.get().starredChanges().byAccount(getAccountId());
+        starredQuery = starredQuery();
       } catch (OrmException e) {
         log.warn("Cannot query starred by user changes", e);
         starredQuery = null;
@@ -374,12 +365,31 @@
     }
   }
 
+  private void checkRequestScope() {
+    if (dbProvider == null) {
+      throw new OutOfScopeException("Not in request scoped user");
+    }
+  }
+
+  private ResultSet<StarredChange> starredQuery() throws OrmException {
+    return dbProvider.get().starredChanges().byAccount(getAccountId());
+  }
+
+  private static ImmutableSet<Change.Id> starredChangeIds(
+      Iterable<StarredChange> scs) {
+    return FluentIterable.from(scs)
+        .transform(new Function<StarredChange, Change.Id>() {
+          @Override
+          public Change.Id apply(StarredChange in) {
+            return in.getChangeId();
+          }
+        }).toSet();
+  }
+
   @Override
   public Collection<AccountProjectWatch> getNotificationFilters() {
     if (notificationFilters == null) {
-      if (dbProvider == null) {
-        throw new OutOfScopeException("Not in request scoped user");
-      }
+      checkRequestScope();
       List<AccountProjectWatch> r;
       try {
         r = dbProvider.get().accountProjectWatches() //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index d5242c2..7b182b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -14,12 +14,18 @@
 
 package com.google.gerrit.server;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -62,6 +68,47 @@
  */
 @Singleton
 public class PatchLineCommentsUtil {
+  public static final Ordering<PatchLineComment> PLC_ORDER =
+      new Ordering<PatchLineComment>() {
+    @Override
+    public int compare(PatchLineComment c1, PatchLineComment c2) {
+      String filename1 = c1.getKey().getParentKey().get();
+      String filename2 = c2.getKey().getParentKey().get();
+      return ComparisonChain.start()
+          .compare(filename1, filename2)
+          .compare(getCommentPsId(c1).get(), getCommentPsId(c2).get())
+          .compare(c1.getSide(), c2.getSide())
+          .compare(c1.getLine(), c2.getLine())
+          .compare(c1.getWrittenOn(), c2.getWrittenOn())
+          .result();
+    }
+  };
+
+  public static final Ordering<CommentInfo> COMMENT_INFO_ORDER =
+      new Ordering<CommentInfo>() {
+        @Override
+        public int compare(CommentInfo a, CommentInfo b) {
+          return ComparisonChain.start()
+              .compare(a.path, b.path, NULLS_FIRST)
+              .compare(a.patchSet, b.patchSet, NULLS_FIRST)
+              .compare(side(a), side(b))
+              .compare(a.line, b.line, NULLS_FIRST)
+              .compare(a.id, b.id)
+              .result();
+        }
+
+        private int side(CommentInfo c) {
+          return firstNonNull(c.side, Side.REVISION).ordinal();
+        }
+      };
+
+  public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
+    return plc.getKey().getParentKey().getParentKey();
+  }
+
+  private static final Ordering<Comparable<?>> NULLS_FIRST =
+      Ordering.natural().nullsFirst();
+
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final DraftCommentNotes.Factory draftFactory;
@@ -106,8 +153,7 @@
 
     notes.load();
     List<PatchLineComment> comments = Lists.newArrayList();
-    comments.addAll(notes.getBaseComments().values());
-    comments.addAll(notes.getPatchSetComments().values());
+    comments.addAll(notes.getComments().values());
     return sort(comments);
   }
 
@@ -166,13 +212,7 @@
       return sort(
           db.patchComments().publishedByChangeFile(changeId, file).toList());
     }
-    notes.load();
-    List<PatchLineComment> comments = Lists.newArrayList();
-
-    addCommentsOnFile(comments, notes.getBaseComments().values(), file);
-    addCommentsOnFile(comments, notes.getPatchSetComments().values(),
-        file);
-    return sort(comments);
+    return commentsOnFile(notes.load().getComments().values(), file);
   }
 
   public List<PatchLineComment> publishedByPatchSet(ReviewDb db,
@@ -181,11 +221,7 @@
       return sort(
           db.patchComments().publishedByPatchSet(psId).toList());
     }
-    notes.load();
-    List<PatchLineComment> comments = new ArrayList<>();
-    comments.addAll(notes.getPatchSetComments().get(psId));
-    comments.addAll(notes.getBaseComments().get(psId));
-    return sort(comments);
+    return commentsOnPatchSet(notes.load().getComments().values(), psId);
   }
 
   public List<PatchLineComment> draftByPatchSetAuthor(ReviewDb db,
@@ -195,11 +231,8 @@
       return sort(
           db.patchComments().draftByPatchSetAuthor(psId, author).toList());
     }
-
-    List<PatchLineComment> comments = Lists.newArrayList();
-    comments.addAll(notes.getDraftBaseComments(author).row(psId).values());
-    comments.addAll(notes.getDraftPsComments(author).row(psId).values());
-    return sort(comments);
+    return commentsOnPatchSet(
+        notes.load().getDraftComments(author).values(), psId);
   }
 
   public List<PatchLineComment> draftByChangeFileAuthor(ReviewDb db,
@@ -211,12 +244,8 @@
             .draftByChangeFileAuthor(notes.getChangeId(), file, author)
             .toList());
     }
-    List<PatchLineComment> comments = Lists.newArrayList();
-    addCommentsOnFile(comments, notes.getDraftBaseComments(author).values(),
-        file);
-    addCommentsOnFile(comments, notes.getDraftPsComments(author).values(),
-        file);
-    return sort(comments);
+    return commentsOnFile(
+        notes.load().getDraftComments(author).values(), file);
   }
 
   public List<PatchLineComment> draftByChangeAuthor(ReviewDb db,
@@ -233,11 +262,10 @@
                   in.getKey().getParentKey().getParentKey().getParentKey();
               return changeId.equals(matchId);
             }
-          }).toSortedList(ChangeNotes.PLC_ORDER);
+          }).toSortedList(PLC_ORDER);
     }
     List<PatchLineComment> comments = Lists.newArrayList();
-    comments.addAll(notes.getDraftBaseComments(author).values());
-    comments.addAll(notes.getDraftPsComments(author).values());
+    comments.addAll(notes.getDraftComments(author).values());
     return sort(comments);
   }
 
@@ -247,9 +275,8 @@
       return sort(db.patchComments().draftByAuthor(author).toList());
     }
 
-    Set<String> refNames =
-        getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
-
+    // TODO(dborowitz): Just scan author space.
+    Set<String> refNames = getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
     List<PatchLineComment> comments = Lists.newArrayList();
     for (String refName : refNames) {
       Account.Id id = Account.Id.fromRefPart(refName);
@@ -257,10 +284,8 @@
         continue;
       }
       Change.Id changeId = Change.Id.parse(refName);
-      DraftCommentNotes draftNotes =
-          draftFactory.create(changeId, author).load();
-      comments.addAll(draftNotes.getDraftBaseComments().values());
-      comments.addAll(draftNotes.getDraftPsComments().values());
+      comments.addAll(
+          draftFactory.create(changeId, author).load().getComments().values());
     }
     return sort(comments);
   }
@@ -297,49 +322,53 @@
     db.patchComments().delete(comments);
   }
 
-  private static Collection<PatchLineComment> addCommentsOnFile(
-      Collection<PatchLineComment> commentsOnFile,
+  private static List<PatchLineComment> commentsOnFile(
       Collection<PatchLineComment> allComments,
       String file) {
+    List<PatchLineComment> result = new ArrayList<>(allComments.size());
     for (PatchLineComment c : allComments) {
       String currentFilename = c.getKey().getParentKey().getFileName();
       if (currentFilename.equals(file)) {
-        commentsOnFile.add(c);
+        result.add(c);
       }
     }
-    return commentsOnFile;
+    return sort(result);
   }
 
-  public static void setCommentRevId(PatchLineComment c,
+  private static List<PatchLineComment> commentsOnPatchSet(
+      Collection<PatchLineComment> allComments,
+      PatchSet.Id psId) {
+    List<PatchLineComment> result = new ArrayList<>(allComments.size());
+    for (PatchLineComment c : allComments) {
+      if (getCommentPsId(c).equals(psId)) {
+        result.add(c);
+      }
+    }
+    return sort(result);
+  }
+
+  public static RevId setCommentRevId(PatchLineComment c,
       PatchListCache cache, Change change, PatchSet ps) throws OrmException {
-    if (c.getRevId() != null) {
-      return;
+    if (c.getRevId() == null) {
+      try {
+        // TODO(dborowitz): Bypass cache if side is REVISION.
+        PatchList patchList = cache.get(change, ps);
+        c.setRevId((c.getSide() == (short) 0)
+          ? new RevId(ObjectId.toString(patchList.getOldId()))
+          : new RevId(ObjectId.toString(patchList.getNewId())));
+      } catch (PatchListNotAvailableException e) {
+        throw new OrmException(e);
+      }
     }
-    PatchList patchList;
-    try {
-      patchList = cache.get(change, ps);
-    } catch (PatchListNotAvailableException e) {
-      throw new OrmException(e);
-    }
-    c.setRevId((c.getSide() == (short) 0)
-      ? new RevId(ObjectId.toString(patchList.getOldId()))
-      : new RevId(ObjectId.toString(patchList.getNewId())));
+    return c.getRevId();
   }
 
   private Set<String> getRefNamesAllUsers(String prefix) throws OrmException {
-    Repository repo;
-    try {
-      repo = repoManager.openRepository(allUsers);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    try {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
       RefDatabase refDb = repo.getRefDatabase();
       return refDb.getRefs(prefix).keySet();
     } catch (IOException e) {
       throw new OrmException(e);
-    } finally {
-      repo.close();
     }
   }
 
@@ -356,7 +385,7 @@
   }
 
   private static List<PatchLineComment> sort(List<PatchLineComment> comments) {
-    Collections.sort(comments, ChangeNotes.PLC_ORDER);
+    Collections.sort(comments, PLC_ORDER);
     return comments;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
index d18fe3d..40c5242 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
@@ -39,15 +39,12 @@
   public static boolean branchExists(final GitRepositoryManager repoManager,
       final Branch.NameKey branch) throws RepositoryNotFoundException,
       IOException {
-    final Repository repo = repoManager.openRepository(branch.getParentKey());
-    try {
-      boolean exists = repo.getRef(branch.get()) != null;
+    try (Repository repo = repoManager.openRepository(branch.getParentKey())) {
+      boolean exists = repo.getRefDatabase().exactRef(branch.get()) != null;
       if (!exists) {
         exists = repo.getFullBranch().equals(branch.get());
       }
       return exists;
-    } finally {
-      repo.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
new file mode 100644
index 0000000..e38f88c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -0,0 +1,286 @@
+// Copyright (C) 2016 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.server;
+
+import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupBaseInfo;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.ReviewerSuggestionCache;
+import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ReviewersUtil {
+  private static final String MAX_SUFFIX = "\u9fa5";
+  private static final Ordering<SuggestedReviewerInfo> ORDERING =
+      Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
+        @Nullable
+        @Override
+        public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
+          if (suggestedReviewerInfo == null) {
+            return null;
+          }
+          return suggestedReviewerInfo.account != null
+              ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
+              Strings.nullToEmpty(suggestedReviewerInfo.account.name))
+              : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
+        }
+      });
+  private final AccountLoader accountLoader;
+  private final AccountCache accountCache;
+  private final ReviewerSuggestionCache reviewerSuggestionCache;
+  private final AccountControl accountControl;
+  private final Provider<ReviewDb> dbProvider;
+  private final GroupBackend groupBackend;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final Provider<CurrentUser> currentUser;
+
+  @Inject
+  ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
+      AccountCache accountCache,
+      ReviewerSuggestionCache reviewerSuggestionCache,
+      AccountControl.Factory accountControlFactory,
+      Provider<ReviewDb> dbProvider,
+      GroupBackend groupBackend,
+      GroupMembers.Factory groupMembersFactory,
+      Provider<CurrentUser> currentUser) {
+    this.accountLoader = accountLoaderFactory.create(true);
+    this.accountCache = accountCache;
+    this.reviewerSuggestionCache = reviewerSuggestionCache;
+    this.accountControl = accountControlFactory.get();
+    this.dbProvider = dbProvider;
+    this.groupBackend = groupBackend;
+    this.groupMembersFactory = groupMembersFactory;
+    this.currentUser = currentUser;
+  }
+
+  public interface VisibilityControl {
+    boolean isVisibleTo(Account.Id account) throws OrmException;
+  }
+
+  public List<SuggestedReviewerInfo> suggestReviewers(
+      SuggestReviewers suggestReviewers, ProjectControl projectControl,
+      VisibilityControl visibilityControl)
+      throws IOException, OrmException, BadRequestException {
+    String query = suggestReviewers.getQuery();
+    boolean suggestAccounts = suggestReviewers.getSuggestAccounts();
+    int suggestFrom = suggestReviewers.getSuggestFrom();
+    boolean useFullTextSearch = suggestReviewers.getUseFullTextSearch();
+    int limit = suggestReviewers.getLimit();
+
+    if (Strings.isNullOrEmpty(query)) {
+      throw new BadRequestException("missing query field");
+    }
+
+    if (!suggestAccounts || query.length() < suggestFrom) {
+      return Collections.emptyList();
+    }
+
+    List<AccountInfo> suggestedAccounts;
+    if (useFullTextSearch) {
+      suggestedAccounts = suggestAccountFullTextSearch(suggestReviewers, visibilityControl);
+    } else {
+      suggestedAccounts = suggestAccount(suggestReviewers, visibilityControl);
+    }
+
+    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
+    for (AccountInfo a : suggestedAccounts) {
+      SuggestedReviewerInfo info = new SuggestedReviewerInfo();
+      info.account = a;
+      reviewer.add(info);
+    }
+
+    for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
+      if (suggestGroupAsReviewer(suggestReviewers, projectControl.getProject(),
+          g, visibilityControl)) {
+        GroupBaseInfo info = new GroupBaseInfo();
+        info.id = Url.encode(g.getUUID().get());
+        info.name = g.getName();
+        SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
+        suggestedReviewerInfo.group = info;
+        reviewer.add(suggestedReviewerInfo);
+      }
+    }
+
+    reviewer = ORDERING.immutableSortedCopy(reviewer);
+    if (reviewer.size() <= limit) {
+      return reviewer;
+    } else {
+      return reviewer.subList(0, limit);
+    }
+  }
+
+  private List<AccountInfo> suggestAccountFullTextSearch(
+      SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
+          throws IOException, OrmException {
+    List<AccountInfo> results = reviewerSuggestionCache.search(
+        suggestReviewers.getQuery(), suggestReviewers.getFullTextMaxMatches());
+
+    Iterator<AccountInfo> it = results.iterator();
+    while (it.hasNext()) {
+      Account.Id accountId = new Account.Id(it.next()._accountId);
+      if (!(visibilityControl.isVisibleTo(accountId)
+          && accountControl.canSee(accountId))) {
+        it.remove();
+      }
+    }
+
+    return results;
+  }
+
+  private List<AccountInfo> suggestAccount(SuggestReviewers suggestReviewers,
+      VisibilityControl visibilityControl)
+      throws OrmException {
+    String query = suggestReviewers.getQuery();
+    int limit = suggestReviewers.getLimit();
+
+    String a = query;
+    String b = a + MAX_SUFFIX;
+
+    Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
+    Map<Account.Id, String> queryEmail = new HashMap<>();
+
+    for (Account p : dbProvider.get().accounts()
+        .suggestByFullName(a, b, limit)) {
+      if (p.isActive()) {
+        addSuggestion(r, p.getId(), visibilityControl);
+      }
+    }
+
+    if (r.size() < limit) {
+      for (Account p : dbProvider.get().accounts()
+          .suggestByPreferredEmail(a, b, limit - r.size())) {
+        if (p.isActive()) {
+          addSuggestion(r, p.getId(), visibilityControl);
+        }
+      }
+    }
+
+    if (r.size() < limit) {
+      for (AccountExternalId e : dbProvider.get().accountExternalIds()
+          .suggestByEmailAddress(a, b, limit - r.size())) {
+        if (!r.containsKey(e.getAccountId())) {
+          Account p = accountCache.get(e.getAccountId()).getAccount();
+          if (p.isActive()) {
+            if (addSuggestion(r, p.getId(), visibilityControl)) {
+              queryEmail.put(e.getAccountId(), e.getEmailAddress());
+            }
+          }
+        }
+      }
+    }
+
+    accountLoader.fill();
+    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
+      AccountInfo info = r.get(p.getKey());
+      if (info != null) {
+        info.email = p.getValue();
+      }
+    }
+    return new ArrayList<>(r.values());
+  }
+
+  private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
+      Account.Id account, VisibilityControl visibilityControl)
+      throws OrmException {
+    if (!map.containsKey(account)
+        // Can the suggestion see the change?
+        && visibilityControl.isVisibleTo(account)
+        // Can the account see the current user?
+        && accountControl.canSee(account)) {
+      map.put(account, accountLoader.get(account));
+      return true;
+    }
+    return false;
+  }
+
+  private List<GroupReference> suggestAccountGroup(
+      SuggestReviewers suggestReviewers, ProjectControl ctl) {
+    return Lists.newArrayList(
+        Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl),
+            suggestReviewers.getLimit()));
+  }
+
+  private boolean suggestGroupAsReviewer(SuggestReviewers suggestReviewers,
+      Project project, GroupReference group,
+      VisibilityControl visibilityControl) throws OrmException, IOException {
+    int maxAllowed = suggestReviewers.getMaxAllowed();
+
+    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
+      return false;
+    }
+
+    try {
+      Set<Account> members = groupMembersFactory
+          .create(currentUser.get())
+          .listAccounts(group.getUUID(), project.getNameKey());
+
+      if (members.isEmpty()) {
+        return false;
+      }
+
+      if (maxAllowed > 0 && members.size() > maxAllowed) {
+        return false;
+      }
+
+      // require that at least one member in the group can see the change
+      for (Account account : members) {
+        if (visibilityControl.isVisibleTo(account.getId())) {
+          return true;
+        }
+      }
+    } catch (NoSuchGroupException e) {
+      return false;
+    } catch (NoSuchProjectException e) {
+      return false;
+    }
+
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
index 1403e60..351de5e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -18,11 +18,13 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
@@ -53,9 +55,26 @@
           return true;
         }
       };
+  private static final Predicate<WebLinkInfoCommon> INVALID_WEBLINK_COMMON =
+      new Predicate<WebLinkInfoCommon>() {
+
+        @Override
+        public boolean apply(WebLinkInfoCommon link) {
+          if (link == null) {
+            return false;
+          } else if (Strings.isNullOrEmpty(link.name)
+              || Strings.isNullOrEmpty(link.url)) {
+            log.warn(String.format("%s is missing name and/or url", link
+                .getClass().getName()));
+            return false;
+          }
+          return true;
+        }
+      };
 
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
   private final DynamicSet<FileWebLink> fileLinks;
+  private final DynamicSet<FileHistoryWebLink> fileHistoryLinks;
   private final DynamicSet<DiffWebLink> diffLinks;
   private final DynamicSet<ProjectWebLink> projectLinks;
   private final DynamicSet<BranchWebLink> branchLinks;
@@ -63,11 +82,14 @@
   @Inject
   public WebLinks(DynamicSet<PatchSetWebLink> patchSetLinks,
       DynamicSet<FileWebLink> fileLinks,
+      DynamicSet<FileHistoryWebLink> fileLogLinks,
       DynamicSet<DiffWebLink> diffLinks,
       DynamicSet<ProjectWebLink> projectLinks,
-      DynamicSet<BranchWebLink> branchLinks) {
+      DynamicSet<BranchWebLink> branchLinks
+      ) {
     this.patchSetLinks = patchSetLinks;
     this.fileLinks = fileLinks;
+    this.fileHistoryLinks = fileLogLinks;
     this.diffLinks = diffLinks;
     this.projectLinks = projectLinks;
     this.branchLinks = branchLinks;
@@ -111,6 +133,46 @@
   /**
    *
    * @param project Project name.
+   * @param revision SHA1 of revision.
+   * @param file File name.
+   * @return Links for file history
+   */
+  public FluentIterable<WebLinkInfo> getFileHistoryLinks(final String project,
+      final String revision, final String file) {
+    return filterLinks(fileHistoryLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((FileHistoryWebLink) webLink).getFileHistoryWebLink(project,
+            revision, file);
+      }
+    });
+  }
+
+  public FluentIterable<WebLinkInfoCommon> getFileHistoryLinksCommon(
+      final String project, final String revision, final String file) {
+    return FluentIterable
+        .from(fileHistoryLinks)
+        .transform(new Function<WebLink, WebLinkInfoCommon>() {
+          @Override
+          public WebLinkInfoCommon apply(WebLink webLink) {
+            WebLinkInfo info =
+                ((FileHistoryWebLink) webLink).getFileHistoryWebLink(project,
+                    revision, file);
+            WebLinkInfoCommon commonInfo = new WebLinkInfoCommon();
+            commonInfo.name = info.name;
+            commonInfo.imageUrl = info.imageUrl;
+            commonInfo.url = info.url;
+            commonInfo.target = info.target;
+            return commonInfo;
+          }
+        })
+        .filter(INVALID_WEBLINK_COMMON);
+  }
+
+  /**
+   *
+   * @param project Project name.
    * @param patchSetIdA Patch set ID of side A, <code>null</code> if no base
    *        patch set was selected.
    * @param revisionA SHA1 of revision of side A.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index 580897f..93a3814 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -252,7 +252,7 @@
     }
   }
 
-  public class AccessSectionInfo {
+  public static class AccessSectionInfo {
     public Map<String, PermissionInfo> permissions;
 
     public AccessSectionInfo(AccessSection section) {
@@ -263,7 +263,7 @@
     }
   }
 
-  public class PermissionInfo {
+  public static class PermissionInfo {
     public String label;
     public Boolean exclusive;
     public Map<String, PermissionRuleInfo> rules;
@@ -278,7 +278,7 @@
     }
   }
 
-  public class PermissionRuleInfo {
+  public static class PermissionRuleInfo {
     public PermissionRule.Action action;
     public Boolean force;
     public Integer min;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
index 7031672..30420e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -16,19 +16,47 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.EmailSender;
+import com.google.inject.Inject;
 
 import java.util.Collection;
-import java.util.Objects;
+import java.util.HashSet;
 import java.util.Set;
 
 /** Basic implementation of {@link Realm}.  */
 public abstract class AbstractRealm implements Realm {
+  private EmailSender emailSender;
+
+  @Inject(optional = true)
+  void setEmailSender(EmailSender emailSender) {
+    this.emailSender = emailSender;
+  }
+
+  @Override
+  public Set<FieldName> getEditableFields() {
+    Set<Account.FieldName> fields = new  HashSet<>();
+    for (Account.FieldName n : Account.FieldName.values()) {
+      if (allowsEdit(n)) {
+        if (n == Account.FieldName.REGISTER_NEW_EMAIL) {
+          if (emailSender != null && emailSender.isEnabled()) {
+            fields.add(n);
+          }
+        } else {
+          fields.add(n);
+        }
+      }
+    }
+    return fields;
+  }
+
   @Override
   public boolean hasEmailAddress(IdentifiedUser user, String email) {
     for (AccountExternalId ext : user.state().getExternalIds()) {
-      if (Objects.equals(ext.getEmailAddress(), email)) {
+      if (email != null && email.equalsIgnoreCase(ext.getEmailAddress())) {
         return true;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 45d3d1f..28df97a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -92,8 +92,7 @@
 
     @Override
     public Set<Account.Id> load(String email) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         Set<Account.Id> r = Sets.newHashSet();
         for (Account a : db.accounts().byPreferredEmail(email)) {
           r.add(a.getId());
@@ -103,8 +102,6 @@
           r.add(a.getAccountId());
         }
         return ImmutableSet.copyOf(r);
-      } finally {
-        db.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index cc62b2b..bedd9f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -142,16 +142,13 @@
 
     @Override
     public AccountState load(Account.Id key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         final AccountState state = load(db, key);
         String user = state.getUserName();
         if (user != null) {
           byName.put(user, Optional.of(state.getAccount().getId()));
         }
         return state;
-      } finally {
-        db.close();
       }
     }
 
@@ -192,8 +189,7 @@
 
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         final AccountExternalId.Key key = new AccountExternalId.Key( //
             AccountExternalId.SCHEME_USERNAME, //
             username);
@@ -202,8 +198,6 @@
           return Optional.of(id.getAccountId());
         }
         return Optional.absent();
-      } finally {
-        db.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index 825bd3b..aad427b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -61,19 +61,19 @@
 
   private final AccountsSection accountsSection;
   private final GroupControl.Factory groupControlFactory;
-  private final CurrentUser currentUser;
+  private final CurrentUser user;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountVisibility accountVisibility;
 
   AccountControl(final ProjectCache projectCache,
         final GroupControl.Factory groupControlFactory,
-        final CurrentUser currentUser,
+        final CurrentUser user,
         final IdentifiedUser.GenericFactory userFactory,
         final AccountVisibility accountVisibility) {
     this.accountsSection =
         projectCache.getAllProjects().getConfig().getAccountsSection();
     this.groupControlFactory = groupControlFactory;
-    this.currentUser = currentUser;
+    this.user = user;
     this.userFactory = userFactory;
     this.accountVisibility = accountVisibility;
   }
@@ -100,11 +100,10 @@
    */
   public boolean canSee(final Account.Id otherUser) {
     // Special case: I can always see myself.
-    if (currentUser.isIdentifiedUser()
-        && ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
+    if (user.isIdentifiedUser() && user.getAccountId().equals(otherUser)) {
       return true;
     }
-    if (currentUser.getCapabilities().canViewAllAccounts()) {
+    if (user.getCapabilities().canViewAllAccounts()) {
       return true;
     }
 
@@ -119,7 +118,7 @@
           }
         }
 
-        if (currentUser.getEffectiveGroups().containsAnyOf(usersGroups)) {
+        if (user.getEffectiveGroups().containsAnyOf(usersGroups)) {
           return true;
         }
         break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
index 445ac6e..b4ca530 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -39,7 +39,7 @@
     USERNAME,
 
     /** Numeric account ID, may be deprecated. */
-    ID;
+    ID
   }
 
   public abstract void fillAccountInfo(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index e1033d0..c0eb947 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -84,13 +84,10 @@
    */
   public Account.Id lookup(String externalId) throws AccountException {
     try {
-      ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         AccountExternalId ext =
             db.accountExternalIds().get(new AccountExternalId.Key(externalId));
         return ext != null ? ext.getAccountId() : null;
-      } finally {
-        db.close();
       }
     } catch (OrmException e) {
       throw new AccountException("Cannot lookup account " + externalId, e);
@@ -108,10 +105,9 @@
   public AuthResult authenticate(AuthRequest who) throws AccountException {
     who = realm.authenticate(who);
     try {
-      ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         AccountExternalId.Key key = id(who);
-        AccountExternalId id = db.accountExternalIds().get(key);
+        AccountExternalId id = getAccountExternalId(db, key);
         if (id == null) {
           // New account, automatically create and return.
           //
@@ -119,8 +115,8 @@
 
         } else { // Account exists
 
-          Account act = db.accounts().get(id.getAccountId());
-          if (act == null || !act.isActive()) {
+          Account act = byIdCache.get(id.getAccountId()).getAccount();
+          if (!act.isActive()) {
             throw new AccountException("Authentication error, account inactive");
           }
 
@@ -129,14 +125,35 @@
           return new AuthResult(id.getAccountId(), key, false);
         }
 
-      } finally {
-        db.close();
       }
     } catch (OrmException e) {
       throw new AccountException("Authentication error", e);
     }
   }
 
+  private AccountExternalId getAccountExternalId(ReviewDb db,
+      AccountExternalId.Key key) throws OrmException {
+    String keyValue = key.get();
+    String keyScheme = keyValue.substring(0, keyValue.indexOf(':') + 1);
+
+    // We don't have at the moment an account_by_external_id cache
+    // but by using the accounts cache we get the list of external_ids
+    // without having to query the DB every time
+    if (keyScheme.equals(AccountExternalId.SCHEME_GERRIT)
+        || keyScheme.equals(AccountExternalId.SCHEME_USERNAME)) {
+      AccountState state = byIdCache.getByUsername(
+          keyValue.substring(keyScheme.length()));
+      if (state != null) {
+        for (AccountExternalId accountExternalId : state.getExternalIds()) {
+          if (accountExternalId.getKey().equals(key)) {
+            return accountExternalId;
+          }
+        }
+      }
+    }
+    return db.accountExternalIds().get(key);
+  }
+
   private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
       throws OrmException {
     IdentifiedUser user = userFactory.create(extId.getAccountId());
@@ -166,8 +183,10 @@
     }
 
     if (!realm.allowsEdit(Account.FieldName.USER_NAME)
+        && who.getUserName() != null
         && !eq(user.getUserName(), who.getUserName())) {
-      changeUserNameFactory.create(db, user, who.getUserName());
+      log.warn(String.format("Not changing already set username %s to %s",
+          user.getUserName(), who.getUserName()));
     }
 
     if (toUpdate != null) {
@@ -326,18 +345,16 @@
    */
   public AuthResult link(Account.Id to, AuthRequest who)
       throws AccountException, OrmException {
-    ReviewDb db = schema.open();
-    try {
+    try (ReviewDb db = schema.open()) {
       who = realm.link(db, to, who);
 
       AccountExternalId.Key key = id(who);
-      AccountExternalId extId = db.accountExternalIds().get(key);
+      AccountExternalId extId = getAccountExternalId(db, key);
       if (extId != null) {
         if (!extId.getAccountId().equals(to)) {
           throw new AccountException("Identity in use by another account");
         }
         update(db, who, extId);
-
       } else {
         extId = createId(to, who);
         extId.setEmailAddress(who.getEmailAddress());
@@ -359,8 +376,6 @@
 
       return new AuthResult(to, key, false);
 
-    } finally {
-      db.close();
     }
   }
 
@@ -379,8 +394,7 @@
    */
   public AuthResult updateLink(Account.Id to, AuthRequest who) throws OrmException,
       AccountException {
-    ReviewDb db = schema.open();
-    try {
+    try (ReviewDb db = schema.open()) {
       AccountExternalId.Key key = id(who);
       List<AccountExternalId.Key> filteredKeysByScheme =
           filterKeysByScheme(key.getScheme(), db.accountExternalIds()
@@ -392,8 +406,6 @@
       }
       byIdCache.evict(to);
       return link(to, who);
-    } finally {
-      db.close();
     }
   }
 
@@ -419,12 +431,11 @@
    */
   public AuthResult unlink(Account.Id from, AuthRequest who)
       throws AccountException, OrmException {
-    ReviewDb db = schema.open();
-    try {
+    try (ReviewDb db = schema.open()) {
       who = realm.unlink(db, from, who);
 
       AccountExternalId.Key key = id(who);
-      AccountExternalId extId = db.accountExternalIds().get(key);
+      AccountExternalId extId = getAccountExternalId(db, key);
       if (extId != null) {
         if (!extId.getAccountId().equals(from)) {
           throw new AccountException("Identity in use by another account");
@@ -448,8 +459,6 @@
 
       return new AuthResult(from, key, false);
 
-    } finally {
-      db.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index efe7322..80ea907 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -77,9 +77,9 @@
    * Parses a account ID from a request body and returns the user.
    *
    * @param id ID of the account, can be a string of the format
-   *        "Full Name <email@example.com>", just the email address, a full name
-   *        if it is unique, an account ID, a user name or 'self' for the
-   *        calling user
+   *        "{@code Full Name <email@example.com>}", just the email address,
+   *        a full name if it is unique, an account ID, a user name or
+   *        "{@code self}" for the calling user
    * @return the user, never null.
    * @throws UnprocessableEntityException thrown if the account ID cannot be
    *         resolved or if the account is not visible to the calling user
@@ -102,9 +102,9 @@
    * check whether the current user can see the account.
    *
    * @param id ID of the account, can be a string of the format
-   *        "Full Name <email@example.com>", just the email address, a full name
-   *        if it is unique, an account ID, a user name or 'self' for the
-   *        calling user
+   *        "{@code Full Name <email@example.com>}", just the email address,
+   *        a full name if it is unique, an account ID, a user name or
+   *        "{@code self}" for the calling user
    * @return the user, null if no user is found for the given account ID
    * @throws AuthException thrown if 'self' is used as account ID and the
    *         current user is not authenticated
@@ -114,7 +114,7 @@
     if (id.equals("self")) {
       CurrentUser user = self.get();
       if (user.isIdentifiedUser()) {
-        return (IdentifiedUser) user;
+        return user.asIdentifiedUser();
       } else if (user instanceof AnonymousUser) {
         throw new AuthException("Authentication required");
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 3c21d17..7ec659e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.io.ByteSource;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AddSshKey.Input;
 import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -37,12 +39,17 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collections;
 
 @Singleton
 public class AddSshKey implements RestModifyView<AccountResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(AddSshKey.class);
+
   public static class Input {
     public RawInput raw;
   }
@@ -50,13 +57,15 @@
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
   private final SshKeyCache sshKeyCache;
+  private final AddKeySender.Factory addKeyFactory;
 
   @Inject
   AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
-      SshKeyCache sshKeyCache) {
+      SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) {
     this.self = self;
     this.dbProvider = dbProvider;
     this.sshKeyCache = sshKeyCache;
+    this.addKeyFactory = addKeyFactory;
   }
 
   @Override
@@ -96,6 +105,12 @@
           sshKeyCache.create(new AccountSshKey.Id(
               user.getAccountId(), max + 1), sshPublicKey);
       dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
+      try {
+        addKeyFactory.create(user, sshKey).send();
+      } catch (EmailException e) {
+        log.error("Cannot send SSH key added message to "
+            + user.getAccount().getPreferredEmail(), e);
+      }
       sshKeyCache.evict(user.getUserName());
       return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
     } catch (InvalidSshKeyException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthenticationFailedException.java
similarity index 64%
rename from gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/AuthenticationFailedException.java
index 2c27240..bdd8e82c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthenticationFailedException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.rules;
+package com.google.gerrit.server.account;
 
-/** Thrown by {@link PrologEnvironment} if a script runs too long. */
-public class ReductionLimitException extends RuntimeException {
+/** Authentication failed due to incorrect user or password. */
+public class AuthenticationFailedException extends AccountException {
   private static final long serialVersionUID = 1L;
 
-  ReductionLimitException(int limit) {
-    super(String.format("exceeded reduction limit of %d", limit));
+  public AuthenticationFailedException(String message, Throwable why) {
+    super(message, why);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 2721057..7bb00c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -58,7 +58,7 @@
   }
 
   /** Identity of the user the control will compute for. */
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
@@ -107,7 +107,7 @@
   /** @return true if the user can kill any running task. */
   public boolean canKillTask() {
     return canPerform(GlobalCapability.KILL_TASK)
-      || canAdministrateServer();
+      || canMaintainServer();
   }
 
   /** @return true if the user can modify an account for another user. */
@@ -125,12 +125,18 @@
   /** @return true if the user can view the server caches. */
   public boolean canViewCaches() {
     return canPerform(GlobalCapability.VIEW_CACHES)
-      || canAdministrateServer();
+      || canMaintainServer();
   }
 
   /** @return true if the user can flush the server's caches. */
   public boolean canFlushCaches() {
     return canPerform(GlobalCapability.FLUSH_CACHES)
+      || canMaintainServer();
+  }
+
+  /** @return true if the user can perform basic server maintenance. */
+  public boolean canMaintainServer() {
+    return canPerform(GlobalCapability.MAINTAIN_SERVER)
       || canAdministrateServer();
   }
 
@@ -149,7 +155,7 @@
   /** @return true if the user can view the entire queue. */
   public boolean canViewQueue() {
     return canPerform(GlobalCapability.VIEW_QUEUE)
-      || canAdministrateServer();
+      || canMaintainServer();
   }
 
   /** @return true if the user can access the database (with gsql). */
@@ -166,7 +172,7 @@
   /** @return true if the user can run the Git garbage collection. */
   public boolean canRunGC() {
     return canPerform(GlobalCapability.RUN_GC)
-        || canAdministrateServer();
+        || canMaintainServer();
   }
 
   /** @return true if the user can impersonate another user. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
index 1cb27f1..c26b1ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.annotations.CapabilityScope;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
@@ -24,6 +25,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.lang.annotation.Annotation;
+import java.util.Arrays;
 
 public class CapabilityUtils {
   private static final Logger log = LoggerFactory
@@ -33,33 +35,80 @@
       String pluginName, Class<?> clazz)
       throws AuthException {
     RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class);
-    if (rc != null) {
-      CurrentUser user = userProvider.get();
-      CapabilityControl ctl = user.getCapabilities();
-      if (ctl.canAdministrateServer()) {
+    RequiresAnyCapability rac =
+        getClassAnnotation(clazz, RequiresAnyCapability.class);
+    if (rc != null && rac != null) {
+      log.error(String.format(
+          "Class %s uses both @%s and @%s",
+          clazz.getName(),
+          RequiresCapability.class.getSimpleName(),
+          RequiresAnyCapability.class.getSimpleName()));
+      throw new AuthException("cannot check capability");
+    }
+    CurrentUser user = userProvider.get();
+    CapabilityControl ctl = user.getCapabilities();
+    if (ctl.canAdministrateServer()) {
+      return;
+    }
+    checkRequiresCapability(ctl, pluginName, clazz, rc);
+    checkRequiresAnyCapability(ctl, pluginName, clazz, rac);
+  }
+
+  private static void checkRequiresCapability(CapabilityControl ctl,
+      String pluginName, Class<?> clazz, RequiresCapability rc)
+      throws AuthException {
+    if (rc == null) {
+      return;
+    }
+    String capability =
+        resolveCapability(pluginName, rc.value(), rc.scope(), clazz);
+    if (!ctl.canPerform(capability)) {
+      throw new AuthException(String.format(
+          "Capability %s is required to access this resource",
+          capability));
+    }
+  }
+
+  private static void checkRequiresAnyCapability(CapabilityControl ctl,
+      String pluginName, Class<?> clazz, RequiresAnyCapability rac)
+      throws AuthException {
+    if (rac == null) {
+      return;
+    }
+    if (rac.value().length == 0) {
+      log.error(String.format(
+          "Class %s uses @%s with no capabilities listed",
+          clazz.getName(),
+          RequiresAnyCapability.class.getSimpleName()));
+      throw new AuthException("cannot check capability");
+    }
+    for (String capability : rac.value()) {
+      capability =
+          resolveCapability(pluginName, capability, rac.scope(), clazz);
+      if (ctl.canPerform(capability)) {
         return;
       }
-
-      String capability = rc.value();
-      if (pluginName != null && !"gerrit".equals(pluginName)
-         && (rc.scope() == CapabilityScope.PLUGIN
-          || rc.scope() == CapabilityScope.CONTEXT)) {
-        capability = String.format("%s-%s", pluginName, rc.value());
-      } else if (rc.scope() == CapabilityScope.PLUGIN) {
-        log.error(String.format(
-            "Class %s uses @%s(scope=%s), but is not within a plugin",
-            clazz.getName(),
-            RequiresCapability.class.getSimpleName(),
-            CapabilityScope.PLUGIN.name()));
-        throw new AuthException("cannot check capability");
-      }
-
-      if (!ctl.canPerform(capability)) {
-        throw new AuthException(String.format(
-            "Capability %s is required to access this resource",
-            capability));
-      }
     }
+    throw new AuthException(
+        "One of the following capabilities is required to access this"
+        + " resource: " + Arrays.asList(rac.value()));
+  }
+
+  private static String resolveCapability(String pluginName, String capability,
+      CapabilityScope scope, Class<?> clazz) throws AuthException {
+    if (pluginName != null && !"gerrit".equals(pluginName)
+       && (scope == CapabilityScope.PLUGIN
+        || scope == CapabilityScope.CONTEXT)) {
+      capability = String.format("%s-%s", pluginName, capability);
+    } else if (scope == CapabilityScope.PLUGIN) {
+      log.error(String.format(
+          "Class %s uses @%s(scope=%s), but is not within a plugin",
+          clazz.getName(),
+          RequiresCapability.class.getSimpleName(),
+          CapabilityScope.PLUGIN.name()));
+      throw new AuthException("cannot check capability");
+    }
+    return capability;
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index 1005569..b413e81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -28,7 +28,6 @@
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.ArrayList;
@@ -39,28 +38,12 @@
 
 /** Operation to change the username of an account. */
 public class ChangeUserName implements Callable<VoidResult> {
+  public static final String USERNAME_CANNOT_BE_CHANGED =
+      "Username cannot be changed.";
+
   private static final Pattern USER_NAME_PATTERN =
       Pattern.compile(Account.USER_NAME_PATTERN);
 
-  /** Factory to change the username for the current user. */
-  public static class CurrentUser {
-    private final Factory factory;
-    private final Provider<ReviewDb> db;
-    private final Provider<IdentifiedUser> user;
-
-    @Inject
-    CurrentUser(Factory factory, Provider<ReviewDb> db,
-        Provider<IdentifiedUser> user) {
-      this.factory = factory;
-      this.db = db;
-      this.user = user;
-    }
-
-    public ChangeUserName create(String newUsername) {
-      return factory.create(db.get(), user.get(), newUsername);
-    }
-  }
-
   /** Generic factory to change any user's username. */
   public interface Factory {
     ChangeUserName create(ReviewDb db, IdentifiedUser user, String newUsername);
@@ -92,7 +75,7 @@
       InvalidUserNameException {
     final Collection<AccountExternalId> old = old();
     if (!old.isEmpty()) {
-      throw new IllegalStateException("Username cannot be changed.");
+      throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
     }
 
     if (newUsername != null && !newUsername.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 38fda4c..c4f35e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateAccount.Input;
 import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.mail.OutgoingEmailValidator;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
@@ -45,8 +46,6 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import org.apache.commons.validator.routines.EmailValidator;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -132,7 +131,7 @@
         throw new UnprocessableEntityException(
             "email '" + input.email + "' already exists");
       }
-      if (!EmailValidator.getInstance().isValid(input.email)) {
+      if (!OutgoingEmailValidator.isValid(input.email)) {
         throw new BadRequestException("invalid email address");
       }
     }
@@ -154,6 +153,7 @@
         try {
           db.accountExternalIds().delete(Collections.singleton(extUser));
         } catch (OrmException cleanupError) {
+          // Ignored
         }
         throw new UnprocessableEntityException(
             "email '" + input.email + "' already exists");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 700e138..5c903d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -27,8 +27,8 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.CreateEmail.Input;
 import com.google.gerrit.server.account.GetEmails.EmailInfo;
+import com.google.gerrit.server.mail.OutgoingEmailValidator;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gwtorm.server.OrmException;
@@ -36,19 +36,11 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import org.apache.commons.validator.routines.EmailValidator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class CreateEmail implements RestModifyView<AccountResource, Input> {
-  private final Logger log = LoggerFactory.getLogger(getClass());
-
-  public static class Input {
-    @DefaultInput
-    public String email;
-    public boolean preferred;
-    public boolean noConfirmation;
-  }
+public class CreateEmail implements RestModifyView<AccountResource, EmailInput> {
+  private static final Logger log = LoggerFactory.getLogger(CreateEmail.class);
 
   public static interface Factory {
     CreateEmail create(String email);
@@ -80,7 +72,7 @@
   }
 
   @Override
-  public Response<EmailInfo> apply(AccountResource rsrc, Input input)
+  public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
       ResourceNotFoundException, OrmException, EmailException,
       MethodNotAllowedException {
@@ -90,10 +82,10 @@
     }
 
     if (input == null) {
-      input = new Input();
+      input = new EmailInput();
     }
 
-    if (!EmailValidator.getInstance().isValid(email)) {
+    if (!OutgoingEmailValidator.isValid(email)) {
       throw new BadRequestException("invalid email address");
     }
 
@@ -102,17 +94,17 @@
       throw new AuthException("not allowed to use no_confirmation");
     }
 
-    return apply(rsrc.getUser(), input);
-  }
-
-  public Response<EmailInfo> apply(IdentifiedUser user, Input input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, OrmException, EmailException,
-      MethodNotAllowedException {
     if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow adding emails");
     }
 
+    return apply(rsrc.getUser(), input);
+  }
+
+  public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      ResourceNotFoundException, OrmException, EmailException,
+      MethodNotAllowedException {
     if (input.email != null && !email.equals(input.email)) {
       throw new BadRequestException("email address must match URL");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateGroupArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateGroupArgs.java
index 807eed6..9ddef3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateGroupArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateGroupArgs.java
@@ -25,7 +25,6 @@
   public boolean visibleToAll;
   public AccountGroup.Id ownerGroupId;
   public Collection<? extends Account.Id> initialMembers;
-  public Collection<? extends AccountGroup.UUID> initialGroups;
 
   public AccountGroup.NameKey getGroup() {
     return groupName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 362a39f..5978703 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -31,8 +31,9 @@
   private final AuthConfig authConfig;
 
   @Inject
-  DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail, final AuthConfig authConfig) {
+  DefaultRealm(EmailExpander emailExpander,
+      AccountByEmailCache byEmail,
+      AuthConfig authConfig) {
     this.emailExpander = emailExpander;
     this.byEmail = byEmail;
     this.authConfig = authConfig;
@@ -47,12 +48,18 @@
         case FULL_NAME:
           return Strings.emptyToNull(authConfig.getHttpDisplaynameHeader()) == null;
         case REGISTER_NEW_EMAIL:
-          return Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
+          return authConfig.isAllowRegisterNewEmail()
+              && Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
         default:
           return true;
       }
     } else {
-      return true;
+      switch (field) {
+        case REGISTER_NEW_EMAIL:
+          return authConfig.isAllowRegisterNewEmail();
+        default:
+          return true;
+      }
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 43b76e2..d231767 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS;
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT;
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
@@ -58,10 +59,6 @@
 import java.util.Set;
 
 class GetCapabilities implements RestReadView<AccountResource> {
-  @Deprecated
-  @Option(name = "--format", usage = "(deprecated) output format")
-  private OutputFormat format;
-
   @Option(name = "-q", metaVar = "CAP", usage = "Capability to inspect")
   void addQuery(String name) {
     if (query == null) {
@@ -115,6 +112,7 @@
     have.put(EMAIL_REVIEWERS, cc.canEmailReviewers());
     have.put(FLUSH_CACHES, cc.canFlushCaches());
     have.put(KILL_TASK, cc.canKillTask());
+    have.put(MAINTAIN_SERVER, cc.canMaintainServer());
     have.put(MODIFY_ACCOUNT, cc.canModifyAccount());
     have.put(RUN_GC, cc.canRunGC());
     have.put(STREAM_EVENTS, cc.canStreamEvents());
@@ -140,22 +138,9 @@
       }
     }
 
-    if (format == OutputFormat.TEXT) {
-      StringBuilder sb = new StringBuilder();
-      for (Map.Entry<String, Object> e : have.entrySet()) {
-        sb.append(e.getKey());
-        if (!(e.getValue() instanceof Boolean)) {
-          sb.append(": ");
-          sb.append(e.getValue().toString());
-        }
-        sb.append('\n');
-      }
-      return BinaryResult.create(sb.toString());
-    } else {
-      return OutputFormat.JSON.newGson().toJsonTree(
-        have,
-        new TypeToken<Map<String, Object>>() {}.getType());
-    }
+    return OutputFormat.JSON.newGson().toJsonTree(
+      have,
+      new TypeToken<Map<String, Object>>() {}.getType());
   }
 
   private boolean want(String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
new file mode 100644
index 0000000..81c860e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2015 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.server.account;
+
+import com.google.common.base.Throwables;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.EnumSet;
+
+@Singleton
+public class GetDetail implements RestReadView<AccountResource> {
+
+  private final InternalAccountDirectory directory;
+
+  @Inject
+  public GetDetail(InternalAccountDirectory directory) {
+    this.directory = directory;
+  }
+
+  @Override
+  public AccountDetailInfo apply(AccountResource rsrc) throws OrmException {
+    Account a = rsrc.getUser().getAccount();
+    AccountDetailInfo info = new AccountDetailInfo(a.getId().get());
+    info.registeredOn = a.getRegisteredOn();
+    try {
+      directory.fillAccountInfo(Collections.singleton(info),
+          EnumSet.allOf(FillOptions.class));
+    } catch (DirectoryException e) {
+      Throwables.propagateIfPossible(e.getCause(), OrmException.class);
+      throw new OrmException(e);
+    }
+    return info;
+  }
+
+  public static class AccountDetailInfo extends AccountInfo {
+    public Timestamp registeredOn;
+
+    public AccountDetailInfo(Integer id) {
+      super(id);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
index 19aeefc..be87ae7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -14,91 +14,65 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.extensions.client.Theme;
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
 @Singleton
 public class GetDiffPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> db;
+  private final Provider<AllUsersName> allUsersName;
+  private final GitRepositoryManager gitMgr;
 
   @Inject
-  GetDiffPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db) {
+  GetDiffPreferences(Provider<CurrentUser> self,
+      Provider<AllUsersName> allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
-    this.db = db;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
   }
 
   @Override
   public DiffPreferencesInfo apply(AccountResource rsrc)
-      throws AuthException, OrmException {
+      throws AuthException, ConfigInvalidException, IOException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("restricted to administrator");
     }
 
-    Account.Id userId = rsrc.getUser().getAccountId();
-    AccountDiffPreference a = db.get().accountDiffPreferences().get(userId);
-    if (a == null) {
-      a = new AccountDiffPreference(userId);
-    }
-    return DiffPreferencesInfo.parse(a);
+    Account.Id id = rsrc.getUser().getAccountId();
+    return readFromGit(id, gitMgr, allUsersName.get(), null);
   }
 
-  public static class DiffPreferencesInfo {
-    static DiffPreferencesInfo parse(AccountDiffPreference p) {
-      DiffPreferencesInfo info = new DiffPreferencesInfo();
-      info.context = p.getContext();
-      info.expandAllComments = p.isExpandAllComments() ? true : null;
-      info.ignoreWhitespace = p.getIgnoreWhitespace();
-      info.intralineDifference = p.isIntralineDifference() ? true : null;
-      info.lineLength = p.getLineLength();
-      info.manualReview = p.isManualReview() ? true : null;
-      info.retainHeader = p.isRetainHeader() ? true : null;
-      info.showLineEndings = p.isShowLineEndings() ? true : null;
-      info.showTabs = p.isShowTabs() ? true : null;
-      info.showWhitespaceErrors = p.isShowWhitespaceErrors() ? true : null;
-      info.skipDeleted = p.isSkipDeleted() ? true : null;
-      info.skipUncommented = p.isSkipUncommented() ? true : null;
-      info.hideTopMenu = p.isHideTopMenu() ? true : null;
-      info.autoHideDiffTableHeader = p.isAutoHideDiffTableHeader() ? true : null;
-      info.hideLineNumbers = p.isHideLineNumbers() ? true : null;
-      info.syntaxHighlighting = p.isSyntaxHighlighting() ? true : null;
-      info.tabSize = p.getTabSize();
-      info.renderEntireFile = p.isRenderEntireFile() ? true : null;
-      info.hideEmptyPane = p.isHideEmptyPane() ? true : null;
-      info.theme = p.getTheme();
-      return info;
+  static DiffPreferencesInfo readFromGit(Account.Id id,
+      GitRepositoryManager gitMgr, AllUsersName allUsersName,
+      DiffPreferencesInfo in)
+      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forUser(id);
+      p.load(git);
+      DiffPreferencesInfo prefs = new DiffPreferencesInfo();
+      loadSection(p.getConfig(), UserConfigSections.DIFF, null, prefs,
+          DiffPreferencesInfo.defaults(), in);
+      return prefs;
     }
-
-    public short context;
-    public Boolean expandAllComments;
-    public Whitespace ignoreWhitespace;
-    public Boolean intralineDifference;
-    public int lineLength;
-    public Boolean manualReview;
-    public Boolean retainHeader;
-    public Boolean showLineEndings;
-    public Boolean showTabs;
-    public Boolean showWhitespaceErrors;
-    public Boolean skipDeleted;
-    public Boolean skipUncommented;
-    public Boolean syntaxHighlighting;
-    public Boolean hideTopMenu;
-    public Boolean autoHideDiffTableHeader;
-    public Boolean hideLineNumbers;
-    public Boolean renderEntireFile;
-    public Boolean hideEmptyPane;
-    public int tabSize;
-    public Theme theme;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
new file mode 100644
index 0000000..d99b68f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2014 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.server.account;
+
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+@Singleton
+public class GetEditPreferences implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
+
+  @Inject
+  GetEditPreferences(Provider<CurrentUser> self,
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
+    this.self = self;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
+  }
+
+  @Override
+  public EditPreferencesInfo apply(AccountResource rsrc) throws AuthException,
+      IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
+    }
+
+    return readFromGit(
+        rsrc.getUser().getAccountId(), gitMgr, allUsersName, null);
+  }
+
+  static EditPreferencesInfo readFromGit(Account.Id id,
+      GitRepositoryManager gitMgr, AllUsersName allUsersName,
+      EditPreferencesInfo in) throws IOException, ConfigInvalidException,
+          RepositoryNotFoundException {
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forUser(id);
+      p.load(git);
+
+      return loadSection(p.getConfig(), UserConfigSections.EDIT, null,
+          new EditPreferencesInfo(), EditPreferencesInfo.defaults(), in);
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
index d335add..f777145 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
@@ -16,12 +16,12 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.GroupJson;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index e45e7cc..08bf83e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -24,13 +24,13 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,16 +44,20 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @Singleton
 public class GetPreferences implements RestReadView<AccountResource> {
   private static final Logger log = LoggerFactory.getLogger(GetPreferences.class);
 
-  public static final String MY = "my";
   public static final String KEY_URL = "url";
   public static final String KEY_TARGET = "target";
   public static final String KEY_ID = "id";
+  public static final String URL_ALIAS = "urlAlias";
+  public static final String KEY_MATCH = "match";
+  public static final String KEY_TOKEN = "token";
 
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
@@ -86,14 +90,11 @@
       throw new ResourceNotFoundException();
     }
 
-    Repository git = gitMgr.openRepository(allUsersName);
-    try {
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
       VersionedAccountPreferences p =
           VersionedAccountPreferences.forUser(rsrc.getUser().getAccountId());
       p.load(git);
       return new PreferenceInfo(a.getGeneralPreferences(), p, git);
-    } finally {
-      git.close();
     }
   }
 
@@ -101,7 +102,7 @@
     Short changesPerPage;
     Boolean showSiteHeader;
     Boolean useFlashClipboard;
-    DownloadScheme downloadScheme;
+    String downloadScheme;
     DownloadCommand downloadCommand;
     Boolean copySelfOnEmail;
     DateFormat dateFormat;
@@ -113,6 +114,7 @@
     ReviewCategoryStrategy reviewCategoryStrategy;
     DiffView diffView;
     List<TopMenu.MenuItem> my;
+    Map<String, String> urlAliases;
 
     public PreferenceInfo(AccountGeneralPreferences p,
         VersionedAccountPreferences v, Repository allUsers) {
@@ -132,12 +134,12 @@
         reviewCategoryStrategy = p.getReviewCategoryStrategy();
         diffView = p.getDiffView();
       }
-      my = my(v, allUsers);
+      loadFromAllUsers(v, allUsers);
     }
 
-    private List<TopMenu.MenuItem> my(VersionedAccountPreferences v,
+    private void loadFromAllUsers(VersionedAccountPreferences v,
         Repository allUsers) {
-      List<TopMenu.MenuItem> my = my(v);
+      my = my(v);
       if (my.isEmpty() && !v.isDefaults()) {
         try {
           VersionedAccountPreferences d = VersionedAccountPreferences.forDefault();
@@ -151,17 +153,19 @@
         my.add(new TopMenu.MenuItem("Changes", "#/dashboard/self", null));
         my.add(new TopMenu.MenuItem("Drafts", "#/q/owner:self+is:draft", null));
         my.add(new TopMenu.MenuItem("Draft Comments", "#/q/has:draft", null));
+        my.add(new TopMenu.MenuItem("Edits", "#/q/has:edit", null));
         my.add(new TopMenu.MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
         my.add(new TopMenu.MenuItem("Starred Changes", "#/q/is:starred", null));
         my.add(new TopMenu.MenuItem("Groups", "#/groups/self", null));
       }
-      return my;
+
+      urlAliases = urlAliases(v);
     }
 
     private List<TopMenu.MenuItem> my(VersionedAccountPreferences v) {
       List<TopMenu.MenuItem> my = new ArrayList<>();
       Config cfg = v.getConfig();
-      for (String subsection : cfg.getSubsections(MY)) {
+      for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
         String url = my(cfg, subsection, KEY_URL, "#/");
         String target = my(cfg, subsection, KEY_TARGET,
             url.startsWith("#") ? null : "_blank");
@@ -174,8 +178,18 @@
 
     private static String my(Config cfg, String subsection, String key,
         String defaultValue) {
-      String val = cfg.getString(MY, subsection, key);
+      String val = cfg.getString(UserConfigSections.MY, subsection, key);
       return !Strings.isNullOrEmpty(val) ? val : defaultValue;
     }
+
+    private static Map<String, String> urlAliases(VersionedAccountPreferences v) {
+      HashMap<String, String> urlAliases = new HashMap<>();
+      Config cfg = v.getConfig();
+      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+        urlAliases.put(cfg.getString(URL_ALIAS, subsection, KEY_MATCH),
+           cfg.getString(URL_ALIAS, subsection, KEY_TOKEN));
+      }
+      return !urlAliases.isEmpty() ? urlAliases : null;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index b33e3f7..bf04234 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -152,13 +152,8 @@
 
   @Override
   public Iterable<AccountGroup> all() {
-    try {
-      ReviewDb db = schema.open();
-      try {
-        return Collections.unmodifiableList(db.accountGroups().all().toList());
-      } finally {
-        db.close();
-      }
+    try (ReviewDb db = schema.open()) {
+      return Collections.unmodifiableList(db.accountGroups().all().toList());
     } catch (OrmException e) {
       log.warn("Cannot list internal groups", e);
       return Collections.emptyList();
@@ -187,11 +182,8 @@
     @Override
     public Optional<AccountGroup> load(final AccountGroup.Id key)
         throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         return Optional.fromNullable(db.accountGroups().get(key));
-      } finally {
-        db.close();
       }
     }
   }
@@ -207,16 +199,13 @@
     @Override
     public Optional<AccountGroup> load(String name)
         throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         AccountGroup.NameKey key = new AccountGroup.NameKey(name);
         AccountGroupName r = db.accountGroupNames().get(key);
         if (r != null) {
           return Optional.fromNullable(db.accountGroups().get(r.getId()));
         }
         return Optional.absent();
-      } finally {
-        db.close();
       }
     }
   }
@@ -232,8 +221,7 @@
     @Override
     public Optional<AccountGroup> load(String uuid)
         throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         List<AccountGroup> r;
 
         r = db.accountGroups().byUUID(new AccountGroup.UUID(uuid)).toList();
@@ -244,8 +232,6 @@
         } else {
           throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid);
         }
-      } finally {
-        db.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 084cfe8..2e03913 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -20,8 +20,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.InternalUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -122,7 +120,7 @@
     return group;
   }
 
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
@@ -132,7 +130,7 @@
      * for visibility of all groups that are not an internal group to
      * server administrators.
      */
-    return user instanceof InternalUser
+    return user.isInternalUser()
       || groupBackend.isVisibleToAll(group.getGroupUUID())
       || user.getEffectiveGroups().contains(group.getGroupUUID())
       || user.getCapabilities().canAdministrateServer()
@@ -145,8 +143,8 @@
       isOwner = false;
     } else if (isOwner == null) {
       AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
-      isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
-             || getCurrentUser().getCapabilities().canAdministrateServer();
+      isOwner = getUser().getEffectiveGroups().contains(ownerUUID)
+             || getUser().getCapabilities().canAdministrateServer();
     }
     return isOwner;
   }
@@ -160,8 +158,7 @@
   }
 
   public boolean canSeeMember(Account.Id id) {
-    if (user.isIdentifiedUser()
-        && ((IdentifiedUser) user).getAccountId().equals(id)) {
+    if (user.isIdentifiedUser() && user.getAccountId().equals(id)) {
       return true;
     }
     return canSeeMembers();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 9e7918d..4b56b81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -144,8 +144,7 @@
 
     @Override
     public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
         if (group.size() != 1) {
           return Collections.emptySet();
@@ -157,8 +156,6 @@
           ids.add(agi.getIncludeUUID());
         }
         return ImmutableSet.copyOf(ids);
-      } finally {
-        db.close();
       }
     }
   }
@@ -174,8 +171,7 @@
 
     @Override
     public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         Set<AccountGroup.Id> ids = Sets.newHashSet();
         for (AccountGroupById agi : db.accountGroupById()
             .byIncludeUUID(key)) {
@@ -187,8 +183,6 @@
           groupArray.add(g.getGroupUUID());
         }
         return ImmutableSet.copyOf(groupArray);
-      } finally {
-        db.close();
       }
     }
   }
@@ -204,8 +198,7 @@
 
     @Override
     public Set<AccountGroup.UUID> load(String key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         Set<AccountGroup.UUID> ids = Sets.newHashSet();
         for (AccountGroupById agi : db.accountGroupById().all()) {
           if (!AccountGroup.isInternalGroup(agi.getIncludeUUID())) {
@@ -213,8 +206,6 @@
           }
         }
         return ImmutableSet.copyOf(ids);
-      } finally {
-        db.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index 3fe9d25..d3d6504 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -87,9 +87,9 @@
       return Collections.emptySet();
     }
 
-    final Set<AccountGroup.UUID> ownerGroups =
+    final Iterable<AccountGroup.UUID> ownerGroups =
         projectControl.controlFor(project, currentUser).getProjectState()
-            .getOwners();
+            .getAllOwners();
 
     final HashSet<Account> projectOwners = new HashSet<>();
     for (final AccountGroup.UUID ownerGroup : ownerGroups) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 14c224f..54d4cc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -22,7 +22,6 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
   @Override
@@ -38,10 +37,12 @@
 
     put(ACCOUNT_KIND).to(PutAccount.class);
     get(ACCOUNT_KIND).to(GetAccount.class);
+    get(ACCOUNT_KIND, "detail").to(GetDetail.class);
     get(ACCOUNT_KIND, "name").to(GetName.class);
     put(ACCOUNT_KIND, "name").to(PutName.class);
     delete(ACCOUNT_KIND, "name").to(PutName.class);
     get(ACCOUNT_KIND, "username").to(GetUsername.class);
+    put(ACCOUNT_KIND, "username").to(PutUsername.class);
     get(ACCOUNT_KIND, "active").to(GetActive.class);
     put(ACCOUNT_KIND, "active").to(PutActive.class);
     delete(ACCOUNT_KIND, "active").to(DeleteActive.class);
@@ -55,16 +56,22 @@
     delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
     child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
     post(ACCOUNT_KIND, "sshkeys").to(AddSshKey.class);
+
     get(SSH_KEY_KIND).to(GetSshKey.class);
     delete(SSH_KEY_KIND).to(DeleteSshKey.class);
+
     get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
     get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
+
     child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
+
     get(ACCOUNT_KIND, "groups").to(GetGroups.class);
     get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
     put(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
     get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
     put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
+    get(ACCOUNT_KIND, "preferences.edit").to(GetEditPreferences.class);
+    put(ACCOUNT_KIND, "preferences.edit").to(SetEditPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
 
     child(ACCOUNT_KIND, "starred.changes").to(StarredChanges.class);
@@ -72,7 +79,7 @@
     delete(STARRED_CHANGE_KIND).to(StarredChanges.Delete.class);
     bind(StarredChanges.Create.class);
 
-    install(new FactoryModuleBuilder().build(CreateAccount.Factory.class));
-    install(new FactoryModuleBuilder().build(CreateEmail.Factory.class));
+    factory(CreateAccount.Factory.class);
+    factory(CreateEmail.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
deleted file mode 100644
index fd1f33e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ /dev/null
@@ -1,158 +0,0 @@
-// Copyright (C) 2011 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.server.account;
-
-import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.lib.PersonIdent;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-public class PerformCreateGroup {
-
-  public interface Factory {
-    PerformCreateGroup create(CreateGroupArgs createGroupArgs);
-  }
-
-  private final ReviewDb db;
-  private final AccountCache accountCache;
-  private final GroupIncludeCache groupIncludeCache;
-  private final IdentifiedUser currentUser;
-  private final PersonIdent serverIdent;
-  private final GroupCache groupCache;
-  private final CreateGroupArgs createGroupArgs;
-  private final AuditService auditService;
-
-  @Inject
-  PerformCreateGroup(ReviewDb db, AccountCache accountCache,
-      GroupIncludeCache groupIncludeCache, IdentifiedUser currentUser,
-      @GerritPersonIdent PersonIdent serverIdent, GroupCache groupCache,
-      @Assisted CreateGroupArgs createGroupArgs, AuditService auditService) {
-    this.db = db;
-    this.accountCache = accountCache;
-    this.groupIncludeCache = groupIncludeCache;
-    this.currentUser = currentUser;
-    this.serverIdent = serverIdent;
-    this.groupCache = groupCache;
-    this.createGroupArgs = createGroupArgs;
-    this.auditService = auditService;
-  }
-
-  /**
-   * Creates a new group.
-
-   * @return the new group
-   * @throws OrmException is thrown in case of any data store read or write
-   *         error
-   * @throws NameAlreadyUsedException is thrown in case a group with the given
-   *         name already exists
-   * @throws PermissionDeniedException user cannot create a group.
-   */
-  public AccountGroup createGroup() throws OrmException,
-      NameAlreadyUsedException, PermissionDeniedException {
-    if (!currentUser.getCapabilities().canCreateGroup()) {
-      throw new PermissionDeniedException(String.format(
-        "%s does not have \"Create Group\" capability.",
-        currentUser.getUserName()));
-    }
-    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
-    AccountGroup.UUID uuid = GroupUUID.make(
-        createGroupArgs.getGroupName(),
-        currentUser.newCommitterIdent(
-            serverIdent.getWhen(),
-            serverIdent.getTimeZone()));
-    AccountGroup group =
-        new AccountGroup(createGroupArgs.getGroup(), groupId, uuid);
-    group.setVisibleToAll(createGroupArgs.visibleToAll);
-    if (createGroupArgs.ownerGroupId != null) {
-      AccountGroup ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);
-      if (ownerGroup != null) {
-        group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
-      }
-    }
-    if (createGroupArgs.groupDescription != null) {
-      group.setDescription(createGroupArgs.groupDescription);
-    }
-    AccountGroupName gn = new AccountGroupName(group);
-    // first insert the group name to validate that the group name hasn't
-    // already been used to create another group
-    try {
-      db.accountGroupNames().insert(Collections.singleton(gn));
-    } catch (OrmDuplicateKeyException e) {
-      throw new NameAlreadyUsedException(createGroupArgs.getGroupName());
-    }
-    db.accountGroups().insert(Collections.singleton(group));
-
-    addMembers(groupId, createGroupArgs.initialMembers);
-
-    if (createGroupArgs.initialGroups != null) {
-      addGroups(groupId, createGroupArgs.initialGroups);
-      groupIncludeCache.evictSubgroupsOf(uuid);
-    }
-
-    groupCache.onCreateGroup(createGroupArgs.getGroup());
-
-    return group;
-  }
-
-  private void addMembers(final AccountGroup.Id groupId,
-      final Collection<? extends Account.Id> members) throws OrmException {
-    List<AccountGroupMember> memberships = new ArrayList<>();
-    for (Account.Id accountId : members) {
-      final AccountGroupMember membership =
-          new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId));
-      memberships.add(membership);
-    }
-    db.accountGroupMembers().insert(memberships);
-    auditService.dispatchAddAccountsToGroup(currentUser.getAccountId(), memberships);
-
-    for (Account.Id accountId : members) {
-      accountCache.evict(accountId);
-    }
-  }
-
-  private void addGroups(final AccountGroup.Id groupId,
-      final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
-    List<AccountGroupById> includeList = new ArrayList<>();
-    for (AccountGroup.UUID includeUUID : groups) {
-      final AccountGroupById groupInclude =
-        new AccountGroupById(new AccountGroupById.Key(groupId, includeUUID));
-      includeList.add(groupInclude);
-    }
-    db.accountGroupById().insert(includeList);
-    auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), includeList);
-
-    for (AccountGroup.UUID uuid : groups) {
-      groupIncludeCache.evictParentGroupsOf(uuid);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
deleted file mode 100644
index 3623cc3..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (C) 2011 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.server.account;
-
-import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.RenameGroupOp;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import java.util.Collections;
-import java.util.Date;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-
-public class PerformRenameGroup {
-
-  public interface Factory {
-    PerformRenameGroup create();
-  }
-
-  private final ReviewDb db;
-  private final GroupCache groupCache;
-  private final GroupControl.Factory groupControlFactory;
-  private final GroupDetailFactory.Factory groupDetailFactory;
-  private final RenameGroupOp.Factory renameGroupOpFactory;
-  private final IdentifiedUser currentUser;
-
-  @Inject
-  PerformRenameGroup(final ReviewDb db, final GroupCache groupCache,
-      final GroupControl.Factory groupControlFactory,
-      final GroupDetailFactory.Factory groupDetailFactory,
-      final RenameGroupOp.Factory renameGroupOpFactory,
-      final IdentifiedUser currentUser) {
-    this.db = db;
-    this.groupCache = groupCache;
-    this.groupControlFactory = groupControlFactory;
-    this.groupDetailFactory = groupDetailFactory;
-    this.renameGroupOpFactory = renameGroupOpFactory;
-    this.currentUser = currentUser;
-  }
-
-  public GroupDetail renameGroup(final String groupName,
-      final String newGroupName) throws OrmException, NameAlreadyUsedException,
-      NoSuchGroupException, InvalidNameException {
-    final AccountGroup.NameKey groupNameKey =
-        new AccountGroup.NameKey(groupName);
-    final AccountGroup group = groupCache.get(groupNameKey);
-    if (group == null) {
-      throw new NoSuchGroupException(groupNameKey);
-    }
-    return renameGroup(group.getId(), newGroupName);
-  }
-
-  public GroupDetail renameGroup(final AccountGroup.Id groupId,
-      final String newName) throws OrmException, NameAlreadyUsedException,
-      NoSuchGroupException, InvalidNameException {
-    final GroupControl ctl = groupControlFactory.validateFor(groupId);
-    final AccountGroup group = db.accountGroups().get(groupId);
-    if (group == null || !ctl.isOwner()) {
-      throw new NoSuchGroupException(groupId);
-    }
-    if (newName.trim().isEmpty()) {
-      throw new InvalidNameException();
-    }
-
-    final AccountGroup.NameKey old = group.getNameKey();
-    final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
-
-    try {
-      final AccountGroupName id = new AccountGroupName(key, groupId);
-      db.accountGroupNames().insert(Collections.singleton(id));
-    } catch (OrmException e) {
-      AccountGroupName other = db.accountGroupNames().get(key);
-      if (other != null) {
-        // If we are using this identity, don't report the exception.
-        //
-        if (other.getId().equals(groupId)) {
-          return groupDetailFactory.create(groupId).call();
-        }
-
-        // Otherwise, someone else has this identity.
-        //
-        throw new NameAlreadyUsedException(newName);
-      } else {
-        throw e;
-      }
-    }
-
-    group.setNameKey(key);
-    db.accountGroups().update(Collections.singleton(group));
-
-    AccountGroupName priorName = db.accountGroupNames().get(old);
-    if (priorName != null) {
-      db.accountGroupNames().delete(Collections.singleton(priorName));
-    }
-
-    groupCache.evict(group);
-    groupCache.evictAfterRename(old, key);
-    renameGroupOpFactory.create( //
-        currentUser.newCommitterIdent(new Date(), TimeZone.getDefault()), //
-        group.getGroupUUID(), //
-        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
-
-    return groupDetailFactory.create(groupId).call();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
index 3831cbf..acdbbf4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.account.CreateEmail.Input;
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutEmail implements RestModifyView<AccountResource.Email, Input> {
+public class PutEmail implements RestModifyView<AccountResource.Email, EmailInput> {
   @Override
-  public Response<?> apply(AccountResource.Email rsrc, Input input)
+  public Response<?> apply(AccountResource.Email rsrc, EmailInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("email exists");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index 8cba72e..64fd11a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -93,7 +93,7 @@
     } else {
       if (!self.get().getCapabilities().canAdministrateServer()) {
         throw new AuthException("not allowed to set HTTP password directly, "
-            + "requires the Generate HTTP Password permission");
+            + "requires the Administrate Server permission");
       }
       newPassword = input.httpPassword;
     }
@@ -121,7 +121,7 @@
         : Response.ok(newPassword);
   }
 
-  private static String generate() {
+  public static String generate() {
     byte[] rand = new byte[LEN];
     rng.nextBytes(rand);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
new file mode 100644
index 0000000..9506b01
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2015 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.server.account;
+
+import com.google.gerrit.common.errors.InvalidUserNameException;
+import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutUsername.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PutUsername implements RestModifyView<AccountResource, Input> {
+  public static class Input {
+    @DefaultInput
+    public String username;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final ChangeUserName.Factory changeUserNameFactory;
+  private final Realm realm;
+  private final Provider<ReviewDb> db;
+
+  @Inject
+  PutUsername(Provider<CurrentUser> self,
+      ChangeUserName.Factory changeUserNameFactory,
+      Realm realm,
+      Provider<ReviewDb> db) {
+    this.self = self;
+    this.changeUserNameFactory = changeUserNameFactory;
+    this.realm = realm;
+    this.db = db;
+  }
+
+  @Override
+  public String apply(AccountResource rsrc, Input input) throws AuthException,
+      MethodNotAllowedException, UnprocessableEntityException,
+      ResourceConflictException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to set username");
+    }
+
+    if (!realm.allowsEdit(Account.FieldName.USER_NAME)) {
+      throw new MethodNotAllowedException("realm does not allow editing username");
+    }
+
+    if (input == null) {
+      input = new Input();
+    }
+
+    try {
+      changeUserNameFactory.create(db.get(), rsrc.getUser(), input.username).call();
+    } catch (IllegalStateException e) {
+      if (ChangeUserName.USERNAME_CANNOT_BE_CHANGED.equals(e.getMessage())) {
+        throw new MethodNotAllowedException(e.getMessage());
+      } else {
+        throw e;
+      }
+    } catch (InvalidUserNameException e) {
+      throw new UnprocessableEntityException("invalid username");
+    } catch (NameAlreadyUsedException e) {
+      throw new ResourceConflictException("username already used");
+    }
+
+    return input.username;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 8dd8de7..056fa85b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -24,6 +24,9 @@
   /** Can the end-user modify this field of their own account? */
   public boolean allowsEdit(Account.FieldName field);
 
+  /** Returns the account fields that the end-user can modify. */
+  public Set<Account.FieldName> getEditableFields();
+
   public AuthRequest authenticate(AuthRequest who) throws AccountException;
 
   public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 4e08756..e14791c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -14,144 +14,85 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.extensions.client.Theme;
+import static com.google.gerrit.server.account.GetDiffPreferences.readFromGit;
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
-import com.google.gerrit.server.account.SetDiffPreferences.Input;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.util.Collections;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 
 @Singleton
-public class SetDiffPreferences implements RestModifyView<AccountResource, Input> {
-  static class Input {
-    Short context;
-    Boolean expandAllComments;
-    Whitespace ignoreWhitespace;
-    Boolean intralineDifference;
-    Integer lineLength;
-    Boolean manualReview;
-    Boolean retainHeader;
-    Boolean showLineEndings;
-    Boolean showTabs;
-    Boolean showWhitespaceErrors;
-    Boolean skipDeleted;
-    Boolean skipUncommented;
-    Boolean syntaxHighlighting;
-    Boolean hideTopMenu;
-    Boolean autoHideDiffTableHeader;
-    Boolean hideLineNumbers;
-    Boolean renderEntireFile;
-    Integer tabSize;
-    Theme theme;
-    Boolean hideEmptyPane;
-  }
-
+public class SetDiffPreferences implements
+    RestModifyView<AccountResource, DiffPreferencesInfo> {
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> db;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
 
   @Inject
-  SetDiffPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db) {
+  SetDiffPreferences(Provider<CurrentUser> self,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
-    this.db = db;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
   }
 
   @Override
-  public DiffPreferencesInfo apply(AccountResource rsrc, Input input)
-      throws AuthException, OrmException {
+  public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo in)
+      throws AuthException, BadRequestException, ConfigInvalidException,
+      RepositoryNotFoundException, IOException, OrmException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("restricted to members of Modify Accounts");
     }
-    if (input == null) {
-      input = new Input();
+
+    if (in == null) {
+      throw new BadRequestException("input must be provided");
     }
 
-    Account.Id accountId = rsrc.getUser().getAccountId();
-    AccountDiffPreference p;
+    Account.Id id = rsrc.getUser().getAccountId();
+    return writeToGit(readFromGit(id, gitMgr, allUsersName, in), id);
+  }
 
-    db.get().accounts().beginTransaction(accountId);
+  private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in,
+      Account.Id userId) throws RepositoryNotFoundException, IOException,
+          ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
+
+    DiffPreferencesInfo out = new DiffPreferencesInfo();
     try {
-      p = db.get().accountDiffPreferences().get(accountId);
-      if (p == null) {
-        p = new AccountDiffPreference(accountId);
-      }
-
-      if (input.context != null) {
-        p.setContext(input.context);
-      }
-      if (input.ignoreWhitespace != null) {
-        p.setIgnoreWhitespace(input.ignoreWhitespace);
-      }
-      if (input.expandAllComments != null) {
-        p.setExpandAllComments(input.expandAllComments);
-      }
-      if (input.intralineDifference != null) {
-        p.setIntralineDifference(input.intralineDifference);
-      }
-      if (input.lineLength != null) {
-        p.setLineLength(input.lineLength);
-      }
-      if (input.manualReview != null) {
-        p.setManualReview(input.manualReview);
-      }
-      if (input.retainHeader != null) {
-        p.setRetainHeader(input.retainHeader);
-      }
-      if (input.showLineEndings != null) {
-        p.setShowLineEndings(input.showLineEndings);
-      }
-      if (input.showTabs != null) {
-        p.setShowTabs(input.showTabs);
-      }
-      if (input.showWhitespaceErrors != null) {
-        p.setShowWhitespaceErrors(input.showWhitespaceErrors);
-      }
-      if (input.skipDeleted != null) {
-        p.setSkipDeleted(input.skipDeleted);
-      }
-      if (input.skipUncommented != null) {
-        p.setSkipUncommented(input.skipUncommented);
-      }
-      if (input.syntaxHighlighting != null) {
-        p.setSyntaxHighlighting(input.syntaxHighlighting);
-      }
-      if (input.hideTopMenu != null) {
-        p.setHideTopMenu(input.hideTopMenu);
-      }
-      if (input.autoHideDiffTableHeader != null) {
-        p.setAutoHideDiffTableHeader(input.autoHideDiffTableHeader);
-      }
-      if (input.hideLineNumbers != null) {
-        p.setHideLineNumbers(input.hideLineNumbers);
-      }
-      if (input.renderEntireFile != null) {
-        p.setRenderEntireFile(input.renderEntireFile);
-      }
-      if (input.tabSize != null) {
-        p.setTabSize(input.tabSize);
-      }
-      if (input.theme != null) {
-        p.setTheme(input.theme);
-      }
-      if (input.hideEmptyPane != null) {
-        p.setHideEmptyPane(input.hideEmptyPane);
-      }
-
-      db.get().accountDiffPreferences().upsert(Collections.singleton(p));
-      db.get().commit();
+      VersionedAccountPreferences prefs = VersionedAccountPreferences.forUser(
+          userId);
+      prefs.load(md);
+      DiffPreferencesInfo defaults = DiffPreferencesInfo.defaults();
+      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in,
+          defaults);
+      prefs.commit(md);
+      loadSection(prefs.getConfig(), UserConfigSections.DIFF, null, out,
+          DiffPreferencesInfo.defaults(), null);
     } finally {
-      db.get().rollback();
+      md.close();
     }
-    return DiffPreferencesInfo.parse(p);
+    return out;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
new file mode 100644
index 0000000..eabe31d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2014 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.server.account;
+
+import static com.google.gerrit.server.account.GetEditPreferences.readFromGit;
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
+@Singleton
+public class SetEditPreferences implements
+    RestModifyView<AccountResource, EditPreferencesInfo> {
+
+  private final Provider<CurrentUser> self;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final GitRepositoryManager gitMgr;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  SetEditPreferences(Provider<CurrentUser> self,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      GitRepositoryManager gitMgr,
+      AllUsersName allUsersName) {
+    this.self = self;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.gitMgr = gitMgr;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  public Response<?> apply(AccountResource rsrc, EditPreferencesInfo in)
+      throws AuthException, BadRequestException, RepositoryNotFoundException,
+      IOException, ConfigInvalidException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
+    }
+
+    if (in == null) {
+      throw new BadRequestException("input must be provided");
+    }
+
+    Account.Id accountId = rsrc.getUser().getAccountId();
+    MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
+
+    VersionedAccountPreferences prefs;
+    try {
+      prefs = VersionedAccountPreferences.forUser(accountId);
+      prefs.load(md);
+      storeSection(prefs.getConfig(), UserConfigSections.EDIT, null,
+          readFromGit(accountId, gitMgr, allUsersName, in),
+          EditPreferencesInfo.defaults());
+      prefs.commit(md);
+    } finally {
+      md.close();
+    }
+
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index d75c5a2..569d128 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -15,12 +15,17 @@
 package com.google.gerrit.server.account;
 
 import static com.google.gerrit.server.account.GetPreferences.KEY_ID;
+import static com.google.gerrit.server.account.GetPreferences.KEY_MATCH;
 import static com.google.gerrit.server.account.GetPreferences.KEY_TARGET;
+import static com.google.gerrit.server.account.GetPreferences.KEY_TOKEN;
 import static com.google.gerrit.server.account.GetPreferences.KEY_URL;
-import static com.google.gerrit.server.account.GetPreferences.MY;
+import static com.google.gerrit.server.account.GetPreferences.URL_ALIAS;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.TopMenu;
@@ -29,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -37,6 +41,7 @@
 import com.google.gerrit.server.account.SetPreferences.Input;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,6 +53,8 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 @Singleton
 public class SetPreferences implements RestModifyView<AccountResource, Input> {
@@ -55,7 +62,7 @@
     public Short changesPerPage;
     public Boolean showSiteHeader;
     public Boolean useFlashClipboard;
-    public DownloadScheme downloadScheme;
+    public String downloadScheme;
     public DownloadCommand downloadCommand;
     public Boolean copySelfOnEmail;
     public DateFormat dateFormat;
@@ -67,29 +74,35 @@
     public ReviewCategoryStrategy reviewCategoryStrategy;
     public DiffView diffView;
     public List<TopMenu.MenuItem> my;
+    public Map<String, String> urlAliases;
   }
 
   private final Provider<CurrentUser> self;
   private final AccountCache cache;
   private final Provider<ReviewDb> db;
-  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
 
   @Inject
-  SetPreferences(Provider<CurrentUser> self, AccountCache cache,
-      Provider<ReviewDb> db, MetaDataUpdate.User metaDataUpdateFactory,
-      AllUsersName allUsersName) {
+  SetPreferences(Provider<CurrentUser> self,
+      AccountCache cache,
+      Provider<ReviewDb> db,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllUsersName allUsersName,
+      DynamicMap<DownloadScheme> downloadSchemes) {
     this.self = self;
     this.cache = cache;
     this.db = db;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
+    this.downloadSchemes = downloadSchemes;
   }
 
   @Override
   public GetPreferences.PreferenceInfo apply(AccountResource rsrc, Input i)
-      throws AuthException, ResourceNotFoundException, OrmException,
-      IOException, ConfigInvalidException {
+      throws AuthException, ResourceNotFoundException, BadRequestException,
+      OrmException, IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("restricted to members of Modify Accounts");
@@ -101,7 +114,7 @@
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountGeneralPreferences p;
     VersionedAccountPreferences versionedPrefs;
-    MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
+    MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
     db.get().accounts().beginTransaction(accountId);
     try {
       Account a = db.get().accounts().get(accountId);
@@ -128,7 +141,7 @@
         p.setUseFlashClipboard(i.useFlashClipboard);
       }
       if (i.downloadScheme != null) {
-        p.setDownloadUrl(i.downloadScheme);
+        setDownloadScheme(p, i.downloadScheme);
       }
       if (i.downloadCommand != null) {
         p.setDownloadCommand(i.downloadCommand);
@@ -164,11 +177,11 @@
       db.get().accounts().update(Collections.singleton(a));
       db.get().commit();
       storeMyMenus(versionedPrefs, i.my);
+      storeUrlAliases(versionedPrefs, i.urlAliases);
       versionedPrefs.commit(md);
       cache.evict(accountId);
       return new GetPreferences.PreferenceInfo(
-          p, versionedPrefs,
-          md.getRepository());
+          p, versionedPrefs, md.getRepository());
     } finally {
       md.close();
       db.get().rollback();
@@ -179,7 +192,7 @@
       List<TopMenu.MenuItem> my) {
     Config cfg = prefs.getConfig();
     if (my != null) {
-      unsetSection(cfg, MY);
+      unsetSection(cfg, UserConfigSections.MY);
       for (TopMenu.MenuItem item : my) {
         set(cfg, item.name, KEY_URL, item.url);
         set(cfg, item.name, KEY_TARGET, item.target);
@@ -190,9 +203,9 @@
 
   private static void set(Config cfg, String section, String key, String val) {
     if (Strings.isNullOrEmpty(val)) {
-      cfg.unset(MY, section, key);
+      cfg.unset(UserConfigSections.MY, section, key);
     } else {
-      cfg.setString(MY, section, key, val);
+      cfg.setString(UserConfigSections.MY, section, key, val);
     }
   }
 
@@ -202,4 +215,33 @@
       cfg.unsetSection(section, subsection);
     }
   }
+
+  public static void storeUrlAliases(VersionedAccountPreferences prefs,
+      Map<String, String> urlAliases) {
+    if (urlAliases != null) {
+      Config cfg = prefs.getConfig();
+      for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+        cfg.unsetSection(URL_ALIAS, subsection);
+      }
+
+      int i = 1;
+      for (Entry<String, String> e : urlAliases.entrySet()) {
+        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_MATCH, e.getKey());
+        cfg.setString(URL_ALIAS, URL_ALIAS + i, KEY_TOKEN, e.getValue());
+        i++;
+      }
+    }
+  }
+
+  private void setDownloadScheme(AccountGeneralPreferences p, String scheme)
+      throws BadRequestException {
+    for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
+      if (e.getExportName().equals(scheme)
+          && e.getProvider().get().isEnabled()) {
+        p.setDownloadUrl(scheme);
+        return;
+      }
+    }
+    throw new BadRequestException("Unsupported download scheme: " + scheme);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
index 5cb05e0..837e1ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
@@ -39,7 +39,7 @@
 import java.util.List;
 import java.util.Map;
 
-class SuggestAccounts implements RestReadView<TopLevelResource> {
+public class SuggestAccounts implements RestReadView<TopLevelResource> {
   private static final int MAX_RESULTS = 100;
   private static final String MAX_SUFFIX = "\u9fa5";
 
@@ -51,9 +51,10 @@
   private final int suggestFrom;
 
   private int limit = 10;
+  private String query;
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
-  void setLimit(int n) {
+  public void setLimit(int n) {
     if (n < 0) {
       limit = 10;
     } else if (n == 0) {
@@ -64,7 +65,9 @@
   }
 
   @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
-  private String query;
+  public void setQuery(String query) {
+    this.query = query;
+  }
 
   @Inject
   SuggestAccounts(AccountControl.Factory accountControlFactory,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
new file mode 100644
index 0000000..426c6f6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2015 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.server.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.DestinationList;
+import com.google.gerrit.server.git.TabFile;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.FileMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/** Preferences for user accounts. */
+public class VersionedAccountDestinations extends VersionedMetaData {
+  private static final Logger log = LoggerFactory.getLogger(VersionedAccountDestinations.class);
+
+  public static VersionedAccountDestinations forUser(Account.Id id) {
+    return new VersionedAccountDestinations(RefNames.refsUsers(id));
+  }
+
+  private final String ref;
+  private final DestinationList destinations = new DestinationList();
+
+  private VersionedAccountDestinations(String ref) {
+    this.ref = ref;
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public DestinationList getDestinationList() {
+    return destinations;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    String prefix = DestinationList.DIR_NAME + "/";
+    for (PathInfo p : getPathInfos(true)) {
+      if (p.fileMode == FileMode.REGULAR_FILE) {
+        String path = p.path;
+        if (path.startsWith(prefix)) {
+          String label = path.substring(prefix.length());
+          ValidationError.Sink errors = TabFile.createLoggerSink(path, log);
+          destinations.parseLabel(label, readUTF8(path), errors);
+        }
+      }
+    }
+  }
+
+  public ValidationError.Sink createSink(String file) {
+    return ValidationError.createLoggerSink(file, log);
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    throw new UnsupportedOperationException("Cannot yet save destinations");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
index c4d4b06..da7d141 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountPreferences.java
@@ -27,7 +27,6 @@
 
 /** Preferences for user accounts. */
 public class VersionedAccountPreferences extends VersionedMetaData {
-  private static final String REFS_USER_DEFAULT = RefNames.REFS_USER + "default";
   private static final String PREFERENCES = "preferences.config";
 
   public static VersionedAccountPreferences forUser(Account.Id id) {
@@ -35,7 +34,7 @@
   }
 
   public static VersionedAccountPreferences forDefault() {
-    return new VersionedAccountPreferences(REFS_USER_DEFAULT);
+    return new VersionedAccountPreferences(RefNames.REFS_USERS_DEFAULT);
   }
 
   private final String ref;
@@ -46,7 +45,7 @@
   }
 
   public boolean isDefaults() {
-    return REFS_USER_DEFAULT.equals(getRefName());
+    return RefNames.REFS_USERS_DEFAULT.equals(getRefName());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
new file mode 100644
index 0000000..2ea6c53
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2015 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.server.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.QueryList;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/** Named Queries for user accounts. */
+public class VersionedAccountQueries extends VersionedMetaData {
+  private static final Logger log = LoggerFactory.getLogger(VersionedAccountQueries.class);
+
+  public static VersionedAccountQueries forUser(Account.Id id) {
+    return new VersionedAccountQueries(RefNames.refsUsers(id));
+  }
+
+  private final String ref;
+  private QueryList queryList;
+
+  private VersionedAccountQueries(String ref) {
+    this.ref = ref;
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public QueryList getQueryList() {
+    return queryList;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    queryList = QueryList.parse(readUTF8(QueryList.FILE_NAME),
+        QueryList.createLoggerSink(QueryList.FILE_NAME, log));
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    throw new UnsupportedOperationException("Cannot yet save named queries");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java
index 5f581c3..577abe1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/GerritApiImpl.java
@@ -17,22 +17,30 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.accounts.Accounts;
 import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.api.config.Config;
+import com.google.gerrit.extensions.api.groups.Groups;
 import com.google.gerrit.extensions.api.projects.Projects;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-class GerritApiImpl extends GerritApi.NotImplemented implements GerritApi {
+class GerritApiImpl implements GerritApi {
   private final Accounts accounts;
   private final Changes changes;
+  private final Config config;
+  private final Groups groups;
   private final Projects projects;
 
   @Inject
   GerritApiImpl(Accounts accounts,
       Changes changes,
+      Config config,
+      Groups groups,
       Projects projects) {
     this.accounts = accounts;
     this.changes = changes;
+    this.config = config;
+    this.groups = groups;
     this.projects = projects;
   }
 
@@ -47,6 +55,16 @@
   }
 
   @Override
+  public Config config() {
+    return config;
+  }
+
+  @Override
+  public Groups groups() {
+    return groups;
+  }
+
+  @Override
   public Projects projects() {
     return projects;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java
index 23f4b8d..6214129 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/Module.java
@@ -22,8 +22,10 @@
   protected void configure() {
     bind(GerritApi.class).to(GerritApiImpl.class);
 
-    install(new com.google.gerrit.server.api.changes.Module());
-    install(new com.google.gerrit.server.api.projects.Module());
     install(new com.google.gerrit.server.api.accounts.Module());
+    install(new com.google.gerrit.server.api.changes.Module());
+    install(new com.google.gerrit.server.api.config.Module());
+    install(new com.google.gerrit.server.api.groups.Module());
+    install(new com.google.gerrit.server.api.projects.Module());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 44413b7..c6e4ad1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -14,13 +14,19 @@
 
 package com.google.gerrit.server.api.accounts;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.CreateEmail;
 import com.google.gerrit.server.account.StarredChanges;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
@@ -28,7 +34,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-public class AccountApiImpl extends AccountApi.NotImplemented implements AccountApi {
+import java.util.List;
+import java.util.Map;
+
+public class AccountApiImpl implements AccountApi {
   interface Factory {
     AccountApiImpl create(AccountResource account);
   }
@@ -38,18 +47,24 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final StarredChanges.Create starredChangesCreate;
   private final StarredChanges.Delete starredChangesDelete;
+  private final CreateEmail.Factory createEmailFactory;
+  private final GpgApiAdapter gpgApiAdapter;
 
   @Inject
   AccountApiImpl(AccountLoader.Factory ailf,
       ChangesCollection changes,
       StarredChanges.Create starredChangesCreate,
       StarredChanges.Delete starredChangesDelete,
+      CreateEmail.Factory createEmailFactory,
+      GpgApiAdapter gpgApiAdapter,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
     this.changes = changes;
     this.starredChangesCreate = starredChangesCreate;
     this.starredChangesDelete = starredChangesDelete;
+    this.createEmailFactory = createEmailFactory;
+    this.gpgApiAdapter = gpgApiAdapter;
   }
 
   @Override
@@ -91,4 +106,43 @@
       throw new RestApiException("Cannot unstar change", e);
     }
   }
+
+  @Override
+  public void addEmail(EmailInput input) throws RestApiException {
+    AccountResource.Email rsrc =
+        new AccountResource.Email(account.getUser(), input.email);
+    try {
+      createEmailFactory.create(input.email).apply(rsrc, input);
+    } catch (EmailException | OrmException e) {
+      throw new RestApiException("Cannot add email", e);
+    }
+  }
+
+  @Override
+  public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
+    try {
+      return gpgApiAdapter.listGpgKeys(account);
+    } catch (GpgException e) {
+      throw new RestApiException("Cannot list GPG keys", e);
+    }
+  }
+
+  @Override
+  public Map<String, GpgKeyInfo> putGpgKeys(List<String> add,
+      List<String> delete) throws RestApiException {
+    try {
+      return gpgApiAdapter.putGpgKeys(account, add, delete);
+    } catch (GpgException e) {
+      throw new RestApiException("Cannot add GPG key", e);
+    }
+  }
+
+  @Override
+  public GpgKeyApi gpgKey(String id) throws RestApiException {
+    try {
+      return gpgApiAdapter.gpgKey(account, IdString.fromDecoded(id));
+    } catch (GpgException e) {
+      throw new RestApiException("Cannot get PGP key", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 0c02c99..7be8299 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -16,32 +16,38 @@
 
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.Accounts;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.account.SuggestAccounts;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.List;
+
 @Singleton
-public class AccountsImpl extends Accounts.NotImplemented implements Accounts {
+public class AccountsImpl implements Accounts {
   private final AccountsCollection accounts;
   private final AccountApiImpl.Factory api;
   private final Provider<CurrentUser> self;
+  private final Provider<SuggestAccounts> suggestAccountsProvider;
 
   @Inject
   AccountsImpl(AccountsCollection accounts,
       AccountApiImpl.Factory api,
-      Provider<CurrentUser> self) {
+      Provider<CurrentUser> self,
+      Provider<SuggestAccounts> suggestAccountsProvider) {
     this.accounts = accounts;
     this.api = api;
     this.self = self;
+    this.suggestAccountsProvider = suggestAccountsProvider;
   }
 
   @Override
@@ -59,6 +65,34 @@
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    return api.create(new AccountResource((IdentifiedUser)self.get()));
+    return api.create(new AccountResource(self.get().asIdentifiedUser()));
+  }
+
+  @Override
+  public SuggestAccountsRequest suggestAccounts() throws RestApiException {
+    return new SuggestAccountsRequest() {
+      @Override
+      public List<AccountInfo> get() throws RestApiException {
+        return AccountsImpl.this.suggestAccounts(this);
+      }
+    };
+  }
+
+  @Override
+  public SuggestAccountsRequest suggestAccounts(String query)
+    throws RestApiException {
+    return suggestAccounts().withQuery(query);
+  }
+
+  private List<AccountInfo> suggestAccounts(SuggestAccountsRequest r)
+    throws RestApiException {
+    try {
+      SuggestAccounts mySuggestAccounts = suggestAccountsProvider.get();
+      mySuggestAccounts.setQuery(r.getQuery());
+      mySuggestAccounts.setLimit(r.getLimit());
+      return mySuggestAccounts.apply(TopLevelResource.INSTANCE);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve suggested accounts", e);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
new file mode 100644
index 0000000..7d65ce9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 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.server.api.accounts;
+
+import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
+import com.google.gerrit.extensions.common.GpgKeyInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+
+import java.util.List;
+import java.util.Map;
+
+public interface GpgApiAdapter {
+  Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
+      throws RestApiException, GpgException;
+
+  Map<String, GpgKeyInfo> putGpgKeys(AccountResource account, List<String> add,
+      List<String> delete) throws RestApiException, GpgException;
+
+  GpgKeyApi gpgKey(AccountResource account, IdString idStr)
+      throws RestApiException, GpgException;
+
+  PushCertificateInfo checkPushCertificate(String certStr,
+      IdentifiedUser expectedUser) throws GpgException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java
index 5e3855e..935c4d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/Module.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.api.accounts;
 
 import com.google.gerrit.extensions.api.accounts.Accounts;
-import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
 public class Module extends FactoryModule {
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 161461d..d4f6851 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.api.changes;
 
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -26,11 +25,13 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.Abandon;
 import com.google.gerrit.server.change.ChangeEdits;
 import com.google.gerrit.server.change.ChangeJson;
@@ -38,14 +39,17 @@
 import com.google.gerrit.server.change.Check;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetTopic;
+import com.google.gerrit.server.change.ListChangeComments;
+import com.google.gerrit.server.change.ListChangeDrafts;
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PutTopic;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
 import com.google.gerrit.server.change.Revisions;
-import com.google.gerrit.server.change.SuggestReviewers;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.change.SubmittedTogether;
+import com.google.gerrit.server.change.SuggestChangeReviewers;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -54,47 +58,57 @@
 import java.io.IOException;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
-class ChangeApiImpl extends ChangeApi.NotImplemented implements ChangeApi {
+class ChangeApiImpl implements ChangeApi {
   interface Factory {
     ChangeApiImpl create(ChangeResource change);
   }
 
+  private final Provider<CurrentUser> user;
   private final Changes changeApi;
   private final Revisions revisions;
   private final RevisionApiImpl.Factory revisionApi;
-  private final Provider<SuggestReviewers> suggestReviewers;
+  private final Provider<SuggestChangeReviewers> suggestReviewers;
   private final ChangeResource change;
   private final Abandon abandon;
   private final Revert revert;
   private final Restore restore;
+  private final SubmittedTogether submittedTogether;
   private final GetTopic getTopic;
   private final PutTopic putTopic;
   private final PostReviewers postReviewers;
-  private final Provider<ChangeJson> changeJson;
+  private final ChangeJson.Factory changeJson;
   private final PostHashtags postHashtags;
   private final GetHashtags getHashtags;
+  private final ListChangeComments listComments;
+  private final ListChangeDrafts listDrafts;
   private final Check check;
   private final ChangeEdits.Detail editDetail;
 
   @Inject
-  ChangeApiImpl(Changes changeApi,
+  ChangeApiImpl(Provider<CurrentUser> user,
+      Changes changeApi,
       Revisions revisions,
       RevisionApiImpl.Factory revisionApi,
-      Provider<SuggestReviewers> suggestReviewers,
+      Provider<SuggestChangeReviewers> suggestReviewers,
       Abandon abandon,
       Revert revert,
       Restore restore,
+      SubmittedTogether submittedTogether,
       GetTopic getTopic,
       PutTopic putTopic,
       PostReviewers postReviewers,
-      Provider<ChangeJson> changeJson,
+      ChangeJson.Factory changeJson,
       PostHashtags postHashtags,
       GetHashtags getHashtags,
+      ListChangeComments listComments,
+      ListChangeDrafts listDrafts,
       Check check,
       ChangeEdits.Detail editDetail,
       @Assisted ChangeResource change) {
+    this.user = user;
     this.changeApi = changeApi;
     this.revert = revert;
     this.revisions = revisions;
@@ -102,12 +116,15 @@
     this.suggestReviewers = suggestReviewers;
     this.abandon = abandon;
     this.restore = restore;
+    this.submittedTogether = submittedTogether;
     this.getTopic = getTopic;
     this.putTopic = putTopic;
     this.postReviewers = postReviewers;
     this.changeJson = changeJson;
     this.postHashtags = postHashtags;
     this.getHashtags = getHashtags;
+    this.listComments = listComments;
+    this.listDrafts = listDrafts;
     this.check = check;
     this.editDetail = editDetail;
     this.change = change;
@@ -147,7 +164,7 @@
   public void abandon(AbandonInput in) throws RestApiException {
     try {
       abandon.apply(change, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot abandon change", e);
     }
   }
@@ -161,7 +178,7 @@
   public void restore(RestoreInput in) throws RestApiException {
     try {
       restore.apply(change, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot restore change", e);
     }
   }
@@ -175,12 +192,21 @@
   public ChangeApi revert(RevertInput in) throws RestApiException {
     try {
       return changeApi.id(revert.apply(change, in)._number);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | IOException | UpdateException e) {
       throw new RestApiException("Cannot revert change", e);
     }
   }
 
   @Override
+  public List<ChangeInfo> submittedTogether() throws RestApiException {
+    try {
+      return submittedTogether.apply(change);
+    } catch (Exception e) {
+      throw new RestApiException("Cannot query submittedTogether", e);
+    }
+  }
+
+  @Override
   public String topic() throws RestApiException {
     return getTopic.apply(change);
   }
@@ -191,7 +217,7 @@
     in.topic = topic;
     try {
       putTopic.apply(change, in);
-    } catch (OrmException | IOException e) {
+    } catch (UpdateException e) {
       throw new RestApiException("Cannot set topic", e);
     }
   }
@@ -207,7 +233,7 @@
   public void addReviewer(AddReviewerInput in) throws RestApiException {
     try {
       postReviewers.apply(change, in);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot add change reviewer", e);
     }
   }
@@ -231,7 +257,7 @@
   private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
       throws RestApiException {
     try {
-      SuggestReviewers mySuggestReviewers = suggestReviewers.get();
+      SuggestChangeReviewers mySuggestReviewers = suggestReviewers.get();
       mySuggestReviewers.setQuery(r.getQuery());
       mySuggestReviewers.setLimit(r.getLimit());
       return mySuggestReviewers.apply(change);
@@ -244,7 +270,11 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
-      return changeJson.get().addOptions(s).format(change);
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        u.asIdentifiedUser().clearStarredChanges();
+      }
+      return changeJson.create(s).format(change);
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
     }
@@ -260,7 +290,7 @@
     try {
       Response<EditInfo> edit = editDetail.apply(change);
       return edit.isNone() ? null : edit.value();
-    } catch (IOException | OrmException | InvalidChangeOperationException e) {
+    } catch (IOException | OrmException e) {
       throw new RestApiException("Cannot retrieve change edit", e);
     }
   }
@@ -274,7 +304,7 @@
   public void setHashtags(HashtagsInput input) throws RestApiException {
     try {
       postHashtags.apply(change, input);
-    } catch (IOException | OrmException e) {
+    } catch (RestApiException | UpdateException e) {
       throw new RestApiException("Cannot post hashtags", e);
     }
   }
@@ -289,6 +319,24 @@
   }
 
   @Override
+  public Map<String, List<CommentInfo>> comments() throws RestApiException {
+    try {
+      return listComments.apply(change);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get comments", e);
+    }
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> drafts() throws RestApiException {
+    try {
+      return listDrafts.apply(change);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get drafts", e);
+    }
+  }
+
+  @Override
   public ChangeInfo check() throws RestApiException {
     try {
       return check.apply(change).value();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 91809ec..acda1ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -24,13 +24,14 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.CreateChange;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
@@ -43,16 +44,19 @@
 
 @Singleton
 class ChangesImpl implements Changes {
+  private final Provider<CurrentUser> user;
   private final ChangesCollection changes;
   private final ChangeApiImpl.Factory api;
   private final CreateChange createChange;
   private final Provider<QueryChanges> queryProvider;
 
   @Inject
-  ChangesImpl(ChangesCollection changes,
+  ChangesImpl(Provider<CurrentUser> user,
+      ChangesCollection changes,
       ChangeApiImpl.Factory api,
       CreateChange createChange,
       Provider<QueryChanges> queryProvider) {
+    this.user = user;
     this.changes = changes;
     this.api = api;
     this.createChange = createChange;
@@ -91,7 +95,8 @@
           TopLevelResource.INSTANCE, in).value();
       return api.create(changes.parse(TopLevelResource.INSTANCE,
           IdString.fromUrl(out.changeId)));
-    } catch (OrmException | IOException | InvalidChangeOperationException e) {
+    } catch (OrmException | IOException | InvalidChangeOperationException
+        | UpdateException e) {
       throw new RestApiException("Cannot create change", e);
     }
   }
@@ -123,6 +128,10 @@
     }
 
     try {
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        u.asIdentifiedUser().clearStarredChanges();
+      }
       List<?> result = qc.apply(TopLevelResource.INSTANCE);
       if (result.isEmpty()) {
         return ImmutableList.of();
@@ -136,7 +145,7 @@
       List<ChangeInfo> infos = (List<ChangeInfo>) result;
 
       return ImmutableList.copyOf(infos);
-    } catch (BadRequestException | AuthException | OrmException e) {
+    } catch (AuthException | OrmException e) {
       throw new RestApiException("Cannot query changes", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index 42c1e23..c09890f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -30,7 +30,7 @@
 
 import java.io.IOException;
 
-class FileApiImpl extends FileApi.NotImplemented implements FileApi {
+class FileApiImpl implements FileApi {
   interface Factory {
     FileApiImpl create(FileResource r);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
index a37f8be..a5e584e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.api.changes;
 
 import com.google.gerrit.extensions.api.changes.Changes;
-import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
 public class Module extends FactoryModule {
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index c36faa2..0926142 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -27,9 +27,11 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -40,16 +42,19 @@
 import com.google.gerrit.server.change.DraftComments;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.Files;
-import com.google.gerrit.server.change.ListComments;
-import com.google.gerrit.server.change.ListDraftComments;
+import com.google.gerrit.server.change.GetPatch;
+import com.google.gerrit.server.change.GetRevisionActions;
+import com.google.gerrit.server.change.ListRevisionComments;
+import com.google.gerrit.server.change.ListRevisionDrafts;
 import com.google.gerrit.server.change.Mergeable;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
 import com.google.gerrit.server.change.Rebase;
+import com.google.gerrit.server.change.RebaseUtil;
 import com.google.gerrit.server.change.Reviewed;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
-import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -60,7 +65,7 @@
 import java.util.Map;
 import java.util.Set;
 
-class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi {
+class RevisionApiImpl implements RevisionApi {
   interface Factory {
     RevisionApiImpl create(RevisionResource r);
   }
@@ -69,7 +74,7 @@
   private final CherryPick cherryPick;
   private final DeleteDraftPatchSet deleteDraft;
   private final Rebase rebase;
-  private final RebaseChange rebaseChange;
+  private final RebaseUtil rebaseUtil;
   private final Submit submit;
   private final PublishDraftPatchSet publish;
   private final Reviewed.PutReviewed putReviewed;
@@ -77,45 +82,49 @@
   private final RevisionResource revision;
   private final Provider<Files> files;
   private final Provider<Files.ListFiles> listFiles;
+  private final Provider<GetPatch> getPatch;
   private final Provider<PostReview> review;
   private final Provider<Mergeable> mergeable;
   private final FileApiImpl.Factory fileApi;
-  private final ListComments listComments;
-  private final ListDraftComments listDrafts;
+  private final ListRevisionComments listComments;
+  private final ListRevisionDrafts listDrafts;
   private final CreateDraftComment createDraft;
   private final DraftComments drafts;
   private final DraftApiImpl.Factory draftFactory;
   private final Comments comments;
   private final CommentApiImpl.Factory commentFactory;
+  private final GetRevisionActions revisionActions;
 
   @Inject
   RevisionApiImpl(Changes changes,
       CherryPick cherryPick,
       DeleteDraftPatchSet deleteDraft,
       Rebase rebase,
-      RebaseChange rebaseChange,
+      RebaseUtil rebaseUtil,
       Submit submit,
       PublishDraftPatchSet publish,
       Reviewed.PutReviewed putReviewed,
       Reviewed.DeleteReviewed deleteReviewed,
       Provider<Files> files,
       Provider<Files.ListFiles> listFiles,
+      Provider<GetPatch> getPatch,
       Provider<PostReview> review,
       Provider<Mergeable> mergeable,
       FileApiImpl.Factory fileApi,
-      ListComments listComments,
-      ListDraftComments listDrafts,
+      ListRevisionComments listComments,
+      ListRevisionDrafts listDrafts,
       CreateDraftComment createDraft,
       DraftComments drafts,
       DraftApiImpl.Factory draftFactory,
       Comments comments,
       CommentApiImpl.Factory commentFactory,
+      GetRevisionActions revisionActions,
       @Assisted RevisionResource r) {
     this.changes = changes;
     this.cherryPick = cherryPick;
     this.deleteDraft = deleteDraft;
     this.rebase = rebase;
-    this.rebaseChange = rebaseChange;
+    this.rebaseUtil = rebaseUtil;
     this.review = review;
     this.submit = submit;
     this.publish = publish;
@@ -123,6 +132,7 @@
     this.putReviewed = putReviewed;
     this.deleteReviewed = deleteReviewed;
     this.listFiles = listFiles;
+    this.getPatch = getPatch;
     this.mergeable = mergeable;
     this.fileApi = fileApi;
     this.listComments = listComments;
@@ -132,6 +142,7 @@
     this.draftFactory = draftFactory;
     this.comments = comments;
     this.commentFactory = commentFactory;
+    this.revisionActions = revisionActions;
     this.revision = r;
   }
 
@@ -139,7 +150,7 @@
   public void review(ReviewInput in) throws RestApiException {
     try {
       review.get().apply(revision, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | UpdateException e) {
       throw new RestApiException("Cannot post review", e);
     }
   }
@@ -147,7 +158,6 @@
   @Override
   public void submit() throws RestApiException {
     SubmitInput in = new SubmitInput();
-    in.waitForMerge = true;
     submit(in);
   }
 
@@ -164,7 +174,7 @@
   public void publish() throws RestApiException {
     try {
       publish.apply(revision, new PublishDraftPatchSet.Input());
-    } catch (OrmException | IOException e) {
+    } catch (UpdateException e) {
       throw new RestApiException("Cannot publish draft patch set", e);
     }
   }
@@ -188,21 +198,21 @@
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
       return changes.id(rebase.apply(revision, in)._number);
-    } catch (OrmException | EmailException e) {
+    } catch (OrmException | EmailException | UpdateException | IOException e) {
       throw new RestApiException("Cannot rebase ps", e);
     }
   }
 
   @Override
   public boolean canRebase() {
-    return rebaseChange.canRebase(revision);
+    return rebaseUtil.canRebase(revision);
   }
 
   @Override
   public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
     try {
       return changes.id(cherryPick.apply(revision, in)._number);
-    } catch (OrmException | EmailException | IOException e) {
+    } catch (OrmException | IOException | UpdateException e) {
       throw new RestApiException("Cannot cherry pick", e);
     }
   }
@@ -293,6 +303,15 @@
   }
 
   @Override
+  public List<CommentInfo> commentsAsList() throws RestApiException {
+    try {
+      return listComments.getComments(revision);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve comments", e);
+    }
+  }
+
+  @Override
   public Map<String, List<CommentInfo>> drafts() throws RestApiException {
     try {
       return listDrafts.apply(revision);
@@ -302,6 +321,15 @@
   }
 
   @Override
+  public List<CommentInfo> draftsAsList() throws RestApiException {
+    try {
+      return listDrafts.getComments(revision);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve drafts", e);
+    }
+  }
+
+  @Override
   public DraftApi draft(String id) throws RestApiException {
     try {
       return draftFactory.create(drafts.parse(revision,
@@ -314,7 +342,11 @@
   @Override
   public DraftApi createDraft(DraftInput in) throws RestApiException {
     try {
-      return draft(createDraft.apply(revision, in).value().id);
+      String id = createDraft.apply(revision, in).value().id;
+      // Reread change to pick up new notes refs.
+      return changes.id(revision.getChange().getId().get())
+          .revision(revision.getPatchSet().getId().get())
+          .draft(id);
     } catch (IOException | OrmException e) {
       throw new RestApiException("Cannot create draft", e);
     }
@@ -329,4 +361,18 @@
       throw new RestApiException("Cannot retrieve comment", e);
     }
   }
+
+  @Override
+  public BinaryResult patch() throws RestApiException {
+    try {
+      return getPatch.get().apply(revision);
+    } catch (IOException e) {
+      throw new RestApiException("Cannot get patch", e);
+    }
+  }
+
+  @Override
+  public Map<String, ActionInfo> actions() throws RestApiException {
+    return revisionActions.apply(revision).value();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ConfigImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ConfigImpl.java
new file mode 100644
index 0000000..78fca6f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ConfigImpl.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2015 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.server.api.config;
+
+import com.google.gerrit.extensions.api.config.Config;
+import com.google.gerrit.extensions.api.config.Server;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ConfigImpl implements Config {
+  private final ServerImpl serverApi;
+
+  @Inject
+  ConfigImpl(ServerImpl serverApi) {
+    this.serverApi = serverApi;
+  }
+
+  @Override
+  public Server server() {
+    return serverApi;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/Module.java
new file mode 100644
index 0000000..b37aa1c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/Module.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2015 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.server.api.config;
+
+import com.google.gerrit.extensions.api.config.Config;
+import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.config.FactoryModule;
+
+public class Module extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(Config.class).to(ConfigImpl.class);
+    bind(Server.class).to(ServerImpl.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
new file mode 100644
index 0000000..a8ee60d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2015 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.server.api.config;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ServerImpl implements Server {
+  @Override
+  public String getVersion() throws RestApiException {
+    return Version.getVersion();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
new file mode 100644
index 0000000..f11ed86
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -0,0 +1,274 @@
+// Copyright (C) 2015 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.server.api.groups;
+
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.group.AddIncludedGroups;
+import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.group.DeleteIncludedGroups;
+import com.google.gerrit.server.group.DeleteMembers;
+import com.google.gerrit.server.group.GetAuditLog;
+import com.google.gerrit.server.group.GetDescription;
+import com.google.gerrit.server.group.GetDetail;
+import com.google.gerrit.server.group.GetGroup;
+import com.google.gerrit.server.group.GetName;
+import com.google.gerrit.server.group.GetOptions;
+import com.google.gerrit.server.group.GetOwner;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.ListIncludedGroups;
+import com.google.gerrit.server.group.ListMembers;
+import com.google.gerrit.server.group.PutDescription;
+import com.google.gerrit.server.group.PutName;
+import com.google.gerrit.server.group.PutOptions;
+import com.google.gerrit.server.group.PutOwner;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import java.util.Arrays;
+import java.util.List;
+
+class GroupApiImpl implements GroupApi {
+  interface Factory {
+    GroupApiImpl create(GroupResource rsrc);
+  }
+
+  private final GetGroup getGroup;
+  private final GetDetail getDetail;
+  private final GetName getName;
+  private final PutName putName;
+  private final GetOwner getOwner;
+  private final PutOwner putOwner;
+  private final GetDescription getDescription;
+  private final PutDescription putDescription;
+  private final GetOptions getOptions;
+  private final PutOptions putOptions;
+  private final Provider<ListMembers> listMembers;
+  private final AddMembers addMembers;
+  private final DeleteMembers deleteMembers;
+  private final ListIncludedGroups listGroups;
+  private final AddIncludedGroups addGroups;
+  private final DeleteIncludedGroups deleteGroups;
+  private final GetAuditLog getAuditLog;
+  private final GroupResource rsrc;
+
+  @AssistedInject
+  GroupApiImpl(
+      GetGroup getGroup,
+      GetDetail getDetail,
+      GetName getName,
+      PutName putName,
+      GetOwner getOwner,
+      PutOwner putOwner,
+      GetDescription getDescription,
+      PutDescription putDescription,
+      GetOptions getOptions,
+      PutOptions putOptions,
+      Provider<ListMembers> listMembers,
+      AddMembers addMembers,
+      DeleteMembers deleteMembers,
+      ListIncludedGroups listGroups,
+      AddIncludedGroups addGroups,
+      DeleteIncludedGroups deleteGroups,
+      GetAuditLog getAuditLog,
+      @Assisted GroupResource rsrc) {
+    this.getGroup = getGroup;
+    this.getDetail = getDetail;
+    this.getName = getName;
+    this.putName = putName;
+    this.getOwner = getOwner;
+    this.putOwner = putOwner;
+    this.getDescription = getDescription;
+    this.putDescription = putDescription;
+    this.getOptions = getOptions;
+    this.putOptions = putOptions;
+    this.listMembers = listMembers;
+    this.addMembers = addMembers;
+    this.deleteMembers = deleteMembers;
+    this.listGroups = listGroups;
+    this.addGroups = addGroups;
+    this.deleteGroups = deleteGroups;
+    this.getAuditLog = getAuditLog;
+    this.rsrc = rsrc;
+  }
+
+  @Override
+  public GroupInfo get() throws RestApiException {
+    try {
+      return getGroup.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve group", e);
+    }
+  }
+
+  @Override
+  public GroupInfo detail() throws RestApiException {
+    try {
+      return getDetail.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve group", e);
+    }
+  }
+
+  @Override
+  public String name() throws RestApiException {
+    return getName.apply(rsrc);
+  }
+
+  @Override
+  public void name(String name) throws RestApiException {
+    PutName.Input in = new PutName.Input();
+    in.name = name;
+    try {
+      putName.apply(rsrc, in);
+    } catch (NoSuchGroupException e) {
+      throw new ResourceNotFoundException(name, e);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot put group name", e);
+    }
+  }
+
+  @Override
+  public GroupInfo owner() throws RestApiException {
+    try {
+      return getOwner.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get group owner", e);
+    }
+  }
+
+  @Override
+  public void owner(String owner) throws RestApiException {
+    PutOwner.Input in = new PutOwner.Input();
+    in.owner = owner;
+    try {
+      putOwner.apply(rsrc, in);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot put group owner", e);
+    }
+  }
+
+  @Override
+  public String description() throws RestApiException {
+    return getDescription.apply(rsrc);
+  }
+
+  @Override
+  public void description(String description) throws RestApiException {
+    PutDescription.Input in = new PutDescription.Input();
+    in.description = description;
+    try {
+      putDescription.apply(rsrc, in);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot put group description", e);
+    }
+  }
+
+  @Override
+  public GroupOptionsInfo options() throws RestApiException {
+    return getOptions.apply(rsrc);
+  }
+
+  @Override
+  public void options(GroupOptionsInfo options) throws RestApiException {
+    try {
+      putOptions.apply(rsrc, options);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot put group options", e);
+    }
+  }
+
+  @Override
+  public List<AccountInfo> members() throws RestApiException {
+    return members(false);
+  }
+
+  @Override
+  public List<AccountInfo> members(boolean recursive) throws RestApiException {
+    ListMembers list = listMembers.get();
+    list.setRecursive(recursive);
+    try {
+      return list.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot list group members", e);
+    }
+  }
+
+  @Override
+  public void addMembers(String... members) throws RestApiException {
+    try {
+      addMembers.apply(
+          rsrc, AddMembers.Input.fromMembers(Arrays.asList(members)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot add group members", e);
+    }
+  }
+
+  @Override
+  public void removeMembers(String... members) throws RestApiException {
+    try {
+      deleteMembers.apply(
+          rsrc, AddMembers.Input.fromMembers(Arrays.asList(members)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot remove group members", e);
+    }
+  }
+
+  @Override
+  public List<GroupInfo> includedGroups() throws RestApiException {
+    try {
+      return listGroups.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot list included groups", e);
+    }
+  }
+
+  @Override
+  public void addGroups(String... groups) throws RestApiException {
+    try {
+      addGroups.apply(
+          rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot add group members", e);
+    }
+  }
+
+  @Override
+  public void removeGroups(String... groups) throws RestApiException {
+    try {
+      deleteGroups.apply(
+          rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot remove group members", e);
+    }
+  }
+
+  @Override
+  public List<? extends GroupAuditEventInfo> auditLog() throws RestApiException {
+    try {
+      return getAuditLog.apply(rsrc);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get audit log", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
new file mode 100644
index 0000000..3d2c960
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2015 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.server.api.groups;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
+
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.api.groups.Groups;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.group.CreateGroup;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.group.ListGroups;
+import com.google.gerrit.server.project.ProjectsCollection;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.SortedMap;
+
+@Singleton
+class GroupsImpl implements Groups {
+  private final AccountsCollection accounts;
+  private final GroupsCollection groups;
+  private final ProjectsCollection projects;
+  private final Provider<ListGroups> listGroups;
+  private final Provider<CurrentUser> user;
+  private final CreateGroup.Factory createGroup;
+  private final GroupApiImpl.Factory api;
+
+  @Inject
+  GroupsImpl(
+      AccountsCollection accounts,
+      GroupsCollection groups,
+      ProjectsCollection projects,
+      Provider<ListGroups> listGroups,
+      Provider<CurrentUser> user,
+      CreateGroup.Factory createGroup,
+      GroupApiImpl.Factory api) {
+    this.accounts = accounts;
+    this.groups = groups;
+    this.projects = projects;
+    this.listGroups = listGroups;
+    this.user = user;
+    this.createGroup = createGroup;
+    this.api = api;
+  }
+
+  @Override
+  public GroupApi id(String id) throws RestApiException {
+    return api.create(
+        groups.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(id)));
+  }
+
+  @Override
+  public GroupApi create(String name) throws RestApiException {
+    GroupInput in = new GroupInput();
+    in.name = name;
+    return create(in);
+  }
+
+  @Override
+  public GroupApi create(GroupInput in) throws RestApiException {
+    if (checkNotNull(in, "GroupInput").name == null) {
+      throw new BadRequestException("GroupInput must specify name");
+    }
+    checkRequiresCapability(user, null, CreateGroup.class);
+    try {
+      GroupInfo info = createGroup.create(in.name)
+          .apply(TopLevelResource.INSTANCE, in);
+      return id(info.id);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot create group " + in.name, e);
+    }
+  }
+
+  @Override
+  public ListRequest list() {
+    return new ListRequest() {
+      @Override
+      public SortedMap<String, GroupInfo> getAsMap() throws RestApiException {
+        return list(this);
+      }
+    };
+  }
+
+  private SortedMap<String, GroupInfo> list(ListRequest req)
+      throws RestApiException {
+    TopLevelResource tlr = TopLevelResource.INSTANCE;
+    ListGroups list = listGroups.get();
+    list.setOptions(req.getOptions());
+
+    for (String project : req.getProjects()) {
+      try {
+        list.addProject(
+            projects.parse(tlr, IdString.fromDecoded(project)).getControl());
+      } catch (IOException e) {
+        throw new RestApiException("Error looking up project " + project, e);
+      }
+    }
+
+    for (String group : req.getGroups()) {
+      list.addGroup(groups.parse(group).getGroupUUID());
+    }
+
+    list.setVisibleToAll(req.getVisibleToAll());
+
+    if (req.getUser() != null) {
+      try {
+        list.setUser(accounts.parse(req.getUser()).getAccountId());
+      } catch (OrmException e) {
+        throw new RestApiException("Error looking up user " + req.getUser(), e);
+      }
+    }
+
+    list.setOwned(req.getOwned());
+    list.setLimit(req.getLimit());
+    list.setStart(req.getStart());
+    list.setMatchSubstring(req.getSubstring());
+    list.setSuggest(req.getSuggest());
+    try {
+      return list.apply(tlr);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot list groups", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/Module.java
new file mode 100644
index 0000000..7d7af4e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/Module.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2015 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.server.api.groups;
+
+import com.google.gerrit.extensions.api.groups.Groups;
+import com.google.gerrit.extensions.config.FactoryModule;
+
+public class Module extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(Groups.class).to(GroupsImpl.class);
+
+    factory(GroupApiImpl.Factory.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index f4dc67e..0bb395f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -15,30 +15,41 @@
 package com.google.gerrit.server.api.projects;
 
 import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.BranchResource;
+import com.google.gerrit.server.project.BranchesCollection;
 import com.google.gerrit.server.project.CreateBranch;
+import com.google.gerrit.server.project.DeleteBranch;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import java.io.IOException;
 
-public class BranchApiImpl extends BranchApi.NotImplemented implements BranchApi {
+public class BranchApiImpl implements BranchApi {
   interface Factory {
     BranchApiImpl create(ProjectResource project, String ref);
   }
 
+  private final BranchesCollection branches;
   private final CreateBranch.Factory createBranchFactory;
+  private final DeleteBranch deleteBranch;
   private final String ref;
   private final ProjectResource project;
 
   @Inject
-  BranchApiImpl(
+  BranchApiImpl(BranchesCollection branches,
       CreateBranch.Factory createBranchFactory,
+      DeleteBranch deleteBranch,
       @Assisted ProjectResource project,
       @Assisted String ref) {
+    this.branches = branches;
     this.createBranchFactory = createBranchFactory;
+    this.deleteBranch = deleteBranch;
     this.project = project;
     this.ref = ref;
   }
@@ -55,4 +66,26 @@
       throw new RestApiException("Cannot create branch", e);
     }
   }
+
+  @Override
+  public BranchInfo get() throws RestApiException {
+    try {
+      return resource().getBranchInfo();
+    } catch (IOException e) {
+      throw new RestApiException("Cannot read branch", e);
+    }
+  }
+
+  @Override
+  public void delete() throws RestApiException {
+    try {
+      deleteBranch.apply(resource(), new DeleteBranch.Input());
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot delete branch", e);
+    }
+  }
+
+  private BranchResource resource() throws RestApiException, IOException {
+    return branches.parse(project, IdString.fromDecoded(ref));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
new file mode 100644
index 0000000..02dc919
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2015 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.server.api.projects;
+
+import com.google.gerrit.extensions.api.projects.ChildProjectApi;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.ChildProjectResource;
+import com.google.gerrit.server.project.GetChildProject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+public class ChildProjectApiImpl implements ChildProjectApi {
+  interface Factory {
+    ChildProjectApiImpl create(ChildProjectResource rsrc);
+  }
+
+  private final Provider<GetChildProject> getProvider;
+  private final ChildProjectResource rsrc;
+
+  @AssistedInject
+  ChildProjectApiImpl(
+      Provider<GetChildProject> getProvider,
+      @Assisted ChildProjectResource rsrc) {
+    this.getProvider = getProvider;
+    this.rsrc = rsrc;
+  }
+
+  @Override
+  public ProjectInfo get() throws RestApiException {
+    return get(false);
+  }
+
+  @Override
+  public ProjectInfo get(boolean recursive) throws RestApiException {
+    GetChildProject get = getProvider.get();
+    get.setRecursive(recursive);
+    return get.apply(rsrc);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
index 0b7e258..975e6c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.api.projects;
 
 import com.google.gerrit.extensions.api.projects.Projects;
-import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
 public class Module extends FactoryModule {
   @Override
@@ -23,6 +23,8 @@
     bind(Projects.class).to(ProjectsImpl.class);
 
     factory(BranchApiImpl.Factory.class);
+    factory(TagApiImpl.Factory.class);
     factory(ProjectApiImpl.Factory.class);
+    factory(ChildProjectApiImpl.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 7f73a38..dbd246c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -14,78 +14,135 @@
 
 package com.google.gerrit.server.api.projects;
 
-import com.google.common.base.Preconditions;
-import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
+
 import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.ChildProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
+import com.google.gerrit.extensions.api.projects.TagApi;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ChildProjectsCollection;
 import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.GetDescription;
+import com.google.gerrit.server.project.ListBranches;
+import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ListTags;
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
+import com.google.gerrit.server.project.PutDescription;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
-import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
-public class ProjectApiImpl extends ProjectApi.NotImplemented implements ProjectApi {
+import java.io.IOException;
+import java.util.List;
+
+public class ProjectApiImpl implements ProjectApi {
   interface Factory {
     ProjectApiImpl create(ProjectResource project);
     ProjectApiImpl create(String name);
   }
 
+  private final Provider<CurrentUser> user;
   private final Provider<CreateProject.Factory> createProjectFactory;
   private final ProjectApiImpl.Factory projectApi;
   private final ProjectsCollection projects;
+  private final GetDescription getDescription;
+  private final PutDescription putDescription;
+  private final ChildProjectApiImpl.Factory childApi;
+  private final ChildProjectsCollection children;
   private final ProjectResource project;
   private final ProjectJson projectJson;
   private final String name;
   private final BranchApiImpl.Factory branchApi;
+  private final TagApiImpl.Factory tagApi;
+  private final Provider<ListBranches> listBranchesProvider;
+  private final Provider<ListTags> listTagsProvider;
 
   @AssistedInject
-  ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+  ProjectApiImpl(Provider<CurrentUser> user,
+      Provider<CreateProject.Factory> createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
+      GetDescription getDescription,
+      PutDescription putDescription,
+      ChildProjectApiImpl.Factory childApi,
+      ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
+      Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       @Assisted ProjectResource project) {
-    this(createProjectFactory, projectApi, projects, projectJson,
-        branchApiFactory, project, null);
+    this(user, createProjectFactory, projectApi, projects, getDescription,
+        putDescription, childApi, children, projectJson, branchApiFactory,
+        tagApiFactory, listBranchesProvider, listTagsProvider, project, null);
   }
 
   @AssistedInject
-  ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+  ProjectApiImpl(Provider<CurrentUser> user,
+      Provider<CreateProject.Factory> createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
+      GetDescription getDescription,
+      PutDescription putDescription,
+      ChildProjectApiImpl.Factory childApi,
+      ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
+      Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       @Assisted String name) {
-    this(createProjectFactory, projectApi, projects, projectJson,
-        branchApiFactory, null, name);
+    this(user, createProjectFactory, projectApi, projects, getDescription,
+        putDescription, childApi, children, projectJson, branchApiFactory,
+        tagApiFactory, listBranchesProvider, listTagsProvider, null, name);
   }
 
-  private ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+  private ProjectApiImpl(Provider<CurrentUser> user,
+      Provider<CreateProject.Factory> createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
+      GetDescription getDescription,
+      PutDescription putDescription,
+      ChildProjectApiImpl.Factory childApi,
+      ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      TagApiImpl.Factory tagApiFactory,
+      Provider<ListBranches> listBranchesProvider,
+      Provider<ListTags> listTagsProvider,
       ProjectResource project,
       String name) {
+    this.user = user;
     this.createProjectFactory = createProjectFactory;
     this.projectApi = projectApi;
     this.projects = projects;
+    this.getDescription = getDescription;
+    this.putDescription = putDescription;
+    this.childApi = childApi;
+    this.children = children;
     this.projectJson = projectJson;
     this.project = project;
     this.name = name;
     this.branchApi = branchApiFactory;
+    this.tagApi = tagApiFactory;
+    this.listBranchesProvider = listBranchesProvider;
+    this.listTagsProvider = listTagsProvider;
   }
 
   @Override
@@ -102,24 +159,122 @@
       if (in.name != null && !name.equals(in.name)) {
         throw new BadRequestException("name must match input.name");
       }
+      checkRequiresCapability(user, null, CreateProject.class);
       createProjectFactory.get().create(name)
           .apply(TopLevelResource.INSTANCE, in);
       return projectApi.create(projects.parse(name));
-    } catch (BadRequestException | UnprocessableEntityException
-        | ResourceNotFoundException | ProjectCreationFailedException
-        | IOException e) {
+    } catch (IOException | ConfigInvalidException e) {
       throw new RestApiException("Cannot create project: " + e.getMessage(), e);
     }
   }
 
   @Override
-  public ProjectInfo get() {
-    Preconditions.checkNotNull(project);
+  public ProjectInfo get() throws RestApiException {
+    if (project == null) {
+      throw new ResourceNotFoundException(name);
+    }
     return projectJson.format(project);
   }
 
   @Override
-  public BranchApi branch(String ref) {
-    return branchApi.create(project, ref);
+  public String description() throws RestApiException {
+    return getDescription.apply(checkExists());
+  }
+
+  @Override
+  public void description(PutDescriptionInput in)
+      throws RestApiException {
+    try {
+      putDescription.apply(checkExists(), in);
+    } catch (IOException e) {
+      throw new RestApiException("Cannot put project description", e);
+    }
+  }
+
+  @Override
+  public ListRefsRequest<BranchInfo> branches() {
+    return new ListRefsRequest<BranchInfo>() {
+      @Override
+      public List<BranchInfo> get() throws RestApiException {
+        return listBranches(this);
+      }
+    };
+  }
+
+  private List<BranchInfo> listBranches(ListRefsRequest<BranchInfo> request)
+      throws RestApiException {
+    ListBranches list = listBranchesProvider.get();
+    list.setLimit(request.getLimit());
+    list.setStart(request.getStart());
+    list.setMatchSubstring(request.getSubstring());
+    list.setMatchRegex(request.getRegex());
+    try {
+      return list.apply(checkExists());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot list branches", e);
+    }
+  }
+
+  @Override
+  public ListRefsRequest<TagInfo> tags() {
+    return new ListRefsRequest<TagInfo>() {
+      @Override
+      public List<TagInfo> get() throws RestApiException {
+        return listTags(this);
+      }
+    };
+  }
+
+  private List<TagInfo> listTags(ListRefsRequest<TagInfo> request)
+      throws RestApiException {
+    ListTags list = listTagsProvider.get();
+    list.setLimit(request.getLimit());
+    list.setStart(request.getStart());
+    list.setMatchSubstring(request.getSubstring());
+    list.setMatchRegex(request.getRegex());
+    try {
+      return list.apply(checkExists());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot list tags", e);
+    }
+  }
+
+  @Override
+  public List<ProjectInfo> children() throws RestApiException {
+    return children(false);
+  }
+
+  @Override
+  public List<ProjectInfo> children(boolean recursive) throws RestApiException {
+    ListChildProjects list = children.list();
+    list.setRecursive(recursive);
+    return list.apply(checkExists());
+  }
+
+  @Override
+  public ChildProjectApi child(String name) throws RestApiException {
+    try {
+      return childApi.create(
+          children.parse(checkExists(), IdString.fromDecoded(name)));
+    } catch (IOException e) {
+      throw new RestApiException("Cannot parse child project", e);
+    }
+  }
+
+  @Override
+  public BranchApi branch(String ref) throws ResourceNotFoundException {
+    return branchApi.create(checkExists(), ref);
+  }
+
+  @Override
+  public TagApi tag(String ref) throws ResourceNotFoundException {
+    return tagApi.create(checkExists(), ref);
+  }
+
+  private ProjectResource checkExists() throws ResourceNotFoundException {
+    if (project == null) {
+      throw new ResourceNotFoundException(name);
+    }
+    return project;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index 86baa1e..db31d42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -14,23 +14,25 @@
 
 package com.google.gerrit.server.api.projects;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.Projects;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.project.ListProjects;
+import com.google.gerrit.server.project.ListProjects.FilterType;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import java.io.IOException;
-import java.util.List;
+import java.util.SortedMap;
 
 @Singleton
-class ProjectsImpl extends Projects.NotImplemented implements Projects {
+class ProjectsImpl implements Projects {
   private final ProjectsCollection projects;
   private final ProjectApiImpl.Factory api;
   private final Provider<ListProjects> listProvider;
@@ -56,22 +58,62 @@
   }
 
   @Override
+  public ProjectApi create(String name) throws RestApiException {
+    ProjectInput in = new ProjectInput();
+    in.name = name;
+    return create(in);
+  }
+
+  @Override
+  public ProjectApi create(ProjectInput in) throws RestApiException {
+    return name(in.name).create(in);
+  }
+
+  @Override
   public ListRequest list() {
     return new ListRequest() {
       @Override
-      public List<ProjectInfo> get() throws RestApiException {
+      public SortedMap<String, ProjectInfo> getAsMap() throws RestApiException {
         return list(this);
       }
     };
   }
 
-  private List<ProjectInfo> list(ListRequest request) throws RestApiException {
+  private SortedMap<String, ProjectInfo> list(ListRequest request)
+      throws RestApiException {
     ListProjects lp = listProvider.get();
     lp.setShowDescription(request.getDescription());
     lp.setLimit(request.getLimit());
     lp.setStart(request.getStart());
     lp.setMatchPrefix(request.getPrefix());
 
-    return ImmutableList.copyOf(lp.apply().values());
+    lp.setMatchSubstring(request.getSubstring());
+    lp.setMatchRegex(request.getRegex());
+    lp.setShowTree(request.getShowTree());
+    for (String branch : request.getBranches()) {
+      lp.addShowBranch(branch);
+    }
+
+    FilterType type;
+    switch (request.getFilterType()) {
+      case ALL:
+        type = FilterType.ALL;
+        break;
+      case CODE:
+        type = FilterType.CODE;
+        break;
+      case PARENT_CANDIDATES:
+        type = FilterType.PARENT_CANDIDATES;
+        break;
+      case PERMISSIONS:
+        type = FilterType.PERMISSIONS;
+        break;
+      default:
+        throw new BadRequestException(
+            "Unknown filter type: " + request.getFilterType());
+    }
+    lp.setFilterType(type);
+
+    return lp.apply();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
new file mode 100644
index 0000000..086447d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 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.server.api.projects;
+
+import com.google.gerrit.extensions.api.projects.TagApi;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.ListTags;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+public class TagApiImpl implements TagApi {
+  interface Factory {
+    TagApiImpl create(ProjectResource project, String ref);
+  }
+
+  private final ListTags listTags;
+  private final String ref;
+  private final ProjectResource project;
+
+  @Inject
+  TagApiImpl(ListTags listTags,
+      @Assisted ProjectResource project,
+      @Assisted String ref) {
+    this.listTags = listTags;
+    this.project = project;
+    this.ref = ref;
+  }
+
+  @Override
+  public TagInfo get() throws RestApiException {
+    try {
+      return listTags.get(project, IdString.fromDecoded(ref));
+    } catch (IOException e) {
+      throw new RestApiException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index 1cbab8a..00eaf94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -57,8 +57,7 @@
     try {
       final Change.Key key = Change.Key.parse(tokens[2]);
       final Project.NameKey project = new Project.NameKey(tokens[0]);
-      final Branch.NameKey branch =
-          new Branch.NameKey(project, "refs/heads/" + tokens[1]);
+      final Branch.NameKey branch = new Branch.NameKey(project, tokens[1]);
       for (final ChangeData cd : queryProvider.get().byBranchKey(branch, key)) {
         setter.addValue(cd.getId());
         return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index b246868..b3ddae0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AuthenticationFailedException;
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -172,7 +173,7 @@
     try {
       return new InitialDirContext(env);
     } catch (NamingException e) {
-      throw new AccountException("Incorrect username or password", e);
+      throw new AuthenticationFailedException("Incorrect username or password", e);
     }
   }
 
@@ -258,6 +259,7 @@
             recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
           }
         } catch (PartialResultException e) {
+          // Ignored
         }
       }
     }
@@ -293,6 +295,7 @@
                 dns.add((String) groups.next());
               }
             } catch (PartialResultException e) {
+              // Ignored
             }
           }
         } catch (NamingException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 158aae4..bd4a3b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -126,7 +126,7 @@
     String groupDn = uuid.get().substring(LDAP_UUID.length());
     CurrentUser user = userProvider.get();
     if (!(user.isIdentifiedUser())
-        || !membershipsOf((IdentifiedUser) user).contains(uuid)) {
+        || !membershipsOf(user.asIdentifiedUser()).contains(uuid)) {
       try {
         if (!existsCache.get(groupDn)) {
           return null;
@@ -228,9 +228,7 @@
           log.warn("Cannot close LDAP query handle", e);
         }
       }
-    } catch (NamingException e) {
-      log.warn("Cannot query LDAP for groups matching requested name", e);
-    } catch (LoginException e) {
+    } catch (NamingException | LoginException e) {
       log.warn("Cannot query LDAP for groups matching requested name", e);
     }
     return out;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
index 1f68011..e9ae8f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
@@ -75,6 +75,7 @@
           r.add(new Result(res.next()));
         }
       } catch (PartialResultException e) {
+        // Ignored
       }
       return r;
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 9060108..cd1c4d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -75,9 +75,9 @@
 
   @Inject
   LdapRealm(
-      final Helper helper,
-      final AuthConfig authConfig,
-      final EmailExpander emailExpander,
+      Helper helper,
+      AuthConfig authConfig,
+      EmailExpander emailExpander,
       @Named(LdapModule.GROUP_CACHE) final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
       @Named(LdapModule.USERNAME_CACHE) final LoadingCache<String, Optional<Account.Id>> usernameCache,
       @GerritServerConfig final Config config) {
@@ -96,6 +96,9 @@
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.USER_NAME);
     }
+    if (!authConfig.isAllowRegisterNewEmail()) {
+      readOnlyAccountFields.add(Account.FieldName.REGISTER_NEW_EMAIL);
+    }
 
     fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
   }
@@ -134,7 +137,7 @@
 
   static List<String> optionalList(final Config config,
       final String name) {
-    String s[] = config.getStringList("ldap", null, name);
+    String[] s = config.getStringList("ldap", null, name);
     return Arrays.asList(s);
   }
 
@@ -158,6 +161,7 @@
       return null;
 
     } else {
+      checkBackendCompliance(n, v[0], Strings.isNullOrEmpty(d));
       return v[0];
     }
   }
@@ -181,6 +185,16 @@
     }
   }
 
+  private static void checkBackendCompliance(String configOption,
+      String suppliedValue, boolean disabledByBackend) {
+    if (disabledByBackend && !Strings.isNullOrEmpty(suppliedValue)) {
+      String msg = String.format("LDAP backend doesn't support: ldap.%s",
+          configOption);
+      log.error(msg);
+      throw new IllegalArgumentException(msg);
+    }
+  }
+
   @Override
   public boolean allowsEdit(final Account.FieldName field) {
     return !readOnlyAccountFields.contains(field);
@@ -307,8 +321,7 @@
 
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         final AccountExternalId extId =
             db.accountExternalIds().get(
                 new AccountExternalId.Key(SCHEME_GERRIT, username));
@@ -316,8 +329,6 @@
           return Optional.of(extId.getAccountId());
         }
         return Optional.absent();
-      } finally {
-        db.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index 2276def..9f51de7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -19,7 +19,7 @@
 import com.google.common.cache.LoadingCache;
 import com.google.common.cache.Weigher;
 import com.google.gerrit.extensions.annotations.Exports;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.Scopes;
@@ -33,7 +33,7 @@
 /**
  * Miniature DSL to support binding {@link Cache} instances in Guice.
  */
-public abstract class CacheModule extends AbstractModule {
+public abstract class CacheModule extends FactoryModule {
   private static final TypeLiteral<Cache<?, ?>> ANY_CACHE =
       new TypeLiteral<Cache<?, ?>>() {};
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index d0424d9..f2d40c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -15,26 +15,30 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,7 +47,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
+import java.util.Collections;
 
 @Singleton
 public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
@@ -53,94 +57,119 @@
   private final ChangeHooks hooks;
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeJson json;
-  private final ChangeIndexer indexer;
-  private final ChangeUpdate.Factory updateFactory;
+  private final ChangeJson.Factory json;
   private final ChangeMessagesUtil cmUtil;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
   Abandon(ChangeHooks hooks,
       AbandonedSender.Factory abandonedSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json,
-      ChangeIndexer indexer,
-      ChangeUpdate.Factory updateFactory,
-      ChangeMessagesUtil cmUtil) {
+      ChangeJson.Factory json,
+      ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.hooks = hooks;
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
-    this.indexer = indexer;
-    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   @Override
-  public ChangeInfo apply(ChangeResource req, AbandonInput input)
-      throws AuthException, ResourceConflictException, OrmException,
-      IOException {
+  public ChangeInfo apply(ChangeResource req,
+      final AbandonInput input)
+      throws RestApiException, UpdateException, OrmException {
     ChangeControl control = req.getControl();
-    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
-    Change change = req.getChange();
+    IdentifiedUser caller = control.getUser().asIdentifiedUser();
     if (!control.canAbandon()) {
       throw new AuthException("abandon not permitted");
-    } else if (!change.getStatus().isOpen()) {
-      throw new ResourceConflictException("change is " + status(change));
-    } else if (change.getStatus() == Change.Status.DRAFT) {
-      throw new ResourceConflictException("draft changes cannot be abandoned");
+    }
+    Change change = abandon(control, input.message, caller.getAccount());
+    return json.create(ChangeJson.NO_OPTIONS).format(change);
+  }
+
+  public Change abandon(ChangeControl control,
+      final String msgTxt, final Account account)
+      throws RestApiException, UpdateException {
+    Op op = new Op(msgTxt, account);
+    Change c = control.getChange();
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        c.getProject(), control.getUser(), TimeUtil.nowTs())) {
+      u.addOp(c.getId(), op).execute();
+    }
+    return op.change;
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final Account account;
+    private final String msgTxt;
+
+    private Change change;
+    private PatchSet patchSet;
+    private ChangeMessage message;
+
+    private Op(String msgTxt, Account account) {
+      this.account = account;
+      this.msgTxt = msgTxt;
     }
 
-    ChangeMessage message;
-    ChangeUpdate update;
-    ReviewDb db = dbProvider.get();
-    db.changes().beginTransaction(change.getId());
-    try {
-      change = db.changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus().isOpen()) {
-              change.setStatus(Change.Status.ABANDONED);
-              ChangeUtil.updated(change);
-              return change;
-            }
-            return null;
-          }
-        });
-      if (change == null) {
-        throw new ResourceConflictException("change is "
-            + status(db.changes().get(req.getChange().getId())));
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException,
+        ResourceConflictException {
+      change = ctx.getChange();
+      if (change == null || !change.getStatus().isOpen()) {
+        throw new ResourceConflictException("change is " + status(change));
+      } else if (change.getStatus() == Change.Status.DRAFT) {
+        throw new ResourceConflictException(
+            "draft changes cannot be abandoned");
+      }
+      patchSet = ctx.getDb().patchSets().get(change.currentPatchSetId());
+      change.setStatus(Change.Status.ABANDONED);
+      change.setLastUpdatedOn(ctx.getWhen());
+      ctx.getDb().changes().update(Collections.singleton(change));
+
+      message = newMessage(ctx.getDb());
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), message);
+    }
+
+    private ChangeMessage newMessage(ReviewDb db) throws OrmException {
+      StringBuilder msg = new StringBuilder();
+      msg.append("Abandoned");
+      if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) {
+        msg.append("\n\n");
+        msg.append(msgTxt.trim());
       }
 
-      //TODO(yyonas): atomic update was not propagated
-      update = updateFactory.create(control, change.getLastUpdatedOn());
-      message = newMessage(input, caller, change);
-      cmUtil.addChangeMessage(db, update, message);
-      db.commit();
-    } finally {
-      db.rollback();
+      ChangeMessage message = new ChangeMessage(
+          new ChangeMessage.Key(
+              change.getId(),
+              ChangeUtil.messageUUID(db)),
+          account != null ? account.getId() : null,
+          change.getLastUpdatedOn(),
+          change.currentPatchSetId());
+      message.setMessage(msg.toString());
+      return message;
     }
-    update.commit();
 
-    CheckedFuture<?, IOException> indexFuture =
-        indexer.indexAsync(change.getId());
-    try {
-      ReplyToChangeSender cm = abandonedSenderFactory.create(change);
-      cm.setFrom(caller.getAccountId());
-      cm.setChangeMessage(message);
-      cm.send();
-    } catch (Exception e) {
-      log.error("Cannot email update for change " + change.getChangeId(), e);
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      try {
+        ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
+        if (account != null) {
+          cm.setFrom(account.getId());
+        }
+        cm.setChangeMessage(message);
+        cm.send();
+      } catch (Exception e) {
+        log.error("Cannot email update for change " + change.getId(), e);
+      }
+      hooks.doChangeAbandonedHook(change,
+          account,
+          patchSet,
+          Strings.emptyToNull(msgTxt),
+          ctx.getDb());
     }
-    indexFuture.checkedGet();
-    hooks.doChangeAbandonedHook(change,
-        caller.getAccount(),
-        db.patchSets().get(change.currentPatchSetId()),
-        Strings.emptyToNull(input.message),
-        db);
-    ChangeInfo result = json.format(change);
-    return result;
   }
 
   @Override
@@ -153,26 +182,6 @@
           && resource.getControl().canAbandon());
   }
 
-  private ChangeMessage newMessage(AbandonInput input, IdentifiedUser caller,
-      Change change) throws OrmException {
-    StringBuilder msg = new StringBuilder();
-    msg.append("Abandoned");
-    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
-      msg.append("\n\n");
-      msg.append(input.message.trim());
-    }
-
-    ChangeMessage message = new ChangeMessage(
-        new ChangeMessage.Key(
-            change.getId(),
-            ChangeUtil.messageUUID(dbProvider.get())),
-        caller.getAccountId(),
-        change.getLastUpdatedOn(),
-        change.currentPatchSetId());
-    message.setMessage(msg.toString());
-    return message;
-  }
-
   private static String status(Change change) {
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
new file mode 100644
index 0000000..b14d4ba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2015 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.server.change;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.ChangeCleanupConfig;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.QueryProcessor;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+public class AbandonUtil {
+  private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class);
+
+  private final ChangeCleanupConfig cfg;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final QueryProcessor queryProcessor;
+  private final ChangeQueryBuilder queryBuilder;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final Abandon abandon;
+
+  @Inject
+  AbandonUtil(
+      ChangeCleanupConfig cfg,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      QueryProcessor queryProcessor,
+      ChangeQueryBuilder queryBuilder,
+      ChangeControl.GenericFactory changeControlFactory,
+      Abandon abandon) {
+    this.cfg = cfg;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.queryProcessor = queryProcessor;
+    this.queryBuilder = queryBuilder;
+    this.changeControlFactory = changeControlFactory;
+    this.abandon = abandon;
+  }
+
+  public void abandonInactiveOpenChanges() {
+    if (cfg.getAbandonAfter() <= 0) {
+      return;
+    }
+
+    try {
+      String query = "status:new age:"
+          + TimeUnit.MILLISECONDS.toMinutes(cfg.getAbandonAfter())
+          + "m";
+      if (!cfg.getAbandonIfMergeable()) {
+        query += " -is:mergeable";
+      }
+      List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false)
+          .queryChanges(queryBuilder.parse(query)).changes();
+      int count = 0;
+      for (ChangeData cd : changesToAbandon) {
+        try {
+          if (noNeedToAbandon(cd, query)){
+            log.debug("Change data \"{}\" does not satisfy the query \"{}\" any"
+                + " more, hence skipping it in clean up", cd, query);
+            continue;
+          }
+          abandon.abandon(changeControl(cd), cfg.getAbandonMessage(), null);
+          count++;
+        } catch (ResourceConflictException e) {
+          // Change was already merged or abandoned.
+        } catch (Throwable e) {
+          log.error(String.format(
+              "Failed to auto-abandon inactive open change %d.",
+                  cd.getId().get()), e);
+        }
+      }
+      log.info(String.format("Auto-Abandoned %d of %d changes.",
+          count, changesToAbandon.size()));
+    } catch (QueryParseException | OrmException e) {
+      log.error("Failed to query inactive open changes for auto-abandoning.", e);
+    }
+  }
+
+  private boolean noNeedToAbandon(ChangeData cd, String query)
+      throws OrmException, QueryParseException {
+    String newQuery = query + " change:" + cd.getId();
+    List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false)
+        .queryChanges(queryBuilder.parse(newQuery)).changes();
+    return changesToAbandon.isEmpty();
+  }
+
+  private ChangeControl changeControl(ChangeData cd)
+      throws NoSuchChangeException, OrmException {
+    Change c = cd.change();
+    return changeControlFactory.controlFor(c,
+        identifiedUserFactory.create(c.getOwner()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index df6b76e..d8574f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.Inject;
@@ -37,16 +36,13 @@
 public class ActionJson {
   private final Revisions revisions;
   private final DynamicMap<RestView<ChangeResource>> changeViews;
-  private final RebaseChange rebaseChange;
 
   @Inject
   ActionJson(
       Revisions revisions,
-      DynamicMap<RestView<ChangeResource>> changeViews,
-      RebaseChange rebaseChange) {
+      DynamicMap<RestView<ChangeResource>> changeViews) {
     this.revisions = revisions;
     this.changeViews = changeViews;
-    this.rebaseChange = rebaseChange;
   }
 
   public Map<String, ActionInfo> format(RevisionResource rsrc) {
@@ -66,18 +62,21 @@
 
   private Map<String, ActionInfo> toActionMap(ChangeControl ctl) {
     Map<String, ActionInfo> out = new LinkedHashMap<>();
-    if (!ctl.getCurrentUser().isIdentifiedUser()) {
+    if (!ctl.getUser().isIdentifiedUser()) {
       return out;
     }
 
-    Provider<CurrentUser> userProvider = Providers.of(ctl.getCurrentUser());
+    Provider<CurrentUser> userProvider = Providers.of(ctl.getUser());
     for (UiAction.Description d : UiActions.from(
         changeViews,
-        new ChangeResource(ctl, rebaseChange),
+        new ChangeResource(ctl),
         userProvider)) {
       out.put(d.getId(), new ActionInfo(d));
     }
-    // TODO(sbeller): why do we need to treat followup specially here?
+
+    // The followup action is a client-side only operation that does not
+    // have a server side handler. It must be manually registered into the
+    // resulting action map.
     if (ctl.getChange().getStatus().isOpen()) {
       UiAction.Description descr = new UiAction.Description();
       PrivateInternals_UiActionDescription.setId(descr, "followup");
@@ -90,9 +89,9 @@
 
   private Map<String, ActionInfo> toActionMap(RevisionResource rsrc) {
     Map<String, ActionInfo> out = new LinkedHashMap<>();
-    if (rsrc.getControl().getCurrentUser().isIdentifiedUser()) {
+    if (rsrc.getControl().getUser().isIdentifiedUser()) {
       Provider<CurrentUser> userProvider = Providers.of(
-          rsrc.getControl().getCurrentUser());
+          rsrc.getControl().getUser());
       for (UiAction.Description d : UiActions.from(
           revisions, rsrc, userProvider)) {
         out.put(d.getId(), new ActionInfo(d));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
index a5054f3..14fa7d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -19,14 +19,14 @@
 import org.eclipse.jgit.archive.Tbz2Format;
 import org.eclipse.jgit.archive.TgzFormat;
 import org.eclipse.jgit.archive.TxzFormat;
+import org.eclipse.jgit.archive.ZipFormat;
 
 public enum ArchiveFormat {
   TGZ("application/x-gzip", new TgzFormat()),
   TAR("application/x-tar", new TarFormat()),
   TBZ2("application/x-bzip2", new Tbz2Format()),
-  TXZ("application/x-xz", new TxzFormat());
-  // Zip is not supported because it may be interpreted by a Java plugin as a
-  // valid JAR file, whose code would have access to cookies on the domain.
+  TXZ("application/x-xz", new TxzFormat()),
+  ZIP("application/x-zip", new ZipFormat());
 
   private final ArchiveCommand.Format<?> format;
   private final String mimeType;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
new file mode 100644
index 0000000..310c8cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2015 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.server.change;
+
+import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.ChangeCleanupConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/** Runnable to enable scheduling change cleanups to run periodically */
+public class ChangeCleanupRunner implements Runnable {
+  private static final Logger log = LoggerFactory
+      .getLogger(ChangeCleanupRunner.class);
+
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      listener().to(Lifecycle.class);
+    }
+  }
+
+  static class Lifecycle implements LifecycleListener {
+    private final WorkQueue queue;
+    private final ChangeCleanupRunner runner;
+    private final ChangeCleanupConfig cfg;
+
+    @Inject
+    Lifecycle(WorkQueue queue,
+        ChangeCleanupRunner runner,
+        ChangeCleanupConfig cfg) {
+      this.queue = queue;
+      this.runner = runner;
+      this.cfg = cfg;
+    }
+
+    @Override
+    public void start() {
+      ScheduleConfig scheduleConfig = cfg.getScheduleConfig();
+      long interval = scheduleConfig.getInterval();
+      long delay = scheduleConfig.getInitialDelay();
+      if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
+        log.info("Ignoring missing changeCleanup schedule configuration");
+      } else if (delay < 0 || interval <= 0) {
+        log.warn(String.format(
+            "Ignoring invalid changeCleanup schedule configuration: %s",
+            scheduleConfig));
+      } else {
+        queue.getDefaultQueue().scheduleAtFixedRate(runner, delay,
+            interval, TimeUnit.MILLISECONDS);
+      }
+    }
+
+    @Override
+    public void stop() {
+      // handled by WorkQueue.stop() already
+    }
+  }
+
+  private final OneOffRequestContext oneOffRequestContext;
+  private final AbandonUtil abandonUtil;
+
+  @Inject
+  ChangeCleanupRunner(
+      OneOffRequestContext oneOffRequestContext,
+      AbandonUtil abandonUtil) {
+    this.oneOffRequestContext = oneOffRequestContext;
+    this.abandonUtil = abandonUtil;
+  }
+
+  @Override
+  public void run() {
+    log.info("Running change cleanups.");
+    try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+      abandonUtil.abandonInactiveOpenChanges();
+    } catch (OrmException e) {
+      log.error("Failed to cleanup changes.", e);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "change cleanup runner";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 9bd625d..cb3729b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -140,7 +140,7 @@
     return deleteFileFactory.create(id.get());
   }
 
-  static class Create implements
+  public static class Create implements
       RestModifyView<ChangeResource, Put.Input> {
 
     interface Factory {
@@ -195,7 +195,7 @@
     }
   }
 
-  static class DeleteFile implements
+  public static class DeleteFile implements
       RestModifyView<ChangeResource, DeleteFile.Input> {
     public static class Input {
     }
@@ -272,8 +272,7 @@
 
     @Override
     public Response<EditInfo> apply(ChangeResource rsrc) throws AuthException,
-        IOException, InvalidChangeOperationException,
-        ResourceNotFoundException, OrmException {
+        IOException, ResourceNotFoundException, OrmException {
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
       if (!edit.isPresent()) {
         return Response.none();
@@ -382,7 +381,7 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
-        throws AuthException, ResourceConflictException, IOException {
+        throws AuthException, ResourceConflictException {
       String path = rsrc.getPath();
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
@@ -407,7 +406,7 @@
    * as reverting or restoring a file to its previous contents.
    */
   @Singleton
-  static class DeleteContent implements
+  public static class DeleteContent implements
       RestModifyView<ChangeEditResource, DeleteContent.Input> {
     public static class Input {
     }
@@ -432,7 +431,7 @@
   }
 
   @Singleton
-  static class Get implements RestReadView<ChangeEditResource> {
+  public static class Get implements RestReadView<ChangeEditResource> {
     private final FileContentUtil fileContentUtil;
 
     @Inject
@@ -442,7 +441,7 @@
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc)
-        throws ResourceNotFoundException, IOException {
+        throws IOException {
       try {
         return Response.ok(fileContentUtil.getContent(
               rsrc.getControl().getProjectControl().getProjectState(),
@@ -455,7 +454,7 @@
   }
 
   @Singleton
-  static class GetMeta implements RestReadView<ChangeEditResource> {
+  public static class GetMeta implements RestReadView<ChangeEditResource> {
     private final WebLinks webLinks;
 
     @Inject
@@ -481,8 +480,8 @@
       return r;
     }
 
-    static class FileInfo {
-      List<DiffWebLinkInfo> webLinks;
+    public static class FileInfo {
+      public List<DiffWebLinkInfo> webLinks;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 15a249f6..47145d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -32,24 +32,32 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.GroupCollector;
+import com.google.gerrit.server.git.SendEmailExecutor;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -57,94 +65,108 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
 
-public class ChangeInserter {
+public class ChangeInserter extends BatchUpdate.InsertChangeOp {
   public static interface Factory {
-    ChangeInserter create(ProjectControl ctl, Change c, RevCommit rc);
+    ChangeInserter create(RefControl ctl, Change c, RevCommit rc);
   }
 
   private static final Logger log =
       LoggerFactory.getLogger(ChangeInserter.class);
 
-  private final Provider<ReviewDb> dbProvider;
-  private final ChangeUpdate.Factory updateFactory;
-  private final GitReferenceUpdated gitRefUpdated;
+  private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeIndexer indexer;
   private final CreateChangeSender.Factory createChangeSenderFactory;
-  private final HashtagsUtil hashtagsUtil;
-  private final AccountCache accountCache;
-  private final WorkQueue workQueue;
+  private final ExecutorService sendEmailExecutor;
+  private final CommitValidators.Factory commitValidatorsFactory;
 
-  private final ProjectControl projectControl;
+  private final RefControl refControl;
+  private final IdentifiedUser user;
   private final Change change;
   private final PatchSet patchSet;
   private final RevCommit commit;
-  private final PatchSetInfo patchSetInfo;
 
-  private ChangeMessage changeMessage;
+  // Fields exposed as setters.
+  private String message;
+  private CommitValidators.Policy validatePolicy =
+      CommitValidators.Policy.GERRIT;
   private Set<Account.Id> reviewers;
   private Set<Account.Id> extraCC;
   private Map<String, Short> approvals;
-  private Set<String> hashtags;
   private RequestScopePropagator requestScopePropagator;
+  private ReceiveCommand updateRefCommand;
   private boolean runHooks;
   private boolean sendMail;
+  private boolean updateRef;
+
+  // Fields set during the insertion process.
+  private ChangeMessage changeMessage;
+  private PatchSetInfo patchSetInfo;
 
   @Inject
-  ChangeInserter(Provider<ReviewDb> dbProvider,
-      ChangeUpdate.Factory updateFactory,
-      PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated,
+  ChangeInserter(PatchSetInfoFactory patchSetInfoFactory,
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      ChangeIndexer indexer,
       CreateChangeSender.Factory createChangeSenderFactory,
-      HashtagsUtil hashtagsUtil,
-      AccountCache accountCache,
-      WorkQueue workQueue,
-      @Assisted ProjectControl projectControl,
+      @SendEmailExecutor ExecutorService sendEmailExecutor,
+      CommitValidators.Factory commitValidatorsFactory,
+      @Assisted RefControl refControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
-    this.dbProvider = dbProvider;
-    this.updateFactory = updateFactory;
-    this.gitRefUpdated = gitRefUpdated;
+    String projectName = refControl.getProjectControl().getProject().getName();
+    String refName = refControl.getRefName();
+    checkArgument(projectName.equals(change.getProject().get())
+          && refName.equals(change.getDest().get()),
+        "RefControl for %s,%s does not match change destination %s",
+        projectName, refName, change.getDest());
+
+    this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.indexer = indexer;
     this.createChangeSenderFactory = createChangeSenderFactory;
-    this.hashtagsUtil = hashtagsUtil;
-    this.accountCache = accountCache;
-    this.workQueue = workQueue;
-    this.projectControl = projectControl;
+    this.sendEmailExecutor = sendEmailExecutor;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+
+    this.refControl = refControl;
     this.change = change;
     this.commit = commit;
     this.reviewers = Collections.emptySet();
     this.extraCC = Collections.emptySet();
     this.approvals = Collections.emptyMap();
-    this.hashtags = Collections.emptySet();
+    this.updateRefCommand = null;
     this.runHooks = true;
     this.sendMail = true;
+    this.updateRef = true;
 
+    user = refControl.getUser().asIdentifiedUser();
     patchSet =
         new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID));
     patchSet.setCreatedOn(change.getCreatedOn());
     patchSet.setUploader(change.getOwner());
     patchSet.setRevision(new RevId(commit.name()));
-    patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId());
-    change.setCurrentPatchSet(patchSetInfo);
   }
 
+  @Override
   public Change getChange() {
     return change;
   }
 
-  public ChangeInserter setMessage(ChangeMessage changeMessage) {
-    this.changeMessage = changeMessage;
+  public IdentifiedUser getUser() {
+    return user;
+  }
+
+  public ChangeInserter setMessage(String message) {
+    this.message = message;
+    return this;
+  }
+
+  public ChangeInserter setValidatePolicy(CommitValidators.Policy validate) {
+    this.validatePolicy = checkNotNull(validate);
     return this;
   }
 
@@ -164,8 +186,8 @@
     return this;
   }
 
-  public ChangeInserter setHashtags(Set<String> hashtags) {
-    this.hashtags = hashtags;
+  public ChangeInserter setGroups(Iterable<String> groups) {
+    patchSet.setGroups(groups);
     return this;
   }
 
@@ -184,6 +206,14 @@
     return this;
   }
 
+  public void setUpdateRefCommand(ReceiveCommand cmd) {
+    updateRefCommand = cmd;
+  }
+
+  public void setPushCertificate(String cert) {
+    patchSet.setPushCertificate(cert);
+  }
+
   public PatchSet getPatchSet() {
     return patchSet;
   }
@@ -193,59 +223,72 @@
     return this;
   }
 
-  public PatchSetInfo getPatchSetInfo() {
-    return patchSetInfo;
+  public ChangeInserter setUpdateRef(boolean updateRef) {
+    this.updateRef = updateRef;
+    return this;
   }
 
-  public Change insert() throws OrmException, IOException {
-    ReviewDb db = dbProvider.get();
-    ChangeControl ctl = projectControl.controlFor(change);
-    ChangeUpdate update = updateFactory.create(
-        ctl,
-        change.getCreatedOn());
-    db.changes().beginTransaction(change.getId());
-    try {
-      ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
-      db.patchSets().insert(Collections.singleton(patchSet));
-      db.changes().insert(Collections.singleton(change));
-      LabelTypes labelTypes = projectControl.getLabelTypes();
-      approvalsUtil.addReviewers(db, update, labelTypes, change,
-          patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
-      approvalsUtil.addApprovals(db, update, labelTypes, patchSet, ctl,
-          approvals);
-      if (messageIsForChange()) {
-        cmUtil.addChangeMessage(db, update, changeMessage);
-      }
-      db.commit();
-    } finally {
-      db.rollback();
+  public ChangeMessage getChangeMessage() {
+    if (message == null) {
+      return null;
     }
+    checkState(changeMessage != null,
+        "getChangeMessage() only valid after inserting change");
+    return changeMessage;
+  }
 
-    update.commit();
-
-    if (hashtags != null && hashtags.size() > 0) {
-      try {
-        HashtagsInput input = new HashtagsInput();
-        input.add = hashtags;
-        hashtagsUtil.setHashtags(ctl, input, false, false);
-      } catch (ValidationException | AuthException e) {
-        log.error("Cannot add hashtags to change " + change.getId(), e);
-      }
+  @Override
+  public void updateRepo(RepoContext ctx)
+      throws InvalidChangeOperationException, IOException {
+    validate(ctx);
+    patchSetInfo = patchSetInfoFactory.get(
+        ctx.getRevWalk(), commit, patchSet.getId());
+    change.setCurrentPatchSet(patchSetInfo);
+    if (!updateRef) {
+      return;
     }
-
-    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
-
-    if (!messageIsForChange()) {
-      commitMessageNotForChange();
+    if (updateRefCommand == null) {
+      ctx.addRefUpdate(
+          new ReceiveCommand(ObjectId.zeroId(), commit, patchSet.getRefName()));
+    } else {
+      ctx.addRefUpdate(updateRefCommand);
     }
+  }
 
+  @Override
+  public void updateChange(ChangeContext ctx) throws OrmException, IOException {
+    ReviewDb db = ctx.getDb();
+    ChangeControl ctl = ctx.getChangeControl();
+    ChangeUpdate update = ctx.getChangeUpdate();
+    if (patchSet.getGroups() == null) {
+      patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
+    }
+    db.patchSets().insert(Collections.singleton(patchSet));
+    db.changes().insert(Collections.singleton(change));
+    LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
+    approvalsUtil.addReviewers(db, update, labelTypes, change,
+        patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
+    approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
+        ctx.getChangeControl(), approvals);
+    if (message != null) {
+      changeMessage =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(db)), user.getAccountId(),
+              patchSet.getCreatedOn(), patchSet.getId());
+      changeMessage.setMessage(message);
+      cmUtil.addChangeMessage(db, update, changeMessage);
+    }
+  }
+
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
     if (sendMail) {
       Runnable sender = new Runnable() {
         @Override
         public void run() {
           try {
             CreateChangeSender cm =
-                createChangeSenderFactory.create(change);
+                createChangeSenderFactory.create(change.getId());
             cm.setFrom(change.getOwner());
             cm.setPatchSet(patchSet, patchSetInfo);
             cm.addReviewers(reviewers);
@@ -262,56 +305,56 @@
         }
       };
       if (requestScopePropagator != null) {
-        workQueue.getDefaultQueue().submit(requestScopePropagator.wrap(sender));
+        sendEmailExecutor.submit(requestScopePropagator.wrap(sender));
       } else {
         sender.run();
       }
     }
-    f.checkedGet();
-
-    gitRefUpdated.fire(change.getProject(), patchSet.getRefName(),
-        ObjectId.zeroId(), commit);
 
     if (runHooks) {
+      ReviewDb db = ctx.getDb();
       hooks.doPatchsetCreatedHook(change, patchSet, db);
-      if (hashtags != null && hashtags.size() > 0) {
-        hooks.doHashtagsChangedHook(change,
-            accountCache.get(change.getOwner()).getAccount(),
-            hashtags, null, hashtags, db);
-      }
-
       if (approvals != null && !approvals.isEmpty()) {
-        hooks.doCommentAddedHook(change,
-            ((IdentifiedUser) ctl.getCurrentUser()).getAccount(), patchSet,
-            null, approvals, db);
+        hooks.doCommentAddedHook(
+            change, user.getAccount(), patchSet, null, approvals, db);
       }
     }
-
-    return change;
   }
 
-  private void commitMessageNotForChange() throws OrmException,
-      IOException {
-    ReviewDb db = dbProvider.get();
-    if (changeMessage != null) {
-      Change otherChange =
-          db.changes().get(changeMessage.getPatchSetId().getParentKey());
-      ChangeUtil.bumpRowVersionNotLastUpdatedOn(
-          changeMessage.getKey().getParentKey(), db);
-      ChangeControl otherControl = projectControl.controlFor(otherChange);
-      ChangeUpdate updateForOtherChange =
-          updateFactory.create(otherControl, change.getLastUpdatedOn());
-      cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
-      updateForOtherChange.commit();
+  private void validate(RepoContext ctx)
+      throws IOException, InvalidChangeOperationException {
+    if (validatePolicy == CommitValidators.Policy.NONE) {
+      return;
     }
-  }
+    CommitValidators cv = commitValidatorsFactory.create(
+        refControl, new NoSshInfo(), ctx.getRepository());
 
-  private boolean messageIsForChange() {
-    if (changeMessage == null) {
-      return false;
+    String refName = patchSet.getId().toRefName();
+    CommitReceivedEvent event = new CommitReceivedEvent(
+        new ReceiveCommand(
+            ObjectId.zeroId(),
+            commit.getId(),
+            refName),
+        refControl.getProjectControl().getProject(),
+        change.getDest().get(),
+        commit,
+        user);
+
+    try {
+      switch (validatePolicy) {
+      case RECEIVE_COMMITS:
+        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(
+            ctx.getRepository(), ctx.getRevWalk());
+        cv.validateForReceiveCommits(event, rejectCommits);
+        break;
+      case GERRIT:
+        cv.validateForGerritCommits(event);
+        break;
+      case NONE:
+        break;
+      }
+    } catch (CommitValidationException e) {
+      throw new InvalidChangeOperationException(e.getMessage());
     }
-    Change.Id id = change.getId();
-    Change.Id msgId = changeMessage.getKey().getParentKey();
-    return msgId.equals(id);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 4bcc82e..733d7a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
+import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
@@ -26,23 +27,23 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
-import static com.google.gerrit.extensions.client.ListChangesOption.DRAFT_COMMENTS;
 import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
+import static com.google.gerrit.server.CommonConverters.toGitPerson;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -65,9 +66,9 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.FetchInfo;
-import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.extensions.common.PushCertificateInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
@@ -80,40 +81,43 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
-import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gerrit.server.query.change.QueryResult;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -122,17 +126,23 @@
 
 public class ChangeJson {
   private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+  public static final Set<ListChangesOption> NO_OPTIONS =
+      Collections.emptySet();
 
-  private static final List<ChangeMessage> NO_MESSAGES =
-      ImmutableList.of();
+  public interface Factory {
+    ChangeJson create(Set<ListChangesOption> options);
+  }
 
   private final Provider<ReviewDb> db;
   private final LabelNormalizer labelNormalizer;
   private final Provider<CurrentUser> userProvider;
   private final AnonymousUser anonymous;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final Submit submit;
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeData.Factory changeDataFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
   private final FileInfoJson fileInfoJson;
   private final AccountLoader.Factory accountLoaderFactory;
   private final DynamicMap<DownloadScheme> downloadSchemes;
@@ -140,61 +150,57 @@
   private final WebLinks webLinks;
   private final EnumSet<ListChangesOption> options;
   private final ChangeMessagesUtil cmUtil;
-  private final PatchLineCommentsUtil plcUtil;
   private final Provider<ConsistencyChecker> checkerProvider;
   private final ActionJson actionJson;
-  private final RebaseChange rebaseChange;
+  private final GpgApiAdapter gpgApi;
 
   private AccountLoader accountLoader;
   private FixInput fix;
 
-  @Inject
+  @AssistedInject
   ChangeJson(
       Provider<ReviewDb> db,
       LabelNormalizer ln,
       Provider<CurrentUser> user,
       AnonymousUser au,
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
+      Submit submit,
       IdentifiedUser.GenericFactory uf,
       ChangeData.Factory cdf,
-      PatchSetInfoFactory psi,
       FileInfoJson fileInfoJson,
       AccountLoader.Factory ailf,
       DynamicMap<DownloadScheme> downloadSchemes,
       DynamicMap<DownloadCommand> downloadCommands,
       WebLinks webLinks,
       ChangeMessagesUtil cmUtil,
-      PatchLineCommentsUtil plcUtil,
       Provider<ConsistencyChecker> checkerProvider,
       ActionJson actionJson,
-      RebaseChange rebaseChange) {
+      GpgApiAdapter gpgApi,
+      @Assisted Set<ListChangesOption> options) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
     this.anonymous = au;
-    this.userFactory = uf;
     this.changeDataFactory = cdf;
-    this.patchSetInfoFactory = psi;
+    this.repoManager = repoManager;
+    this.userFactory = uf;
+    this.projectCache = projectCache;
+    this.submit = submit;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.fileInfoJson = fileInfoJson;
     this.accountLoaderFactory = ailf;
     this.downloadSchemes = downloadSchemes;
     this.downloadCommands = downloadCommands;
     this.webLinks = webLinks;
     this.cmUtil = cmUtil;
-    this.plcUtil = plcUtil;
     this.checkerProvider = checkerProvider;
     this.actionJson = actionJson;
-    this.rebaseChange = rebaseChange;
-    options = EnumSet.noneOf(ListChangesOption.class);
-  }
-
-  public ChangeJson addOption(ListChangesOption o) {
-    options.add(o);
-    return this;
-  }
-
-  public ChangeJson addOptions(Collection<ListChangesOption> o) {
-    options.addAll(o);
-    return this;
+    this.gpgApi = gpgApi;
+    this.options = options.isEmpty()
+        ? EnumSet.noneOf(ListChangesOption.class)
+        : EnumSet.copyOf(options);
   }
 
   public ChangeJson fix(FixInput fix) {
@@ -223,24 +229,42 @@
     return format(changeDataFactory.create(db.get(), c));
   }
 
-  public ChangeInfo format(ChangeData cd) throws OrmException {
-    return format(cd, Optional.<PatchSet.Id> absent());
+  public List<ChangeInfo> format(Collection<Change.Id> ids) throws OrmException {
+    List<ChangeData> changes = new ArrayList<>(ids.size());
+    List<ChangeInfo> ret = new ArrayList<>(ids.size());
+    ReviewDb reviewDb = db.get();
+    for (Change.Id id : ids) {
+      changes.add(changeDataFactory.create(reviewDb, id));
+    }
+    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    for (ChangeData cd : changes) {
+      ret.add(format(cd, Optional.<PatchSet.Id> absent(), false));
+    }
+    accountLoader.fill();
+    return ret;
   }
 
-  private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
+  public ChangeInfo format(ChangeData cd) throws OrmException {
+    return format(cd, Optional.<PatchSet.Id> absent(), true);
+  }
+
+  private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId,
+      boolean fillAccountLoader)
       throws OrmException {
     try {
-      accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-      Set<Change.Id> reviewed = Sets.newHashSet();
-      if (has(REVIEWED)) {
-        reviewed = loadReviewed(Collections.singleton(cd));
+      if (fillAccountLoader) {
+        accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+        ChangeInfo res = toChangeInfo(cd, limitToPsId);
+        accountLoader.fill();
+        return res;
+      } else {
+        return toChangeInfo(cd, limitToPsId);
       }
-      ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
-      accountLoader.fill();
-      return res;
-    } catch (OrmException | RuntimeException e) {
+    } catch (PatchListNotAvailableException | GpgException | OrmException
+        | IOException | RuntimeException e) {
       if (!has(CHECK)) {
-        throw e;
+        Throwables.propagateIfPossible(e, OrmException.class);
+        throw new OrmException(e);
       }
       return checkOnly(cd);
     }
@@ -248,36 +272,25 @@
 
   public ChangeInfo format(RevisionResource rsrc) throws OrmException {
     ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
-    return format(cd, Optional.of(rsrc.getPatchSet().getId()));
+    return format(cd, Optional.of(rsrc.getPatchSet().getId()), true);
   }
 
   public List<List<ChangeInfo>> formatQueryResults(List<QueryResult> in)
       throws OrmException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    Iterable<ChangeData> all = FluentIterable.from(in)
+    ensureLoaded(FluentIterable.from(in)
         .transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
           @Override
           public List<ChangeData> apply(QueryResult in) {
             return in.changes();
           }
-        });
-    ChangeData.ensureChangeLoaded(all);
-    if (has(ALL_REVISIONS)) {
-      ChangeData.ensureAllPatchSetsLoaded(all);
-    } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
-      ChangeData.ensureCurrentPatchSetLoaded(all);
-    }
-    Set<Change.Id> reviewed = Sets.newHashSet();
-    if (has(REVIEWED)) {
-      reviewed = loadReviewed(all);
-    }
-    ChangeData.ensureCurrentApprovalsLoaded(all);
+        }));
 
     List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
     Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
     for (QueryResult r : in) {
-      List<ChangeInfo> infos = toChangeInfo(out, r.changes(), reviewed);
-      if (r.moreChanges()) {
+      List<ChangeInfo> infos = toChangeInfo(out, r.changes());
+      if (!infos.isEmpty() && r.moreChanges()) {
         infos.get(infos.size() - 1)._moreChanges = true;
       }
       res.add(infos);
@@ -286,19 +299,45 @@
     return res;
   }
 
+  public List<ChangeInfo> formatChangeDatas(Collection<ChangeData> in)
+      throws OrmException {
+    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    ensureLoaded(in);
+    List<ChangeInfo> out = new ArrayList<>(in.size());
+    for (ChangeData cd : in) {
+      out.add(format(cd));
+    }
+    accountLoader.fill();
+    return out;
+  }
+
+  private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
+    ChangeData.ensureChangeLoaded(all);
+    if (has(ALL_REVISIONS)) {
+      ChangeData.ensureAllPatchSetsLoaded(all);
+    } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
+      ChangeData.ensureCurrentPatchSetLoaded(all);
+    }
+    if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
+      ChangeData.ensureReviewedByLoadedForOpenChanges(all);
+    }
+    ChangeData.ensureCurrentApprovalsLoaded(all);
+  }
+
   private boolean has(ListChangesOption option) {
     return options.contains(option);
   }
 
   private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out,
-      List<ChangeData> changes, Set<Change.Id> reviewed) {
+      List<ChangeData> changes) {
     List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
     for (ChangeData cd : changes) {
       ChangeInfo i = out.get(cd.getId());
       if (i == null) {
         try {
-          i = toChangeInfo(cd, reviewed, Optional.<PatchSet.Id> absent());
-        } catch (OrmException | RuntimeException e) {
+          i = toChangeInfo(cd, Optional.<PatchSet.Id> absent());
+        } catch (PatchListNotAvailableException | GpgException | OrmException
+            | IOException | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
           } else {
@@ -340,8 +379,9 @@
     return info;
   }
 
-  private ChangeInfo toChangeInfo(ChangeData cd, Set<Change.Id> reviewed,
-      Optional<PatchSet.Id> limitToPsId) throws OrmException {
+  private ChangeInfo toChangeInfo(ChangeData cd,
+      Optional<PatchSet.Id> limitToPsId) throws PatchListNotAvailableException,
+      GpgException, OrmException, IOException {
     ChangeInfo out = new ChangeInfo();
 
     if (has(CHECK)) {
@@ -356,7 +396,8 @@
     }
 
     Change in = cd.change();
-    ChangeControl ctl = cd.changeControl().forUser(userProvider.get());
+    CurrentUser user = userProvider.get();
+    ChangeControl ctl = cd.changeControl().forUser(user);
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
     out.topic = in.getTopic();
@@ -366,6 +407,7 @@
     // the response and avoid making a request to /submit_type from the UI.
     out.mergeable = in.getStatus() == Change.Status.MERGED
         ? null : cd.isMergeable();
+    out.submittable = submit.submittable(cd);
     ChangedLines changedLines = cd.changedLines();
     if (changedLines != null) {
       out.insertions = changedLines.insertions;
@@ -377,12 +419,13 @@
     out.created = in.getCreatedOn();
     out.updated = in.getLastUpdatedOn();
     out._number = in.getId().get();
-    out.starred = userProvider.get().getStarredChanges().contains(in.getId())
+    out.starred = user.getStarredChanges().contains(in.getId())
         ? true
         : null;
-    out.reviewed = in.getStatus().isOpen()
-        && has(REVIEWED)
-        && reviewed.contains(cd.getId()) ? true : null;
+    if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
+      Account.Id accountId = user.getAccountId();
+      out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
+    }
 
     out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS));
 
@@ -412,7 +455,7 @@
     finish(out);
 
     if (needRevisions) {
-      out.revisions = revisions(ctl, cd, src);
+      out.revisions = revisions(ctl, src);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -579,6 +622,12 @@
         PatchSetApproval psa = current.get(accountId, lt.getName());
         if (psa != null) {
           value = Integer.valueOf(psa.getValue());
+          if (value == 0) {
+            // This may be a dummy approval that was inserted when the reviewer
+            // was added. Explicitly check whether the user can vote on this
+            // label.
+            value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
+          }
           date = psa.getGranted();
         } else {
           // Either the user cannot vote on this label, or they were added as a
@@ -735,14 +784,6 @@
       return Collections.emptyList();
     }
 
-    // chronological order
-    Collections.sort(messages, new Comparator<ChangeMessage>() {
-      @Override
-      public int compare(ChangeMessage a, ChangeMessage b) {
-        return a.getWrittenOn().compareTo(b.getWrittenOn());
-      }
-    });
-
     List<ChangeMessageInfo> result =
         Lists.newArrayListWithCapacity(messages.size());
     for (ChangeMessage message : messages) {
@@ -787,57 +828,15 @@
     return result;
   }
 
-  private Set<Change.Id> loadReviewed(Iterable<ChangeData> all)
-      throws OrmException {
-    Set<Change.Id> reviewed = Sets.newHashSet();
-    if (userProvider.get().isIdentifiedUser()) {
-      Account.Id self = ((IdentifiedUser) userProvider.get()).getAccountId();
-      for (List<ChangeData> batch : Iterables.partition(all, 50)) {
-        List<List<ChangeMessage>> m =
-            Lists.newArrayListWithCapacity(batch.size());
-        for (ChangeData cd : batch) {
-          PatchSet.Id ps = cd.change().currentPatchSetId();
-          if (ps != null && cd.change().getStatus().isOpen()) {
-            m.add(cmUtil.byPatchSet(db.get(), cd.notes(), ps));
-          } else {
-            m.add(NO_MESSAGES);
-          }
-        }
-        for (int i = 0; i < m.size(); i++) {
-          if (isChangeReviewed(self, batch.get(i), m.get(i))) {
-            reviewed.add(batch.get(i).getId());
-          }
-        }
-      }
-    }
-    return reviewed;
-  }
-
-  private boolean isChangeReviewed(Account.Id self, ChangeData cd,
-      List<ChangeMessage> msgs) throws OrmException {
-    // Sort messages to keep the most recent ones at the beginning.
-    msgs = ChangeNotes.MESSAGE_BY_TIME.sortedCopy(msgs);
-    Collections.reverse(msgs);
-
-    Account.Id changeOwnerId = cd.change().getOwner();
-    for (ChangeMessage cm : msgs) {
-      if (self.equals(cm.getAuthor())) {
-        return true;
-      } else if (changeOwnerId.equals(cm.getAuthor())) {
-        return false;
-      }
-    }
-    return false;
-  }
-
-  private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
-      Map<PatchSet.Id, PatchSet> map) throws OrmException {
+  private Map<String, RevisionInfo> revisions(ChangeControl ctl,
+      Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
+      GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
     for (PatchSet in : map.values()) {
       if ((has(ALL_REVISIONS)
-          || in.getId().equals(cd.change().currentPatchSetId()))
+          || in.getId().equals(ctl.getChange().currentPatchSetId()))
           && ctl.isPatchVisible(in, db.get())) {
-        res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in));
+        res.put(in.getRevision().get(), toRevisionInfo(ctl, in));
       }
     }
     return res;
@@ -847,11 +846,11 @@
       Optional<PatchSet.Id> limitToPsId) throws OrmException {
     Collection<PatchSet> src;
     if (has(ALL_REVISIONS) || has(MESSAGES)) {
-      src = cd.patches();
+      src = cd.patchSets();
     } else {
       PatchSet ps;
       if (limitToPsId.isPresent()) {
-        ps = cd.patch(limitToPsId.get());
+        ps = cd.patchSet(limitToPsId.get());
         if (ps == null) {
           throw new OrmException("missing patch set " + limitToPsId.get());
         }
@@ -871,10 +870,12 @@
     return map;
   }
 
-  private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in) throws OrmException {
+  private RevisionInfo toRevisionInfo(ChangeControl ctl, PatchSet in)
+      throws PatchListNotAvailableException, GpgException, OrmException,
+      IOException {
+    Change c = ctl.getChange();
     RevisionInfo out = new RevisionInfo();
-    out.isCurrent = in.getId().equals(cd.change().currentPatchSetId());
+    out.isCurrent = in.getId().equals(c.currentPatchSetId());
     out._number = in.getId().get();
     out.ref = in.getRefName();
     out.created = in.getCreatedOn();
@@ -882,21 +883,30 @@
     out.draft = in.isDraft() ? true : null;
     out.fetch = makeFetchMap(ctl, in);
 
-    if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
-      try {
-        out.commit = toCommit(in, cd.change().getProject(), has(WEB_LINKS));
-      } catch (PatchSetInfoNotAvailableException e) {
-        throw new OrmException(e);
+    boolean setCommit = has(ALL_COMMITS)
+        || (out.isCurrent && has(CURRENT_COMMIT));
+    boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
+    if (setCommit || addFooters) {
+      Project.NameKey project = c.getProject();
+      try (Repository repo = repoManager.openRepository(project);
+          RevWalk rw = new RevWalk(repo)) {
+        String rev = in.getRevision().get();
+        RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+        rw.parseBody(commit);
+        if (setCommit) {
+          out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS));
+        }
+        if (addFooters) {
+          out.commitWithFooters = mergeUtilFactory
+              .create(projectCache.get(project))
+              .createCherryPickCommitMessage(commit, ctl, in.getId());
+        }
       }
     }
 
     if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
-      try {
-        out.files = fileInfoJson.toFileInfoMap(cd.change(), in);
-        out.files.remove(Patch.COMMIT_MSG);
-      } catch (PatchListNotAvailableException e) {
-        throw new OrmException(e);
-      }
+      out.files = fileInfoJson.toFileInfoMap(c, in);
+      out.files.remove(Patch.COMMIT_MSG);
     }
 
     if ((out.isCurrent || (out.draft != null && out.draft))
@@ -904,49 +914,51 @@
         && userProvider.get().isIdentifiedUser()) {
 
       actionJson.addRevisionActions(out,
-          new RevisionResource(new ChangeResource(ctl, rebaseChange), in));
+          new RevisionResource(new ChangeResource(ctl), in));
     }
 
-    if (has(DRAFT_COMMENTS)
-        && userProvider.get().isIdentifiedUser()) {
-      IdentifiedUser user = (IdentifiedUser)userProvider.get();
-      out.hasDraftComments =
-          plcUtil.draftByPatchSetAuthor(db.get(), in.getId(),
-              user.getAccountId(), ctl.getNotes()).iterator().hasNext()
-          ? true
-          : null;
+    if (has(PUSH_CERTIFICATES)) {
+      if (in.getPushCertificate() != null) {
+        out.pushCertificate = gpgApi.checkPushCertificate(
+            in.getPushCertificate(),
+            userFactory.create(db, in.getUploader()));
+      } else {
+        out.pushCertificate = new PushCertificateInfo();
+      }
     }
+
     return out;
   }
 
-  CommitInfo toCommit(PatchSet in, Project.NameKey project, boolean addLinks)
-      throws PatchSetInfoNotAvailableException {
-    PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
-    CommitInfo commit = new CommitInfo();
-    commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
-    commit.author = toGitPerson(info.getAuthor());
-    commit.committer = toGitPerson(info.getCommitter());
-    commit.subject = info.getSubject();
-    commit.message = info.getMessage();
+  CommitInfo toCommit(ChangeControl ctl, RevWalk rw, RevCommit commit,
+      boolean addLinks) throws IOException {
+    Project.NameKey project = ctl.getChange().getProject();
+    CommitInfo info = new CommitInfo();
+    info.parents = new ArrayList<>(commit.getParentCount());
+    info.author = toGitPerson(commit.getAuthorIdent());
+    info.committer = toGitPerson(commit.getCommitterIdent());
+    info.subject = commit.getShortMessage();
+    info.message = commit.getFullMessage();
 
     if (addLinks) {
       FluentIterable<WebLinkInfo> links =
-          webLinks.getPatchSetLinks(project, in.getRevision().get());
-      commit.webLinks = links.isEmpty() ? null : links.toList();
+          webLinks.getPatchSetLinks(project, commit.name());
+      info.webLinks = links.isEmpty() ? null : links.toList();
     }
 
-    for (ParentInfo parent : info.getParents()) {
+    for (RevCommit parent : commit.getParents()) {
+      rw.parseBody(parent);
       CommitInfo i = new CommitInfo();
-      i.commit = parent.id.get();
-      i.subject = parent.shortMessage;
+      i.commit = parent.name();
+      i.subject = parent.getShortMessage();
       if (addLinks) {
         FluentIterable<WebLinkInfo> parentLinks =
-            webLinks.getPatchSetLinks(project, parent.id.get());
+            webLinks.getPatchSetLinks(project, parent.name());
         i.webLinks = parentLinks.isEmpty() ? null : parentLinks.toList();
       }
-      commit.parents.add(i);
+      info.parents.add(i);
     }
-    return commit;
+    return info;
   }
 
   private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, PatchSet in)
@@ -1002,15 +1014,6 @@
     fetchInfo.commands.put(commandName, c);
   }
 
-  private static GitPerson toGitPerson(UserIdentity committer) {
-    GitPerson p = new GitPerson();
-    p.name = committer.getName();
-    p.email = committer.getEmail();
-    p.date = committer.getDate();
-    p.tz = committer.getTimeZone();
-    return p;
-  }
-
   static void finish(ChangeInfo info) {
     info.id = Joiner.on('~').join(
         Url.encode(info.project),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
index d22d6ff..6e6f6fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
@@ -26,5 +26,5 @@
   NO_CODE_CHANGE,
 
   /** Same tree, parent tree, same commit message. */
-  NO_CHANGE;
+  NO_CHANGE
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index a2b78aa..389d53a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -19,8 +19,7 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -28,6 +27,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -35,12 +35,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -54,6 +54,7 @@
 import java.io.Serializable;
 import java.util.Collection;
 import java.util.Objects;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
 public class ChangeKindCacheImpl implements ChangeKindCache {
@@ -69,8 +70,7 @@
         bind(ChangeKindCache.class).to(ChangeKindCacheImpl.class);
         persist(ID_CACHE, Key.class, ChangeKind.class)
             .maximumWeight(2 << 20)
-            .weigher(ChangeKindWeigher.class)
-            .loader(Loader.class);
+            .weigher(ChangeKindWeigher.class);
       }
     };
   }
@@ -99,8 +99,8 @@
     public ChangeKind getChangeKind(ProjectState project, Repository repo,
         ObjectId prior, ObjectId next) {
       try {
-        return new Loader().load(
-            new Key(repo, prior, next, useRecursiveMerge));
+        Key key = new Key(prior, next, useRecursiveMerge);
+        return new Loader(key, repo).call();
       } catch (IOException e) {
         log.warn("Cannot check trivial rebase of new patch set " + next.name()
             + " in " + project.getProject().getName(), e);
@@ -122,17 +122,13 @@
     private transient ObjectId next;
     private transient String strategyName;
 
-    private transient Repository repo; // Passed through to loader on miss.
-
-    private Key(Repository repo, ObjectId prior,
-        ObjectId next, boolean useRecursiveMerge) {
+    private Key(ObjectId prior, ObjectId next, boolean useRecursiveMerge) {
       checkNotNull(next, "next");
       String strategyName = MergeUtil.mergeStrategyName(
           true, useRecursiveMerge);
       this.prior = prior.copy();
       this.next = next.copy();
       this.strategyName = strategyName;
-      this.repo = repo;
     }
 
     public Key(ObjectId prior, ObjectId next, String strategyName) {
@@ -182,15 +178,22 @@
     }
   }
 
-  @Singleton
-  private static class Loader extends CacheLoader<Key, ChangeKind> {
+  private static class Loader implements Callable<ChangeKind> {
+    private final Key key;
+    private final Repository repo;
+
+    private Loader(Key key, Repository repo) {
+      this.key = key;
+      this.repo = repo;
+    }
+
     @Override
-    public ChangeKind load(Key key) throws IOException {
+    public ChangeKind call() throws IOException {
       if (Objects.equals(key.prior, key.next)) {
         return ChangeKind.NO_CODE_CHANGE;
       }
 
-      try (RevWalk walk = new RevWalk(key.repo)) {
+      try (RevWalk walk = new RevWalk(repo)) {
         RevCommit prior = walk.parseCommit(key.prior);
         walk.parseBody(prior);
         RevCommit next = walk.parseCommit(key.next);
@@ -216,10 +219,10 @@
         // A trivial rebase can be detected by looking for the next commit
         // having the same tree as would exist when the prior commit is
         // cherry-picked onto the next commit's new first parent.
-        ThreeWayMerger merger = MergeUtil.newThreeWayMerger(
-            key.repo, MergeUtil.createDryRunInserter(key.repo), key.strategyName);
-        merger.setBase(prior.getParent(0));
-        try {
+        try (ObjectInserter ins = new InMemoryInserter(repo)) {
+          ThreeWayMerger merger =
+              MergeUtil.newThreeWayMerger(repo, ins, key.strategyName);
+          merger.setBase(prior.getParent(0));
           if (merger.merge(next.getParent(0), prior)
               && merger.getResultTreeId().equals(next.getTree())) {
             return ChangeKind.TRIVIAL_REBASE;
@@ -229,8 +232,6 @@
           // it was a rework.
         }
         return ChangeKind.REWORK;
-      } finally {
-        key.repo = null;
       }
     }
 
@@ -264,7 +265,7 @@
     }
   }
 
-  private final LoadingCache<Key, ChangeKind> cache;
+  private final Cache<Key, ChangeKind> cache;
   private final boolean useRecursiveMerge;
   private final ChangeData.Factory changeDataFactory;
   private final ProjectCache projectCache;
@@ -273,7 +274,7 @@
   @Inject
   ChangeKindCacheImpl(
       @GerritServerConfig Config serverConfig,
-      @Named(ID_CACHE) LoadingCache<Key, ChangeKind> cache,
+      @Named(ID_CACHE) Cache<Key, ChangeKind> cache,
       ChangeData.Factory changeDataFactory,
       ProjectCache projectCache,
       GitRepositoryManager repoManager) {
@@ -288,7 +289,8 @@
   public ChangeKind getChangeKind(ProjectState project, Repository repo,
       ObjectId prior, ObjectId next) {
     try {
-      return cache.get(new Key(repo, prior, next, useRecursiveMerge));
+      Key key = new Key(prior, next, useRecursiveMerge);
+      return cache.get(key, new Loader(key, repo));
     } catch (ExecutionException e) {
       log.warn("Cannot check trivial rebase of new patch set " + next.name()
           + " in " + project.getProject().getName(), e);
@@ -310,19 +312,15 @@
       ChangeData.Factory changeDataFactory,
       ProjectCache projectCache,
       GitRepositoryManager repoManager) {
-    Repository repo = null;
     // TODO - dborowitz: add NEW_CHANGE type for default.
     ChangeKind kind = ChangeKind.REWORK;
     // Trivial case: if we're on the first patch, we don't need to open
     // the repository.
     if (patch.getId().get() > 1) {
-      try {
+      try (Repository repo = repoManager.openRepository(change.getProject())) {
         ProjectState projectState = projectCache.checkedGet(change.getProject());
-
-        repo = repoManager.openRepository(change.getProject());
-
         ChangeData cd = changeDataFactory.create(db, change);
-        Collection<PatchSet> patchSetCollection = cd.patches();
+        Collection<PatchSet> patchSetCollection = cd.patchSets();
         PatchSet priorPs = patch;
         for (PatchSet ps : patchSetCollection) {
           if (ps.getId().get() < patch.getId().get() &&
@@ -347,10 +345,6 @@
         // Do nothing; assume we have a complex change
         log.warn("Unable to get change kind for patchSet " + patch.getPatchSetId() +
             "of change " + change.getChangeId(), e);
-      } finally {
-        if (repo != null) {
-          repo.close();
-        }
       }
     }
     return kind;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index ff4a9f7..03d189f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -20,10 +20,9 @@
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
@@ -37,16 +36,13 @@
       new TypeLiteral<RestView<ChangeResource>>() {};
 
   private final ChangeControl control;
-  private final RebaseChange rebaseChange;
 
-  public ChangeResource(ChangeControl control, RebaseChange rebaseChange) {
+  public ChangeResource(ChangeControl control) {
     this.control = control;
-    this.rebaseChange = rebaseChange;
   }
 
   protected ChangeResource(ChangeResource copy) {
     this.control = copy.control;
-    this.rebaseChange = copy.rebaseChange;
   }
 
   public ChangeControl getControl() {
@@ -61,17 +57,18 @@
     return getControl().getNotes();
   }
 
-  @Override
-  public String getETag() {
-    CurrentUser user = control.getCurrentUser();
-    Hasher h = Hashing.md5().newHasher()
-      .putLong(getChange().getLastUpdatedOn().getTime())
+  // This includes all information relevant for ETag computation
+  // unrelated to the UI.
+  public void prepareETag(Hasher h, CurrentUser user) {
+    h.putLong(getChange().getLastUpdatedOn().getTime())
       .putInt(getChange().getRowVersion())
-      .putBoolean(user.getStarredChanges().contains(getChange().getId()))
-      .putInt(user.isIdentifiedUser()
-          ? ((IdentifiedUser) user).getAccountId().get()
-          : 0)
-      .putBoolean(rebaseChange != null && rebaseChange.canRebase(this));
+      .putInt(user.isIdentifiedUser() ? user.getAccountId().get() : 0);
+
+    if (user.isIdentifiedUser()) {
+      for (AccountGroup.UUID uuid : user.getEffectiveGroups().getKnownGroups()) {
+        h.putBytes(uuid.get().getBytes());
+      }
+    }
 
     byte[] buf = new byte[20];
     ObjectId noteId;
@@ -87,6 +84,14 @@
     for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
       hashObjectId(h, p.getConfig().getRevision(), buf);
     }
+  }
+
+  @Override
+  public String getETag() {
+    CurrentUser user = control.getUser();
+    Hasher h = Hashing.md5().newHasher()
+        .putBoolean(user.getStarredChanges().contains(getChange().getId()));
+    prepareETag(h, user);
     return h.hash().toString();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
index 45bb1d4..7069e6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -21,8 +21,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 
-import org.eclipse.jgit.lib.Constants;
-
 @AutoValue
 public abstract class ChangeTriplet {
   public static String format(Change change) {
@@ -53,10 +51,6 @@
     String branch = Url.decode(triplet.substring(t1 + 1, t2));
     String changeId = Url.decode(triplet.substring(t2 + 1));
 
-    if (!branch.startsWith(Constants.R_REFS)) {
-      branch = Constants.R_HEADS + branch;
-    }
-
     ChangeTriplet result = new AutoValue_ChangeTriplet(
         new Branch.NameKey(new Project.NameKey(project), branch),
         new Change.Key(changeId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 42f16a3..1648a5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -50,7 +49,6 @@
   private final ChangeUtil changeUtil;
   private final CreateChange createChange;
   private final ChangeIndexer changeIndexer;
-  private final RebaseChange rebaseChange;
 
   @Inject
   ChangesCollection(
@@ -60,8 +58,7 @@
       DynamicMap<RestView<ChangeResource>> views,
       ChangeUtil changeUtil,
       CreateChange createChange,
-      ChangeIndexer changeIndexer,
-      RebaseChange rebaseChange) {
+      ChangeIndexer changeIndexer) {
     this.user = user;
     this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
@@ -69,7 +66,6 @@
     this.changeUtil = changeUtil;
     this.createChange = createChange;
     this.changeIndexer = changeIndexer;
-    this.rebaseChange = rebaseChange;
   }
 
   @Override
@@ -106,7 +102,7 @@
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(id);
     }
-    return new ChangeResource(control, rebaseChange);
+    return new ChangeResource(control);
   }
 
   public ChangeResource parse(Change.Id id)
@@ -116,7 +112,7 @@
   }
 
   public ChangeResource parse(ChangeControl control) {
-    return new ChangeResource(control, rebaseChange);
+    return new ChangeResource(control);
   }
 
   @SuppressWarnings("unchecked")
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index 5c9fe91..d7fe43b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -25,19 +25,20 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import java.util.EnumSet;
+
 public class Check implements RestReadView<ChangeResource>,
     RestModifyView<ChangeResource, FixInput> {
-  private final ChangeJson json;
+  private final ChangeJson.Factory jsonFactory;
 
   @Inject
-  Check(ChangeJson json) {
-    this.json = json;
-    json.addOption(ListChangesOption.CHECK);
+  Check(ChangeJson.Factory json) {
+    this.jsonFactory = json;
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.format(rsrc));
+    return Response.withMustRevalidate(newChangeJson().format(rsrc));
   }
 
   @Override
@@ -46,9 +47,13 @@
     ChangeControl ctl = rsrc.getControl();
     if (!ctl.isOwner()
         && !ctl.getProjectControl().isOwner()
-        && !ctl.getCurrentUser().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("Not owner");
+        && !ctl.getUser().getCapabilities().canMaintainServer()) {
+      throw new AuthException("Cannot fix change");
     }
-    return Response.withMustRevalidate(json.fix(input).format(rsrc));
+    return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc));
+  }
+
+  private ChangeJson newChangeJson() {
+    return jsonFactory.create(EnumSet.of(ListChangesOption.CHECK));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index efcd6d9..82aaecb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -14,18 +14,19 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -42,12 +43,12 @@
     UiAction<RevisionResource> {
   private final Provider<ReviewDb> dbProvider;
   private final CherryPickChange cherryPickChange;
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
 
   @Inject
   CherryPick(Provider<ReviewDb> dbProvider,
       CherryPickChange cherryPickChange,
-      ChangeJson json) {
+      ChangeJson.Factory json) {
     this.dbProvider = dbProvider;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
@@ -55,8 +56,7 @@
 
   @Override
   public ChangeInfo apply(RevisionResource revision, CherryPickInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, OrmException, IOException, EmailException {
+      throws OrmException, IOException, UpdateException, RestApiException {
     final ChangeControl control = revision.getControl();
 
     if (input.message == null || input.message.trim().isEmpty()) {
@@ -65,16 +65,13 @@
       throw new BadRequestException("destination must be non-empty");
     }
 
+    @SuppressWarnings("resource")
     ReviewDb db = dbProvider.get();
     if (!control.isVisible(db)) {
       throw new AuthException("Cherry pick not permitted");
     }
 
-    String refName = input.destination;
-    if (!refName.startsWith("refs/")) {
-      refName = "refs/heads/" + input.destination;
-    }
-
+    String refName = RefNames.fullName(input.destination);
     RefControl refControl = control.getProjectControl().controlForRef(refName);
     if (!refControl.canUpload()) {
       throw new AuthException("Not allowed to cherry pick "
@@ -85,15 +82,13 @@
     try {
       Change.Id cherryPickedChangeId =
           cherryPickChange.cherryPick(revision.getChange(),
-              revision.getPatchSet(), input.message, input.destination,
+              revision.getPatchSet(), input.message, refName,
               refControl);
-      return json.format(cherryPickedChangeId);
+      return json.create(ChangeJson.NO_OPTIONS).format(cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
-    } catch (MergeException e) {
+    } catch (IntegrationException | NoSuchChangeException e) {
       throw new ResourceConflictException(e.getMessage());
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException(e.getMessage());
     }
   }
 
@@ -102,6 +97,7 @@
     return new UiAction.Description()
       .setLabel("Cherry Pick")
       .setTitle("Cherry pick change to a different branch")
-      .setVisible(resource.getControl().getProjectControl().canUpload());
+      .setVisible(resource.getControl().getProjectControl().canUpload()
+          && resource.isCurrent());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index b386894..fc4893c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -14,26 +14,30 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
@@ -43,7 +47,6 @@
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -56,11 +59,8 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 import java.io.IOException;
@@ -74,64 +74,66 @@
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitRepositoryManager gitManager;
   private final TimeZone serverTimeZone;
-  private final Provider<CurrentUser> currentUser;
-  private final CommitValidators.Factory commitValidatorsFactory;
+  private final Provider<IdentifiedUser> user;
   private final ChangeInserter.Factory changeInserterFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final ChangeUpdate.Factory updateFactory;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
   CherryPickChange(Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
       @GerritPersonIdent PersonIdent myIdent,
       GitRepositoryManager gitManager,
-      Provider<CurrentUser> currentUser,
-      CommitValidators.Factory commitValidatorsFactory,
+      Provider<IdentifiedUser> user,
       ChangeInserter.Factory changeInserterFactory,
       PatchSetInserter.Factory patchSetInserterFactory,
       MergeUtil.Factory mergeUtilFactory,
       ChangeMessagesUtil changeMessagesUtil,
-      ChangeUpdate.Factory updateFactory) {
+      ChangeUpdate.Factory updateFactory,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.db = db;
     this.queryProvider = queryProvider;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
-    this.currentUser = currentUser;
-    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.user = user;
     this.changeInserterFactory = changeInserterFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeMessagesUtil = changeMessagesUtil;
     this.updateFactory = updateFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   public Change.Id cherryPick(Change change, PatchSet patch,
-      final String message, final String destinationBranch,
+      final String message, final String ref,
       final RefControl refControl) throws NoSuchChangeException,
       OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException, MergeException {
+      InvalidChangeOperationException, IntegrationException, UpdateException,
+      RestApiException {
 
-    if (destinationBranch == null || destinationBranch.length() == 0) {
+    if (Strings.isNullOrEmpty(ref)) {
       throw new InvalidChangeOperationException(
           "Cherry Pick: Destination branch cannot be null or empty");
     }
 
     Project.NameKey project = change.getProject();
-    IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
+    String destinationBranch = RefNames.shortName(ref);
+    IdentifiedUser identifiedUser = user.get();
     try (Repository git = gitManager.openRepository(project);
-        RevWalk revWalk = new RevWalk(git)) {
-      Ref destRef = git.getRef(destinationBranch);
+        CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(git)) {
+      Ref destRef = git.getRefDatabase().exactRef(ref);
       if (destRef == null) {
-        throw new InvalidChangeOperationException("Branch "
-            + destinationBranch + " does not exist.");
+        throw new InvalidChangeOperationException(String.format(
+            "Branch %s does not exist.", destinationBranch));
       }
 
-      final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
+      CodeReviewCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
 
-      RevCommit commitToCherryPick =
+      CodeReviewCommit commitToCherryPick =
           revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
       PersonIdent committerIdent =
@@ -145,127 +147,115 @@
       String commitMessage =
           ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
 
-      RevCommit cherryPickCommit;
+      CodeReviewCommit cherryPickCommit;
       try (ObjectInserter oi = git.newObjectInserter()) {
         ProjectState projectState = refControl.getProjectControl().getProjectState();
         cherryPickCommit =
             mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
                 commitToCherryPick, committerIdent, commitMessage, revWalk);
+
+        Change.Key changeKey;
+        final List<String> idList = cherryPickCommit.getFooterLines(
+            FooterConstants.CHANGE_ID);
+        if (!idList.isEmpty()) {
+          final String idStr = idList.get(idList.size() - 1).trim();
+          changeKey = new Change.Key(idStr);
+        } else {
+          changeKey = new Change.Key("I" + computedChangeId.name());
+        }
+
+        Branch.NameKey newDest =
+            new Branch.NameKey(change.getProject(), destRef.getName());
+        List<ChangeData> destChanges = queryProvider.get()
+            .setLimit(2)
+            .byBranchKey(newDest, changeKey);
+        if (destChanges.size() > 1) {
+          throw new InvalidChangeOperationException("Several changes with key "
+              + changeKey + " reside on the same branch. "
+              + "Cannot create a new patch set.");
+        } else if (destChanges.size() == 1) {
+          // The change key exists on the destination branch. The cherry pick
+          // will be added as a new patch set.
+          return insertPatchSet(git, revWalk, oi, destChanges.get(0).change(),
+              cherryPickCommit, refControl, identifiedUser);
+        } else {
+          // Change key not found on destination branch. We can create a new
+          // change.
+          String newTopic = null;
+          if (!Strings.isNullOrEmpty(change.getTopic())) {
+            newTopic = change.getTopic() + "-" + newDest.getShortName();
+          }
+          Change newChange = createNewChange(git, revWalk, oi, changeKey,
+              project, destRef, cherryPickCommit, refControl, identifiedUser,
+              newTopic, change.getDest());
+
+          addMessageToSourceChange(change, patch.getId(), destinationBranch,
+              cherryPickCommit, identifiedUser, refControl);
+
+          return newChange.getId();
+        }
       } catch (MergeIdenticalTreeException | MergeConflictException e) {
-        throw new MergeException("Cherry pick failed: " + e.getMessage());
-      }
-
-      Change.Key changeKey;
-      final List<String> idList = cherryPickCommit.getFooterLines(
-          FooterConstants.CHANGE_ID);
-      if (!idList.isEmpty()) {
-        final String idStr = idList.get(idList.size() - 1).trim();
-        changeKey = new Change.Key(idStr);
-      } else {
-        changeKey = new Change.Key("I" + computedChangeId.name());
-      }
-
-      Branch.NameKey newDest =
-          new Branch.NameKey(change.getProject(), destRef.getName());
-      List<ChangeData> destChanges = queryProvider.get()
-          .setLimit(2)
-          .byBranchKey(newDest, changeKey);
-      if (destChanges.size() > 1) {
-        throw new InvalidChangeOperationException("Several changes with key "
-            + changeKey + " reside on the same branch. "
-            + "Cannot create a new patch set.");
-      } else if (destChanges.size() == 1) {
-        // The change key exists on the destination branch. The cherry pick
-        // will be added as a new patch set.
-        return insertPatchSet(git, revWalk, destChanges.get(0).change(),
-            cherryPickCommit, refControl, identifiedUser);
-      } else {
-        // Change key not found on destination branch. We can create a new
-        // change.
-        Change newChange = createNewChange(git, revWalk, changeKey, project,
-            destRef, cherryPickCommit, refControl,
-            identifiedUser, change.getTopic());
-
-        addMessageToSourceChange(change, patch.getId(), destinationBranch,
-            cherryPickCommit, identifiedUser, refControl);
-
-        addMessageToDestinationChange(newChange, change.getDest().getShortName(),
-            identifiedUser, refControl);
-
-        return newChange.getId();
+        throw new IntegrationException("Cherry pick failed: " + e.getMessage());
       }
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(change.getId(), e);
     }
   }
 
-  private Change.Id insertPatchSet(Repository git, RevWalk revWalk, Change change,
-      RevCommit cherryPickCommit, RefControl refControl,
-      IdentifiedUser identifiedUser)
-      throws InvalidChangeOperationException, IOException, OrmException,
-      NoSuchChangeException {
-    final ChangeControl changeControl =
-        refControl.getProjectControl().controlFor(change);
-    final PatchSetInserter inserter = patchSetInserterFactory
-        .create(git, revWalk, changeControl, cherryPickCommit);
-    final PatchSet.Id newPatchSetId = inserter.getPatchSetId();
+  private Change.Id insertPatchSet(Repository git, RevWalk revWalk,
+      ObjectInserter oi, Change change, CodeReviewCommit cherryPickCommit,
+      RefControl refControl, IdentifiedUser identifiedUser)
+      throws IOException, OrmException, UpdateException, RestApiException {
+    PatchSet.Id psId =
+        ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
+    PatchSetInserter inserter = patchSetInserterFactory
+        .create(refControl, psId, cherryPickCommit);
+    PatchSet.Id newPatchSetId = inserter.getPatchSetId();
     PatchSet current = db.get().patchSets().get(change.currentPatchSetId());
-    inserter
-      .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
-      .setDraft(current.isDraft())
-      .setUploader(identifiedUser.getAccountId())
-      .setSendMail(false)
-      .insert();
+
+    try (BatchUpdate bu = batchUpdateFactory.create(
+        db.get(), change.getDest().getParentKey(), identifiedUser,
+        TimeUtil.nowTs())) {
+      bu.setRepository(git, revWalk, oi);
+      bu.addOp(change.getId(), inserter
+          .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
+          .setDraft(current.isDraft())
+          .setUploader(identifiedUser.getAccountId())
+          .setSendMail(false));
+      bu.execute();
+    }
     return change.getId();
   }
 
   private Change createNewChange(Repository git, RevWalk revWalk,
-      Change.Key changeKey, Project.NameKey project,
-      Ref destRef, RevCommit cherryPickCommit, RefControl refControl,
-      IdentifiedUser identifiedUser, String topic)
-      throws OrmException, InvalidChangeOperationException, IOException {
+      ObjectInserter oi, Change.Key changeKey, Project.NameKey project,
+      Ref destRef, CodeReviewCommit cherryPickCommit, RefControl refControl,
+      IdentifiedUser identifiedUser, String topic, Branch.NameKey sourceBranch)
+      throws RestApiException, UpdateException, OrmException {
     Change change =
         new Change(changeKey, new Change.Id(db.get().nextChangeId()),
             identifiedUser.getAccountId(), new Branch.NameKey(project,
                 destRef.getName()), TimeUtil.nowTs());
     change.setTopic(topic);
-    ChangeInserter ins =
-        changeInserterFactory.create(refControl.getProjectControl(), change,
-            cherryPickCommit);
-    PatchSet newPatchSet = ins.getPatchSet();
+    ChangeInserter ins = changeInserterFactory.create(
+          refControl, change, cherryPickCommit)
+        .setValidatePolicy(CommitValidators.Policy.GERRIT);
 
-    CommitValidators commitValidators =
-        commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
-    CommitReceivedEvent commitReceivedEvent =
-        new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
-            cherryPickCommit.getId(), newPatchSet.getRefName()), refControl
-            .getProjectControl().getProject(), refControl.getRefName(),
-            cherryPickCommit, identifiedUser);
-
-    try {
-      commitValidators.validateForGerritCommits(commitReceivedEvent);
-    } catch (CommitValidationException e) {
-      throw new InvalidChangeOperationException(e.getMessage());
+    ins.setMessage(
+        messageForDestinationChange(ins.getPatchSet().getId(), sourceBranch));
+    try (BatchUpdate bu = batchUpdateFactory.create(
+        db.get(), change.getProject(), identifiedUser, TimeUtil.nowTs())) {
+      bu.setRepository(git, revWalk, oi);
+      bu.insertChange(ins);
+      bu.execute();
     }
-
-    final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(cherryPickCommit);
-    ru.disableRefLog();
-    if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-      throw new IOException(String.format(
-          "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
-          change.getDest().getParentKey().get(), ru.getResult()));
-    }
-
-    ins.insert();
-
-    return change;
+    return ins.getChange();
   }
 
   private void addMessageToSourceChange(Change change, PatchSet.Id patchSetId,
-      String destinationBranch, RevCommit cherryPickCommit,
-      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+      String destinationBranch, CodeReviewCommit cherryPickCommit,
+      IdentifiedUser identifiedUser, RefControl refControl)
+          throws OrmException, IOException {
     ChangeMessage changeMessage = new ChangeMessage(
         new ChangeMessage.Key(
             patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
@@ -281,28 +271,18 @@
     changeMessage.setMessage(sb.toString());
 
     ChangeControl ctl = refControl.getProjectControl().controlFor(change);
-    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+    ChangeUpdate update = updateFactory.create(ctl, TimeUtil.nowTs());
     changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+    update.commit();
   }
 
-  private void addMessageToDestinationChange(Change change, String sourceBranch,
-      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
-    PatchSet.Id patchSetId =
-        db.get().patchSets().get(change.currentPatchSetId()).getId();
-    ChangeMessage changeMessage = new ChangeMessage(
-        new ChangeMessage.Key(
-            patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
-            identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
-
-    StringBuilder sb = new StringBuilder("Patch Set ")
+  private String messageForDestinationChange(PatchSet.Id patchSetId,
+      Branch.NameKey sourceBranch) {
+    return new StringBuilder("Patch Set ")
       .append(patchSetId.get())
       .append(": Cherry Picked from branch ")
-      .append(sourceBranch)
-      .append(".");
-    changeMessage.setMessage(sb.toString());
-
-    ChangeControl ctl = refControl.getProjectControl().controlFor(change);
-    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
-    changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+      .append(sourceBranch.getShortName())
+      .append(".")
+      .toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index 4a52fc5..b155b84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.server.PatchLineCommentsUtil.COMMENT_INFO_ORDER;
 
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -26,45 +28,51 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
-@Singleton
 class CommentJson {
 
   private final AccountLoader.Factory accountLoaderFactory;
 
+  private boolean fillAccounts = true;
+  private boolean fillPatchSet;
+
   @Inject
   CommentJson(AccountLoader.Factory accountLoaderFactory) {
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
-  CommentInfo format(PatchLineComment c) throws OrmException {
-    return format(c, true);
+  CommentJson setFillAccounts(boolean fillAccounts) {
+    this.fillAccounts = fillAccounts;
+    return this;
   }
 
-  CommentInfo format(PatchLineComment c, boolean fill) throws OrmException {
+  CommentJson setFillPatchSet(boolean fillPatchSet) {
+    this.fillPatchSet = fillPatchSet;
+    return this;
+  }
+
+  CommentInfo format(PatchLineComment c) throws OrmException {
     AccountLoader loader = null;
-    if (fill) {
+    if (fillAccounts) {
       loader = accountLoaderFactory.create(true);
     }
     CommentInfo commentInfo = toCommentInfo(c, loader);
-    if (fill) {
+    if (fillAccounts) {
       loader.fill();
     }
     return commentInfo;
   }
 
-  Map<String, List<CommentInfo>> format(Iterable<PatchLineComment> l,
-      boolean fill) throws OrmException {
+  Map<String, List<CommentInfo>> format(Iterable<PatchLineComment> l)
+      throws OrmException {
     Map<String, List<CommentInfo>> out = new TreeMap<>();
-    AccountLoader accountLoader = fill
+    AccountLoader accountLoader = fillAccounts
         ? accountLoaderFactory.create(true)
         : null;
 
@@ -80,20 +88,7 @@
     }
 
     for (List<CommentInfo> list : out.values()) {
-      Collections.sort(list, new Comparator<CommentInfo>() {
-        @Override
-        public int compare(CommentInfo a, CommentInfo b) {
-          int c = firstNonNull(a.side, Side.REVISION).ordinal()
-                - firstNonNull(b.side, Side.REVISION).ordinal();
-          if (c == 0) {
-            c = firstNonNull(a.line, 0) - firstNonNull(b.line, 0);
-          }
-          if (c == 0) {
-            c = a.id.compareTo(b.id);
-          }
-          return c;
-        }
-      });
+      Collections.sort(list, COMMENT_INFO_ORDER);
     }
 
     if (accountLoader != null) {
@@ -103,8 +98,32 @@
     return out;
   }
 
+  List<CommentInfo> formatAsList(Iterable<PatchLineComment> l)
+      throws OrmException {
+    final AccountLoader accountLoader = fillAccounts
+        ? accountLoaderFactory.create(true)
+        : null;
+    List<CommentInfo> out = FluentIterable
+        .from(l)
+        .transform(new Function<PatchLineComment, CommentInfo>() {
+          @Override
+          public CommentInfo apply(PatchLineComment c) {
+            return toCommentInfo(c, accountLoader);
+          }
+        }).toSortedList(COMMENT_INFO_ORDER);
+
+    if (accountLoader != null) {
+      accountLoader.fill();
+    }
+
+    return out;
+  }
+
   private CommentInfo toCommentInfo(PatchLineComment c, AccountLoader loader) {
     CommentInfo r = new CommentInfo();
+    if (fillPatchSet) {
+      r.patchSet = c.getKey().getParentKey().getParentKey().get();
+    }
     r.id = Url.encode(c.getKey().get());
     r.path = c.getKey().getParentKey().getFileName();
     if (c.getSide() == 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
index eff408e..8f78f0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -31,13 +31,13 @@
 @Singleton
 public class Comments implements ChildCollection<RevisionResource, CommentResource> {
   private final DynamicMap<RestView<CommentResource>> views;
-  private final ListComments list;
+  private final ListRevisionComments list;
   private final Provider<ReviewDb> dbProvider;
   private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   Comments(DynamicMap<RestView<CommentResource>> views,
-      ListComments list, Provider<ReviewDb> dbProvider,
+      ListRevisionComments list, Provider<ReviewDb> dbProvider,
       PatchLineCommentsUtil plcUtil) {
     this.views = views;
     this.list = list;
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public ListComments list() {
+  public ListRevisionComments list() {
     return list;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index ee28ed2..ddeb5c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -14,27 +14,42 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
+import static com.google.gerrit.server.ChangeUtil.TO_PS_ID;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
+import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.ProblemInfo.Status;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -45,6 +60,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -60,6 +76,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Checks changes for various kinds of inconsistency and corruption.
@@ -93,13 +110,18 @@
   private final GitRepositoryManager repoManager;
   private final Provider<CurrentUser> user;
   private final Provider<PersonIdent> serverIdent;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final BatchUpdate.Factory updateFactory;
 
   private FixInput fix;
   private Change change;
   private Repository repo;
   private RevWalk rw;
 
+  private RevCommit tip;
+  private Multimap<ObjectId, PatchSet> patchSetsBySha;
   private PatchSet currPs;
   private RevCommit currPsCommit;
 
@@ -110,12 +132,18 @@
       GitRepositoryManager repoManager,
       Provider<CurrentUser> user,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      PatchSetInfoFactory patchSetInfoFactory) {
+      ProjectControl.GenericFactory projectControlFactory,
+      PatchSetInfoFactory patchSetInfoFactory,
+      PatchSetInserter.Factory patchSetInserterFactory,
+      BatchUpdate.Factory updateFactory) {
     this.db = db;
     this.repoManager = repoManager;
     this.user = user;
     this.serverIdent = serverIdent;
+    this.projectControlFactory = projectControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.updateFactory = updateFactory;
     reset();
   }
 
@@ -210,17 +238,6 @@
     }
   }
 
-  private static final Function<PatchSet, Integer> TO_PS_ID =
-      new Function<PatchSet, Integer>() {
-        @Override
-        public Integer apply(PatchSet in) {
-          return in.getId().get();
-        }
-      };
-
-  private static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
-    .onResultOf(TO_PS_ID);
-
   private boolean checkPatchSets() {
     List<PatchSet> all;
     try {
@@ -231,41 +248,47 @@
     // Iterate in descending order so deletePatchSet can assume the latest patch
     // set exists.
     Collections.sort(all, PS_ID_ORDER.reverse());
-    Multimap<ObjectId, PatchSet> bySha = MultimapBuilder.hashKeys(all.size())
+    patchSetsBySha = MultimapBuilder.hashKeys(all.size())
         .treeSetValues(PS_ID_ORDER)
         .build();
+
+    Map<String, Ref> refs;
+    try {
+    refs = repo.getRefDatabase().exactRef(
+        Lists.transform(all, new Function<PatchSet, String>() {
+          @Override
+          public String apply(PatchSet ps) {
+            return ps.getId().toRefName();
+          }
+        }).toArray(new String[all.size()]));
+    } catch (IOException e) {
+      error("error reading refs", e);
+      refs = Collections.emptyMap();
+    }
+
     for (PatchSet ps : all) {
       // Check revision format.
-      ObjectId objId;
-      String rev = ps.getRevision().get();
       int psNum = ps.getId().get();
       String refName = ps.getId().toRefName();
-      try {
-        objId = ObjectId.fromString(rev);
-      } catch (IllegalArgumentException e) {
-        error(String.format("Invalid revision on patch set %d: %s", psNum, rev),
-            e);
+      ObjectId objId =
+          parseObjectId(ps.getRevision().get(), "patch set " + psNum);
+      if (objId == null) {
         continue;
       }
-      bySha.put(objId, ps);
+      patchSetsBySha.put(objId, ps);
 
       // Check ref existence.
       ProblemInfo refProblem = null;
-      try {
-        Ref ref = repo.getRef(refName);
-        if (ref == null) {
-          refProblem = problem("Ref missing: " + refName);
-        } else if (!objId.equals(ref.getObjectId())) {
-          String actual = ref.getObjectId() != null
-              ? ref.getObjectId().name()
-              : "null";
-          refProblem = problem(String.format(
-              "Expected %s to point to %s, found %s",
-              ref.getName(), objId.name(), actual));
-        }
-      } catch (IOException e) {
-        error("Error reading ref: " + refName, e);
-        refProblem = lastProblem();
+      Ref ref = refs.get(refName);
+      if (ref == null) {
+        refProblem = problem("Ref missing: " + refName);
+      } else if (!objId.equals(ref.getObjectId())) {
+        String actual = ref.getObjectId() != null
+            ? ref.getObjectId().name()
+            : "null";
+        refProblem = problem(String.format(
+            "Expected %s to point to %s, found %s",
+            ref.getName(), objId.name(), actual));
       }
 
       // Check object existence.
@@ -286,7 +309,7 @@
 
     // Check for duplicates.
     for (Map.Entry<ObjectId, Collection<PatchSet>> e
-        : bySha.asMap().entrySet()) {
+        : patchSetsBySha.asMap().entrySet()) {
       if (e.getValue().size() > 1) {
         problem(String.format("Multiple patch sets pointing to %s: %s",
             e.getKey().name(),
@@ -301,33 +324,43 @@
     String refName = change.getDest().get();
     Ref dest;
     try {
-      dest = repo.getRef(refName);
+      dest = repo.getRefDatabase().exactRef(refName);
     } catch (IOException e) {
       problem("Failed to look up destination ref: " + refName);
       return;
     }
     if (dest == null) {
-      problem("Destination ref not found (may be new branch): "
-          + change.getDest().get());
+      problem("Destination ref not found (may be new branch): " + refName);
       return;
     }
-    RevCommit tip = parseCommit(dest.getObjectId(),
+    tip = parseCommit(dest.getObjectId(),
         "destination ref " + refName);
     if (tip == null) {
       return;
     }
-    boolean merged;
-    try {
-      merged = rw.isMergedInto(currPsCommit, tip);
-    } catch (IOException e) {
-      problem("Error checking whether patch set " + currPs.getId().get()
-          + " is merged");
-      return;
+
+    if (fix != null && fix.expectMergedAs != null) {
+      checkExpectMergedAs();
+    } else {
+      boolean merged;
+      try {
+        merged = rw.isMergedInto(currPsCommit, tip);
+      } catch (IOException e) {
+        problem("Error checking whether patch set " + currPs.getId().get()
+            + " is merged");
+        return;
+      }
+      checkMergedBitMatchesStatus(currPs.getId(), currPsCommit, merged);
     }
+  }
+
+  private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit,
+      boolean merged) {
+    String refName = change.getDest().get();
     if (merged && change.getStatus() != Change.Status.MERGED) {
       ProblemInfo p = problem(String.format(
           "Patch set %d (%s) is merged into destination ref %s (%s), but change"
-          + " status is %s", currPs.getId().get(), currPsCommit.name(),
+          + " status is %s", psId.get(), commit.name(),
           refName, tip.name(), change.getStatus()));
       if (fix != null) {
         fixMerged(p);
@@ -335,11 +368,135 @@
     } else if (!merged && change.getStatus() == Change.Status.MERGED) {
       problem(String.format("Patch set %d (%s) is not merged into"
             + " destination ref %s (%s), but change status is %s",
-            currPs.getId().get(), currPsCommit.name(), refName, tip.name(),
+            currPs.getId().get(), commit.name(), refName, tip.name(),
             change.getStatus()));
     }
   }
 
+  private void checkExpectMergedAs() {
+    ObjectId objId =
+        parseObjectId(fix.expectMergedAs, "expected merged commit");
+    RevCommit commit = parseCommit(objId, "expected merged commit");
+    if (commit == null) {
+      return;
+    }
+    if (Objects.equals(commit, currPsCommit)) {
+      // Caller gave us latest patch set SHA-1; verified in checkPatchSets.
+      return;
+    }
+
+    try {
+      if (!rw.isMergedInto(commit, tip)) {
+        problem(String.format("Expected merged commit %s is not merged into"
+              + " destination ref %s (%s)",
+              commit.name(), change.getDest().get(), tip.name()));
+        return;
+      }
+
+      RevId revId = new RevId(commit.name());
+      List<PatchSet> patchSets = FluentIterable
+          .from(db.get().patchSets().byRevision(revId))
+          .filter(new Predicate<PatchSet>() {
+            @Override
+            public boolean apply(PatchSet ps) {
+              try {
+                Change c = db.get().changes().get(ps.getId().getParentKey());
+                return c != null && c.getDest().equals(change.getDest());
+              } catch (OrmException e) {
+                warn(e);
+                return true; // Should cause an error below, that's good.
+              }
+            }
+          }).toSortedList(ChangeUtil.PS_ID_ORDER);
+      switch (patchSets.size()) {
+        case 0:
+          // No patch set for this commit; insert one.
+          rw.parseBody(commit);
+          String changeId = Iterables.getFirst(
+              commit.getFooterLines(FooterConstants.CHANGE_ID), null);
+          // Missing Change-Id footer is ok, but mismatched is not.
+          if (changeId != null && !changeId.equals(change.getKey().get())) {
+            problem(String.format("Expected merged commit %s has Change-Id: %s,"
+                  + " but expected %s",
+                  commit.name(), changeId, change.getKey().get()));
+            return;
+          }
+          PatchSet.Id psId = insertPatchSet(commit);
+          if (psId != null) {
+            checkMergedBitMatchesStatus(psId, commit, true);
+          }
+          break;
+
+        case 1:
+          // Existing patch set of this commit; check that it is the current
+          // patch set.
+          // TODO(dborowitz): This could be fixed if it's an older patch set of
+          // the current change.
+          PatchSet.Id id = patchSets.get(0).getId();
+          if (!id.equals(change.currentPatchSetId())) {
+            problem(String.format("Expected merged commit %s corresponds to"
+                  + " patch set %s, which is not the current patch set %s",
+                  commit.name(), id, change.currentPatchSetId()));
+          }
+          break;
+
+        default:
+          problem(String.format(
+                "Multiple patch sets for expected merged commit %s: %s",
+                commit.name(), patchSets));
+          break;
+      }
+    } catch (OrmException | IOException e) {
+      error("Error looking up expected merged commit " + fix.expectMergedAs,
+          e);
+    }
+  }
+
+  private PatchSet.Id insertPatchSet(RevCommit commit) {
+    ProblemInfo p =
+        problem("No patch set found for merged commit " + commit.name());
+    if (!user.get().isIdentifiedUser()) {
+      p.status = Status.FIX_FAILED;
+      p.outcome =
+          "Must be called by an identified user to insert new patch set";
+      return null;
+    }
+
+    try {
+      RefControl ctl = projectControlFactory
+          .controlFor(change.getProject(), user.get())
+          .controlForRef(change.getDest());
+      PatchSet.Id psId =
+          ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+      PatchSetInserter inserter =
+          patchSetInserterFactory.create(ctl, psId, commit);
+      try (BatchUpdate bu = updateFactory.create(
+            db.get(), change.getProject(), ctl.getUser(), TimeUtil.nowTs());
+          ObjectInserter oi = repo.newObjectInserter()) {
+        bu.setRepository(repo, rw, oi);
+        bu.addOp(change.getId(), inserter
+            .setValidatePolicy(CommitValidators.Policy.NONE)
+            .setRunHooks(false)
+            .setSendMail(false)
+            .setAllowClosed(true)
+            .setUploader(user.get().getAccountId())
+            .setMessage(
+                "Patch set for merged commit inserted by consistency checker"));
+        bu.execute();
+      }
+      change = inserter.getChange();
+      p.status = Status.FIXED;
+      p.outcome = "Inserted as patch set " + psId.get();
+      return psId;
+    } catch (IOException | NoSuchProjectException | UpdateException
+        | RestApiException e) {
+      warn(e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = "Error inserting new patch set";
+      return null;
+    }
+  }
+
   private void fixMerged(ProblemInfo p) {
     try {
       change = db.get().changes().atomicUpdate(change.getId(),
@@ -426,8 +583,6 @@
         // historical information.
         db.accountPatchReviews().delete(
             db.accountPatchReviews().byPatchSet(psId));
-        db.patchSetAncestors().delete(
-            db.patchSetAncestors().byPatchSet(psId));
         db.patchSetApprovals().delete(
             db.patchSetApprovals().byPatchSet(psId));
         db.patchComments().delete(
@@ -451,12 +606,21 @@
   private PersonIdent newRefLogIdent() {
     CurrentUser u = user.get();
     if (u.isIdentifiedUser()) {
-      return ((IdentifiedUser) u).newRefLogIdent();
+      return u.asIdentifiedUser().newRefLogIdent();
     } else {
       return serverIdent.get();
     }
   }
 
+  private ObjectId parseObjectId(String objIdStr, String desc) {
+    try {
+      return ObjectId.fromString(objIdStr);
+    } catch (IllegalArgumentException e) {
+      problem(String.format("Invalid revision on %s: %s", desc, objIdStr));
+      return null;
+    }
+  }
+
   private RevCommit parseCommit(ObjectId objId, String desc) {
     try {
       return rw.parseCommit(objId);
@@ -484,7 +648,11 @@
   private boolean error(String msg, Throwable t) {
     problem(msg);
     // TODO(dborowitz): Expose stack trace to administrators.
-    log.warn("Error in consistency check of change " + change.getId(), t);
+    warn(t);
     return false;
   }
+
+  private void warn(Throwable t) {
+    log.warn("Error in consistency check of change " + change.getId(), t);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 22aa84a..3768738 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -24,32 +24,30 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 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.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.RefControl;
-import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -57,16 +55,13 @@
 
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
 
 import java.io.IOException;
@@ -82,43 +77,41 @@
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final TimeZone serverTimeZone;
-  private final Provider<CurrentUser> userProvider;
+  private final Provider<CurrentUser> user;
   private final ProjectsCollection projectsCollection;
-  private final CommitValidators.Factory commitValidatorsFactory;
   private final ChangeInserter.Factory changeInserterFactory;
-  private final ChangeJson json;
+  private final ChangeJson.Factory jsonFactory;
   private final ChangeUtil changeUtil;
+  private final BatchUpdate.Factory updateFactory;
   private final boolean allowDrafts;
 
   @Inject
   CreateChange(Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       @GerritPersonIdent PersonIdent myIdent,
-      Provider<CurrentUser> userProvider,
+      Provider<CurrentUser> user,
       ProjectsCollection projectsCollection,
-      CommitValidators.Factory commitValidatorsFactory,
       ChangeInserter.Factory changeInserterFactory,
-      ChangeJson json,
+      ChangeJson.Factory json,
       ChangeUtil changeUtil,
+      BatchUpdate.Factory updateFactory,
       @GerritServerConfig Config config) {
     this.db = db;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
-    this.userProvider = userProvider;
+    this.user = user;
     this.projectsCollection = projectsCollection;
-    this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
-    this.json = json;
+    this.jsonFactory = json;
     this.changeUtil = changeUtil;
+    this.updateFactory = updateFactory;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
   }
 
   @Override
-  public Response<ChangeInfo> apply(TopLevelResource parent,
-      ChangeInfo input) throws AuthException, OrmException,
-      BadRequestException, UnprocessableEntityException, IOException,
-      InvalidChangeOperationException, ResourceNotFoundException,
-      MethodNotAllowedException, ResourceConflictException {
+  public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInfo input)
+      throws OrmException, IOException, InvalidChangeOperationException,
+      RestApiException, UpdateException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
@@ -142,11 +135,7 @@
       }
     }
 
-    String refName = input.branch;
-    if (!refName.startsWith(Constants.R_REFS)) {
-      refName = Constants.R_HEADS + input.branch;
-    }
-
+    String refName = RefNames.fullName(input.branch);
     ProjectResource rsrc = projectsCollection.parse(input.project);
 
     Capable r = rsrc.getControl().canPushToAtLeastOneRef();
@@ -163,6 +152,7 @@
     try (Repository git = gitManager.openRepository(project);
         RevWalk rw = new RevWalk(git)) {
       ObjectId parentCommit;
+      List<String> groups;
       if (input.baseChange != null) {
         List<Change> changes = changeUtil.findChanges(input.baseChange);
         if (changes.size() != 1) {
@@ -178,90 +168,58 @@
             new PatchSet.Id(change.getId(),
             change.currentPatchSetId().get()));
         parentCommit = ObjectId.fromString(ps.getRevision().get());
+        groups = ps.getGroups();
       } else {
-        Ref destRef = git.getRef(refName);
+        Ref destRef = git.getRefDatabase().exactRef(refName);
         if (destRef == null) {
           throw new UnprocessableEntityException(String.format(
               "Branch %s does not exist.", refName));
         }
         parentCommit = destRef.getObjectId();
+        groups = null;
       }
       RevCommit mergeTip = rw.parseCommit(parentCommit);
 
       Timestamp now = TimeUtil.nowTs();
-      IdentifiedUser me = (IdentifiedUser) userProvider.get();
+      IdentifiedUser me = user.get().asIdentifiedUser();
       PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
 
       ObjectId id = ChangeIdUtil.computeChangeId(mergeTip.getTree(),
           mergeTip, author, author, input.subject);
       String commitMessage = ChangeIdUtil.insertId(input.subject, id);
 
-      RevCommit c = newCommit(git, rw, author, mergeTip, commitMessage);
+      try (ObjectInserter oi = git.newObjectInserter()) {
+        RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage);
 
-      Change change = new Change(
-          getChangeId(id, c),
-          new Change.Id(db.get().nextChangeId()),
-          me.getAccountId(),
-          new Branch.NameKey(project, refName),
-          now);
+        Change change = new Change(
+            getChangeId(id, c),
+            new Change.Id(db.get().nextChangeId()),
+            me.getAccountId(),
+            new Branch.NameKey(project, refName),
+            now);
 
-      ChangeInserter ins =
-          changeInserterFactory.create(refControl.getProjectControl(),
-              change, c);
+        ChangeInserter ins = changeInserterFactory
+            .create(refControl, change, c)
+            .setValidatePolicy(CommitValidators.Policy.GERRIT);
+        ins.setMessage(String.format("Uploaded patch set %s.",
+            ins.getPatchSet().getPatchSetId()));
+        String topic = input.topic;
+        if (topic != null) {
+          topic = Strings.emptyToNull(topic.trim());
+        }
+        change.setTopic(topic);
+        ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
+        ins.setGroups(groups);
+        try (BatchUpdate bu = updateFactory.create(
+            db.get(), change.getProject(), me, now)) {
+          bu.setRepository(git, rw, oi);
+          bu.insertChange(ins);
+          bu.execute();
+        }
+        ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
+        return Response.created(json.format(change.getId()));
+      }
 
-      ChangeMessage msg = new ChangeMessage(new ChangeMessage.Key(change.getId(),
-          ChangeUtil.messageUUID(db.get())),
-          me.getAccountId(),
-          ins.getPatchSet().getCreatedOn(),
-          ins.getPatchSet().getId());
-      msg.setMessage(String.format("Uploaded patch set %s.",
-          ins.getPatchSet().getPatchSetId()));
-
-      ins.setMessage(msg);
-      validateCommit(git, refControl, c, me, ins);
-      updateRef(git, rw, c, change, ins.getPatchSet());
-
-      change.setTopic(input.topic);
-      ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
-      ins.insert();
-
-      return Response.created(json.format(change.getId()));
-    }
-  }
-
-  private void validateCommit(Repository git, RefControl refControl,
-      RevCommit c, IdentifiedUser me, ChangeInserter ins)
-      throws ResourceConflictException {
-    PatchSet newPatchSet = ins.getPatchSet();
-    CommitValidators commitValidators =
-        commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
-    CommitReceivedEvent commitReceivedEvent =
-        new CommitReceivedEvent(new ReceiveCommand(
-            ObjectId.zeroId(),
-            c.getId(),
-            newPatchSet.getRefName()),
-            refControl.getProjectControl().getProject(),
-            refControl.getRefName(),
-            c,
-            me);
-
-    try {
-      commitValidators.validateForGerritCommits(commitReceivedEvent);
-    } catch (CommitValidationException e) {
-      throw new ResourceConflictException(e.getMessage());
-    }
-  }
-
-  private static void updateRef(Repository git, RevWalk rw, RevCommit c,
-      Change change, PatchSet newPatchSet) throws IOException {
-    RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(c);
-    ru.disableRefLog();
-    if (ru.update(rw) != RefUpdate.Result.NEW) {
-      throw new IOException(String.format(
-          "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
-          change.getDest().getParentKey().get(), ru.getResult()));
     }
   }
 
@@ -274,20 +232,16 @@
     return changeKey;
   }
 
-  private static RevCommit newCommit(Repository git, RevWalk rw,
+  private static RevCommit newCommit(ObjectInserter oi, RevWalk rw,
       PersonIdent authorIdent, RevCommit mergeTip, String commitMessage)
       throws IOException {
-    RevCommit emptyCommit;
-    try (ObjectInserter oi = git.newObjectInserter()) {
-      CommitBuilder commit = new CommitBuilder();
-      commit.setTreeId(mergeTip.getTree().getId());
-      commit.setParentId(mergeTip);
-      commit.setAuthor(authorIdent);
-      commit.setCommitter(authorIdent);
-      commit.setMessage(commitMessage);
-      emptyCommit = rw.parseCommit(insert(oi, commit));
-    }
-    return emptyCommit;
+    CommitBuilder commit = new CommitBuilder();
+    commit.setTreeId(mergeTip.getTree().getId());
+    commit.setParentId(mergeTip);
+    commit.setAuthor(authorIdent);
+    commit.setCommitter(authorIdent);
+    commit.setMessage(commitMessage);
+    return rw.parseCommit(insert(oi, commit));
   }
 
   private static ObjectId insert(ObjectInserter inserter,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 36b9692..a503721 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -45,14 +45,14 @@
 public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> {
   private final Provider<ReviewDb> db;
   private final ChangeUpdate.Factory updateFactory;
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
   private final PatchLineCommentsUtil plcUtil;
   private final PatchListCache patchListCache;
 
   @Inject
   CreateDraftComment(Provider<ReviewDb> db,
       ChangeUpdate.Factory updateFactory,
-      CommentJson commentJson,
+      Provider<CommentJson> commentJson,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache) {
     this.db = db;
@@ -93,6 +93,6 @@
     setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
     plcUtil.insertComments(db.get(), update, Collections.singleton(c));
     update.commit();
-    return Response.created(commentJson.format(c, false));
+    return Response.created(commentJson.get().setFillAccounts(false).format(c));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
index 9bb1f02..495e695 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.change.DeleteChangeEdit.Input;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -42,8 +41,7 @@
 
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, IOException,
-      InvalidChangeOperationException {
+      throws AuthException, ResourceNotFoundException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
     if (edit.isPresent()) {
       editUtil.delete(edit.get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index d9512eb..6c37252 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -111,7 +111,7 @@
         ChangeMessage changeMessage =
             new ChangeMessage(new ChangeMessage.Key(rsrc.getChange().getId(),
                 ChangeUtil.messageUUID(db)),
-                ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
+                control.getUser().getAccountId(),
                 TimeUtil.nowTs(), rsrc.getChange().currentPatchSetId());
         changeMessage.setMessage(msg.toString());
         cmUtil.addChangeMessage(db, update, changeMessage);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
new file mode 100644
index 0000000..24c2f0e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2015 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.server.change;
+
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+
+public class DownloadContent implements RestReadView<FileResource> {
+  private final FileContentUtil fileContentUtil;
+
+  @Option(name = "--parent")
+  private Integer parent;
+
+  @Inject
+  DownloadContent(FileContentUtil fileContentUtil) {
+    this.fileContentUtil = fileContentUtil;
+  }
+
+  @Override
+  public BinaryResult apply(FileResource rsrc)
+      throws ResourceNotFoundException, IOException, NoSuchChangeException,
+      OrmException {
+    String path = rsrc.getPatchKey().get();
+    ProjectState projectState =
+        rsrc.getRevision().getControl().getProjectControl().getProjectState();
+    ObjectId revstr = ObjectId.fromString(
+        rsrc.getRevision().getPatchSet().getRevision().get());
+    return fileContentUtil.downloadContent(projectState, revstr, path, parent);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
index 006b1ec..3dc0c78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 
@@ -57,6 +56,6 @@
   }
 
   Account.Id getAuthorId() {
-    return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+    return getControl().getUser().getAccountId();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
index 7edd679..acb50ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
@@ -33,14 +33,14 @@
 public class DraftComments implements ChildCollection<RevisionResource, DraftCommentResource> {
   private final DynamicMap<RestView<DraftCommentResource>> views;
   private final Provider<CurrentUser> user;
-  private final ListDraftComments list;
+  private final ListRevisionDrafts list;
   private final Provider<ReviewDb> dbProvider;
   private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   DraftComments(DynamicMap<RestView<DraftCommentResource>> views,
       Provider<CurrentUser> user,
-      ListDraftComments list,
+      ListRevisionDrafts list,
       Provider<ReviewDb> dbProvider,
       PatchLineCommentsUtil plcUtil) {
     this.views = views;
@@ -56,7 +56,7 @@
   }
 
   @Override
-  public ListDraftComments list() throws AuthException {
+  public ListRevisionDrafts list() throws AuthException {
     checkIdentifiedUser();
     return list;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index 337d10b..f1dff88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Lists;
+import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
+
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -23,8 +24,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.EmailReviewCommentsExecutor;
-import com.google.gerrit.server.git.WorkQueue.Executor;
+import com.google.gerrit.server.git.SendEmailExecutor;
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.util.RequestContext;
@@ -39,9 +39,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
 public class EmailReviewComments implements Runnable, RequestContext {
   private static final Logger log = LoggerFactory.getLogger(EmailReviewComments.class);
@@ -56,7 +55,7 @@
         List<PatchLineComment> comments);
   }
 
-  private final Executor sendEmailsExecutor;
+  private final ExecutorService sendEmailsExecutor;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final CommentSender.Factory commentSenderFactory;
   private final SchemaFactory<ReviewDb> schemaFactory;
@@ -72,7 +71,7 @@
 
   @Inject
   EmailReviewComments(
-      @EmailReviewCommentsExecutor final Executor executor,
+      @SendEmailExecutor ExecutorService executor,
       PatchSetInfoFactory patchSetInfoFactory,
       CommentSender.Factory commentSenderFactory,
       SchemaFactory<ReviewDb> schemaFactory,
@@ -93,7 +92,7 @@
     this.patchSet = patchSet;
     this.user = user;
     this.message = message;
-    this.comments = comments;
+    this.comments = PLC_ORDER.sortedCopy(comments);
   }
 
   void sendAsync() {
@@ -102,33 +101,10 @@
 
   @Override
   public void run() {
+    RequestContext old = requestContext.setContext(this);
     try {
-      requestContext.setContext(this);
 
-      comments = Lists.newArrayList(comments);
-      Collections.sort(comments, new Comparator<PatchLineComment>() {
-        @Override
-        public int compare(PatchLineComment a, PatchLineComment b) {
-          int cmp = path(a).compareTo(path(b));
-          if (cmp != 0) {
-            return cmp;
-          }
-
-          // 0 is ancestor, 1 is revision. Sort ancestor first.
-          cmp = a.getSide() - b.getSide();
-          if (cmp != 0) {
-            return cmp;
-          }
-
-          return a.getLine() - b.getLine();
-        }
-
-        private String path(PatchLineComment c) {
-          return c.getKey().getParentKey().getFileName();
-        }
-      });
-
-      CommentSender cm = commentSenderFactory.create(notify, change);
+      CommentSender cm = commentSenderFactory.create(notify, change.getId());
       cm.setFrom(user.getAccountId());
       cm.setPatchSet(patchSet, patchSetInfoFactory.get(change, patchSet));
       cm.setChangeMessage(message);
@@ -137,7 +113,7 @@
     } catch (Exception e) {
       log.error("Cannot email comments for " + patchSet.getId(), e);
     } finally {
-      requestContext.setContext(null);
+      requestContext.setContext(old);
       if (db != null) {
         db.close();
         db = null;
@@ -151,7 +127,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user.getRealUser();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index a8e1793..d145ddf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -16,6 +16,11 @@
 
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
+import com.google.common.base.Strings;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.PatchScript.FileMode;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -26,8 +31,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import eu.medsea.mimeutil.MimeType;
+
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -35,9 +43,13 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.NB;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Random;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 @Singleton
 public class FileContentUtil {
@@ -45,6 +57,8 @@
   private static final String X_GIT_SYMLINK = "x-git/symlink";
   private static final String X_GIT_GITLINK = "x-git/gitlink";
   private static final int MAX_SIZE = 5 << 20;
+  private static final String ZIP_TYPE = "application/zip";
+  private static final Random rng = new Random();
 
   private final GitRepositoryManager repoManager;
   private final FileTypeRegistry registry;
@@ -75,7 +89,7 @@
             .base64();
       }
 
-      final ObjectLoader obj = repo.open(id, OBJ_BLOB);
+      ObjectLoader obj = repo.open(id, OBJ_BLOB);
       byte[] raw;
       try {
         raw = obj.getCachedBytes(MAX_SIZE);
@@ -83,13 +97,6 @@
         raw = null;
       }
 
-      BinaryResult result;
-      if (raw != null) {
-        result = BinaryResult.create(raw);
-      } else {
-        result = asBinaryResult(obj);
-      }
-
       String type;
       if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
         type = X_GIT_SYMLINK;
@@ -97,11 +104,16 @@
         type = registry.getMimeType(path, raw).toString();
         type = resolveContentType(project, path, FileMode.FILE, type);
       }
-      return result.setContentType(type).base64();
+
+      return asBinaryResult(raw, obj).setContentType(type).base64();
     }
   }
 
-  private static BinaryResult asBinaryResult(final ObjectLoader obj) {
+  private static BinaryResult asBinaryResult(byte[] raw,
+      final ObjectLoader obj) {
+    if (raw != null) {
+      return BinaryResult.create(raw);
+    }
     BinaryResult result = new BinaryResult() {
       @Override
       public void writeTo(OutputStream os) throws IOException {
@@ -112,6 +124,139 @@
     return result;
   }
 
+  public BinaryResult downloadContent(ProjectState project, ObjectId revstr,
+      String path, @Nullable Integer parent)
+          throws ResourceNotFoundException, IOException {
+    try (Repository repo = openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      String suffix = "new";
+      RevCommit commit = rw.parseCommit(revstr);
+      if (parent != null && parent > 0) {
+        if (commit.getParentCount() == 1) {
+          suffix = "old";
+        } else {
+          suffix = "old" + parent;
+        }
+        commit = rw.parseCommit(commit.getParent(parent - 1));
+      }
+      ObjectReader reader = rw.getObjectReader();
+      TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
+      if (tw == null) {
+        throw new ResourceNotFoundException();
+      }
+
+      int mode = tw.getFileMode(0).getObjectType();
+      if (mode != Constants.OBJ_BLOB) {
+        throw new ResourceNotFoundException();
+      }
+
+      ObjectId id = tw.getObjectId(0);
+      ObjectLoader obj = repo.open(id, OBJ_BLOB);
+      byte[] raw;
+      try {
+        raw = obj.getCachedBytes(MAX_SIZE);
+      } catch (LargeObjectException e) {
+        raw = null;
+      }
+
+      MimeType contentType = registry.getMimeType(path, raw);
+      return registry.isSafeInline(contentType)
+          ? wrapBlob(path, obj, raw, contentType, suffix)
+          : zipBlob(path, obj, commit, suffix);
+    }
+  }
+
+  private BinaryResult wrapBlob(String path, final ObjectLoader obj, byte[] raw,
+      MimeType contentType, @Nullable String suffix) {
+    return asBinaryResult(raw, obj)
+        .setContentType(contentType.toString())
+        .setAttachmentName(safeFileName(path, suffix));
+  }
+
+  @SuppressWarnings("resource")
+  private BinaryResult zipBlob(final String path, final ObjectLoader obj,
+      RevCommit commit, @Nullable final String suffix) {
+    final String commitName = commit.getName();
+    final long when = commit.getCommitTime() * 1000L;
+    return new BinaryResult() {
+      @Override
+      public void writeTo(OutputStream os) throws IOException {
+        try (ZipOutputStream zipOut = new ZipOutputStream(os)) {
+          String decoration = randSuffix();
+          if (!Strings.isNullOrEmpty(suffix)) {
+            decoration = suffix + '-' + decoration;
+          }
+          ZipEntry e = new ZipEntry(safeFileName(path, decoration));
+          e.setComment(commitName + ":" + path);
+          e.setSize(obj.getSize());
+          e.setTime(when);
+          zipOut.putNextEntry(e);
+          obj.copyTo(zipOut);
+          zipOut.closeEntry();
+        }
+      }
+    }.setContentType(ZIP_TYPE)
+        .setAttachmentName(safeFileName(path, suffix) + ".zip")
+        .disableGzip();
+  }
+
+  private static String safeFileName(String fileName, @Nullable String suffix) {
+    // Convert a file path (e.g. "src/Init.c") to a safe file name with
+    // no meta-characters that might be unsafe on any given platform.
+    //
+    int slash = fileName.lastIndexOf('/');
+    if (slash >= 0) {
+      fileName = fileName.substring(slash + 1);
+    }
+
+    StringBuilder r = new StringBuilder(fileName.length());
+    for (int i = 0; i < fileName.length(); i++) {
+      final char c = fileName.charAt(i);
+      if (c == '_' || c == '-' || c == '.' || c == '@') {
+        r.append(c);
+      } else if ('0' <= c && c <= '9') {
+        r.append(c);
+      } else if ('A' <= c && c <= 'Z') {
+        r.append(c);
+      } else if ('a' <= c && c <= 'z') {
+        r.append(c);
+      } else if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
+        r.append('-');
+      } else {
+        r.append('_');
+      }
+    }
+    fileName = r.toString();
+
+    int ext = fileName.lastIndexOf('.');
+    if (suffix == null) {
+      return fileName;
+    } else if (ext <= 0) {
+      return fileName + "_" + suffix;
+    } else {
+      return fileName.substring(0, ext) + "_" + suffix
+          + fileName.substring(ext);
+    }
+  }
+
+  private static String randSuffix() {
+    // Produce a random suffix that is difficult (or nearly impossible)
+    // for an attacker to guess in advance. This reduces the risk that
+    // an attacker could upload a *.class file and have us send a ZIP
+    // that can be invoked through an applet tag in the victim's browser.
+    //
+    Hasher h = Hashing.md5().newHasher();
+    byte[] buf = new byte[8];
+
+    NB.encodeInt64(buf, 0, TimeUtil.nowMs());
+    h.putBytes(buf);
+
+    rng.nextBytes(buf);
+    h.putBytes(buf);
+
+    return h.hash().toString();
+  }
+
   public static String resolveContentType(ProjectState project, String path,
       FileMode fileMode, String mimeType) {
     switch (fileMode) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 6ae87a3..71974c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -55,7 +55,7 @@
         : ObjectId.fromString(base.getRevision().get());
     ObjectId b = ObjectId.fromString(revision.get());
     PatchList list = patchListCache.get(
-        new PatchListKey(change.getProject(), a, b, Whitespace.IGNORE_NONE));
+        new PatchListKey(a, b, Whitespace.IGNORE_NONE), change.getProject());
 
     Map<String, FileInfo> files = Maps.newTreeMap();
     for (PatchListEntry e : list.getPatches()) {
@@ -63,6 +63,7 @@
       d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
           ? e.getChangeType().getCode() : null;
       d.oldPath = e.getOldName();
+      d.sizeDelta = e.getSizeDelta();
       if (e.getPatchType() == Patch.PatchType.BINARY) {
         d.binary = true;
       } else {
@@ -76,6 +77,7 @@
         // when the file was rewritten and too little content survived. Write
         // a single record with data from both sides.
         d.status = Patch.ChangeType.REWRITE.getCode();
+        d.sizeDelta = o.sizeDelta;
         if (o.binary != null && o.binary) {
           d.binary = true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
index 1662237..ca47fb9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
@@ -44,7 +44,7 @@
     return rev.getAccountId();
   }
 
-  RevisionResource getRevision() {
+  public RevisionResource getRevision() {
     return rev;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index e19b6a9..e28d796 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -207,7 +206,7 @@
         throw new AuthException("Authentication required");
       }
 
-      Account.Id userId = ((IdentifiedUser) user).getAccountId();
+      Account.Id userId = user.getAccountId();
       List<String> r = scan(userId, resource.getPatchSet().getId());
 
       if (r.isEmpty() && 1 < resource.getPatchSet().getPatchSetId()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
index 913f69e..a2fd004 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -14,20 +14,21 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -36,11 +37,7 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -51,33 +48,35 @@
     final Set<ArchiveFormat> allowed;
 
     @Inject
-    AllowedFormats(@GerritServerConfig Config cfg) {
-      Collection<ArchiveFormat> enabled;
-      String v = cfg.getString("download", null, "archive");
-      if (v == null) {
-        enabled = Arrays.asList(ArchiveFormat.values());
-      } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
-        enabled = Collections.emptyList();
-      } else {
-        enabled = ConfigUtil.getEnumList(cfg,
-            "download", null, "archive",
-            ArchiveFormat.TGZ);
-      }
-
+    AllowedFormats(DownloadConfig cfg) {
       Map<String, ArchiveFormat> exts = new HashMap<>();
-      for (ArchiveFormat format : enabled) {
+      for (ArchiveFormat format : cfg.getArchiveFormats()) {
         for (String ext : format.getSuffixes()) {
           exts.put(ext, format);
         }
         exts.put(format.name().toLowerCase(), format);
       }
       extensions = ImmutableMap.copyOf(exts);
-      allowed = Collections.unmodifiableSet(new LinkedHashSet<>(enabled));
+
+      // Zip is not supported because it may be interpreted by a Java plugin as a
+      // valid JAR file, whose code would have access to cookies on the domain.
+      allowed = Sets.filter(
+          cfg.getArchiveFormats(),
+          new Predicate<ArchiveFormat>() {
+            @Override
+            public boolean apply(ArchiveFormat format) {
+              return (format != ArchiveFormat.ZIP);
+            }
+          });
     }
 
     public Set<ArchiveFormat> getAllowed() {
       return allowed;
     }
+
+    public ImmutableMap<String, ArchiveFormat> getExtensions() {
+      return extensions;
+    }
   }
 
   private final GitRepositoryManager repoManager;
@@ -93,8 +92,8 @@
   }
 
   @Override
-  public BinaryResult apply(RevisionResource rsrc)
-      throws BadRequestException, IOException {
+  public BinaryResult apply(RevisionResource rsrc) throws BadRequestException,
+      IOException, MethodNotAllowedException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
@@ -102,46 +101,47 @@
     if (f == null) {
       throw new BadRequestException("unknown archive format");
     }
+    if (f == ArchiveFormat.ZIP) {
+      throw new MethodNotAllowedException("zip format is disabled");
+    }
     boolean close = true;
     final Repository repo = repoManager
         .openRepository(rsrc.getControl().getProject().getNameKey());
     try {
-      final RevWalk rw = new RevWalk(repo);
-      try {
-        final RevCommit commit =
-            rw.parseCommit(ObjectId.fromString(rsrc.getPatchSet()
-                .getRevision().get()));
-        BinaryResult bin = new BinaryResult() {
-          @Override
-          public void writeTo(OutputStream out) throws IOException {
-            try {
-              new ArchiveCommand(repo)
-                  .setFormat(f.name())
-                  .setTree(commit.getTree())
-                  .setOutputStream(out).call();
-            } catch (GitAPIException e) {
-              throw new IOException(e);
-            }
-          }
-
-          @Override
-          public void close() throws IOException {
-            rw.close();
-            repo.close();
-          }
-        };
-
-        bin.disableGzip()
-            .setContentType(f.getMimeType())
-            .setAttachmentName(name(f, rw, commit));
-
-        close = false;
-        return bin;
-      } finally {
-        if (close) {
-          rw.close();
-        }
+      final RevCommit commit;
+      String name;
+      try (RevWalk rw = new RevWalk(repo)) {
+        commit = rw.parseCommit(ObjectId.fromString(
+            rsrc.getPatchSet().getRevision().get()));
+        name = name(f, rw, commit);
       }
+
+      BinaryResult bin = new BinaryResult() {
+        @Override
+        public void writeTo(OutputStream out) throws IOException {
+          try {
+            new ArchiveCommand(repo)
+                .setFormat(f.name())
+                .setTree(commit.getTree())
+                .setOutputStream(out)
+                .call();
+          } catch (GitAPIException e) {
+            throw new IOException(e);
+          }
+        }
+
+        @Override
+        public void close() throws IOException {
+          repo.close();
+        }
+      };
+
+      bin.disableGzip()
+          .setContentType(f.getMimeType())
+          .setAttachmentName(name);
+
+      close = false;
+      return bin;
     } finally {
       if (close) {
         repo.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
index 8b64c1b..b66a18b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
@@ -23,30 +23,34 @@
 
 import org.kohsuke.args4j.Option;
 
+import java.util.EnumSet;
+
 public class GetChange implements RestReadView<ChangeResource> {
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
+  private final EnumSet<ListChangesOption> options =
+      EnumSet.noneOf(ListChangesOption.class);
 
   @Option(name = "-o", usage = "Output options")
   void addOption(ListChangesOption o) {
-    json.addOption(o);
+    options.add(o);
   }
 
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
-    json.addOptions(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
   }
 
   @Inject
-  GetChange(ChangeJson json) {
+  GetChange(ChangeJson.Factory json) {
     this.json = json;
   }
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.format(rsrc));
+    return Response.withMustRevalidate(json.create(options).format(rsrc));
   }
 
   Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
-    return Response.withMustRevalidate(json.format(rsrc));
+    return Response.withMustRevalidate(json.create(options).format(rsrc));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
index ea84f50..d87c7eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -18,20 +18,21 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetComment implements RestReadView<CommentResource> {
 
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
 
   @Inject
-  GetComment(CommentJson commentJson) {
+  GetComment(Provider<CommentJson> commentJson) {
     this.commentJson = commentJson;
   }
 
   @Override
   public CommentInfo apply(CommentResource rsrc) throws OrmException {
-    return commentJson.format(rsrc.getComment());
+    return commentJson.get().format(rsrc.getComment());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index 296a262..72f8c35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -18,38 +18,48 @@
 import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Option;
 
+import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
 public class GetCommit implements RestReadView<RevisionResource> {
-  private final ChangeJson json;
+  private final GitRepositoryManager repoManager;
+  private final ChangeJson.Factory json;
 
   @Option(name = "--links", usage = "Add weblinks")
   private boolean addLinks;
 
   @Inject
-  GetCommit(ChangeJson json) {
+  GetCommit(GitRepositoryManager repoManager, ChangeJson.Factory json) {
+    this.repoManager = repoManager;
     this.json = json;
   }
 
   @Override
-  public Response<CommitInfo> apply(RevisionResource resource)
-      throws OrmException {
-    try {
-      Response<CommitInfo> r =
-          Response.ok(json.toCommit(resource.getPatchSet(), resource
-              .getChange().getProject(), addLinks));
-      if (resource.isCacheable()) {
+  public Response<CommitInfo> apply(RevisionResource rsrc) throws IOException {
+    Project.NameKey p = rsrc.getChange().getProject();
+    try (Repository repo = repoManager.openRepository(p);
+        RevWalk rw = new RevWalk(repo)) {
+      String rev = rsrc.getPatchSet().getRevision().get();
+      RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+      rw.parseBody(commit);
+      CommitInfo info = json.create(ChangeJson.NO_OPTIONS)
+          .toCommit(rsrc.getControl(), rw, commit, addLinks);
+      info.commit = commit.name();
+      Response<CommitInfo> r = Response.ok(info);
+      if (rsrc.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
       }
       return r;
-    } catch (PatchSetInfoNotAvailableException e) {
-      throw new OrmException(e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 8e3a5d1..3dcc442 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
@@ -39,8 +40,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -52,7 +51,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
@@ -69,6 +67,8 @@
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
+import javax.inject.Inject;
+
 public class GetDiff implements RestReadView<FileResource> {
   private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
       Maps.immutableEnumMap(
@@ -93,7 +93,7 @@
   IgnoreWhitespace ignoreWhitespace = IgnoreWhitespace.NONE;
 
   @Option(name = "--context", handler = ContextOptionHandler.class)
-  short context = AccountDiffPreference.DEFAULT_CONTEXT;
+  int context = DiffPreferencesInfo.DEFAULT_CONTEXT;
 
   @Option(name = "--intraline")
   boolean intraline;
@@ -122,10 +122,10 @@
           resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
       basePatchSet = baseResource.getPatchSet();
     }
-    AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0));
-    prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace);
-    prefs.setContext(context);
-    prefs.setIntralineDifference(intraline);
+    DiffPreferencesInfo prefs = new DiffPreferencesInfo();
+    prefs.ignoreWhitespace = ignoreWhitespace.whitespace;
+    prefs.context = context;
+    prefs.intralineDifference = intraline;
 
     try {
       PatchScriptFactory psf = patchScriptFactoryFactory.create(
@@ -135,7 +135,7 @@
           resource.getPatchKey().getParentKey(),
           prefs);
       psf.setLoadHistory(false);
-      psf.setLoadComments(context != AccountDiffPreference.WHOLE_FILE_CONTEXT);
+      psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
       PatchScript ps = psf.call();
       Content content = new Content(ps);
       for (Edit edit : ps.getEdits()) {
@@ -201,6 +201,7 @@
           result.metaA.lines = ps.getA().size();
           result.metaA.webLinks =
               getFileWebLinks(state.getProject(), revA, result.metaA.name);
+          result.metaA.commitId = content.commitIdA;
         }
 
         if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
@@ -211,6 +212,7 @@
           result.metaB.lines = ps.getB().size();
           result.metaB.webLinks =
               getFileWebLinks(state.getProject(), revB, result.metaB.name);
+          result.metaB.commitId = content.commitIdB;
         }
 
         if (intraline) {
@@ -264,6 +266,8 @@
     final SparseFileContent fileA;
     final SparseFileContent fileB;
     final boolean ignoreWS;
+    final String commitIdA;
+    final String commitIdB;
 
     int nextA;
     int nextB;
@@ -273,6 +277,8 @@
       fileA = ps.getA();
       fileB = ps.getB();
       ignoreWS = ps.isIgnoreWhitespace();
+      commitIdA = ps.getCommitIdA();
+      commitIdB = ps.getCommitIdB();
     }
 
     void addCommon(int end) {
@@ -363,14 +369,14 @@
   }
 
   enum IgnoreWhitespace {
-    NONE(AccountDiffPreference.Whitespace.IGNORE_NONE),
-    TRAILING(AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL),
-    CHANGED(AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE),
-    ALL(AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE);
+    NONE(DiffPreferencesInfo.Whitespace.IGNORE_NONE),
+    TRAILING(DiffPreferencesInfo.Whitespace.IGNORE_TRAILING),
+    CHANGED(DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING),
+    ALL(DiffPreferencesInfo.Whitespace.IGNORE_ALL);
 
-    private final AccountDiffPreference.Whitespace whitespace;
+    private final DiffPreferencesInfo.Whitespace whitespace;
 
-    private IgnoreWhitespace(AccountDiffPreference.Whitespace whitespace) {
+    private IgnoreWhitespace(DiffPreferencesInfo.Whitespace whitespace) {
       this.whitespace = whitespace;
     }
   }
@@ -387,7 +393,7 @@
       final String value = params.getParameter(0);
       short context;
       if ("all".equalsIgnoreCase(value)) {
-        context = AccountDiffPreference.WHOLE_FILE_CONTEXT;
+        context = DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
       } else {
         try {
           context = Short.parseShort(value, 10);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
index a13ecdf..22f90c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
@@ -18,20 +18,21 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetDraftComment implements RestReadView<DraftCommentResource> {
 
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
 
   @Inject
-  GetDraftComment(CommentJson commentJson) {
+  GetDraftComment(Provider<CommentJson> commentJson) {
     this.commentJson = commentJson;
   }
 
   @Override
   public CommentInfo apply(DraftCommentResource rsrc) throws OrmException {
-    return commentJson.format(rsrc.getComment());
+    return commentJson.get().format(rsrc.getComment());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
index 4846c0b..d0c1e83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
@@ -30,9 +30,8 @@
 @Singleton
 public class GetHashtags implements RestReadView<ChangeResource> {
   @Override
-  public Response<? extends Set<String>> apply(ChangeResource req)
+  public Response<Set<String>> apply(ChangeResource req)
       throws AuthException, OrmException, IOException, BadRequestException {
-
     ChangeControl control = req.getControl();
     ChangeNotes notes = control.getNotes().load();
     Set<String> hashtags = notes.getHashtags();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
index afd11569..3c4d79d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -144,7 +144,7 @@
     }
     b.append("From ").append(commit.getName())
      .append(' ')
-     .append("Mon Sep 17 00:00:00 2001\n")
+     .append("Mon Sep 17 00:00:00 2001\n") // Fixed timestamp to match output of C Git's format-patch
      .append("From: ").append(author.getName())
      .append(" <").append(author.getEmailAddress()).append(">\n")
      .append("Date: ").append(formatDate(author)).append('\n')
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 0144b94..6aa1a47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -14,259 +14,110 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.Collection;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedList;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 @Singleton
 public class GetRelated implements RestReadView<RevisionResource> {
-  private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
-
-  private final GitRepositoryManager gitMgr;
-  private final Provider<ReviewDb> dbProvider;
+  private final Provider<ReviewDb> db;
   private final Provider<InternalChangeQuery> queryProvider;
+  private final RelatedChangesSorter sorter;
 
   @Inject
-  GetRelated(GitRepositoryManager gitMgr,
-      Provider<ReviewDb> db,
-      Provider<InternalChangeQuery> queryProvider) {
-    this.gitMgr = gitMgr;
-    this.dbProvider = db;
+  GetRelated(Provider<ReviewDb> db,
+      Provider<InternalChangeQuery> queryProvider,
+      RelatedChangesSorter sorter) {
+    this.db = db;
     this.queryProvider = queryProvider;
+    this.sorter = sorter;
   }
 
   @Override
   public RelatedInfo apply(RevisionResource rsrc)
       throws RepositoryNotFoundException, IOException, OrmException {
-    try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
-        RevWalk rw = new RevWalk(git)) {
-      Ref ref = git.getRef(rsrc.getChange().getDest().get());
-      RelatedInfo info = new RelatedInfo();
-      info.changes = walk(rsrc, rw, ref);
-      return info;
-    }
+    RelatedInfo relatedInfo = new RelatedInfo();
+    relatedInfo.changes = getRelated(rsrc);
+    return relatedInfo;
   }
 
-  private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
+  private List<ChangeAndCommit> getRelated(RevisionResource rsrc)
       throws OrmException, IOException {
-    Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
-    Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
-
-    Map<String, PatchSet> commits = Maps.newHashMap();
-    for (PatchSet p : patchSets.values()) {
-      commits.put(p.getRevision().get(), p);
+    Set<String> groups = getAllGroups(rsrc.getChange().getId());
+    if (groups.isEmpty()) {
+      return Collections.emptyList();
     }
 
-    RevCommit rev = rw.parseCommit(ObjectId.fromString(
-        rsrc.getPatchSet().getRevision().get()));
-    rw.sort(RevSort.TOPO);
-    rw.markStart(rev);
+    List<ChangeData> cds = queryProvider.get()
+        .enforceVisibility(true)
+        .byProjectGroups(rsrc.getChange().getProject(), groups);
+    if (cds.isEmpty()) {
+      return Collections.emptyList();
+    } if (cds.size() == 1
+        && cds.get(0).getId().equals(rsrc.getChange().getId())) {
+      return Collections.emptyList();
+    }
+    List<ChangeAndCommit> result = new ArrayList<>(cds.size());
 
-    if (ref != null && ref.getObjectId() != null) {
-      try {
-        rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
-      } catch (IncorrectObjectTypeException notCommit) {
-        // Ignore and treat as new branch.
+    boolean isEdit = rsrc.getEdit().isPresent();
+    PatchSet basePs = isEdit
+        ? rsrc.getEdit().get().getBasePatchSet()
+        : rsrc.getPatchSet();
+    for (PatchSetData d : sorter.sort(cds, basePs)) {
+      PatchSet ps = d.patchSet();
+      RevCommit commit;
+      if (isEdit && ps.getId().equals(basePs.getId())) {
+        // Replace base of an edit with the edit itself.
+        ps = rsrc.getPatchSet();
+        commit = rsrc.getEdit().get().getEditCommit();
+      } else {
+        commit = d.commit();
       }
+      result.add(new ChangeAndCommit(d.data().change(), ps, commit));
     }
 
-    Set<Change.Id> added = Sets.newHashSet();
-    List<ChangeAndCommit> parents = Lists.newArrayList();
-    for (RevCommit c; (c = rw.next()) != null;) {
-      PatchSet p = commits.get(c.name());
-      Change g = null;
-      if (p != null) {
-        ChangeData cd = changes.get(p.getId().getParentKey());
-        if (cd != null) {
-          g = cd.change();
-        }
-        added.add(p.getId().getParentKey());
-      }
-      parents.add(new ChangeAndCommit(g, p, c));
-    }
-    List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets, added);
-    list.addAll(parents);
-
-    if (list.size() == 1) {
-      ChangeAndCommit r = list.get(0);
-      if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
+    if (result.size() == 1) {
+      ChangeAndCommit r = result.get(0);
+      if (r.commit != null
+          && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
         return Collections.emptyList();
       }
     }
-    return list;
+    return result;
   }
 
-  private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
-      throws OrmException {
-    return ChangeData.asMap(
-        queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
-  }
-
-  private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
-      Collection<ChangeData> cds) throws OrmException {
-    Map<PatchSet.Id, PatchSet> r =
-        Maps.newHashMapWithExpectedSize(cds.size() * 2);
-    for (ChangeData cd : cds) {
-      for (PatchSet p : cd.patches()) {
-        r.put(p.getId(), p);
+  private Set<String> getAllGroups(Change.Id changeId) throws OrmException {
+    Set<String> result = new HashSet<>();
+    for (PatchSet ps : db.get().patchSets().byChange(changeId)) {
+      List<String> groups = ps.getGroups();
+      if (groups != null) {
+        result.addAll(groups);
       }
     }
-
-    if (rsrc.getEdit().isPresent()) {
-      r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
-    }
-    return r;
-  }
-
-  private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
-      Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
-      Set<Change.Id> added)
-      throws OrmException, IOException {
-    // children is a map of parent commit name to PatchSet built on it.
-    Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
-
-    RevFlag seenCommit = rw.newFlag("seenCommit");
-    LinkedList<String> q = Lists.newLinkedList();
-    seedQueue(rsrc, rw, seenCommit, patchSets, q);
-
-    ProjectControl projectCtl = rsrc.getControl().getProjectControl();
-    Set<Change.Id> seenChange = Sets.newHashSet();
-    List<ChangeAndCommit> graph = Lists.newArrayList();
-    while (!q.isEmpty()) {
-      String id = q.remove();
-
-      // For every matching change find the most recent patch set.
-      Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
-      for (PatchSet.Id psId : children.get(id)) {
-        PatchSet.Id e = matches.get(psId.getParentKey());
-        if ((e == null || e.get() < psId.get())
-            && isVisible(projectCtl, changes, patchSets, psId))  {
-          matches.put(psId.getParentKey(), psId);
-        }
-      }
-
-      for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
-        ChangeData cd = changes.get(e.getKey());
-        PatchSet ps = patchSets.get(e.getValue());
-        if (cd == null || ps == null || !seenChange.add(e.getKey())) {
-          continue;
-        }
-
-        RevCommit c = rw.parseCommit(ObjectId.fromString(
-            ps.getRevision().get()));
-        if (!c.has(seenCommit)) {
-          c.add(seenCommit);
-          q.addFirst(ps.getRevision().get());
-          if (added.add(ps.getId().getParentKey())) {
-            rw.parseBody(c);
-            graph.add(new ChangeAndCommit(cd.change(), ps, c));
-          }
-        }
-      }
-    }
-    Collections.reverse(graph);
-    return graph;
-  }
-
-  private boolean isVisible(ProjectControl projectCtl,
-      Map<Change.Id, ChangeData> changes,
-      Map<PatchSet.Id, PatchSet> patchSets,
-      PatchSet.Id psId) throws OrmException {
-    ChangeData cd = changes.get(psId.getParentKey());
-    PatchSet ps = patchSets.get(psId);
-    if (cd != null && ps != null) {
-      // Related changes are in the same project, so reuse the existing
-      // ProjectControl.
-      ChangeControl ctl = projectCtl.controlFor(cd.change());
-      return ctl.isVisible(dbProvider.get())
-          && ctl.isPatchVisible(ps, dbProvider.get());
-    }
-    return false;
-  }
-
-  private void seedQueue(RevisionResource rsrc, RevWalk rw,
-      RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
-      LinkedList<String> q) throws IOException {
-    RevCommit tip = rw.parseCommit(ObjectId.fromString(
-        rsrc.getPatchSet().getRevision().get()));
-    tip.add(seenCommit);
-    q.add(tip.name());
-
-    Change.Id cId = rsrc.getChange().getId();
-    for (PatchSet p : patchSets.values()) {
-      if (cId.equals(p.getId().getParentKey())) {
-        try {
-          RevCommit c = rw.parseCommit(ObjectId.fromString(
-              p.getRevision().get()));
-          if (!c.has(seenCommit)) {
-            c.add(seenCommit);
-            q.add(c.name());
-          }
-        } catch (IOException e) {
-          log.warn(String.format(
-              "Cannot read patch set %d of %d",
-              p.getPatchSetId(), cId.get()), e);
-        }
-      }
-    }
-  }
-
-  private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
-      throws OrmException {
-    ReviewDb db = dbProvider.get();
-    List<ResultSet<PatchSetAncestor>> t =
-        Lists.newArrayListWithCapacity(ids.size());
-    for (Change.Id id : ids) {
-      t.add(db.patchSetAncestors().byChange(id));
-    }
-
-    Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
-    for (ResultSet<PatchSetAncestor> rs : t) {
-      for (PatchSetAncestor a : rs) {
-        r.put(a.getAncestorRevision().get(), a.getPatchSet());
-      }
-    }
-    return r;
+    return result;
   }
 
   public static class RelatedInfo {
@@ -279,6 +130,10 @@
     public Integer _changeNumber;
     public Integer _revisionNumber;
     public Integer _currentRevisionNumber;
+    public String status;
+
+    public ChangeAndCommit() {
+    }
 
     ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
       if (change != null) {
@@ -287,6 +142,7 @@
         _revisionNumber = ps != null ? ps.getPatchSetId() : null;
         PatchSet.Id curr = change.currentPatchSetId();
         _currentRevisionNumber = curr != null ? curr.get() : null;
+        status = change.getStatus().asChangeStatus().toString();
       }
 
       commit = new CommitInfo();
@@ -300,5 +156,29 @@
       commit.author = CommonConverters.toGitPerson(c.getAuthorIdent());
       commit.subject = c.getShortMessage();
     }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("changeId", changeId)
+          .add("commit", toString(commit))
+          .add("_changeNumber", _changeNumber)
+          .add("_revisionNumber", _revisionNumber)
+          .add("_currentRevisionNumber", _currentRevisionNumber)
+          .add("status", status)
+          .toString();
+    }
+
+    private static String toString(CommitInfo commit) {
+      return MoreObjects.toStringHelper(commit)
+        .add("commit", commit.commit)
+        .add("parent", commit.parents)
+        .add("author", commit.author)
+        .add("committer", commit.committer)
+        .add("subject", commit.subject)
+        .add("message", commit.message)
+        .add("webLinks", commit.webLinks)
+        .toString();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index d58c8d2..6b08469 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -14,22 +14,67 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ChangeSet;
+import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.Map;
+
 @Singleton
-public class GetRevisionActions implements RestReadView<RevisionResource> {
+public class GetRevisionActions implements ETagView<RevisionResource> {
   private final ActionJson delegate;
+  private final Config config;
+  private final Provider<ReviewDb> dbProvider;
+  private final MergeSuperSet mergeSuperSet;
 
   @Inject
-  GetRevisionActions(ActionJson delegate) {
+  GetRevisionActions(
+      ActionJson delegate,
+      Provider<ReviewDb> dbProvider,
+      MergeSuperSet mergeSuperSet,
+      @GerritServerConfig Config config) {
     this.delegate = delegate;
+    this.dbProvider = dbProvider;
+    this.mergeSuperSet = mergeSuperSet;
+    this.config = config;
   }
 
   @Override
-  public Object apply(RevisionResource rsrc) {
+  public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) {
     return Response.withMustRevalidate(delegate.format(rsrc));
   }
+
+  @Override
+  public String getETag(RevisionResource rsrc) {
+    Hasher h = Hashing.md5().newHasher();
+    CurrentUser user = rsrc.getControl().getUser();
+    try {
+      rsrc.getChangeResource().prepareETag(h, user);
+      h.putBoolean(Submit.wholeTopicEnabled(config));
+      ReviewDb db = dbProvider.get();
+      ChangeSet cs = mergeSuperSet.completeChangeSet(db, rsrc.getChange());
+      for (ChangeData cd : cs.changes()) {
+        new ChangeResource(cd.changeControl()).prepareETag(h, user);
+      }
+    } catch (IOException | OrmException e) {
+      throw new OrmRuntimeException(e);
+    }
+    return h.hash().toString();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
index cd3b3b1..eb260f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
@@ -14,62 +14,23 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.CharMatcher.WHITESPACE;
-
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.validators.HashtagValidationListener;
-import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-@Singleton
 public class HashtagsUtil {
-  private static final CharMatcher LEADER = WHITESPACE.or(CharMatcher.is('#'));
+  private static final CharMatcher LEADER =
+      CharMatcher.whitespace().or(CharMatcher.is('#'));
   private static final String PATTERN = "(?:\\s|\\A)#[\\p{L}[0-9]-_]+";
 
-  private final ChangeUpdate.Factory updateFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final ChangeIndexer indexer;
-  private final ChangeHooks hooks;
-  private final DynamicSet<HashtagValidationListener> hashtagValidationListeners;
-
-  @Inject
-  HashtagsUtil(ChangeUpdate.Factory updateFactory,
-      Provider<ReviewDb> dbProvider,
-      ChangeIndexer indexer,
-      ChangeHooks hooks,
-      DynamicSet<HashtagValidationListener> hashtagValidationListeners) {
-    this.updateFactory = updateFactory;
-    this.dbProvider = dbProvider;
-    this.indexer = indexer;
-    this.hooks = hooks;
-    this.hashtagValidationListeners = hashtagValidationListeners;
-  }
-
   public static String cleanupHashtag(String hashtag) {
     hashtag = LEADER.trimLeadingFrom(hashtag);
-    hashtag = WHITESPACE.trimTrailingFrom(hashtag);
+    hashtag = CharMatcher.whitespace().trimTrailingFrom(hashtag);
     return hashtag;
   }
 
@@ -84,7 +45,7 @@
     return result;
   }
 
-  private Set<String> extractTags(Set<String> input)
+  static Set<String> extractTags(Set<String> input)
       throws IllegalArgumentException {
     if (input == null) {
       return Collections.emptySet();
@@ -103,54 +64,6 @@
     }
   }
 
-  public TreeSet<String> setHashtags(ChangeControl control,
-      HashtagsInput input, boolean runHooks, boolean index)
-          throws IllegalArgumentException, IOException,
-          ValidationException, AuthException, OrmException {
-    if (input == null
-        || (input.add == null && input.remove == null)) {
-      throw new IllegalArgumentException("Hashtags are required");
-    }
-
-    if (!control.canEditHashtags()) {
-      throw new AuthException("Editing hashtags not permitted");
-    }
-    ChangeUpdate update = updateFactory.create(control);
-    ChangeNotes notes = control.getNotes().load();
-
-    Set<String> existingHashtags = notes.getHashtags();
-    Set<String> updatedHashtags = new HashSet<>();
-    Set<String> toAdd = new HashSet<>(extractTags(input.add));
-    Set<String> toRemove = new HashSet<>(extractTags(input.remove));
-
-    for (HashtagValidationListener validator : hashtagValidationListeners) {
-      validator.validateHashtags(update.getChange(), toAdd, toRemove);
-    }
-
-    if (existingHashtags != null && !existingHashtags.isEmpty()) {
-      updatedHashtags.addAll(existingHashtags);
-      toAdd.removeAll(existingHashtags);
-      toRemove.retainAll(existingHashtags);
-    }
-
-    if (toAdd.size() > 0 || toRemove.size() > 0) {
-      updatedHashtags.addAll(toAdd);
-      updatedHashtags.removeAll(toRemove);
-      update.setHashtags(updatedHashtags);
-      update.commit();
-
-      if (index) {
-        indexer.index(dbProvider.get(), update.getChange());
-      }
-
-      if (runHooks) {
-        IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
-        hooks.doHashtagsChangedHook(
-            update.getChange(), currentUser.getAccount(),
-            toAdd, toRemove, updatedHashtags,
-            dbProvider.get());
-      }
-    }
-    return new TreeSet<>(updatedHashtags);
+  private HashtagsUtil() {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
index 7e9bb14..61b9545 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.data.IncludedInDetail;
+import com.google.gerrit.extensions.config.ExternalIncludedIn;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -37,17 +39,23 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 
 @Singleton
 class IncludedIn implements RestReadView<ChangeResource> {
 
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
+  private final DynamicMap<ExternalIncludedIn> includedIn;
 
   @Inject
-  IncludedIn(Provider<ReviewDb> db, GitRepositoryManager repoManager) {
+  IncludedIn(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager,
+      DynamicMap<ExternalIncludedIn> includedIn) {
     this.db = db;
     this.repoManager = repoManager;
+    this.includedIn = includedIn;
   }
 
   @Override
@@ -68,17 +76,27 @@
       } catch (MissingObjectException err) {
         throw new ResourceConflictException(err.getMessage());
       }
-      return new IncludedInInfo(IncludedInResolver.resolve(r, rw, rev));
+
+      IncludedInDetail d = IncludedInResolver.resolve(r, rw, rev);
+      Map<String, Collection<String>> external = new HashMap<>();
+      for (DynamicMap.Entry<ExternalIncludedIn> i : includedIn) {
+        external.put(i.getExportName(),
+            i.getProvider().get().getIncludedIn(
+                project.get(), rev.name(), d.getTags(), d.getBranches()));
+      }
+      return new IncludedInInfo(d, (!external.isEmpty() ? external : null));
     }
   }
 
   static class IncludedInInfo {
     Collection<String> branches;
     Collection<String> tags;
+    Map<String, Collection<String>> external;
 
-    IncludedInInfo(IncludedInDetail in) {
+    IncludedInInfo(IncludedInDetail in, Map<String, Collection<String>> e) {
       branches = in.getBranches();
       tags = in.getTags();
+      external = e;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
index 280d078..a0be718 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.Index.Input;
 import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,7 +43,14 @@
   }
 
   @Override
-  public Response<?> apply(ChangeResource rsrc, Input input) throws IOException {
+  public Response<?> apply(ChangeResource rsrc, Input input)
+      throws IOException, AuthException {
+    ChangeControl ctl = rsrc.getControl();
+    if (!ctl.isOwner()
+        && !ctl.getUser().getCapabilities().canMaintainServer()) {
+      throw new AuthException(
+          "Only change owner or server maintainer can reindex");
+    }
     indexer.index(db.get(), rsrc.getChange());
     return Response.none();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
new file mode 100644
index 0000000..97befa0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2015 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.server.change;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListChangeComments implements RestReadView<ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final ChangeData.Factory changeDataFactory;
+  private final Provider<CommentJson> commentJson;
+  private final PatchLineCommentsUtil plcUtil;
+
+  @Inject
+  ListChangeComments(Provider<ReviewDb> db,
+      ChangeData.Factory changeDataFactory,
+      Provider<CommentJson> commentJson,
+      PatchLineCommentsUtil plcUtil) {
+    this.db = db;
+    this.changeDataFactory = changeDataFactory;
+    this.commentJson = commentJson;
+    this.plcUtil = plcUtil;
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> apply(
+      ChangeResource rsrc) throws AuthException, OrmException {
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    return commentJson.get()
+        .setFillAccounts(true)
+        .setFillPatchSet(true)
+        .format(plcUtil.publishedByChange(db.get(), cd.notes()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
new file mode 100644
index 0000000..561a040
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2015 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.server.change;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListChangeDrafts implements RestReadView<ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final ChangeData.Factory changeDataFactory;
+  private final Provider<CommentJson> commentJson;
+  private final PatchLineCommentsUtil plcUtil;
+
+  @Inject
+  ListChangeDrafts(Provider<ReviewDb> db,
+      ChangeData.Factory changeDataFactory,
+      Provider<CommentJson> commentJson,
+      PatchLineCommentsUtil plcUtil) {
+    this.db = db;
+    this.changeDataFactory = changeDataFactory;
+    this.commentJson = commentJson;
+    this.plcUtil = plcUtil;
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> apply(
+      ChangeResource rsrc) throws AuthException, OrmException {
+    if (!rsrc.getControl().getUser().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    List<PatchLineComment> drafts = plcUtil.draftByChangeAuthor(
+        db.get(), cd.notes(), rsrc.getControl().getUser().getAccountId());
+    return commentJson.get()
+        .setFillAccounts(false)
+        .setFillPatchSet(true)
+        .format(drafts);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
similarity index 90%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
index b50e243..2392781 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
@@ -24,10 +24,10 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class ListComments extends ListDraftComments {
+public class ListRevisionComments extends ListRevisionDrafts {
   @Inject
-  ListComments(Provider<ReviewDb> db,
-      CommentJson commentJson,
+  ListRevisionComments(Provider<ReviewDb> db,
+      Provider<CommentJson> commentJson,
       PatchLineCommentsUtil plcUtil) {
     super(db, commentJson, plcUtil);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
similarity index 76%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
index 3375cba..ef12b2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
@@ -28,14 +28,14 @@
 import java.util.Map;
 
 @Singleton
-public class ListDraftComments implements RestReadView<RevisionResource> {
+public class ListRevisionDrafts implements RestReadView<RevisionResource> {
   protected final Provider<ReviewDb> db;
-  protected CommentJson commentJson;
+  protected final Provider<CommentJson> commentJson;
   protected final PatchLineCommentsUtil plcUtil;
 
   @Inject
-  ListDraftComments(Provider<ReviewDb> db,
-      CommentJson commentJson,
+  ListRevisionDrafts(Provider<ReviewDb> db,
+      Provider<CommentJson> commentJson,
       PatchLineCommentsUtil plcUtil) {
     this.db = db;
     this.commentJson = commentJson;
@@ -55,6 +55,15 @@
   @Override
   public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
       throws OrmException {
-    return commentJson.format(listComments(rsrc), includeAuthorInfo());
+    return commentJson.get()
+        .setFillAccounts(includeAuthorInfo())
+        .format(listComments(rsrc));
+  }
+
+  public List<CommentInfo> getComments(RevisionResource rsrc)
+      throws OrmException {
+    return commentJson.get()
+        .setFillAccounts(includeAuthorInfo())
+        .formatAsList(listComments(rsrc));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index a840651..0f55e58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
@@ -23,19 +22,20 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
 import com.google.common.base.MoreObjects;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
@@ -63,6 +63,7 @@
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
 @Singleton
@@ -90,8 +91,7 @@
       protected void configure() {
         persist(CACHE_NAME, EntryKey.class, Boolean.class)
             .maximumWeight(1 << 20)
-            .weigher(MergeabilityWeigher.class)
-            .loader(Loader.class);
+            .weigher(MergeabilityWeigher.class);
         bind(MergeabilityCache.class).to(MergeabilityCacheImpl.class);
       }
     };
@@ -111,9 +111,6 @@
     private SubmitType submitType;
     private String mergeStrategy;
 
-    // Only used for loading, not stored.
-    private transient LoadHelper load;
-
     public EntryKey(ObjectId commit, ObjectId into, SubmitType submitType,
         String mergeStrategy) {
       this.commit = checkNotNull(commit, "commit");
@@ -122,13 +119,6 @@
       this.mergeStrategy = checkNotNull(mergeStrategy, "mergeStrategy");
     }
 
-    private EntryKey(ObjectId commit, ObjectId into, SubmitType submitType,
-        String mergeStrategy, Branch.NameKey dest, Repository repo,
-        ReviewDb db) {
-      this(commit, into, submitType, mergeStrategy);
-      load = new LoadHelper(dest, repo, db);
-    }
-
     public ObjectId getCommit() {
       return commit;
     }
@@ -195,63 +185,51 @@
     }
   }
 
-  private static class LoadHelper {
+  private class Loader implements Callable<Boolean> {
+    private final EntryKey key;
     private final Branch.NameKey dest;
     private final Repository repo;
     private final ReviewDb db;
 
-    private LoadHelper(Branch.NameKey dest, Repository repo, ReviewDb db) {
-      this.dest = checkNotNull(dest, "dest");
-      this.repo = checkNotNull(repo, "repo");
-      this.db = checkNotNull(db, "db");
-    }
-  }
-
-  @Singleton
-  public static class Loader extends CacheLoader<EntryKey, Boolean> {
-    private final SubmitStrategyFactory submitStrategyFactory;
-
-    @Inject
-    Loader(SubmitStrategyFactory submitStrategyFactory) {
-      this.submitStrategyFactory = submitStrategyFactory;
+    Loader(EntryKey key, Branch.NameKey dest, Repository repo, ReviewDb db) {
+      this.key = key;
+      this.dest = dest;
+      this.repo = repo;
+      this.db = db;
     }
 
     @Override
-    public Boolean load(EntryKey key)
-        throws NoSuchProjectException, MergeException, IOException {
-      checkArgument(key.load != null, "Key cannot be loaded: %s", key);
+    public Boolean call()
+        throws NoSuchProjectException, IntegrationException, IOException {
       if (key.into.equals(ObjectId.zeroId())) {
         return true; // Assume yes on new branch.
       }
-      try {
-        RefDatabase refDatabase = key.load.repo.getRefDatabase();
-        Iterable<Ref> refs = Iterables.concat(
-            refDatabase.getRefs(Constants.R_HEADS).values(),
-            refDatabase.getRefs(Constants.R_TAGS).values());
-        try (RevWalk rw = CodeReviewCommit.newRevWalk(key.load.repo)) {
-          RevFlag canMerge = rw.newFlag("CAN_MERGE");
-          CodeReviewCommit rev = parse(rw, key.commit);
-          rev.add(canMerge);
-          CodeReviewCommit tip = parse(rw, key.into);
-          Set<RevCommit> accepted = alreadyAccepted(rw, refs);
-          accepted.add(tip);
-          accepted.addAll(Arrays.asList(rev.getParents()));
-          return submitStrategyFactory.create(
-              key.submitType,
-              key.load.db,
-              key.load.repo,
-              rw,
-              null /*inserter*/,
-              canMerge,
-              accepted,
-              key.load.dest).dryRun(tip, rev);
-        }
-      } finally {
-        key.load = null;
+      RefDatabase refDatabase = repo.getRefDatabase();
+      Iterable<Ref> refs = Iterables.concat(
+          refDatabase.getRefs(Constants.R_HEADS).values(),
+          refDatabase.getRefs(Constants.R_TAGS).values());
+      try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+        RevFlag canMerge = rw.newFlag("CAN_MERGE");
+        CodeReviewCommit rev = rw.parseCommit(key.commit);
+        rev.add(canMerge);
+        CodeReviewCommit tip = rw.parseCommit(key.into);
+        Set<RevCommit> accepted = alreadyAccepted(rw, refs);
+        accepted.add(tip);
+        accepted.addAll(Arrays.asList(rev.getParents()));
+        return submitStrategyFactory.create(
+            key.submitType,
+            db,
+            repo,
+            rw,
+            null /*inserter*/,
+            canMerge,
+            accepted,
+            dest,
+            null).dryRun(tip, rev);
       }
     }
 
-    private static Set<RevCommit> alreadyAccepted(RevWalk rw, Iterable<Ref> refs)
+    private Set<RevCommit> alreadyAccepted(RevWalk rw, Iterable<Ref> refs)
         throws MissingObjectException, IOException {
       Set<RevCommit> accepted = Sets.newHashSet();
       for (Ref r : refs) {
@@ -263,12 +241,6 @@
       }
       return accepted;
     }
-
-    private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
-        throws MissingObjectException, IncorrectObjectTypeException,
-        IOException {
-      return (CodeReviewCommit) rw.parseCommit(id);
-    }
   }
 
   public static class MergeabilityWeigher
@@ -280,10 +252,14 @@
     }
   }
 
-  private final LoadingCache<EntryKey, Boolean> cache;
+  private final SubmitStrategyFactory submitStrategyFactory;
+  private final Cache<EntryKey, Boolean> cache;
 
   @Inject
-  MergeabilityCacheImpl(@Named(CACHE_NAME) LoadingCache<EntryKey, Boolean> cache) {
+  MergeabilityCacheImpl(
+      SubmitStrategyFactory submitStrategyFactory,
+      @Named(CACHE_NAME) Cache<EntryKey, Boolean> cache) {
+    this.submitStrategyFactory = submitStrategyFactory;
     this.cache = cache;
   }
 
@@ -291,11 +267,10 @@
   public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
       String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db) {
     ObjectId into = intoRef != null ? intoRef.getObjectId() : ObjectId.zeroId();
-    EntryKey key =
-        new EntryKey(commit, into, submitType, mergeStrategy, dest, repo, db);
+    EntryKey key = new EntryKey(commit, into, submitType, mergeStrategy);
     try {
-      return cache.get(key);
-    } catch (ExecutionException e) {
+      return cache.get(key, new Loader(key, dest, repo, db));
+    } catch (ExecutionException | UncheckedExecutionException e) {
       log.error(String.format("Error checking mergeability of %s into %s (%s)",
             key.commit.name(), key.into.name(), key.submitType.name()),
           e.getCause());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index 2653f1b..007c233 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -47,6 +47,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Map;
 import java.util.Objects;
 
 public class Mergeable implements RestReadView<RevisionResource> {
@@ -108,15 +109,14 @@
     }
     result.submitType = rec.type;
 
-    Repository git = gitManager.openRepository(change.getProject());
-    try {
+    try (Repository git = gitManager.openRepository(change.getProject())) {
       ObjectId commit = toId(ps);
       if (commit == null) {
         result.mergeable = false;
         return result;
       }
 
-      Ref ref = git.getRef(change.getDest().get());
+      Ref ref = git.getRefDatabase().exactRef(change.getDest().get());
       ProjectState projectState = projectCache.get(change.getProject());
       String strategy = mergeUtilFactory.create(projectState)
           .mergeStrategyName();
@@ -135,8 +135,10 @@
         BranchOrderSection branchOrder = projectState.getBranchOrderSection();
         if (branchOrder != null) {
           int prefixLen = Constants.R_HEADS.length();
-          for (String n : branchOrder.getMoreStable(ref.getName())) {
-            Ref other = git.getRef(n);
+          String[] names = branchOrder.getMoreStable(ref.getName());
+          Map<String, Ref> refs = git.getRefDatabase().exactRef(names);
+          for (String n : names) {
+            Ref other = refs.get(n);
             if (other == null) {
               continue;
             }
@@ -147,8 +149,6 @@
           }
         }
       }
-    } finally {
-      git.close();
     }
     return result;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index d0e4c99..be93c29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.change.Reviewed.PutReviewed;
-import com.google.gerrit.server.config.FactoryModule;
 
 public class Module extends RestApiModule {
   @Override
@@ -52,6 +51,8 @@
     get(CHANGE_KIND, "topic").to(GetTopic.class);
     get(CHANGE_KIND, "in").to(IncludedIn.class);
     get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
+    get(CHANGE_KIND, "comments").to(ListChangeComments.class);
+    get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
     get(CHANGE_KIND, "check").to(Check.class);
     post(CHANGE_KIND, "check").to(Check.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
@@ -63,11 +64,12 @@
     post(CHANGE_KIND, "restore").to(Restore.class);
     post(CHANGE_KIND, "revert").to(Revert.class);
     post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
+    get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
     post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
     post(CHANGE_KIND, "index").to(Index.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
-    get(CHANGE_KIND, "suggest_reviewers").to(SuggestReviewers.class);
+    get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
     get(REVIEWER_KIND).to(GetReviewer.class);
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
@@ -103,6 +105,7 @@
     put(FILE_KIND, "reviewed").to(PutReviewed.class);
     delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
     get(FILE_KIND, "content").to(GetContent.class);
+    get(FILE_KIND, "download").to(DownloadContent.class);
     get(FILE_KIND, "diff").to(GetDiff.class);
 
     child(CHANGE_KIND, "edit").to(ChangeEdits.class);
@@ -116,17 +119,14 @@
     get(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Get.class);
     get(CHANGE_EDIT_KIND, "meta").to(ChangeEdits.GetMeta.class);
 
-    install(new FactoryModule() {
-      @Override
-      protected void configure() {
-        factory(ReviewerResource.Factory.class);
-        factory(AccountLoader.Factory.class);
-        factory(EmailReviewComments.Factory.class);
-        factory(ChangeInserter.Factory.class);
-        factory(PatchSetInserter.Factory.class);
-        factory(ChangeEdits.Create.Factory.class);
-        factory(ChangeEdits.DeleteFile.Factory.class);
-      }
-    });
+    factory(AccountLoader.Factory.class);
+    factory(ChangeEdits.Create.Factory.class);
+    factory(ChangeEdits.DeleteFile.Factory.class);
+    factory(ChangeInserter.Factory.class);
+    factory(EmailReviewComments.Factory.class);
+    factory(PatchSetInserter.Factory.class);
+    factory(RebaseChangeOp.Factory.class);
+    factory(ReviewerResource.Factory.class);
+    factory(SetHashtagsOp.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index baddd40..4445343d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -31,34 +32,32 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.ReviewerState;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ChangeModifiedException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -66,122 +65,78 @@
 import java.io.IOException;
 import java.util.Collections;
 
-public class PatchSetInserter {
+public class PatchSetInserter extends BatchUpdate.Op {
   private static final Logger log =
       LoggerFactory.getLogger(PatchSetInserter.class);
 
   public static interface Factory {
-    PatchSetInserter create(Repository git, RevWalk revWalk, ChangeControl ctl,
+    PatchSetInserter create(RefControl refControl, PatchSet.Id psId,
         RevCommit commit);
   }
 
-  /**
-   * Whether to use {@link CommitValidators#validateForGerritCommits},
-   * {@link CommitValidators#validateForReceiveCommits}, or no commit
-   * validation.
-   */
-  public static enum ValidatePolicy {
-    GERRIT, RECEIVE_COMMITS, NONE
-  }
-
+  // Injected fields.
   private final ChangeHooks hooks;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ReviewDb db;
-  private final ChangeUpdate.Factory updateFactory;
-  private final ChangeControl.GenericFactory ctlFactory;
-  private final GitReferenceUpdated gitRefUpdated;
   private final CommitValidators.Factory commitValidatorsFactory;
-  private final ChangeIndexer indexer;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
   private final ChangeMessagesUtil cmUtil;
 
-  private final Repository git;
-  private final RevWalk revWalk;
+  // Assisted-injected fields.
+  private final PatchSet.Id psId;
   private final RevCommit commit;
-  private final ChangeControl ctl;
-  private final IdentifiedUser user;
+  private final RefControl refControl;
 
-  private PatchSet patchSet;
-  private ChangeMessage changeMessage;
+  // Fields exposed as setters.
   private SshInfo sshInfo;
-  private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
+  private String message;
+  private CommitValidators.Policy validatePolicy =
+      CommitValidators.Policy.GERRIT;
   private boolean draft;
-  private boolean runHooks;
-  private boolean sendMail;
+  private Iterable<String> groups;
+  private boolean runHooks = true;
+  private boolean sendMail = true;
   private Account.Id uploader;
+  private boolean allowClosed;
 
-  @Inject
+  // Fields set during some phase of BatchUpdate.Op.
+  private Change change;
+  private PatchSet patchSet;
+  private PatchSetInfo patchSetInfo;
+  private ChangeMessage changeMessage;
+  private SetMultimap<ReviewerState, Account.Id> oldReviewers;
+
+  @AssistedInject
   public PatchSetInserter(ChangeHooks hooks,
-      ReviewDb db,
-      ChangeUpdate.Factory updateFactory,
-      ChangeControl.GenericFactory ctlFactory,
       ApprovalsUtil approvalsUtil,
       ApprovalCopier approvalCopier,
       ChangeMessagesUtil cmUtil,
       PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
-      ChangeIndexer indexer,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
-      @Assisted Repository git,
-      @Assisted RevWalk revWalk,
-      @Assisted ChangeControl ctl,
+      @Assisted RefControl refControl,
+      @Assisted PatchSet.Id psId,
       @Assisted RevCommit commit) {
-    checkArgument(ctl.getCurrentUser().isIdentifiedUser(),
-        "only IdentifiedUser may create patch set on change %s",
-        ctl.getChange().getId());
     this.hooks = hooks;
-    this.db = db;
-    this.updateFactory = updateFactory;
-    this.ctlFactory = ctlFactory;
     this.approvalsUtil = approvalsUtil;
     this.approvalCopier = approvalCopier;
     this.cmUtil = cmUtil;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.indexer = indexer;
     this.replacePatchSetFactory = replacePatchSetFactory;
 
-    this.git = git;
-    this.revWalk = revWalk;
+    this.refControl = refControl;
+    this.psId = psId;
     this.commit = commit;
-    this.ctl = ctl;
-    this.user = (IdentifiedUser) ctl.getCurrentUser();
-    this.runHooks = true;
-    this.sendMail = true;
   }
 
-  public PatchSetInserter setPatchSet(PatchSet patchSet) {
-    Change c = ctl.getChange();
-    PatchSet.Id psid = patchSet.getId();
-    checkArgument(psid.getParentKey().equals(c.getId()),
-        "patch set %s not for change %s", psid, c.getId());
-    checkArgument(psid.get() > c.currentPatchSetId().get(),
-        "new patch set ID %s is not greater than current patch set ID %s",
-        psid.get(), c.currentPatchSetId().get());
-    this.patchSet = patchSet;
-    return this;
+  public PatchSet.Id getPatchSetId() {
+    return psId;
   }
 
-  public PatchSet.Id getPatchSetId() throws IOException {
-    init();
-    return patchSet.getId();
-  }
-
-  public PatchSetInserter setMessage(String message) throws OrmException {
-    changeMessage = new ChangeMessage(
-        new ChangeMessage.Key(
-            ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
-        user.getAccountId(), TimeUtil.nowTs(), patchSet.getId());
-    changeMessage.setMessage(message);
-    return this;
-  }
-
-  public PatchSetInserter setMessage(ChangeMessage changeMessage) {
-    this.changeMessage = changeMessage;
+  public PatchSetInserter setMessage(String message) {
+    this.message = message;
     return this;
   }
 
@@ -190,7 +145,7 @@
     return this;
   }
 
-  public PatchSetInserter setValidatePolicy(ValidatePolicy validate) {
+  public PatchSetInserter setValidatePolicy(CommitValidators.Policy validate) {
     this.validatePolicy = checkNotNull(validate);
     return this;
   }
@@ -200,6 +155,11 @@
     return this;
   }
 
+  public PatchSetInserter setGroups(Iterable<String> groups) {
+    this.groups = groups;
+    return this;
+  }
+
   public PatchSetInserter setRunHooks(boolean runHooks) {
     this.runHooks = runHooks;
     return this;
@@ -210,160 +170,153 @@
     return this;
   }
 
+  public PatchSetInserter setAllowClosed(boolean allowClosed) {
+    this.allowClosed = allowClosed;
+    return this;
+  }
+
   public PatchSetInserter setUploader(Account.Id uploader) {
     this.uploader = uploader;
     return this;
   }
 
-  public Change insert() throws InvalidChangeOperationException, OrmException,
-      IOException, NoSuchChangeException {
+  public Change getChange() {
+    checkState(change != null, "getChange() only valid after executing update");
+    return change;
+  }
+
+  public PatchSet getPatchSet() {
+    checkState(patchSet != null, "getPatchSet() only valid after executing update");
+    return patchSet;
+  }
+
+  @Override
+  public void updateRepo(RepoContext ctx)
+      throws ResourceConflictException, IOException {
     init();
-    validate();
+    validate(ctx);
+    patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+    ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(),
+        commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
+  }
 
-    Change c = ctl.getChange();
-    Change updatedChange;
-    RefUpdate ru = git.updateRef(patchSet.getRefName());
-    ru.setExpectedOldObjectId(ObjectId.zeroId());
-    ru.setNewObjectId(commit);
-    ru.disableRefLog();
-    if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-      throw new IOException(String.format(
-          "Failed to create ref %s in %s: %s", patchSet.getRefName(),
-          c.getDest().getParentKey().get(), ru.getResult()));
+  @Override
+  public void updateChange(ChangeContext ctx) throws OrmException,
+      InvalidChangeOperationException {
+    ReviewDb db = ctx.getDb();
+    ChangeControl ctl = ctx.getChangeControl();
+
+    change = ctx.getChange();
+    Change.Id id = change.getId();
+    final PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+    if (!change.getStatus().isOpen() && !allowClosed) {
+      throw new InvalidChangeOperationException(String.format(
+          "Change %s is closed", change.getId()));
     }
-    gitRefUpdated.fire(c.getProject(), ru);
 
-    final PatchSet.Id currentPatchSetId = c.currentPatchSetId();
+    patchSet = new PatchSet(psId);
+    patchSet.setCreatedOn(ctx.getWhen());
+    patchSet.setUploader(firstNonNull(uploader, ctl.getChange().getOwner()));
+    patchSet.setRevision(new RevId(commit.name()));
+    patchSet.setDraft(draft);
 
-    ChangeUpdate update = updateFactory.create(ctl, patchSet.getCreatedOn());
+    if (groups != null) {
+      patchSet.setGroups(groups);
+    } else {
+      patchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
+    }
+    db.patchSets().insert(Collections.singleton(patchSet));
 
-    db.changes().beginTransaction(c.getId());
-    try {
-      if (!db.changes().get(c.getId()).getStatus().isOpen()) {
-        throw new InvalidChangeOperationException(String.format(
-            "Change %s is closed", c.getId()));
-      }
+    if (sendMail) {
+      oldReviewers = approvalsUtil.getReviewers(db, ctl.getNotes());
+    }
 
-      ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
-      db.patchSets().insert(Collections.singleton(patchSet));
+    if (message != null) {
+      changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(
+              ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
+          ctx.getUser().getAccountId(), ctx.getWhen(), patchSet.getId());
+      changeMessage.setMessage(message);
+    }
 
-      SetMultimap<ReviewerState, Account.Id> oldReviewers = sendMail
-          ? approvalsUtil.getReviewers(db, ctl.getNotes())
-          : null;
-
-      updatedChange =
-          db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change change) {
-              if (change.getStatus().isClosed()) {
-                return null;
-              }
-              if (!change.currentPatchSetId().equals(currentPatchSetId)) {
-                return null;
-              }
-              if (change.getStatus() != Change.Status.DRAFT) {
-                change.setStatus(Change.Status.NEW);
-              }
-              change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
-                  patchSet.getId()));
-              ChangeUtil.updated(change);
-              return change;
-            }
-          });
-      if (updatedChange == null) {
-        throw new ChangeModifiedException(String.format(
-            "Change %s was modified", c.getId()));
-      }
-
-      if (messageIsForChange()) {
-        cmUtil.addChangeMessage(db, update, changeMessage);
-      }
-
-      approvalCopier.copy(db, ctl, patchSet);
-      db.commit();
-      if (messageIsForChange()) {
-        update.commit();
-      }
-
-      if (!messageIsForChange()) {
-        commitMessageNotForChange(updatedChange);
-      }
-
-      if (sendMail) {
-        try {
-          PatchSetInfo info = patchSetInfoFactory.get(commit, patchSet.getId());
-          ReplacePatchSetSender cm =
-              replacePatchSetFactory.create(updatedChange);
-          cm.setFrom(user.getAccountId());
-          cm.setPatchSet(patchSet, info);
-          cm.setChangeMessage(changeMessage);
-          cm.addReviewers(oldReviewers.get(ReviewerState.REVIEWER));
-          cm.addExtraCC(oldReviewers.get(ReviewerState.CC));
-          cm.send();
-        } catch (Exception err) {
-          log.error("Cannot send email for new patch set on change "
-              + updatedChange.getId(), err);
+    // TODO(dborowitz): Throw ResourceConflictException instead of using
+    // AtomicUpdate.
+    change = db.changes().atomicUpdate(id, new AtomicUpdate<Change>() {
+      @Override
+      public Change update(Change change) {
+        if (change.getStatus().isClosed() && !allowClosed) {
+          return null;
         }
+        if (!change.currentPatchSetId().equals(currentPatchSetId)) {
+          return null;
+        }
+        if (change.getStatus() != Change.Status.DRAFT && !allowClosed) {
+          change.setStatus(Change.Status.NEW);
+        }
+        change.setCurrentPatchSet(patchSetInfo);
+        ChangeUtil.updated(change);
+        return change;
       }
-
-    } finally {
-      db.rollback();
+    });
+    if (change == null) {
+      throw new ChangeModifiedException(String.format(
+          "Change %s was modified", id));
     }
-    indexer.index(db, updatedChange);
-    if (runHooks) {
-      hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
-    }
-    return updatedChange;
-  }
 
-  private void commitMessageNotForChange(Change updatedChange)
-      throws OrmException, NoSuchChangeException, IOException {
+    approvalCopier.copy(db, ctl, patchSet);
     if (changeMessage != null) {
-      Change otherChange =
-          db.changes().get(changeMessage.getPatchSetId().getParentKey());
-      ChangeControl otherControl =
-          ctlFactory.controlFor(otherChange, user);
-      ChangeUpdate updateForOtherChange =
-          updateFactory.create(otherControl, updatedChange.getLastUpdatedOn());
-      cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
-      updateForOtherChange.commit();
+      cmUtil.addChangeMessage(db, ctx.getChangeUpdate(), changeMessage);
     }
   }
 
-  private void init() throws IOException {
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
+    if (sendMail) {
+      try {
+        ReplacePatchSetSender cm = replacePatchSetFactory.create(
+            change.getId());
+        cm.setFrom(ctx.getUser().getAccountId());
+        cm.setPatchSet(patchSet, patchSetInfo);
+        cm.setChangeMessage(changeMessage);
+        cm.addReviewers(oldReviewers.get(ReviewerState.REVIEWER));
+        cm.addExtraCC(oldReviewers.get(ReviewerState.CC));
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email for new patch set on change "
+            + change.getId(), err);
+      }
+    }
+
+    if (runHooks) {
+      hooks.doPatchsetCreatedHook(change, patchSet, ctx.getDb());
+    }
+  }
+
+  private void init() {
     if (sshInfo == null) {
       sshInfo = new NoSshInfo();
     }
-    if (patchSet == null) {
-      patchSet = new PatchSet(
-          ChangeUtil.nextPatchSetId(git, ctl.getChange().currentPatchSetId()));
-      patchSet.setCreatedOn(TimeUtil.nowTs());
-      patchSet.setUploader(ctl.getChange().getOwner());
-      patchSet.setRevision(new RevId(commit.name()));
-    }
-    patchSet.setDraft(draft);
-    if (uploader != null) {
-      patchSet.setUploader(uploader);
-    }
   }
 
-  private void validate() throws InvalidChangeOperationException, IOException {
-    CommitValidators cv =
-        commitValidatorsFactory.create(ctl.getRefControl(), sshInfo, git);
+  private void validate(RepoContext ctx)
+      throws ResourceConflictException, IOException {
+    CommitValidators cv = commitValidatorsFactory.create(
+        refControl, sshInfo, ctx.getRepository());
 
-    String refName = patchSet.getRefName();
+    String refName = getPatchSetId().toRefName();
     CommitReceivedEvent event = new CommitReceivedEvent(
         new ReceiveCommand(
             ObjectId.zeroId(),
             commit.getId(),
             refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
-        ctl.getProjectControl().getProject(), ctl.getRefControl().getRefName(),
-        commit, user);
+        refControl.getProjectControl().getProject(), refControl.getRefName(),
+        commit, ctx.getUser().asIdentifiedUser());
 
     try {
       switch (validatePolicy) {
       case RECEIVE_COMMITS:
-        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(git, revWalk);
+        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(
+            ctx.getRepository(), ctx.getRevWalk());
         cv.validateForReceiveCommits(event, rejectCommits);
         break;
       case GERRIT:
@@ -373,12 +326,7 @@
         break;
       }
     } catch (CommitValidationException e) {
-      throw new InvalidChangeOperationException(e.getMessage());
+      throw new ResourceConflictException(e.getMessage());
     }
   }
-
-  private boolean messageIsForChange() {
-    return changeMessage != null && changeMessage.getKey().getParentKey()
-        .equals(patchSet.getId().getParentKey());
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index 6638f91..a3fc2e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -14,41 +14,54 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.io.IOException;
-import java.util.Set;
-
 @Singleton
-public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput> {
-  private HashtagsUtil hashtagsUtil;
+public class PostHashtags
+    implements RestModifyView<ChangeResource, HashtagsInput>,
+    UiAction<ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final SetHashtagsOp.Factory hashtagsFactory;
 
   @Inject
-  PostHashtags(HashtagsUtil hashtagsUtil) {
-    this.hashtagsUtil = hashtagsUtil;
+  PostHashtags(Provider<ReviewDb> db,
+      BatchUpdate.Factory batchUpdateFactory,
+      SetHashtagsOp.Factory hashtagsFactory) {
+    this.db = db;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.hashtagsFactory = hashtagsFactory;
   }
 
   @Override
-  public Response<? extends Set<String>> apply(ChangeResource req, HashtagsInput input)
-      throws AuthException, OrmException, IOException, BadRequestException,
-      ResourceConflictException {
-
-    try {
-      return Response.ok(hashtagsUtil.setHashtags(
-          req.getControl(), input, true, true));
-    } catch (IllegalArgumentException e) {
-      throw new BadRequestException(e.getMessage());
-    } catch (ValidationException e) {
-      throw new ResourceConflictException(e.getMessage());
+  public Response<ImmutableSortedSet<String>> apply(ChangeResource req,
+      HashtagsInput input) throws RestApiException, UpdateException {
+    try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+          req.getChange().getProject(), req.getControl().getUser(),
+          TimeUtil.nowTs())) {
+      SetHashtagsOp op = hashtagsFactory.create(input);
+      bu.addOp(req.getChange().getId(), op);
+      bu.execute();
+      return Response.<ImmutableSortedSet<String>> ok(op.getUpdatedHashtags());
     }
   }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Edit Hashtags")
+      .setVisible(resource.getControl().canEditHashtags());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 4ecaebd..7a575c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -14,17 +14,20 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.MoreObjects;
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.CheckedFuture;
-import com.google.common.util.concurrent.Futures;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -38,6 +41,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
@@ -46,6 +50,7 @@
 import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -54,7 +59,10 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
@@ -63,18 +71,22 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+@Singleton
 public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
   private static final Logger log = LoggerFactory.getLogger(PostReview.class);
 
@@ -83,47 +95,37 @@
   }
 
   private final Provider<ReviewDb> db;
+  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangesCollection changes;
   private final ChangeData.Factory changeDataFactory;
-  private final ChangeUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
   private final PatchLineCommentsUtil plcUtil;
   private final PatchListCache patchListCache;
-  private final ChangeIndexer indexer;
   private final AccountsCollection accounts;
   private final EmailReviewComments.Factory email;
-  @Deprecated private final ChangeHooks hooks;
-
-  private Change change;
-  private ChangeMessage message;
-  private Timestamp timestamp;
-  private List<PatchLineComment> comments = Lists.newArrayList();
-  private List<String> labelDelta = Lists.newArrayList();
-  private Map<String, Short> categories = Maps.newHashMap();
+  private final ChangeHooks hooks;
 
   @Inject
   PostReview(Provider<ReviewDb> db,
+      BatchUpdate.Factory batchUpdateFactory,
       ChangesCollection changes,
       ChangeData.Factory changeDataFactory,
-      ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
-      ChangeIndexer indexer,
       AccountsCollection accounts,
       EmailReviewComments.Factory email,
       ChangeHooks hooks) {
     this.db = db;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.changes = changes;
     this.changeDataFactory = changeDataFactory;
-    this.updateFactory = updateFactory;
     this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.indexer = indexer;
     this.accounts = accounts;
     this.email = email;
     this.hooks = hooks;
@@ -131,16 +133,12 @@
 
   @Override
   public Output apply(RevisionResource revision, ReviewInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      UnprocessableEntityException, OrmException, IOException {
+      throws RestApiException, UpdateException, OrmException {
     return apply(revision, input, TimeUtil.nowTs());
   }
 
   public Output apply(RevisionResource revision, ReviewInput input,
-      Timestamp ts) throws AuthException, BadRequestException,
-      ResourceConflictException, UnprocessableEntityException, OrmException,
-      IOException {
-    timestamp = ts;
+      Timestamp ts) throws RestApiException, UpdateException, OrmException {
     if (revision.getEdit().isPresent()) {
       throw new ResourceConflictException("cannot post review on edit");
     }
@@ -158,50 +156,15 @@
       input.notify = NotifyHandling.NONE;
     }
 
-    db.get().changes().beginTransaction(revision.getChange().getId());
-    boolean dirty = false;
-    try {
-      change = db.get().changes().get(revision.getChange().getId());
-      if (change.getLastUpdatedOn().before(timestamp)) {
-        change.setLastUpdatedOn(timestamp);
-      }
-
-      ChangeUpdate update = updateFactory.create(revision.getControl(), timestamp);
-      update.setPatchSetId(revision.getPatchSet().getId());
-      dirty |= insertComments(revision, update, input.comments, input.drafts);
-      dirty |= updateLabels(revision, update, input.labels);
-      dirty |= insertMessage(revision, input.message, update);
-      if (dirty) {
-        db.get().changes().update(Collections.singleton(change));
-        db.get().commit();
-      }
-      update.commit();
-    } finally {
-      db.get().rollback();
+    try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+          revision.getChange().getProject(), revision.getUser(), ts)) {
+      bu.addOp(
+          revision.getChange().getId(),
+          new Op(revision.getPatchSet().getId(), input));
+      bu.execute();
     }
-
-    CheckedFuture<?, IOException> indexWrite;
-    if (dirty) {
-      indexWrite = indexer.indexAsync(change.getId());
-    } else {
-      indexWrite = Futures.<Void, IOException> immediateCheckedFuture(null);
-    }
-    if (message != null && input.notify.compareTo(NotifyHandling.NONE) > 0) {
-      email.create(
-          input.notify,
-          change,
-          revision.getPatchSet(),
-          revision.getUser(),
-          message,
-          comments).sendAsync();
-    }
-
     Output output = new Output();
     output.labels = input.labels;
-    indexWrite.checkedGet();
-    if (message != null) {
-      fireCommentAddedHook(revision);
-    }
     return output;
   }
 
@@ -342,245 +305,346 @@
     }
   }
 
-  private boolean insertComments(RevisionResource rsrc,
-      ChangeUpdate update, Map<String, List<CommentInput>> in, DraftHandling draftsHandling)
-      throws OrmException {
-    if (in == null) {
-      in = Collections.emptyMap();
+  /**
+   * Used to compare PatchLineComments with CommentInput comments.
+   */
+  @AutoValue
+  abstract static class CommentSetEntry {
+    private static CommentSetEntry create(Patch.Key key,
+        Integer line, Side side, HashCode message, CommentRange range) {
+      return new AutoValue_PostReview_CommentSetEntry(key, line, side, message,
+          range);
     }
 
-    Map<String, PatchLineComment> drafts = Collections.emptyMap();
-    if (!in.isEmpty() || draftsHandling != DraftHandling.KEEP) {
-      drafts = scanDraftComments(rsrc);
+    public static CommentSetEntry create(PatchLineComment comment) {
+      return create(comment.getKey().getParentKey(),
+          comment.getLine(),
+          Side.fromShort(comment.getSide()),
+          Hashing.sha1().hashString(comment.getMessage(), UTF_8),
+          comment.getRange());
     }
 
-    List<PatchLineComment> del = Lists.newArrayList();
-    List<PatchLineComment> ups = Lists.newArrayList();
+    abstract Patch.Key key();
+    @Nullable abstract Integer line();
+    abstract Side side();
+    abstract HashCode message();
+    @Nullable abstract CommentRange range();
+  }
 
-    for (Map.Entry<String, List<CommentInput>> ent : in.entrySet()) {
-      String path = ent.getKey();
-      for (CommentInput c : ent.getValue()) {
-        String parent = Url.decode(c.inReplyTo);
-        PatchLineComment e = drafts.remove(Url.decode(c.id));
-        if (e == null) {
-          e = new PatchLineComment(
-              new PatchLineComment.Key(
-                  new Patch.Key(rsrc.getPatchSet().getId(), path),
-                  ChangeUtil.messageUUID(db.get())),
-              c.line != null ? c.line : 0,
-              rsrc.getAccountId(),
-              parent, timestamp);
-        } else if (parent != null) {
-          e.setParentUuid(parent);
-        }
-        e.setStatus(PatchLineComment.Status.PUBLISHED);
-        e.setWrittenOn(timestamp);
-        e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
-        setCommentRevId(e, patchListCache, rsrc.getChange(),
-            rsrc.getPatchSet());
-        e.setMessage(c.message);
-        if (c.range != null) {
-          e.setRange(new CommentRange(
-              c.range.startLine,
-              c.range.startCharacter,
-              c.range.endLine,
-              c.range.endCharacter));
-          e.setLine(c.range.endLine);
-        }
-        ups.add(e);
+  private class Op extends BatchUpdate.Op {
+    private final PatchSet.Id psId;
+    private final ReviewInput in;
+
+    private IdentifiedUser user;
+    private Change change;
+    private PatchSet ps;
+    private ChangeMessage message;
+    private List<PatchLineComment> comments = new ArrayList<>();
+    private List<String> labelDelta = new ArrayList<>();
+    private Map<String, Short> categories = new HashMap<>();
+
+    private Op(PatchSet.Id psId, ReviewInput in) {
+      this.psId = psId;
+      this.in = in;
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException {
+      user = ctx.getUser().asIdentifiedUser();
+      change = ctx.getChange();
+      if (change.getLastUpdatedOn().before(ctx.getWhen())) {
+        change.setLastUpdatedOn(ctx.getWhen());
+      }
+      ps = ctx.getDb().patchSets().get(psId);
+      ctx.getChangeUpdate().setPatchSetId(psId);
+      boolean dirty = false;
+      dirty |= insertComments(ctx);
+      dirty |= updateLabels(ctx);
+      dirty |= insertMessage(ctx);
+      if (dirty) {
+        ctx.getDb().changes().update(Collections.singleton(change));
       }
     }
 
-    switch (MoreObjects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
-      case KEEP:
-      default:
-        break;
-      case DELETE:
-        del.addAll(drafts.values());
-        break;
-      case PUBLISH:
-        for (PatchLineComment e : drafts.values()) {
+    @Override
+    public void postUpdate(Context ctx) {
+      if (message == null) {
+        return;
+      }
+      if (in.notify.compareTo(NotifyHandling.NONE) > 0) {
+        email.create(
+            in.notify,
+            change,
+            ps,
+            user,
+            message,
+            comments).sendAsync();
+      }
+      try {
+        hooks.doCommentAddedHook(change, user.getAccount(), ps,
+            message.getMessage(), categories, ctx.getDb());
+      } catch (OrmException e) {
+        log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
+      }
+    }
+
+    private boolean insertComments(ChangeContext ctx) throws OrmException {
+      Map<String, List<CommentInput>> map = in.comments;
+      if (map == null) {
+        map = Collections.emptyMap();
+      }
+
+      Map<String, PatchLineComment> drafts = Collections.emptyMap();
+      if (!map.isEmpty() || in.drafts != DraftHandling.KEEP) {
+        if (in.drafts == DraftHandling.PUBLISH_ALL_REVISIONS) {
+          drafts = changeDrafts(ctx);
+        } else {
+          drafts = patchSetDrafts(ctx);
+        }
+      }
+
+      List<PatchLineComment> del = Lists.newArrayList();
+      List<PatchLineComment> ups = Lists.newArrayList();
+
+      Set<CommentSetEntry> existingIds = in.omitDuplicateComments
+          ? readExistingComments(ctx)
+          : Collections.<CommentSetEntry>emptySet();
+
+      for (Map.Entry<String, List<CommentInput>> ent : map.entrySet()) {
+        String path = ent.getKey();
+        for (CommentInput c : ent.getValue()) {
+          String parent = Url.decode(c.inReplyTo);
+          PatchLineComment e = drafts.remove(Url.decode(c.id));
+          if (e == null) {
+            e = new PatchLineComment(
+                new PatchLineComment.Key(new Patch.Key(psId, path), null),
+                c.line != null ? c.line : 0,
+                user.getAccountId(),
+                parent, ctx.getWhen());
+          } else if (parent != null) {
+            e.setParentUuid(parent);
+          }
           e.setStatus(PatchLineComment.Status.PUBLISHED);
-          e.setWrittenOn(timestamp);
-          setCommentRevId(e, patchListCache, rsrc.getChange(),
-              rsrc.getPatchSet());
+          e.setWrittenOn(ctx.getWhen());
+          e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
+          setCommentRevId(e, patchListCache, ctx.getChange(), ps);
+          e.setMessage(c.message);
+          if (c.range != null) {
+            e.setRange(new CommentRange(
+                c.range.startLine,
+                c.range.startCharacter,
+                c.range.endLine,
+                c.range.endCharacter));
+            e.setLine(c.range.endLine);
+          }
+          if (existingIds.contains(CommentSetEntry.create(e))) {
+            continue;
+          }
+          if (e.getKey().get() == null) {
+            e.getKey().set(ChangeUtil.messageUUID(ctx.getDb()));
+          }
           ups.add(e);
         }
-        break;
-    }
-    plcUtil.deleteComments(db.get(), update, del);
-    plcUtil.upsertComments(db.get(), update, ups);
-    comments.addAll(ups);
-    return !del.isEmpty() || !ups.isEmpty();
-  }
-
-  private Map<String, PatchLineComment> scanDraftComments(
-      RevisionResource rsrc) throws OrmException {
-    Map<String, PatchLineComment> drafts = Maps.newHashMap();
-    for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(db.get(),
-        rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes())) {
-      drafts.put(c.getKey().get(), c);
-    }
-    return drafts;
-  }
-
-  private boolean updateLabels(RevisionResource rsrc, ChangeUpdate update,
-      Map<String, Short> labels) throws OrmException {
-    if (labels == null) {
-      labels = Collections.emptyMap();
-    }
-
-    List<PatchSetApproval> del = Lists.newArrayList();
-    List<PatchSetApproval> ups = Lists.newArrayList();
-    Map<String, PatchSetApproval> current = scanLabels(rsrc, del);
-
-    LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
-    for (Map.Entry<String, Short> ent : labels.entrySet()) {
-      String name = ent.getKey();
-      LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
-      if (change.getStatus().isClosed()) {
-        // TODO Allow updating some labels even when closed.
-        continue;
       }
 
-      PatchSetApproval c = current.remove(lt.getName());
-      String normName = lt.getName();
-      if (ent.getValue() == null || ent.getValue() == 0) {
-        // User requested delete of this label.
-        if (c != null) {
-          if (c.getValue() != 0) {
-            addLabelDelta(normName, (short) 0);
+      switch (firstNonNull(in.drafts, DraftHandling.DELETE)) {
+        case KEEP:
+        default:
+          break;
+        case DELETE:
+          del.addAll(drafts.values());
+          break;
+        case PUBLISH:
+        case PUBLISH_ALL_REVISIONS:
+          for (PatchLineComment e : drafts.values()) {
+            e.setStatus(PatchLineComment.Status.PUBLISHED);
+            e.setWrittenOn(ctx.getWhen());
+            setCommentRevId(e, patchListCache, ctx.getChange(), ps);
+            ups.add(e);
           }
-          del.add(c);
-          update.putApproval(ent.getKey(), (short) 0);
+          break;
+      }
+      plcUtil.deleteComments(ctx.getDb(), ctx.getChangeUpdate(), del);
+      plcUtil.upsertComments(ctx.getDb(), ctx.getChangeUpdate(), ups);
+      comments.addAll(ups);
+      return !del.isEmpty() || !ups.isEmpty();
+    }
+
+    private Set<CommentSetEntry> readExistingComments(ChangeContext ctx)
+        throws OrmException {
+      Set<CommentSetEntry> r = new HashSet<>();
+      for (PatchLineComment c : plcUtil.publishedByChange(ctx.getDb(),
+            ctx.getChangeNotes())) {
+        r.add(CommentSetEntry.create(c));
+      }
+      return r;
+    }
+
+    private Map<String, PatchLineComment> changeDrafts(ChangeContext ctx)
+        throws OrmException {
+      Map<String, PatchLineComment> drafts = Maps.newHashMap();
+      for (PatchLineComment c : plcUtil.draftByChangeAuthor(
+          ctx.getDb(), ctx.getChangeNotes(), user.getAccountId())) {
+        drafts.put(c.getKey().get(), c);
+      }
+      return drafts;
+    }
+
+    private Map<String, PatchLineComment> patchSetDrafts(ChangeContext ctx)
+        throws OrmException {
+      Map<String, PatchLineComment> drafts = Maps.newHashMap();
+      for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(ctx.getDb(),
+          psId, user.getAccountId(), ctx.getChangeNotes())) {
+        drafts.put(c.getKey().get(), c);
+      }
+      return drafts;
+    }
+
+    private boolean updateLabels(ChangeContext ctx) throws OrmException {
+      Map<String, Short> labels = in.labels;
+      if (labels == null) {
+        labels = Collections.emptyMap();
+      }
+
+      List<PatchSetApproval> del = Lists.newArrayList();
+      List<PatchSetApproval> ups = Lists.newArrayList();
+      Map<String, PatchSetApproval> current = scanLabels(ctx, del);
+
+      ChangeUpdate update = ctx.getChangeUpdate();
+      LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
+      for (Map.Entry<String, Short> ent : labels.entrySet()) {
+        String name = ent.getKey();
+        LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
+        if (ctx.getChange().getStatus().isClosed()) {
+          // TODO Allow updating some labels even when closed.
+          continue;
         }
-      } else if (c != null && c.getValue() != ent.getValue()) {
-        c.setValue(ent.getValue());
-        c.setGranted(timestamp);
-        ups.add(c);
-        addLabelDelta(normName, c.getValue());
-        categories.put(normName, c.getValue());
-        update.putApproval(ent.getKey(), ent.getValue());
-      } else if (c != null && c.getValue() == ent.getValue()) {
-        current.put(normName, c);
-      } else if (c == null) {
-        c = new PatchSetApproval(new PatchSetApproval.Key(
-                rsrc.getPatchSet().getId(),
-                rsrc.getAccountId(),
-                lt.getLabelId()),
-            ent.getValue(), TimeUtil.nowTs());
-        c.setGranted(timestamp);
-        ups.add(c);
-        addLabelDelta(normName, c.getValue());
-        categories.put(normName, c.getValue());
-        update.putApproval(ent.getKey(), ent.getValue());
+
+        PatchSetApproval c = current.remove(lt.getName());
+        String normName = lt.getName();
+        if (ent.getValue() == null || ent.getValue() == 0) {
+          // User requested delete of this label.
+          if (c != null) {
+            if (c.getValue() != 0) {
+              addLabelDelta(normName, (short) 0);
+            }
+            del.add(c);
+            update.putApproval(ent.getKey(), (short) 0);
+          }
+        } else if (c != null && c.getValue() != ent.getValue()) {
+          c.setValue(ent.getValue());
+          c.setGranted(ctx.getWhen());
+          ups.add(c);
+          addLabelDelta(normName, c.getValue());
+          categories.put(normName, c.getValue());
+          update.putApproval(ent.getKey(), ent.getValue());
+        } else if (c != null && c.getValue() == ent.getValue()) {
+          current.put(normName, c);
+        } else if (c == null) {
+          c = new PatchSetApproval(new PatchSetApproval.Key(
+                  psId,
+                  user.getAccountId(),
+                  lt.getLabelId()),
+              ent.getValue(), TimeUtil.nowTs());
+          c.setGranted(ctx.getWhen());
+          ups.add(c);
+          addLabelDelta(normName, c.getValue());
+          categories.put(normName, c.getValue());
+          update.putApproval(ent.getKey(), ent.getValue());
+        }
+      }
+
+      forceCallerAsReviewer(ctx, current, ups, del);
+      ctx.getDb().patchSetApprovals().delete(del);
+      ctx.getDb().patchSetApprovals().upsert(ups);
+      return !del.isEmpty() || !ups.isEmpty();
+    }
+
+    private void forceCallerAsReviewer(ChangeContext ctx,
+        Map<String, PatchSetApproval> current, List<PatchSetApproval> ups,
+        List<PatchSetApproval> del) {
+      if (current.isEmpty() && ups.isEmpty()) {
+        // TODO Find another way to link reviewers to changes.
+        if (del.isEmpty()) {
+          // If no existing label is being set to 0, hack in the caller
+          // as a reviewer by picking the first server-wide LabelType.
+          PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
+              psId,
+              user.getAccountId(),
+              ctx.getChangeControl().getLabelTypes().getLabelTypes().get(0)
+                  .getLabelId()),
+              (short) 0, TimeUtil.nowTs());
+          c.setGranted(ctx.getWhen());
+          ups.add(c);
+        } else {
+          // Pick a random label that is about to be deleted and keep it.
+          Iterator<PatchSetApproval> i = del.iterator();
+          PatchSetApproval c = i.next();
+          c.setValue((short) 0);
+          c.setGranted(ctx.getWhen());
+          i.remove();
+          ups.add(c);
+        }
       }
     }
 
-    forceCallerAsReviewer(rsrc, current, ups, del);
-    db.get().patchSetApprovals().delete(del);
-    db.get().patchSetApprovals().upsert(ups);
-    return !del.isEmpty() || !ups.isEmpty();
-  }
+    private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx,
+        List<PatchSetApproval> del) throws OrmException {
+      LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
+      Map<String, PatchSetApproval> current = Maps.newHashMap();
 
-  private void forceCallerAsReviewer(RevisionResource rsrc,
-      Map<String, PatchSetApproval> current, List<PatchSetApproval> ups,
-      List<PatchSetApproval> del) {
-    if (current.isEmpty() && ups.isEmpty()) {
-      // TODO Find another way to link reviewers to changes.
-      if (del.isEmpty()) {
-        // If no existing label is being set to 0, hack in the caller
-        // as a reviewer by picking the first server-wide LabelType.
-        PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
-            rsrc.getPatchSet().getId(),
-            rsrc.getAccountId(),
-            rsrc.getControl().getLabelTypes().getLabelTypes().get(0)
-                .getLabelId()),
-            (short) 0, TimeUtil.nowTs());
-        c.setGranted(timestamp);
-        ups.add(c);
-      } else {
-        // Pick a random label that is about to be deleted and keep it.
-        Iterator<PatchSetApproval> i = del.iterator();
-        PatchSetApproval c = i.next();
-        c.setValue((short) 0);
-        c.setGranted(timestamp);
-        i.remove();
-        ups.add(c);
+      for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
+          ctx.getDb(), ctx.getChangeControl(), psId, user.getAccountId())) {
+        if (a.isSubmit()) {
+          continue;
+        }
+
+        LabelType lt = labelTypes.byLabel(a.getLabelId());
+        if (lt != null) {
+          current.put(lt.getName(), a);
+        } else {
+          del.add(a);
+        }
       }
+      return current;
     }
-  }
 
-  private Map<String, PatchSetApproval> scanLabels(RevisionResource rsrc,
-      List<PatchSetApproval> del) throws OrmException {
-    LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
-    Map<String, PatchSetApproval> current = Maps.newHashMap();
+    private boolean insertMessage(ChangeContext ctx)
+        throws OrmException {
+      String msg = Strings.nullToEmpty(in.message).trim();
 
-    for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
-        db.get(), rsrc.getControl(), rsrc.getPatchSet().getId(),
-        rsrc.getAccountId())) {
-      if (a.isSubmit()) {
-        continue;
+      StringBuilder buf = new StringBuilder();
+      for (String d : labelDelta) {
+        buf.append(" ").append(d);
+      }
+      if (comments.size() == 1) {
+        buf.append("\n\n(1 comment)");
+      } else if (comments.size() > 1) {
+        buf.append(String.format("\n\n(%d comments)", comments.size()));
+      }
+      if (!msg.isEmpty()) {
+        buf.append("\n\n").append(msg);
+      }
+      if (buf.length() == 0) {
+        return false;
       }
 
-      LabelType lt = labelTypes.byLabel(a.getLabelId());
-      if (lt != null) {
-        current.put(lt.getName(), a);
-      } else {
-        del.add(a);
-      }
-    }
-    return current;
-  }
-
-  private void addLabelDelta(String name, short value) {
-    labelDelta.add(LabelVote.create(name, value).format());
-  }
-
-  private boolean insertMessage(RevisionResource rsrc, String msg,
-      ChangeUpdate update) throws OrmException {
-    msg = Strings.nullToEmpty(msg).trim();
-
-    StringBuilder buf = new StringBuilder();
-    for (String d : labelDelta) {
-      buf.append(" ").append(d);
-    }
-    if (comments.size() == 1) {
-      buf.append("\n\n(1 comment)");
-    } else if (comments.size() > 1) {
-      buf.append(String.format("\n\n(%d comments)", comments.size()));
-    }
-    if (!msg.isEmpty()) {
-      buf.append("\n\n").append(msg);
-    }
-    if (buf.length() == 0) {
-      return false;
+      message = new ChangeMessage(
+          new ChangeMessage.Key(
+            psId.getParentKey(), ChangeUtil.messageUUID(ctx.getDb())),
+          user.getAccountId(),
+          ctx.getWhen(),
+          psId);
+      message.setMessage(String.format(
+          "Patch Set %d:%s",
+          psId.get(),
+          buf.toString()));
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), message);
+      return true;
     }
 
-    message = new ChangeMessage(
-        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db.get())),
-        rsrc.getAccountId(),
-        timestamp,
-        rsrc.getPatchSet().getId());
-    message.setMessage(String.format(
-        "Patch Set %d:%s",
-        rsrc.getPatchSet().getPatchSetId(),
-        buf.toString()));
-    cmUtil.addChangeMessage(db.get(), update, message);
-    return true;
-  }
-
-  @Deprecated
-  private void fireCommentAddedHook(RevisionResource rsrc) {
-    IdentifiedUser user = (IdentifiedUser) rsrc.getControl().getCurrentUser();
-    try {
-      hooks.doCommentAddedHook(change,
-          user.getAccount(),
-          rsrc.getPatchSet(),
-          message.getMessage(),
-          categories, db.get());
-    } catch (OrmException e) {
-      log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
+    private void addLabelDelta(String name, short value) {
+      labelDelta.add(LabelVote.create(name, value).format());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index ecfb43c..3ab84ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -21,7 +21,6 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,7 +35,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
@@ -84,7 +82,7 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeUpdate.Factory updateFactory;
-  private final Provider<CurrentUser> currentUser;
+  private final Provider<IdentifiedUser> user;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
   private final ChangeHooks hooks;
@@ -102,7 +100,7 @@
       AccountLoader.Factory accountLoaderFactory,
       Provider<ReviewDb> db,
       ChangeUpdate.Factory updateFactory,
-      Provider<CurrentUser> currentUser,
+      Provider<IdentifiedUser> user,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
       ChangeHooks hooks,
@@ -118,7 +116,7 @@
     this.accountLoaderFactory = accountLoaderFactory;
     this.dbProvider = db;
     this.updateFactory = updateFactory;
-    this.currentUser = currentUser;
+    this.user = user;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
     this.hooks = hooks;
@@ -130,7 +128,7 @@
   @Override
   public PostResult apply(ChangeResource rsrc, AddReviewerInput input)
       throws AuthException, BadRequestException, UnprocessableEntityException,
-      OrmException, EmailException, IOException {
+      OrmException, IOException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
@@ -175,7 +173,7 @@
     ChangeControl control = rsrc.getControl();
     Set<Account> members;
     try {
-      members = groupMembersFactory.create(control.getCurrentUser()).listAccounts(
+      members = groupMembersFactory.create(control.getUser()).listAccounts(
               group.getGroupUUID(), control.getProject().getNameKey());
     } catch (NoSuchGroupException e) {
       throw new UnprocessableEntityException(e.getMessage());
@@ -255,8 +253,8 @@
           ImmutableList.of(psa)));
     }
     accountLoaderFactory.create(true).fill(result.reviewers);
-    emailReviewers(rsrc.getChange(), added);
     indexFuture.checkedGet();
+    emailReviewers(rsrc.getChange(), added);
     if (!added.isEmpty()) {
       PatchSet patchSet = dbProvider.get().patchSets().get(rsrc.getChange().currentPatchSetId());
       for (PatchSetApproval psa : added) {
@@ -275,16 +273,16 @@
     //
     // The user knows they added themselves, don't bother emailing them.
     List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size());
-    IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
+    Account.Id userId = user.get().getAccountId();
     for (PatchSetApproval psa : added) {
-      if (!psa.getAccountId().equals(identifiedUser.getAccountId())) {
+      if (!psa.getAccountId().equals(userId)) {
         toMail.add(psa.getAccountId());
       }
     }
     if (!toMail.isEmpty()) {
       try {
-        AddReviewerSender cm = addReviewerSenderFactory.create(change);
-        cm.setFrom(identifiedUser.getAccountId());
+        AddReviewerSender cm = addReviewerSenderFactory.create(change.getId());
+        cm.setFrom(userId);
         cm.addReviewers(toMail);
         cm.send();
       } catch (Exception err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index b88931e..e137ac4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -29,7 +29,8 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -83,8 +84,8 @@
 
     @Override
     public Response<?> apply(ChangeResource rsrc, Publish.Input in)
-        throws AuthException, ResourceConflictException, NoSuchChangeException,
-        IOException, OrmException {
+        throws NoSuchProjectException, IOException, OrmException,
+        RestApiException, UpdateException {
       Capable r =
           rsrc.getControl().getProjectControl().canPushToAtLeastOneRef();
       if (r != Capable.OK) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 9f77f0e..9f2a3f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -14,115 +14,111 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.util.concurrent.CheckedFuture;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.PublishDraftPatchSet.Input;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.mail.PatchSetNotificationSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 @Singleton
 public class PublishDraftPatchSet implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublishDraftPatchSet.class);
+
   public static class Input {
   }
 
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeUpdate.Factory updateFactory;
-  private final PatchSetNotificationSender sender;
+  private final BatchUpdate.Factory updateFactory;
   private final ChangeHooks hooks;
-  private final ChangeIndexer indexer;
+  private final ApprovalsUtil approvalsUtil;
+  private final AccountResolver accountResolver;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final CreateChangeSender.Factory createChangeSenderFactory;
+  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
 
   @Inject
-  public PublishDraftPatchSet(Provider<ReviewDb> dbProvider,
-      ChangeUpdate.Factory updateFactory,
-      PatchSetNotificationSender sender,
+  public PublishDraftPatchSet(
+      Provider<ReviewDb> dbProvider,
+      BatchUpdate.Factory updateFactory,
       ChangeHooks hooks,
-      ChangeIndexer indexer) {
+      ApprovalsUtil approvalsUtil,
+      AccountResolver accountResolver,
+      PatchSetInfoFactory patchSetInfoFactory,
+      CreateChangeSender.Factory createChangeSenderFactory,
+      ReplacePatchSetSender.Factory replacePatchSetFactory) {
     this.dbProvider = dbProvider;
     this.updateFactory = updateFactory;
-    this.sender = sender;
     this.hooks = hooks;
-    this.indexer = indexer;
+    this.approvalsUtil = approvalsUtil;
+    this.accountResolver = accountResolver;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.createChangeSenderFactory = createChangeSenderFactory;
+    this.replacePatchSetFactory = replacePatchSetFactory;
   }
 
   @Override
   public Response<?> apply(RevisionResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, OrmException, IOException {
-    if (!rsrc.getPatchSet().isDraft()) {
-      throw new ResourceConflictException("Patch set is not a draft");
+      throws RestApiException, UpdateException {
+    return apply(rsrc.getUser(), rsrc.getChange(), rsrc.getPatchSet().getId(),
+        rsrc.getPatchSet());
+  }
+
+  private Response<?> apply(CurrentUser u, Change c, PatchSet.Id psId,
+      PatchSet ps) throws RestApiException, UpdateException {
+    try (BatchUpdate bu = updateFactory.create(
+        dbProvider.get(), c.getProject(), u, TimeUtil.nowTs())) {
+      bu.addOp(c.getId(), new Op(psId, ps));
+      bu.execute();
     }
-
-    if (!rsrc.getControl().canPublish(dbProvider.get())) {
-      throw new AuthException("Cannot publish this draft patch set");
-    }
-
-    PatchSet updatedPatchSet = updateDraftPatchSet(rsrc);
-    Change updatedChange = updateDraftChange(rsrc);
-    ChangeUpdate update = updateFactory.create(rsrc.getControl(),
-        updatedChange.getLastUpdatedOn());
-
-    if (!updatedPatchSet.isDraft()
-        || updatedChange.getStatus() == Change.Status.NEW) {
-      CheckedFuture<?, IOException> indexFuture =
-          indexer.indexAsync(updatedChange.getId());
-      sender.send(rsrc.getNotes(), update,
-          rsrc.getChange().getStatus() == Change.Status.DRAFT,
-          rsrc.getUser(), updatedChange, updatedPatchSet,
-          rsrc.getControl().getLabelTypes());
-      indexFuture.checkedGet();
-      hooks.doDraftPublishedHook(updatedChange, updatedPatchSet,
-          dbProvider.get());
-    }
-
     return Response.none();
   }
 
-  private Change updateDraftChange(RevisionResource rsrc) throws OrmException {
-    return dbProvider.get().changes()
-        .atomicUpdate(rsrc.getChange().getId(),
-        new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change change) {
-        if (change.getStatus() == Change.Status.DRAFT) {
-          change.setStatus(Change.Status.NEW);
-          ChangeUtil.updated(change);
-        }
-        return change;
-      }
-    });
-  }
-
-  private PatchSet updateDraftPatchSet(RevisionResource rsrc) throws OrmException {
-    return dbProvider.get().patchSets()
-        .atomicUpdate(rsrc.getPatchSet().getId(),
-        new AtomicUpdate<PatchSet>() {
-      @Override
-      public PatchSet update(PatchSet patchset) {
-        patchset.setDraft(false);
-        return patchset;
-      }
-    });
-  }
-
   @Override
   public UiAction.Description getDescription(RevisionResource rsrc) {
     try {
@@ -138,28 +134,146 @@
 
   public static class CurrentRevision implements
       RestModifyView<ChangeResource, Input> {
-    private final Provider<ReviewDb> dbProvider;
     private final PublishDraftPatchSet publish;
 
     @Inject
-    CurrentRevision(Provider<ReviewDb> dbProvider,
-        PublishDraftPatchSet publish) {
-      this.dbProvider = dbProvider;
+    CurrentRevision(PublishDraftPatchSet publish) {
       this.publish = publish;
     }
 
     @Override
     public Response<?> apply(ChangeResource rsrc, Input input)
-        throws AuthException, ResourceConflictException,
-        ResourceNotFoundException, IOException, OrmException {
-      PatchSet ps = dbProvider.get().patchSets()
-        .get(rsrc.getChange().currentPatchSetId());
+        throws RestApiException, UpdateException {
+      return publish.apply(rsrc.getControl().getUser(), rsrc.getChange(),
+          rsrc.getChange().currentPatchSetId(), null);
+    }
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final PatchSet.Id psId;
+
+    private PatchSet patchSet;
+    private Change change;
+    private boolean wasDraftChange;
+    private RevCommit commit;
+    private PatchSetInfo patchSetInfo;
+    private MailRecipients recipients;
+
+    private Op(PatchSet.Id psId, @Nullable PatchSet patchSet) {
+      this.psId = psId;
+      this.patchSet = patchSet;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx)
+        throws RestApiException, OrmException, IOException {
+      PatchSet ps = patchSet;
       if (ps == null) {
-        throw new ResourceConflictException("current revision is missing");
-      } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
-        throw new AuthException("current revision not accessible");
+        // Don't save in patchSet, since we're not in a transaction. Here we
+        // just need the revision, which is immutable.
+        ps = ctx.getDb().patchSets().get(psId);
+        if (ps == null) {
+          throw new ResourceNotFoundException(psId.toString());
+        }
       }
-      return publish.apply(new RevisionResource(rsrc, ps), input);
+      commit = ctx.getRevWalk().parseCommit(
+          ObjectId.fromString(ps.getRevision().get()));
+      patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx)
+        throws RestApiException, OrmException {
+      if (!ctx.getChangeControl().canPublish(ctx.getDb())) {
+        throw new AuthException("Cannot publish this draft patch set");
+      }
+      saveChange(ctx);
+      savePatchSet(ctx);
+      addReviewers(ctx);
+    }
+
+    private void saveChange(ChangeContext ctx) throws OrmException {
+      change = ctx.getChange();
+      wasDraftChange = change.getStatus() == Change.Status.DRAFT;
+      if (wasDraftChange) {
+        change.setStatus(Change.Status.NEW);
+        ChangeUtil.updated(change);
+        ctx.getDb().changes().update(Collections.singleton(change));
+      }
+    }
+
+    private void savePatchSet(ChangeContext ctx)
+        throws RestApiException, OrmException {
+      patchSet = ctx.getDb().patchSets().get(psId);
+      if (!patchSet.isDraft()) {
+        throw new ResourceConflictException("Patch set is not a draft");
+      }
+      patchSet.setDraft(false);
+      // Force ETag invalidation if not done already
+      if (!wasDraftChange) {
+        ChangeUtil.updated(change);
+        ctx.getDb().changes().update(Collections.singleton(change));
+      }
+      ctx.getDb().patchSets().update(Collections.singleton(patchSet));
+    }
+
+    private void addReviewers(ChangeContext ctx) throws OrmException {
+      LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
+      Collection<Account.Id> oldReviewers = approvalsUtil.getReviewers(
+          ctx.getDb(), ctx.getChangeNotes()).values();
+      List<FooterLine> footerLines = commit.getFooterLines();
+      recipients =
+          getRecipientsFromFooters(accountResolver, patchSet, footerLines);
+      recipients.remove(ctx.getUser().getAccountId());
+      approvalsUtil.addReviewers(ctx.getDb(), ctx.getChangeUpdate(), labelTypes,
+          change, patchSet, patchSetInfo, recipients.getReviewers(),
+          oldReviewers);
+    }
+
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      hooks.doDraftPublishedHook(change, patchSet, ctx.getDb());
+      if (patchSet.isDraft() && change.getStatus() == Change.Status.DRAFT) {
+        // Skip emails if the patch set is still a draft.
+        return;
+      }
+      try {
+        if (wasDraftChange) {
+          sendCreateChange(ctx);
+        } else {
+          sendReplacePatchSet(ctx);
+        }
+      } catch (EmailException | OrmException e) {
+        log.error("Cannot send email for publishing draft " + psId, e);
+      }
+    }
+
+    private void sendCreateChange(Context ctx) throws EmailException {
+      CreateChangeSender cm =
+          createChangeSenderFactory.create(change.getId());
+      cm.setFrom(ctx.getUser().getAccountId());
+      cm.setPatchSet(patchSet, patchSetInfo);
+      cm.addReviewers(recipients.getReviewers());
+      cm.addExtraCC(recipients.getCcOnly());
+      cm.send();
+    }
+
+    private void sendReplacePatchSet(Context ctx)
+        throws EmailException, OrmException {
+      Account.Id accountId = ctx.getUser().getAccountId();
+      ChangeMessage msg =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())), accountId,
+              ctx.getWhen(), psId);
+      msg.setMessage("Uploaded patch set " + psId.get() + ".");
+      ReplacePatchSetSender cm =
+          replacePatchSetFactory.create(change.getId());
+      cm.setFrom(accountId);
+      cm.setPatchSet(patchSet, patchSetInfo);
+      cm.setChangeMessage(msg);
+      cm.addReviewers(recipients.getReviewers());
+      cm.addExtraCC(recipients.getCcOnly());
+      cm.send();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 2a0bcb3..a4a5e16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -45,7 +45,7 @@
   private final DeleteDraftComment delete;
   private final PatchLineCommentsUtil plcUtil;
   private final ChangeUpdate.Factory updateFactory;
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
   private final PatchListCache patchListCache;
 
   @Inject
@@ -53,7 +53,7 @@
       DeleteDraftComment delete,
       PatchLineCommentsUtil plcUtil,
       ChangeUpdate.Factory updateFactory,
-      CommentJson commentJson,
+      Provider<CommentJson> commentJson,
       PatchListCache patchListCache) {
     this.db = db;
     this.delete = delete;
@@ -96,14 +96,13 @@
           Collections.singleton(update(c, in)));
     } else {
       if (c.getRevId() == null) {
-        setCommentRevId(c, patchListCache, rsrc.getChange(),
-            rsrc.getPatchSet());
+        setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
       }
       plcUtil.updateComments(db.get(), update,
           Collections.singleton(update(c, in)));
     }
     update.commit();
-    return Response.ok(commentJson.format(c, false));
+    return Response.ok(commentJson.get().setFillAccounts(false).format(c));
   }
 
   private PatchLineComment update(PatchLineComment e, DraftInput in) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index d171785..ae12497 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,25 +30,25 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.PutTopic.Input;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import java.io.IOException;
+import java.util.Collections;
 
 @Singleton
 public class PutTopic implements RestModifyView<ChangeResource, Input>,
     UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeIndexer indexer;
   private final ChangeHooks hooks;
-  private final ChangeUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   public static class Input {
     @DefaultInput
@@ -55,80 +56,86 @@
   }
 
   @Inject
-  PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer,
-      ChangeHooks hooks, ChangeUpdate.Factory updateFactory,
-      ChangeMessagesUtil cmUtil) {
+  PutTopic(Provider<ReviewDb> dbProvider,
+      ChangeHooks hooks,
+      ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.dbProvider = dbProvider;
-    this.indexer = indexer;
     this.hooks = hooks;
-    this.updateFactory = updateFactory;
     this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   @Override
   public Response<String> apply(ChangeResource req, Input input)
-      throws AuthException, OrmException, IOException {
-    if (input == null) {
-      input = new Input();
-    }
-
-    ChangeControl control = req.getControl();
-    Change change = req.getChange();
-    if (!control.canEditTopicName()) {
+      throws UpdateException, RestApiException {
+    ChangeControl ctl = req.getControl();
+    if (!ctl.canEditTopicName()) {
       throw new AuthException("changing topic not permitted");
     }
 
-    ReviewDb db = dbProvider.get();
-    final String newTopicName = Strings.nullToEmpty(input.topic);
-    String oldTopicName = Strings.nullToEmpty(change.getTopic());
-    if (!oldTopicName.equals(newTopicName)) {
+    Op op = new Op(ctl, input != null ? input : new Input());
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+      u.addOp(req.getChange().getId(), op);
+      u.execute();
+    }
+    return Strings.isNullOrEmpty(op.newTopicName)
+        ? Response.<String> none()
+        : Response.ok(op.newTopicName);
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final Input input;
+    private final IdentifiedUser caller;
+
+    private Change change;
+    private String oldTopicName;
+    private String newTopicName;
+
+    public Op(ChangeControl ctl, Input input) {
+      this.input = input;
+      this.caller = ctl.getUser().asIdentifiedUser();
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException {
+      change = ctx.getChange();
+      newTopicName = Strings.nullToEmpty(input.topic);
+      oldTopicName = Strings.nullToEmpty(change.getTopic());
+      if (oldTopicName.equals(newTopicName)) {
+        return;
+      }
       String summary;
       if (oldTopicName.isEmpty()) {
         summary = "Topic set to " + newTopicName;
       } else if (newTopicName.isEmpty()) {
         summary = "Topic " + oldTopicName + " removed";
       } else {
-        summary = String.format(
-            "Topic changed from %s to %s",
+        summary = String.format("Topic changed from %s to %s",
             oldTopicName, newTopicName);
       }
+      change.setTopic(Strings.emptyToNull(newTopicName));
+      ChangeUtil.updated(change);
+      ctx.getDb().changes().update(Collections.singleton(change));
 
-      IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
       ChangeMessage cmsg = new ChangeMessage(
-          new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
-          currentUser.getAccountId(), TimeUtil.nowTs(),
+          new ChangeMessage.Key(
+              change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+          caller.getAccountId(), ctx.getWhen(),
           change.currentPatchSetId());
       cmsg.setMessage(summary);
-      ChangeUpdate update;
-
-      db.changes().beginTransaction(change.getId());
-      try {
-        change = db.changes().atomicUpdate(change.getId(),
-          new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change change) {
-              change.setTopic(Strings.emptyToNull(newTopicName));
-              ChangeUtil.updated(change);
-              return change;
-            }
-          });
-
-        //TODO(yyonas): atomic update was not propagated
-        update = updateFactory.create(control);
-        cmUtil.addChangeMessage(db, update, cmsg);
-
-        db.commit();
-      } finally {
-        db.rollback();
-      }
-      update.commit();
-      indexer.index(db, change);
-      hooks.doTopicChangedHook(change, currentUser.getAccount(),
-          oldTopicName, db);
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), cmsg);
     }
-    return Strings.isNullOrEmpty(newTopicName)
-        ? Response.<String>none()
-        : Response.ok(newTopicName);
+
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      if (change != null) {
+        hooks.doTopicChangedHook(change, caller.getAccount(),
+            oldTopicName, ctx.getDb());
+      }
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index d2afd9a..60f285f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -14,217 +14,223 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-
-import java.util.ArrayList;
+import java.util.EnumSet;
 
 @Singleton
 public class Rebase implements RestModifyView<RevisionResource, RebaseInput>,
     UiAction<RevisionResource> {
+  private static final Logger log = LoggerFactory.getLogger(Rebase.class);
+  private static final EnumSet<ListChangesOption> OPTIONS = EnumSet.of(
+      ListChangesOption.CURRENT_REVISION,
+      ListChangesOption.CURRENT_COMMIT);
 
-  private static final Logger log =
-      LoggerFactory.getLogger(Rebase.class);
-
-  private final Provider<RebaseChange> rebaseChange;
-  private final ChangeJson json;
+  private final BatchUpdate.Factory updateFactory;
+  private final GitRepositoryManager repoManager;
+  private final RebaseChangeOp.Factory rebaseFactory;
+  private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json,
+  public Rebase(BatchUpdate.Factory updateFactory,
+      GitRepositoryManager repoManager,
+      RebaseChangeOp.Factory rebaseFactory,
+      ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider) {
-    this.rebaseChange = rebaseChange;
-    this.json = json
-        .addOption(ListChangesOption.CURRENT_REVISION)
-        .addOption(ListChangesOption.CURRENT_COMMIT);
+    this.updateFactory = updateFactory;
+    this.repoManager = repoManager;
+    this.rebaseFactory = rebaseFactory;
+    this.json = json;
     this.dbProvider = dbProvider;
   }
 
   @Override
   public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
-      throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, EmailException, OrmException {
+      throws EmailException, OrmException, UpdateException, RestApiException,
+      IOException {
     ChangeControl control = rsrc.getControl();
     Change change = rsrc.getChange();
-    if (!control.canRebase()) {
-      throw new AuthException("rebase not permitted");
-    } else if (!change.getStatus().isOpen()) {
-      throw new ResourceConflictException("change is "
-          + change.getStatus().name().toLowerCase());
-    } else if (!hasOneParent(rsrc.getPatchSet().getId())) {
-      throw new ResourceConflictException(
-          "cannot rebase merge commits or commit with no ancestor");
-    }
-
-    String baseRev = null;
-    if (input != null && input.base != null) {
-      String base = input.base.trim();
-      do {
-        if (base.equals("")) {
-          // remove existing dependency to other patch set
-          baseRev = change.getDest().get();
-          break;
-        }
-
-        ReviewDb db = dbProvider.get();
-        PatchSet basePatchSet = parseBase(base);
-        if (basePatchSet == null) {
-          throw new ResourceConflictException("base revision is missing: " + base);
-        } else if (!rsrc.getControl().isPatchVisible(basePatchSet, db)) {
-          throw new AuthException("base revision not accessible: " + base);
-        } else if (change.getId().equals(basePatchSet.getId().getParentKey())) {
-          throw new ResourceConflictException("cannot depend on self");
-        }
-
-        Change baseChange = db.changes().get(basePatchSet.getId().getParentKey());
-        if (baseChange != null) {
-          if (!baseChange.getProject().equals(change.getProject())) {
-            throw new ResourceConflictException("base change is in wrong project: "
-                                                + baseChange.getProject());
-          } else if (!baseChange.getDest().equals(change.getDest())) {
-            throw new ResourceConflictException("base change is targetting wrong branch: "
-                                                + baseChange.getDest());
-          } else if (baseChange.getStatus() == Status.ABANDONED) {
-            throw new ResourceConflictException("base change is abandoned: "
-                                                + baseChange.getKey());
-          } else if (isDescendantOf(baseChange.getId(), rsrc.getPatchSet().getRevision())) {
-            throw new ResourceConflictException("base change " + baseChange.getKey()
-                                                + " is a descendant of the current "
-                                                + " change - recursion not allowed");
-          }
-          baseRev = basePatchSet.getRevision().get();
-          break;
-        }
-      } while (false);  // just wanted to use the break statement
-    }
-
-    try {
-      rebaseChange.get().rebase(change, rsrc.getPatchSet().getId(),
-          rsrc.getUser(), baseRev);
-    } catch (InvalidChangeOperationException e) {
-      throw new ResourceConflictException(e.getMessage());
-    } catch (IOException e) {
-      throw new ResourceConflictException(e.getMessage());
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException(change.getId().toString());
-    }
-
-    return json.format(change.getId());
-  }
-
-  private boolean isDescendantOf(Change.Id child, RevId ancestor)
-      throws OrmException {
-    ReviewDb db = dbProvider.get();
-
-    ArrayList<RevId> parents = new ArrayList<>();
-    parents.add(ancestor);
-    while (!parents.isEmpty()) {
-      RevId parent = parents.remove(0);
-      // get direct descendants of change
-      for (PatchSetAncestor desc : db.patchSetAncestors().descendantsOf(parent)) {
-        PatchSet descPatchSet = db.patchSets().get(desc.getPatchSet());
-        Change.Id descChangeId = descPatchSet.getId().getParentKey();
-        if (child.equals(descChangeId)) {
-          PatchSet.Id descCurrentPatchSetId =
-              db.changes().get(descChangeId).currentPatchSetId();
-          // it's only bad if the descendant patch set is current
-          return descPatchSet.getId().equals(descCurrentPatchSetId);
-        } else {
-          // process indirect descendants as well
-          parents.add(descPatchSet.getRevision());
-        }
+    try (Repository repo = repoManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter oi = repo.newObjectInserter();
+        BatchUpdate bu = updateFactory.create(dbProvider.get(),
+          change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+      if (!control.canRebase()) {
+        throw new AuthException("rebase not permitted");
+      } else if (!change.getStatus().isOpen()) {
+        throw new ResourceConflictException("change is "
+            + change.getStatus().name().toLowerCase());
+      } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
+        throw new ResourceConflictException(
+            "cannot rebase merge commits or commit with no ancestor");
       }
+      bu.setRepository(repo, rw, oi);
+      bu.addOp(change.getId(), rebaseFactory.create(
+            control, rsrc.getPatchSet(),
+            findBaseRev(rw, rsrc, input))
+          .setForceContentMerge(true)
+          .setRunHooks(true)
+          .setValidatePolicy(CommitValidators.Policy.GERRIT));
+      bu.execute();
     }
-
-    return false;
+    return json.create(OPTIONS).format(change.getId());
   }
 
-  private PatchSet parseBase(final String base) throws OrmException {
+  private String findBaseRev(RevWalk rw, RevisionResource rsrc,
+      RebaseInput input) throws AuthException, ResourceConflictException,
+      OrmException, IOException {
+    if (input == null || input.base == null) {
+      return null;
+    }
+
+    Change change = rsrc.getChange();
+    String base = input.base.trim();
+    if (base.equals("")) {
+      // remove existing dependency to other patch set
+      return change.getDest().get();
+    }
+
+    @SuppressWarnings("resource")
+    ReviewDb db = dbProvider.get();
+    PatchSet basePatchSet = parseBase(base);
+    if (basePatchSet == null) {
+      throw new ResourceConflictException("base revision is missing: " + base);
+    } else if (!rsrc.getControl().isPatchVisible(basePatchSet, db)) {
+      throw new AuthException("base revision not accessible: " + base);
+    } else if (change.getId().equals(basePatchSet.getId().getParentKey())) {
+      throw new ResourceConflictException("cannot depend on self");
+    }
+
+    Change baseChange = db.changes().get(basePatchSet.getId().getParentKey());
+    if (baseChange == null) {
+      return null;
+    }
+
+    if (!baseChange.getProject().equals(change.getProject())) {
+      throw new ResourceConflictException(
+          "base change is in wrong project: " + baseChange.getProject());
+    } else if (!baseChange.getDest().equals(change.getDest())) {
+      throw new ResourceConflictException(
+          "base change is targeting wrong branch: " + baseChange.getDest());
+    } else if (baseChange.getStatus() == Status.ABANDONED) {
+      throw new ResourceConflictException(
+          "base change is abandoned: " + baseChange.getKey());
+    } else if (isMergedInto(rw, rsrc.getPatchSet(), basePatchSet)) {
+      throw new ResourceConflictException(
+          "base change " + baseChange.getKey()
+          + " is a descendant of the current  change - recursion not allowed");
+    }
+    return basePatchSet.getRevision().get();
+  }
+
+  private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip)
+      throws IOException {
+    ObjectId baseId = ObjectId.fromString(base.getRevision().get());
+    ObjectId tipId = ObjectId.fromString(tip.getRevision().get());
+    return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
+  }
+
+  private PatchSet parseBase(String base) throws OrmException {
     ReviewDb db = dbProvider.get();
 
     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
     if (basePatchSetId != null) {
-      // try parsing the base as a ref string
+      // Try parsing the base as a ref string.
       return db.patchSets().get(basePatchSetId);
     }
 
-    // try parsing base as a change number (assume current patch set)
+    // Try parsing base as a change number (assume current patch set).
     PatchSet basePatchSet = null;
-    try {
-      Change.Id baseChangeId = Change.Id.parse(base);
-      if (baseChangeId != null) {
-        for (PatchSet ps : db.patchSets().byChange(baseChangeId)) {
-          if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()){
-            basePatchSet = ps;
-          }
-        }
-      }
-    } catch (NumberFormatException e) {  // probably a SHA1
-    }
-
-    // try parsing as SHA1
-    if (basePatchSet == null) {
-      for (PatchSet ps : db.patchSets().byRevision(new RevId(base))) {
-        if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()) {
+    Integer baseChangeId = Ints.tryParse(base);
+    if (baseChangeId != null) {
+      for (PatchSet ps : db.patchSets().byChange(new Change.Id(baseChangeId))) {
+        if (basePatchSet == null
+            || basePatchSet.getId().get() < ps.getId().get()) {
           basePatchSet = ps;
         }
       }
+      if (basePatchSet != null) {
+        return basePatchSet;
+      }
     }
 
+    // Try parsing as SHA-1.
+    for (PatchSet ps : db.patchSets().byRevision(new RevId(base))) {
+      if (basePatchSet == null
+          || basePatchSet.getId().get() < ps.getId().get()) {
+        basePatchSet = ps;
+      }
+    }
     return basePatchSet;
   }
 
-  private boolean hasOneParent(final PatchSet.Id patchSetId) {
-    try {
-      // prevent rebase of exotic changes (merge commit, no ancestor).
-      return (dbProvider.get().patchSetAncestors()
-          .ancestorsOf(patchSetId).toList().size() == 1);
-    } catch (OrmException e) {
-      log.error("Failed to get ancestors of patch set "
-          + patchSetId.toRefName(), e);
-      return false;
-    }
+  private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
+    // Prevent rebase of exotic changes (merge commit, no ancestor).
+    RevCommit c = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+    return c.getParentCount() == 1;
   }
 
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
+    PatchSet patchSet = resource.getPatchSet();
+    Branch.NameKey dest = resource.getChange().getDest();
+    boolean visible = resource.getChange().getStatus().isOpen()
+          && resource.isCurrent()
+          && resource.getControl().canRebase();
+    boolean enabled = true;
+
+    if (visible) {
+      try (Repository repo = repoManager.openRepository(dest.getParentKey());
+          RevWalk rw = new RevWalk(repo)) {
+        visible = hasOneParent(rw, resource.getPatchSet());
+        enabled =
+            RebaseUtil.canRebase(patchSet, dest, repo, rw, dbProvider.get());
+      } catch (IOException e) {
+        log.error("Failed to check if patch set can be rebased: "
+            + resource.getPatchSet(), e);
+        visible = false;
+      }
+    }
     UiAction.Description descr = new UiAction.Description()
       .setLabel("Rebase")
       .setTitle("Rebase onto tip of branch or parent change")
-      .setVisible(resource.getChange().getStatus().isOpen()
-          && resource.getControl().canRebase()
-          && hasOneParent(resource.getPatchSet().getId()));
-    if (descr.isVisible()) {
-      // Disable the rebase button in the RebaseDialog if
-      // the change cannot be rebased.
-      descr.setEnabled(rebaseChange.get().canRebase(resource));
-    }
+      .setVisible(visible)
+      .setEnabled(enabled);
     return descr;
   }
 
@@ -239,8 +245,8 @@
 
     @Override
     public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
-        throws AuthException, ResourceNotFoundException,
-        ResourceConflictException, EmailException, OrmException {
+        throws EmailException, OrmException, UpdateException, RestApiException,
+        IOException {
       PatchSet ps =
           rebase.dbProvider.get().patchSets()
               .get(rsrc.getChange().currentPatchSetId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
new file mode 100644
index 0000000..e737293
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -0,0 +1,231 @@
+// Copyright (C) 2015 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.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RebaseChangeOp extends BatchUpdate.Op {
+  public interface Factory {
+    RebaseChangeOp create(ChangeControl ctl, PatchSet originalPatchSet,
+        @Nullable String baseCommitish);
+  }
+
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final MergeUtil.Factory mergeUtilFactory;
+
+  private final ChangeControl ctl;
+  private final PatchSet originalPatchSet;
+
+  private String baseCommitish;
+  private PersonIdent committerIdent;
+  private boolean runHooks = true;
+  private CommitValidators.Policy validate;
+  private boolean forceContentMerge;
+
+  private RevCommit rebasedCommit;
+  private PatchSet.Id rebasedPatchSetId;
+  private PatchSetInserter patchSetInserter;
+  private PatchSet rebasedPatchSet;
+
+  @AssistedInject
+  RebaseChangeOp(
+      PatchSetInserter.Factory patchSetInserterFactory,
+      MergeUtil.Factory mergeUtilFactory,
+      @Assisted ChangeControl ctl,
+      @Assisted PatchSet originalPatchSet,
+      @Assisted @Nullable String baseCommitish) {
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.ctl = ctl;
+    this.originalPatchSet = originalPatchSet;
+    this.baseCommitish = baseCommitish;
+  }
+
+  public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
+    this.committerIdent = committerIdent;
+    return this;
+  }
+
+  public RebaseChangeOp setValidatePolicy(CommitValidators.Policy validate) {
+    this.validate = validate;
+    return this;
+  }
+
+  public RebaseChangeOp setRunHooks(boolean runHooks) {
+    this.runHooks = runHooks;
+    return this;
+  }
+
+  public RebaseChangeOp setForceContentMerge(boolean forceContentMerge) {
+    this.forceContentMerge = forceContentMerge;
+    return this;
+  }
+
+  @Override
+  public void updateRepo(RepoContext ctx) throws MergeConflictException,
+       InvalidChangeOperationException, RestApiException, IOException,
+       OrmException {
+    // Ok that originalPatchSet was not read in a transaction, since we just
+    // need its revision.
+    RevId oldRev = originalPatchSet.getRevision();
+
+    RevWalk rw = ctx.getRevWalk();
+    RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get()));
+    rw.parseBody(original);
+    RevCommit baseCommit;
+    if (baseCommitish != null) {
+       baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish));
+    } else {
+       baseCommit = rw.parseCommit(RebaseUtil.findBaseRevision(
+           originalPatchSet, ctl.getChange().getDest(),
+           ctx.getRepository(), ctx.getRevWalk(), ctx.getDb()));
+    }
+
+    ObjectId newId = rebaseCommit(ctx, original, baseCommit);
+    rebasedCommit = rw.parseCommit(newId);
+
+    List<String> baseCommitPatchSetGroups = new ArrayList<>();
+    List<String> groups;
+    RevId patchSetByRev = new RevId((baseCommitish != null) ? baseCommitish
+        : ObjectId.toString(baseCommit.getId()));
+    ResultSet<PatchSet> relatedPatchSets =
+        ctx.getDb().patchSets().byRevision(patchSetByRev);
+    for (PatchSet ps : relatedPatchSets) {
+      groups = ps.getGroups();
+      if (groups != null) {
+        baseCommitPatchSetGroups.addAll(groups);
+      }
+    }
+
+    rebasedPatchSetId = ChangeUtil.nextPatchSetId(
+        ctx.getRepository(), ctl.getChange().currentPatchSetId());
+    patchSetInserter = patchSetInserterFactory
+        .create(ctl.getRefControl(), rebasedPatchSetId, rebasedCommit)
+        .setGroups(baseCommitPatchSetGroups)
+        .setDraft(originalPatchSet.isDraft())
+        .setUploader(ctx.getUser().getAccountId())
+        .setSendMail(false)
+        .setRunHooks(runHooks)
+        .setMessage(
+          "Patch Set " + rebasedPatchSetId.get()
+          + ": Patch Set " + originalPatchSet.getId().get() + " was rebased");
+    if (validate != null) {
+      patchSetInserter.setValidatePolicy(validate);
+    }
+    patchSetInserter.updateRepo(ctx);
+  }
+
+  @Override
+  public void updateChange(ChangeContext ctx)
+      throws OrmException, InvalidChangeOperationException {
+    patchSetInserter.updateChange(ctx);
+    rebasedPatchSet = patchSetInserter.getPatchSet();
+  }
+
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
+    patchSetInserter.postUpdate(ctx);
+  }
+
+  public PatchSet getPatchSet() {
+    checkState(rebasedPatchSet != null,
+        "getPatchSet() only valid after executing update");
+    return rebasedPatchSet;
+  }
+
+  private MergeUtil newMergeUtil() {
+    ProjectState project = ctl.getProjectControl().getProjectState();
+    return forceContentMerge
+        ? mergeUtilFactory.create(project, true)
+        : mergeUtilFactory.create(project);
+  }
+
+  /**
+   * Rebase a commit.
+   *
+   * @param ctx repo context.
+   * @param original the commit to rebase.
+   * @param base base to rebase against.
+   * @return the rebased commit.
+   * @throws MergeConflictException the rebase failed due to a merge conflict.
+   * @throws IOException the merge failed for another reason.
+   */
+  private RevCommit rebaseCommit(RepoContext ctx, RevCommit original,
+      ObjectId base) throws ResourceConflictException, MergeConflictException,
+      IOException {
+    RevCommit parentCommit = original.getParent(0);
+
+    if (base.equals(parentCommit)) {
+      throw new ResourceConflictException("Change is already up to date.");
+    }
+
+    ThreeWayMerger merger = newMergeUtil().newThreeWayMerger(
+        ctx.getRepository(), ctx.getInserter());
+    merger.setBase(parentCommit);
+    merger.merge(original, base);
+
+    if (merger.getResultTreeId() == null) {
+      throw new MergeConflictException(
+          "The change could not be rebased due to a conflict during merge.");
+    }
+
+    CommitBuilder cb = new CommitBuilder();
+    cb.setTreeId(merger.getResultTreeId());
+    cb.setParentId(base);
+    cb.setAuthor(original.getAuthorIdent());
+    cb.setMessage(original.getFullMessage());
+    if (committerIdent != null) {
+      cb.setCommitter(committerIdent);
+    } else {
+      cb.setCommitter(ctx.getUser().asIdentifiedUser()
+          .newCommitterIdent(ctx.getWhen(), ctx.getTimeZone()));
+    }
+    ObjectId objectId = ctx.getInserter().insert(cb);
+    ctx.getInserter().flush();
+    return ctx.getRevWalk().parseCommit(objectId);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
new file mode 100644
index 0000000..aea49f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2015 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.server.change;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/** Utility methods related to rebasing changes. */
+public class RebaseUtil {
+  private static final Logger log = LoggerFactory.getLogger(RebaseUtil.class);
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager gitManager;
+
+  @Inject
+  RebaseUtil(Provider<ReviewDb> db,
+      GitRepositoryManager gitManager) {
+    this.db = db;
+    this.gitManager = gitManager;
+  }
+
+  public boolean canRebase(RevisionResource r) {
+    PatchSet patchSet = r.getPatchSet();
+    Branch.NameKey dest = r.getChange().getDest();
+    try (Repository git = gitManager.openRepository(dest.getParentKey());
+        RevWalk rw = new RevWalk(git)) {
+      return canRebase(
+          r.getPatchSet(), dest, git, rw, db.get());
+    } catch (IOException e) {
+      log.warn(String.format(
+          "Error checking if patch set %s on %s can be rebased",
+          patchSet.getId(), dest), e);
+      return false;
+    }
+  }
+
+  public static boolean canRebase(PatchSet patchSet, Branch.NameKey dest,
+      Repository git, RevWalk rw, ReviewDb db) {
+    try {
+      findBaseRevision(patchSet, dest, git, rw, db);
+      return true;
+    } catch (RestApiException e) {
+      return false;
+    } catch (OrmException | IOException e) {
+      log.warn(String.format(
+          "Error checking if patch set %s on %s can be rebased",
+          patchSet.getId(), dest), e);
+      return false;
+    }
+  }
+
+  /**
+   * Find the commit onto which a patch set should be rebased.
+   * <p>
+   * This is defined as the latest patch set of the change corresponding to
+   * this commit's parent, or the destination branch tip in the case where the
+   * parent's change is merged.
+   *
+   * @param patchSet patch set for which the new base commit should be found.
+   * @param destBranch the destination branch.
+   * @param git the repository.
+   * @param rw the RevWalk.
+   * @return the commit onto which the patch set should be rebased.
+   * @throws RestApiException if rebase is not possible.
+   * @throws IOException if accessing the repository fails.
+   * @throws OrmException if accessing the database fails.
+   */
+  static ObjectId findBaseRevision(PatchSet patchSet,
+      Branch.NameKey destBranch, Repository git, RevWalk rw, ReviewDb db)
+      throws RestApiException, IOException, OrmException {
+    String baseRev = null;
+    RevCommit commit = rw.parseCommit(
+        ObjectId.fromString(patchSet.getRevision().get()));
+
+    if (commit.getParentCount() > 1) {
+      throw new UnprocessableEntityException(
+          "Cannot rebase a change with multiple parents.");
+    } else if (commit.getParentCount() == 0) {
+      throw new UnprocessableEntityException(
+          "Cannot rebase a change without any parents"
+          + " (is this the initial commit?).");
+    }
+
+    RevId parentRev = new RevId(commit.getParent(0).name());
+
+    for (PatchSet depPatchSet : db.patchSets().byRevision(parentRev)) {
+      Change.Id depChangeId = depPatchSet.getId().getParentKey();
+      Change depChange = db.changes().get(depChangeId);
+      if (!depChange.getDest().equals(destBranch)) {
+        continue;
+      }
+
+      if (depChange.getStatus() == Status.ABANDONED) {
+        throw new ResourceConflictException(
+            "Cannot rebase a change with an abandoned parent: "
+            + depChange.getKey());
+      }
+
+      if (depChange.getStatus().isOpen()) {
+        if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+          throw new ResourceConflictException(
+              "Change is already based on the latest patch set of the"
+              + " dependent change.");
+        }
+        PatchSet latestDepPatchSet =
+            db.patchSets().get(depChange.currentPatchSetId());
+        baseRev = latestDepPatchSet.getRevision().get();
+      }
+      break;
+    }
+
+    if (baseRev == null) {
+      // We are dependent on a merged PatchSet or have no PatchSet
+      // dependencies at all.
+      Ref destRef = git.getRefDatabase().exactRef(destBranch.get());
+      if (destRef == null) {
+        throw new UnprocessableEntityException(
+            "The destination branch does not exist: " + destBranch.get());
+      }
+      baseRev = destRef.getObjectId().getName();
+      if (baseRev.equals(parentRev.get())) {
+        throw new ResourceConflictException("Change is already up to date.");
+      }
+    }
+    return ObjectId.fromString(baseRev);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
new file mode 100644
index 0000000..8cd82a9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -0,0 +1,253 @@
+// Copyright (C) 2015 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.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@Singleton
+class RelatedChangesSorter {
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  RelatedChangesSorter(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  public List<PatchSetData> sort(List<ChangeData> in, PatchSet startPs)
+      throws OrmException, IOException {
+    checkArgument(!in.isEmpty(), "Input may not be empty");
+    // Map of all patch sets, keyed by commit SHA-1.
+    Map<String, PatchSetData> byId = collectById(in);
+    PatchSetData start = byId.get(startPs.getRevision().get());
+    checkArgument(start != null, "%s not found in %s", startPs, in);
+    ProjectControl ctl = start.data().changeControl().getProjectControl();
+
+    // Map of patch set -> immediate parent.
+    ListMultimap<PatchSetData, PatchSetData> parents =
+        ArrayListMultimap.create(in.size(), 3);
+    // Map of patch set -> immediate children.
+    ListMultimap<PatchSetData, PatchSetData> children =
+        ArrayListMultimap.create(in.size(), 3);
+    // All other patch sets of the same change as startPs.
+    List<PatchSetData> otherPatchSetsOfStart = new ArrayList<>();
+
+    for (ChangeData cd : in) {
+      for (PatchSet ps : cd.patchSets()) {
+        PatchSetData thisPsd = checkNotNull(byId.get(ps.getRevision().get()));
+        if (cd.getId().equals(start.id()) && !ps.getId().equals(start.psId())) {
+          otherPatchSetsOfStart.add(thisPsd);
+        }
+        for (RevCommit p : thisPsd.commit().getParents()) {
+          PatchSetData parentPsd = byId.get(p.name());
+          if (parentPsd != null) {
+            parents.put(thisPsd, parentPsd);
+            children.put(parentPsd, thisPsd);
+          }
+        }
+      }
+    }
+
+    Collection<PatchSetData> ancestors = walkAncestors(ctl, parents, start);
+    List<PatchSetData> descendants =
+        walkDescendants(ctl, children, start, otherPatchSetsOfStart, ancestors);
+    List<PatchSetData> result =
+        new ArrayList<>(ancestors.size() + descendants.size() - 1);
+    result.addAll(Lists.reverse(descendants));
+    result.addAll(ancestors);
+    return result;
+  }
+
+  private Map<String, PatchSetData> collectById(List<ChangeData> in)
+      throws OrmException, IOException {
+    Project.NameKey project = in.get(0).change().getProject();
+    Map<String, PatchSetData> result =
+        Maps.newHashMapWithExpectedSize(in.size() * 3);
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      rw.setRetainBody(true);
+      for (ChangeData cd : in) {
+        checkArgument(cd.change().getProject().equals(project),
+            "Expected change %s in project %s, found %s",
+            cd.getId(), project, cd.change().getProject());
+        for (PatchSet ps : cd.patchSets()) {
+          String id = ps.getRevision().get();
+          RevCommit c = rw.parseCommit(ObjectId.fromString(id));
+          PatchSetData psd = PatchSetData.create(cd, ps, c);
+          result.put(id, psd);
+        }
+      }
+    }
+    return result;
+  }
+
+  private static Collection<PatchSetData> walkAncestors(ProjectControl ctl,
+      ListMultimap<PatchSetData, PatchSetData> parents, PatchSetData start)
+      throws OrmException {
+    LinkedHashSet<PatchSetData> result = new LinkedHashSet<>();
+    Deque<PatchSetData> pending = new ArrayDeque<>();
+    pending.add(start);
+    while (!pending.isEmpty()) {
+      PatchSetData psd = pending.remove();
+      if (result.contains(psd) || !isVisible(psd, ctl)) {
+        continue;
+      }
+      result.add(psd);
+      pending.addAll(Lists.reverse(parents.get(psd)));
+    }
+    return result;
+  }
+
+  private static List<PatchSetData> walkDescendants(ProjectControl ctl,
+      ListMultimap<PatchSetData, PatchSetData> children,
+      PatchSetData start, List<PatchSetData> otherPatchSetsOfStart,
+      Iterable<PatchSetData> ancestors)
+      throws OrmException {
+    Set<Change.Id> alreadyEmittedChanges = new HashSet<>();
+    addAllChangeIds(alreadyEmittedChanges, ancestors);
+
+    // Prefer descendants found by following the original patch set passed in.
+    List<PatchSetData> result = walkDescendentsImpl(
+        ctl, alreadyEmittedChanges, children, ImmutableList.of(start));
+    addAllChangeIds(alreadyEmittedChanges, result);
+
+    // Then, go back and add new indirect descendants found by following any
+    // other patch sets of start. These show up after all direct descendants,
+    // because we wouldn't know where in the walk to insert them.
+    result.addAll(walkDescendentsImpl(
+          ctl, alreadyEmittedChanges, children, otherPatchSetsOfStart));
+    return result;
+  }
+
+  private static void addAllChangeIds(Collection<Change.Id> changeIds,
+      Iterable<PatchSetData> psds) {
+    for (PatchSetData psd : psds) {
+      changeIds.add(psd.id());
+    }
+  }
+
+  private static List<PatchSetData> walkDescendentsImpl(ProjectControl ctl,
+      Set<Change.Id> alreadyEmittedChanges,
+      ListMultimap<PatchSetData, PatchSetData> children,
+      List<PatchSetData> start) throws OrmException {
+    if (start.isEmpty()) {
+      return ImmutableList.of();
+    }
+    Map<Change.Id, PatchSet.Id> maxPatchSetIds = new HashMap<>();
+    Set<PatchSetData> seen = new HashSet<>();
+    List<PatchSetData> allPatchSets = new ArrayList<>();
+    Deque<PatchSetData> pending = new ArrayDeque<>();
+    pending.addAll(start);
+    while (!pending.isEmpty()) {
+      PatchSetData psd = pending.remove();
+      if (seen.contains(psd) || !isVisible(psd, ctl)) {
+        continue;
+      }
+      seen.add(psd);
+      if (!alreadyEmittedChanges.contains(psd.id())) {
+        // Don't emit anything for changes that were previously emitted, even
+        // though different patch sets might show up later. However, do
+        // continue walking through them for the purposes of finding indirect
+        // descendants.
+        PatchSet.Id oldMax = maxPatchSetIds.get(psd.id());
+        if (oldMax == null || psd.psId().get() > oldMax.get()) {
+          maxPatchSetIds.put(psd.id(), psd.psId());
+        }
+        allPatchSets.add(psd);
+      }
+      // Depth-first search with newest children first.
+      for (PatchSetData child : children.get(psd)) {
+        pending.addFirst(child);
+      }
+    }
+
+    // If we saw the same change multiple times, prefer the latest patch set.
+    List<PatchSetData> result = new ArrayList<>(allPatchSets.size());
+    for (PatchSetData psd : allPatchSets) {
+      if (checkNotNull(maxPatchSetIds.get(psd.id())).equals(psd.psId())) {
+        result.add(psd);
+      }
+    }
+    return result;
+  }
+
+  private static boolean isVisible(PatchSetData psd, ProjectControl ctl)
+      throws OrmException {
+    // Reuse existing project control rather than lazily creating a new one for
+    // each ChangeData.
+    return ctl.controlFor(psd.data().change())
+        .isPatchVisible(psd.patchSet(), psd.data());
+  }
+
+  @AutoValue
+  abstract static class PatchSetData {
+    @VisibleForTesting
+    static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
+      return new AutoValue_RelatedChangesSorter_PatchSetData(cd, ps, commit);
+    }
+
+    abstract ChangeData data();
+    abstract PatchSet patchSet();
+    abstract RevCommit commit();
+
+    PatchSet.Id psId() {
+      return patchSet().getId();
+    }
+
+    Change.Id id() {
+      return psId().getParentKey();
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(patchSet().getId(), commit());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index cce362a..5b0eb6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -15,27 +15,30 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,7 +47,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
+import java.util.Collections;
 
 @Singleton
 public class Restore implements RestModifyView<ChangeResource, RestoreInput>,
@@ -54,92 +57,105 @@
   private final ChangeHooks hooks;
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeJson json;
-  private final ChangeIndexer indexer;
+  private final ChangeJson.Factory json;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeUpdate.Factory updateFactory;
+  private final BatchUpdate.Factory batchUpdateFactory;
 
   @Inject
   Restore(ChangeHooks hooks,
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json,
-      ChangeIndexer indexer,
+      ChangeJson.Factory json,
       ChangeMessagesUtil cmUtil,
-      ChangeUpdate.Factory updateFactory) {
+      BatchUpdate.Factory batchUpdateFactory) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
-    this.indexer = indexer;
     this.cmUtil = cmUtil;
-    this.updateFactory = updateFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
   }
 
   @Override
   public ChangeInfo apply(ChangeResource req, RestoreInput input)
-      throws AuthException, ResourceConflictException, OrmException,
-      IOException {
-    ChangeControl control = req.getControl();
-    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
-    Change change = req.getChange();
-    if (!control.canRestore()) {
+      throws RestApiException, UpdateException, OrmException {
+    ChangeControl ctl = req.getControl();
+    if (!ctl.canRestore()) {
       throw new AuthException("restore not permitted");
-    } else if (change.getStatus() != Status.ABANDONED) {
-      throw new ResourceConflictException("change is " + status(change));
     }
 
-    ChangeMessage message;
-    ChangeUpdate update;
-    ReviewDb db = dbProvider.get();
-    db.changes().beginTransaction(change.getId());
-    try {
-      change = db.changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus() == Status.ABANDONED) {
-              change.setStatus(Status.NEW);
-              ChangeUtil.updated(change);
-              return change;
-            }
-            return null;
-          }
-        });
-      if (change == null) {
-        throw new ResourceConflictException("change is "
-            + status(db.changes().get(req.getChange().getId())));
+    Op op = new Op(input);
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+      u.addOp(req.getChange().getId(), op).execute();
+    }
+    return json.create(ChangeJson.NO_OPTIONS).format(op.change);
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final RestoreInput input;
+
+    private Change change;
+    private PatchSet patchSet;
+    private ChangeMessage message;
+    private IdentifiedUser caller;
+
+    private Op(RestoreInput input) {
+      this.input = input;
+    }
+
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException,
+        ResourceConflictException {
+      caller = ctx.getUser().asIdentifiedUser();
+      change = ctx.getChange();
+      if (change == null || change.getStatus() != Status.ABANDONED) {
+        throw new ResourceConflictException("change is " + status(change));
+      }
+      patchSet = ctx.getDb().patchSets().get(change.currentPatchSetId());
+      change.setStatus(Status.NEW);
+      change.setLastUpdatedOn(ctx.getWhen());
+      ctx.getDb().changes().update(Collections.singleton(change));
+
+      message = newMessage(ctx.getDb());
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), message);
+    }
+
+    private ChangeMessage newMessage(ReviewDb db) throws OrmException {
+      StringBuilder msg = new StringBuilder();
+      msg.append("Restored");
+      if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+        msg.append("\n\n");
+        msg.append(input.message.trim());
       }
 
-      //TODO(yyonas): atomic update was not propagated
-      update = updateFactory.create(control);
-      message = newMessage(input, caller, change);
-      cmUtil.addChangeMessage(db, update, message);
-      db.commit();
-    } finally {
-      db.rollback();
+      ChangeMessage message = new ChangeMessage(
+          new ChangeMessage.Key(
+              change.getId(),
+              ChangeUtil.messageUUID(db)),
+          caller.getAccountId(),
+          change.getLastUpdatedOn(),
+          change.currentPatchSetId());
+      message.setMessage(msg.toString());
+      return message;
     }
-    update.commit();
 
-    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
-
-    try {
-      ReplyToChangeSender cm = restoredSenderFactory.create(change);
-      cm.setFrom(caller.getAccountId());
-      cm.setChangeMessage(message);
-      cm.send();
-    } catch (Exception e) {
-      log.error("Cannot email update for change " + change.getChangeId(), e);
+    @Override
+    public void postUpdate(Context ctx) throws OrmException {
+      try {
+        ReplyToChangeSender cm = restoredSenderFactory.create(change.getId());
+        cm.setFrom(caller.getAccountId());
+        cm.setChangeMessage(message);
+        cm.send();
+      } catch (Exception e) {
+        log.error("Cannot email update for change " + change.getId(), e);
+      }
+      hooks.doChangeRestoredHook(change,
+          caller.getAccount(),
+          patchSet,
+          Strings.emptyToNull(input.message),
+          ctx.getDb());
     }
-    f.checkedGet();
-    hooks.doChangeRestoredHook(change,
-        caller.getAccount(),
-        db.patchSets().get(change.currentPatchSetId()),
-        Strings.emptyToNull(input.message),
-        dbProvider.get());
-    ChangeInfo result = json.format(change);
-    return result;
   }
 
   @Override
@@ -151,26 +167,6 @@
           && resource.getControl().canRestore());
   }
 
-  private ChangeMessage newMessage(RestoreInput input, IdentifiedUser caller,
-      Change change) throws OrmException {
-    StringBuilder msg = new StringBuilder();
-    msg.append("Restored");
-    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
-      msg.append("\n\n");
-      msg.append(input.message.trim());
-    }
-
-    ChangeMessage message = new ChangeMessage(
-        new ChangeMessage.Key(
-            change.getId(),
-            ChangeUtil.messageUUID(dbProvider.get())),
-        caller.getAccountId(),
-        change.getLastUpdatedOn(),
-        change.currentPatchSetId());
-    message.setMessage(msg.toString());
-    return message;
-  }
-
   private static String status(Change change) {
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index e6846e0..dc2ed5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -16,23 +16,21 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -44,12 +42,12 @@
 @Singleton
 public class Revert implements RestModifyView<ChangeResource, RevertInput>,
     UiAction<ChangeResource> {
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
   private final ChangeUtil changeUtil;
   private final PersonIdent myIdent;
 
   @Inject
-  Revert(ChangeJson json,
+  Revert(ChangeJson.Factory json,
       ChangeUtil changeUtil,
       @GerritPersonIdent PersonIdent myIdent) {
     this.json = json;
@@ -59,8 +57,8 @@
 
   @Override
   public ChangeInfo apply(ChangeResource req, RevertInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, IOException, OrmException, EmailException {
+      throws IOException, OrmException, RestApiException,
+      UpdateException {
     ChangeControl control = req.getControl();
     Change change = req.getChange();
     if (!control.canAddPatchSet()) {
@@ -69,18 +67,16 @@
       throw new ResourceConflictException("change is " + status(change));
     }
 
+    Change.Id revertedChangeId;
     try {
-      Change.Id revertedChangeId =
-          changeUtil.revert(control, change.currentPatchSetId(),
-              Strings.emptyToNull(input.message),
-              new PersonIdent(myIdent, TimeUtil.nowTs()), new NoSshInfo());
-
-      return json.format(revertedChangeId);
-    } catch (InvalidChangeOperationException e) {
-      throw new BadRequestException(e.getMessage());
+      revertedChangeId = changeUtil.revert(control,
+            change.currentPatchSetId(),
+            Strings.emptyToNull(input.message),
+            new PersonIdent(myIdent, TimeUtil.nowTs()));
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
     }
+    return json.create(ChangeJson.NO_OPTIONS).format(revertedChangeId);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
index 1a2551c..20078cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.common.base.Splitter;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
@@ -41,6 +43,7 @@
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause.Occur;
 import org.apache.lucene.search.BooleanQuery;
@@ -49,7 +52,6 @@
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.store.RAMDirectory;
-import org.apache.lucene.util.Version;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -63,8 +65,8 @@
 /**
  * The suggest oracle may be called many times in rapid succession during the
  * course of one operation.
- * It would be easy to have a simple Cache<Boolean, List<Account>> with a short
- * expiration time of 30s.
+ * It would be easy to have a simple {@code Cache<Boolean, List<Account>>}
+ * with a short expiration time of 30s.
  * Cache only has a single key we're just using Cache for the expiration behavior.
  */
 @Singleton
@@ -91,7 +93,7 @@
         TimeUnit.MILLISECONDS);
     this.cache =
         CacheBuilder.newBuilder().maximumSize(1)
-            .expireAfterWrite(expiration, TimeUnit.MILLISECONDS)
+            .refreshAfterWrite(expiration, TimeUnit.MILLISECONDS)
             .build(new CacheLoader<Boolean, IndexSearcher>() {
               @Override
               public IndexSearcher load(Boolean key) throws Exception {
@@ -100,7 +102,7 @@
             });
   }
 
-  List<AccountInfo> search(String query, int n) throws IOException {
+  public List<AccountInfo> search(String query, int n) throws IOException {
     IndexSearcher searcher = get();
     if (searcher == null) {
       return Collections.emptyList();
@@ -108,16 +110,16 @@
 
     List<String> segments = Splitter.on(' ').omitEmptyStrings().splitToList(
         query.toLowerCase());
-    BooleanQuery q = new BooleanQuery();
+    BooleanQuery.Builder q = new BooleanQuery.Builder();
     for (String field : ALL) {
-      BooleanQuery and = new BooleanQuery();
+      BooleanQuery.Builder and = new BooleanQuery.Builder();
       for (String s : segments) {
         and.add(new PrefixQuery(new Term(field, s)), Occur.MUST);
       }
-      q.add(and, Occur.SHOULD);
+      q.add(and.build(), Occur.SHOULD);
     }
 
-    TopDocs results = searcher.search(q, n);
+    TopDocs results = searcher.search(q.build(), n);
     ScoreDoc[] hits = results.scoreDocs;
 
     List<AccountInfo> result = new LinkedList<>();
@@ -125,8 +127,8 @@
     for (ScoreDoc h : hits) {
       Document doc = searcher.doc(h.doc);
 
-      AccountInfo info = new AccountInfo(
-          doc.getField(ID).numericValue().intValue());
+      IndexableField idField = checkNotNull(doc.getField(ID));
+      AccountInfo info = new AccountInfo(idField.numericValue().intValue());
       info.name = doc.get(NAME);
       info.email = doc.get(EMAIL);
       info.username = doc.get(USERNAME);
@@ -147,9 +149,7 @@
 
   private IndexSearcher index() throws IOException, OrmException {
     RAMDirectory idx = new RAMDirectory();
-    @SuppressWarnings("deprecation")
     IndexWriterConfig config = new IndexWriterConfig(
-        Version.LUCENE_4_10_1,
         new StandardAnalyzer(CharArraySet.EMPTY_SET));
     config.setOpenMode(OpenMode.CREATE);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index e58d1a1..6731dd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -84,7 +84,7 @@
   }
 
   IdentifiedUser getUser() {
-    return (IdentifiedUser) getControl().getCurrentUser();
+    return getControl().getUser().asIdentifiedUser();
   }
 
   RevisionResource doNotCache() {
@@ -92,7 +92,7 @@
     return this;
   }
 
-  Optional<ChangeEdit> getEdit() {
+  public Optional<ChangeEdit> getEdit() {
     return edit;
   }
 
@@ -104,4 +104,8 @@
     }
     return s;
   }
+
+  public boolean isCurrent() {
+    return ps.getId().equals(getChange().currentPatchSetId());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
new file mode 100644
index 0000000..61baeb2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2015 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.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.change.HashtagsUtil.extractTags;
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.validators.HashtagValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SetHashtagsOp extends BatchUpdate.Op {
+  public interface Factory {
+    SetHashtagsOp create(HashtagsInput input);
+  }
+
+  private final ChangeHooks hooks;
+  private final DynamicSet<HashtagValidationListener> validationListeners;
+  private final HashtagsInput input;
+
+  private boolean runHooks = true;
+
+  private Change change;
+  private Set<String> toAdd;
+  private Set<String> toRemove;
+  private ImmutableSortedSet<String> updatedHashtags;
+
+  @AssistedInject
+  SetHashtagsOp(
+      ChangeHooks hooks,
+      DynamicSet<HashtagValidationListener> validationListeners,
+      @Assisted @Nullable HashtagsInput input) {
+    this.hooks = hooks;
+    this.validationListeners = validationListeners;
+    this.input = input;
+  }
+
+  public SetHashtagsOp setRunHooks(boolean runHooks) {
+    this.runHooks = runHooks;
+    return this;
+  }
+
+  @Override
+  public void updateChange(ChangeContext ctx)
+      throws AuthException, BadRequestException, OrmException, IOException {
+    if (input == null
+        || (input.add == null && input.remove == null)) {
+      updatedHashtags = ImmutableSortedSet.of();
+      return;
+    }
+    if (!ctx.getChangeControl().canEditHashtags()) {
+      throw new AuthException("Editing hashtags not permitted");
+    }
+    ChangeUpdate update = ctx.getChangeUpdate();
+    ChangeNotes notes = update.getChangeNotes().load();
+
+    Set<String> existingHashtags = notes.getHashtags();
+    Set<String> updated = new HashSet<>();
+    toAdd = new HashSet<>(extractTags(input.add));
+    toRemove = new HashSet<>(extractTags(input.remove));
+
+    try {
+      for (HashtagValidationListener validator : validationListeners) {
+        validator.validateHashtags(update.getChange(), toAdd, toRemove);
+      }
+    } catch (ValidationException e) {
+      throw new BadRequestException(e.getMessage());
+    }
+
+    if (existingHashtags != null && !existingHashtags.isEmpty()) {
+      updated.addAll(existingHashtags);
+      toAdd.removeAll(existingHashtags);
+      toRemove.retainAll(existingHashtags);
+    }
+    if (updated()) {
+      updated.addAll(toAdd);
+      updated.removeAll(toRemove);
+      update.setHashtags(updated);
+    }
+
+    change = update.getChange();
+    updatedHashtags = ImmutableSortedSet.copyOf(updated);
+  }
+
+  @Override
+  public void postUpdate(Context ctx) throws OrmException {
+    if (updated() && runHooks) {
+      hooks.doHashtagsChangedHook(
+          change, ctx.getUser().asIdentifiedUser().getAccount(),
+          toAdd, toRemove, updatedHashtags,
+          ctx.getDb());
+    }
+  }
+
+  public ImmutableSortedSet<String> getUpdatedHashtags() {
+    checkState(updatedHashtags != null,
+        "getUpdatedHashtags() only valid after executing op");
+    return updatedHashtags;
+  }
+
+  private boolean updated() {
+    return !isNullOrEmpty(toAdd) || !isNullOrEmpty(toRemove);
+  }
+
+  private static boolean isNullOrEmpty(Collection<?> coll) {
+    return coll == null || coll.isEmpty();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 935d448..22bdae5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -14,23 +14,17 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.gerrit.common.data.SubmitRecord.Status.OK;
-
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Table;
-import com.google.gerrit.common.TimeUtil;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.ParameterizedString;
-import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -38,32 +32,26 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LabelNormalizer;
-import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeSuperSet;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
@@ -71,20 +59,21 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @Singleton
 public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
@@ -93,83 +82,86 @@
 
   private static final String DEFAULT_TOOLTIP =
       "Submit patch set ${patchSet} into ${branch}";
+  private static final String DEFAULT_TOOLTIP_ANCESTORS =
+      "Submit patch set ${patchSet} and ancestors (${submitSize} changes " +
+      "altogether) into ${branch}";
   private static final String DEFAULT_TOPIC_TOOLTIP =
-      "Submit all ${topicSize} changes of the same topic";
-  private static final String BLOCKED_TOPIC_TOOLTIP =
-      "Other changes in this topic are not ready";
-  private static final String BLOCKED_HIDDEN_TOPIC_TOOLTIP =
-      "Other hidden changes in this topic are not ready";
-
-  public enum Status {
-    SUBMITTED, MERGED
-  }
+      "Submit all ${topicSize} changes of the same topic " +
+      "(${submitSize} changes including ancestors and other " +
+      "changes related by topic)";
+  private static final String BLOCKED_SUBMIT_TOOLTIP =
+      "This change depends on other changes which are not ready";
+  private static final String BLOCKED_HIDDEN_SUBMIT_TOOLTIP =
+      "This change depends on other hidden changes which are not ready";
+  private static final String CLICK_FAILURE_TOOLTIP =
+      "Clicking the button would fail";
+  private static final String CHANGE_UNMERGEABLE =
+      "Problems with integrating this change";
+  private static final String CHANGES_NOT_MERGEABLE =
+      "Problems with change(s): ";
 
   public static class Output {
-    public Status status;
     transient Change change;
 
-    private Output(Status s, Change c) {
-      status = s;
+    private Output(Change c) {
       change = c;
     }
   }
 
-  private final PersonIdent serverIdent;
   private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
-  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeData.Factory changeDataFactory;
-  private final ChangeUpdate.Factory updateFactory;
-  private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
-  private final MergeQueue mergeQueue;
-  private final ChangeIndexer indexer;
-  private final LabelNormalizer labelNormalizer;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final Provider<MergeOp> mergeOpProvider;
+  private final MergeSuperSet mergeSuperSet;
   private final AccountsCollection accounts;
   private final ChangesCollection changes;
   private final String label;
+  private final String labelWithParents;
   private final ParameterizedString titlePattern;
+  private final ParameterizedString titlePatternWithAncestors;
   private final String submitTopicLabel;
   private final ParameterizedString submitTopicTooltip;
   private final boolean submitWholeTopic;
   private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
-  Submit(@GerritPersonIdent PersonIdent serverIdent,
-      Provider<ReviewDb> dbProvider,
+  Submit(Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
-      IdentifiedUser.GenericFactory userFactory,
       ChangeData.Factory changeDataFactory,
-      ChangeUpdate.Factory updateFactory,
-      ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      MergeQueue mergeQueue,
+      ChangeControl.GenericFactory changeControlFactory,
+      Provider<MergeOp> mergeOpProvider,
+      MergeSuperSet mergeSuperSet,
       AccountsCollection accounts,
       ChangesCollection changes,
-      ChangeIndexer indexer,
-      LabelNormalizer labelNormalizer,
       @GerritServerConfig Config cfg,
       Provider<InternalChangeQuery> queryProvider) {
-    this.serverIdent = serverIdent;
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
-    this.userFactory = userFactory;
     this.changeDataFactory = changeDataFactory;
-    this.updateFactory = updateFactory;
-    this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.mergeQueue = mergeQueue;
+    this.changeControlFactory = changeControlFactory;
+    this.mergeOpProvider = mergeOpProvider;
+    this.mergeSuperSet = mergeSuperSet;
     this.accounts = accounts;
     this.changes = changes;
-    this.indexer = indexer;
-    this.labelNormalizer = labelNormalizer;
     this.label = MoreObjects.firstNonNull(
         Strings.emptyToNull(cfg.getString("change", null, "submitLabel")),
         "Submit");
+    this.labelWithParents = MoreObjects.firstNonNull(
+        Strings.emptyToNull(
+            cfg.getString("change", null, "submitLabelWithParents")),
+        "Submit including parents");
     this.titlePattern = new ParameterizedString(MoreObjects.firstNonNull(
         cfg.getString("change", null, "submitTooltip"),
         DEFAULT_TOOLTIP));
-    submitWholeTopic = false;
+    this.titlePatternWithAncestors = new ParameterizedString(
+        MoreObjects.firstNonNull(
+            cfg.getString("change", null, "submitTooltipAncestors"),
+            DEFAULT_TOOLTIP_ANCESTORS));
+    submitWholeTopic = wholeTopicEnabled(cfg);
     this.submitTopicLabel = MoreObjects.firstNonNull(
         Strings.emptyToNull(cfg.getString("change", null, "submitTopicLabel")),
         "Submit whole topic");
@@ -189,7 +181,7 @@
       rsrc = onBehalfOf(rsrc, input);
     }
     ChangeControl control = rsrc.getControl();
-    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+    IdentifiedUser caller = control.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
     if (input.onBehalfOf == null && !control.canSubmit()) {
       throw new AuthException("submit not permitted");
@@ -206,27 +198,20 @@
           rsrc.getPatchSet().getRevision().get()));
     }
 
-    change = submit(rsrc, caller, false);
-    if (change == null) {
-      throw new ResourceConflictException("change is "
-          + status(dbProvider.get().changes().get(rsrc.getChange().getId())));
-    }
-
-    if (input.waitForMerge) {
-      mergeQueue.merge(change.getDest());
-      change = dbProvider.get().changes().get(change.getId());
-    } else {
-      mergeQueue.schedule(change.getDest());
+    try {
+      ReviewDb db = dbProvider.get();
+      mergeOpProvider.get().merge(db, change, caller, true);
+      change = db.changes().get(change.getId());
+    } catch (NoSuchChangeException e) {
+      throw new OrmException("Submission failed", e);
     }
 
     if (change == null) {
       throw new ResourceConflictException("change is deleted");
     }
     switch (change.getStatus()) {
-      case SUBMITTED:
-        return new Output(Status.SUBMITTED, change);
       case MERGED:
-        return new Output(Status.MERGED, change);
+        return new Output(change);
       case NEW:
         ChangeMessage msg = getConflictMessage(rsrc);
         if (msg != null) {
@@ -239,33 +224,73 @@
   }
 
   /**
-   * @param changes list of changes to be submitted at once
+   * @param cd the change the user is currently looking at
+   * @param cs set of changes to be submitted at once
    * @param identifiedUser the user who is checking to submit
    * @return a reason why any of the changes is not submittable or null
    */
-  private String problemsForSubmittingChanges(List<ChangeData> changes,
+  private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs,
       IdentifiedUser identifiedUser) {
-    for (ChangeData c : changes) {
-      try {
-        ChangeControl changeControl = c.changeControl().forUser(
-            identifiedUser);
-        if (!changeControl.isVisible(dbProvider.get())) {
-          return BLOCKED_HIDDEN_TOPIC_TOOLTIP;
+    try {
+      @SuppressWarnings("resource")
+      ReviewDb db = dbProvider.get();
+      for (PatchSet.Id psId : cs.patchIds()) {
+        ChangeControl changeControl = changeControlFactory
+            .controlFor(psId.getParentKey(), identifiedUser);
+        ChangeData c = changeDataFactory.create(db, changeControl);
+
+        if (!changeControl.isVisible(db)) {
+          return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
         }
         if (!changeControl.canSubmit()) {
-          return BLOCKED_TOPIC_TOOLTIP;
+          return BLOCKED_SUBMIT_TOOLTIP;
         }
-        checkSubmitRule(c, c.currentPatchSet(), false);
-      } catch (OrmException e) {
-        log.error("Error checking if change is submittable", e);
-        throw new OrmRuntimeException(e);
-      } catch (ResourceConflictException e) {
-        return BLOCKED_TOPIC_TOOLTIP;
+        MergeOp.checkSubmitRule(c);
       }
+
+      Collection<ChangeData> unmergeable = unmergeableChanges(cs);
+      if (unmergeable == null) {
+        return CLICK_FAILURE_TOOLTIP;
+      } else if (!unmergeable.isEmpty()) {
+        for (ChangeData c : unmergeable) {
+          if (c.change().getKey().equals(cd.change().getKey())) {
+            return CHANGE_UNMERGEABLE;
+          }
+        }
+        return CHANGES_NOT_MERGEABLE + Joiner.on(", ").join(
+            Iterables.transform(unmergeable,
+                new Function<ChangeData, String>() {
+              @Override
+              public String apply(ChangeData cd) {
+                return String.valueOf(cd.getId().get());
+              }
+            }));
+      }
+    } catch (ResourceConflictException e) {
+      return BLOCKED_SUBMIT_TOOLTIP;
+    } catch (NoSuchChangeException | OrmException | IOException e) {
+      log.error("Error checking if change is submittable", e);
+      throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
     return null;
   }
 
+  /**
+   * Check if there are any problems with the given change. It doesn't take
+   * any problems of related changes into account.
+   * <p>
+   * @param cd the change to check for submittability
+   * @return if the change has any problems for submission
+   */
+  public boolean submittable(ChangeData cd) {
+    try {
+      MergeOp.checkSubmitRule(cd);
+      return true;
+    } catch (ResourceConflictException | OrmException e) {
+      return false;
+    }
+  }
+
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
     PatchSet.Id current = resource.getChange().currentPatchSetId();
@@ -274,12 +299,16 @@
         && resource.getChange().getStatus().isOpen()
         && resource.getPatchSet().getId().equals(current)
         && resource.getControl().canSubmit();
-
     ReviewDb db = dbProvider.get();
     ChangeData cd = changeDataFactory.create(db, resource.getControl());
-    if (problemsForSubmittingChanges(Arrays.asList(cd), resource.getUser())
-        != null) {
+
+    try {
+      MergeOp.checkSubmitRule(cd);
+    } catch (ResourceConflictException e) {
       visible = false;
+    } catch (OrmException e) {
+      log.error("Error checking if change is submittable", e);
+      throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
 
     if (!visible) {
@@ -288,41 +317,75 @@
         .setTitle("")
         .setVisible(false);
     }
-    if (submitWholeTopic && !Strings.isNullOrEmpty(topic)) {
-      List<ChangeData> changesByTopic = null;
-      try {
-        changesByTopic = queryProvider.get().byTopicOpen(topic);
-      } catch (OrmException e) {
-        throw new OrmRuntimeException(e);
-      }
+
+    ChangeSet cs;
+    try {
+      cs = mergeSuperSet.completeChangeSet(db, cd.change());
+    } catch (OrmException | IOException e) {
+      throw new OrmRuntimeException("Could not determine complete set of " +
+          "changes to be submitted", e);
+    }
+
+    int topicSize = 0;
+    if (!Strings.isNullOrEmpty(topic)) {
+      topicSize = getChangesByTopic(topic).size();
+    }
+    boolean treatWithTopic = submitWholeTopic
+        && !Strings.isNullOrEmpty(topic)
+        && topicSize > 1;
+
+    String submitProblems =
+        problemsForSubmittingChangeset(cd, cs, resource.getUser());
+
+    Boolean enabled;
+    try {
+      // Recheck mergeability rather than using value stored in the index,
+      // which may be stale.
+      // TODO(dborowitz): This is ugly; consider providing a way to not read
+      // stored fields from the index in the first place.
+      // cd.setMergeable(null);
+      // That was done in unmergeableChanges which was called by
+      // problemsForSubmittingChangeset, so now it is safe to read from
+      // the cache, as it yields the same result.
+      enabled = cd.isMergeable();
+    } catch (OrmException e) {
+      throw new OrmRuntimeException("Could not determine mergeability", e);
+    }
+
+    if (submitProblems != null) {
+      return new UiAction.Description()
+        .setLabel(treatWithTopic
+            ? submitTopicLabel : (cs.size() > 1)
+                ? labelWithParents : label)
+        .setTitle(submitProblems)
+        .setVisible(true)
+        .setEnabled(false);
+    }
+
+    if (treatWithTopic) {
       Map<String, String> params = ImmutableMap.of(
-          "topicSize", String.valueOf(changesByTopic.size()));
-      String topicProblems = problemsForSubmittingChanges(changesByTopic,
-          resource.getUser());
-      if (topicProblems != null) {
-        return new UiAction.Description()
-          .setLabel(submitTopicLabel)
-          .setTitle(topicProblems)
-          .setVisible(true)
-          .setEnabled(false);
-      } else {
-        return new UiAction.Description()
+          "topicSize", String.valueOf(topicSize),
+          "submitSize", String.valueOf(cs.size()));
+      return new UiAction.Description()
           .setLabel(submitTopicLabel)
           .setTitle(Strings.emptyToNull(
               submitTopicTooltip.replace(params)))
           .setVisible(true)
-          .setEnabled(true);
-      }
+          .setEnabled(Boolean.TRUE.equals(enabled));
     } else {
       RevId revId = resource.getPatchSet().getRevision();
       Map<String, String> params = ImmutableMap.of(
           "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
           "branch", resource.getChange().getDest().getShortName(),
-          "commit", ObjectId.fromString(revId.get()).abbreviate(7).name());
+          "commit", ObjectId.fromString(revId.get()).abbreviate(7).name(),
+          "submitSize", String.valueOf(cs.size()));
+      ParameterizedString tp = cs.size() > 1 ? titlePatternWithAncestors :
+          titlePattern;
       return new UiAction.Description()
-        .setLabel(label)
-        .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
-        .setVisible(true);
+        .setLabel(cs.size() > 1 ? labelWithParents : label)
+        .setTitle(Strings.emptyToNull(tp.replace(params)))
+        .setVisible(true)
+        .setEnabled(Boolean.TRUE.equals(enabled));
     }
   }
 
@@ -345,292 +408,79 @@
         .orNull();
   }
 
-  private Change submitToDatabase(ReviewDb db, Change.Id changeId,
-      final Timestamp timestamp) throws OrmException {
-    return db.changes().atomicUpdate(changeId,
-      new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          if (change.getStatus().isOpen()) {
-            change.setStatus(Change.Status.SUBMITTED);
-            change.setLastUpdatedOn(timestamp);
-            return change;
-          }
-          return null;
-        }
-      });
-  }
-
-  private Change submitThisChange(RevisionResource rsrc, IdentifiedUser caller,
-      boolean force) throws ResourceConflictException, OrmException,
-      IOException {
-    ReviewDb db = dbProvider.get();
-    ChangeData cd = changeDataFactory.create(db, rsrc.getControl());
-    List<SubmitRecord> submitRecords = checkSubmitRule(cd,
-        rsrc.getPatchSet(), force);
-
-    final Timestamp timestamp = TimeUtil.nowTs();
-    Change change = rsrc.getChange();
-    ChangeUpdate update = updateFactory.create(rsrc.getControl(), timestamp);
-    update.submit(submitRecords);
-
-    db.changes().beginTransaction(change.getId());
-    try {
-      BatchMetaDataUpdate batch = approve(rsrc, update, caller, timestamp);
-      // Write update commit after all normalized label commits.
-      batch.write(update, new CommitBuilder());
-      change = submitToDatabase(db, change.getId(), timestamp);
-      if (change == null) {
-        return null;
-      }
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-    indexer.index(db, change);
-    return change;
-  }
-
-  private Change submitWholeTopic(RevisionResource rsrc, IdentifiedUser caller,
-      boolean force, String topic) throws ResourceConflictException, OrmException,
-      IOException {
-    Preconditions.checkNotNull(topic);
-    final Timestamp timestamp = TimeUtil.nowTs();
-
-    ReviewDb db = dbProvider.get();
-    ChangeData cd = changeDataFactory.create(db, rsrc.getControl());
-
-    List<ChangeData> changesByTopic = queryProvider.get().byTopicOpen(topic);
-    String problems = problemsForSubmittingChanges(changesByTopic, caller);
-    if (problems != null) {
-      throw new ResourceConflictException(problems);
-    }
-
-    Change change = rsrc.getChange();
-    ChangeUpdate update = updateFactory.create(rsrc.getControl(), timestamp);
-
-    List<SubmitRecord> submitRecords = checkSubmitRule(cd,
-        rsrc.getPatchSet(), force);
-    update.submit(submitRecords);
-
-    db.changes().beginTransaction(change.getId());
-    try {
-      BatchMetaDataUpdate batch = approve(rsrc, update, caller, timestamp);
-      // Write update commit after all normalized label commits.
-      batch.write(update, new CommitBuilder());
-
-      for (ChangeData c : changesByTopic) {
-        if (submitToDatabase(db, c.getId(), timestamp) == null) {
-          return null;
-        }
-      }
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-    List<Change.Id> ids = new ArrayList<>(changesByTopic.size());
-    for (ChangeData c : changesByTopic) {
-      ids.add(c.getId());
-    }
-    indexer.indexAsync(ids).checkedGet();
-    return change;
-  }
-
-  public Change submit(RevisionResource rsrc, IdentifiedUser caller,
-      boolean force) throws ResourceConflictException, OrmException,
-      IOException {
-    String topic = rsrc.getChange().getTopic();
-    if (submitWholeTopic && !Strings.isNullOrEmpty(topic)) {
-      return submitWholeTopic(rsrc, caller, force, topic);
-    } else {
-      return submitThisChange(rsrc, caller, force);
-    }
-  }
-
-  private BatchMetaDataUpdate approve(RevisionResource rsrc,
-      ChangeUpdate update, IdentifiedUser caller, Timestamp timestamp)
-      throws OrmException {
-    PatchSet.Id psId = rsrc.getPatchSet().getId();
-    Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
-    for (PatchSetApproval psa :
-        approvalsUtil.byPatchSet(dbProvider.get(), rsrc.getControl(), psId)) {
-      if (!byKey.containsKey(psa.getKey())) {
-        byKey.put(psa.getKey(), psa);
-      }
-    }
-
-    PatchSetApproval submit = ApprovalsUtil.getSubmitter(psId, byKey.values());
-    if (submit == null
-        || !submit.getAccountId().equals(caller.getAccountId())) {
-      submit = new PatchSetApproval(
-          new PatchSetApproval.Key(
-              rsrc.getPatchSet().getId(),
-              caller.getAccountId(),
-              LabelId.SUBMIT),
-          (short) 1, TimeUtil.nowTs());
-      byKey.put(submit.getKey(), submit);
-    }
-    submit.setValue((short) 1);
-    submit.setGranted(timestamp);
-
-    // Flatten out existing approvals for this patch set based upon the current
-    // permissions. Once the change is closed the approvals are not updated at
-    // presentation view time, except for zero votes used to indicate a reviewer
-    // was added. So we need to make sure votes are accurate now. This way if
-    // permissions get modified in the future, historical records stay accurate.
-    LabelNormalizer.Result normalized =
-        labelNormalizer.normalize(rsrc.getControl(), byKey.values());
-
-    // TODO(dborowitz): Don't use a label in notedb; just check when status
-    // change happened.
-    update.putApproval(submit.getLabel(), submit.getValue());
-
-    dbProvider.get().patchSetApprovals().upsert(normalized.getNormalized());
-    dbProvider.get().patchSetApprovals().delete(normalized.deleted());
-
-    try {
-      return saveToBatch(rsrc, update, normalized, timestamp);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-  }
-
-  private BatchMetaDataUpdate saveToBatch(RevisionResource rsrc,
-      ChangeUpdate callerUpdate, LabelNormalizer.Result normalized,
-      Timestamp timestamp) throws IOException {
-    Table<Account.Id, String, Optional<Short>> byUser = HashBasedTable.create();
-    for (PatchSetApproval psa : normalized.updated()) {
-      byUser.put(psa.getAccountId(), psa.getLabel(),
-          Optional.of(psa.getValue()));
-    }
-    for (PatchSetApproval psa : normalized.deleted()) {
-      byUser.put(psa.getAccountId(), psa.getLabel(), Optional.<Short> absent());
-    }
-
-    ChangeControl ctl = rsrc.getControl();
-    BatchMetaDataUpdate batch = callerUpdate.openUpdate();
-    for (Account.Id accountId : byUser.rowKeySet()) {
-      if (!accountId.equals(callerUpdate.getUser().getAccountId())) {
-        ChangeUpdate update = updateFactory.create(
-            ctl.forUser(userFactory.create(dbProvider, accountId)), timestamp);
-        update.setSubject("Finalize approvals at submit");
-        putApprovals(update, byUser.row(accountId));
-
-        CommitBuilder commit = new CommitBuilder();
-        commit.setCommitter(new PersonIdent(serverIdent, timestamp));
-        batch.write(update, commit);
-      }
-    }
-
-    putApprovals(callerUpdate,
-        byUser.row(callerUpdate.getUser().getAccountId()));
-    return batch;
-  }
-
-  private static void putApprovals(ChangeUpdate update,
-      Map<String, Optional<Short>> approvals) {
-    for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) {
-      if (e.getValue().isPresent()) {
-        update.putApproval(e.getKey(), e.getValue().get());
-      } else {
-        update.removeApproval(e.getKey());
-      }
-    }
-  }
-
-  private List<SubmitRecord> checkSubmitRule(ChangeData cd,
-      PatchSet patchSet, boolean force)
-          throws ResourceConflictException, OrmException {
-    List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
-        .setPatchSet(patchSet)
-        .evaluate();
-    Optional<SubmitRecord> ok = findOkRecord(results);
-    if (ok.isPresent()) {
-      // Rules supplied a valid solution.
-      return ImmutableList.of(ok.get());
-    } else if (force) {
-      return results;
-    } else if (results.isEmpty()) {
-      throw new IllegalStateException(String.format(
-          "SubmitRuleEvaluator.evaluate returned empty list for %s in %s",
-          patchSet.getId(),
-          cd.change().getProject().get()));
-    }
-
-    for (SubmitRecord record : results) {
-      switch (record.status) {
-        case CLOSED:
-          throw new ResourceConflictException("change is closed");
-
-        case RULE_ERROR:
-          throw new ResourceConflictException(String.format(
-              "rule error: %s",
-              record.errorMessage));
-
-        case NOT_READY:
-          StringBuilder msg = new StringBuilder();
-          for (SubmitRecord.Label lbl : record.labels) {
-            switch (lbl.status) {
-              case OK:
-              case MAY:
-                continue;
-
-              case REJECT:
-                if (msg.length() > 0) {
-                  msg.append("; ");
-                }
-                msg.append("blocked by ").append(lbl.label);
-                continue;
-
-              case NEED:
-                if (msg.length() > 0) {
-                  msg.append("; ");
-                }
-                msg.append("needs ").append(lbl.label);
-                continue;
-
-              case IMPOSSIBLE:
-                if (msg.length() > 0) {
-                  msg.append("; ");
-                }
-                msg.append("needs ").append(lbl.label)
-                   .append(" (check project access)");
-                continue;
-
-              default:
-                throw new IllegalStateException(String.format(
-                    "Unsupported SubmitRecord.Label %s for %s in %s",
-                    lbl.toString(),
-                    patchSet.getId(),
-                    cd.change().getProject().get()));
-            }
-          }
-          throw new ResourceConflictException(msg.toString());
-
-        default:
-          throw new IllegalStateException(String.format(
-              "Unsupported SubmitRecord %s for %s in %s",
-              record,
-              patchSet.getId().getId(),
-              cd.change().getProject().get()));
-      }
-    }
-    throw new IllegalStateException();
-  }
-
-  private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
-    return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
-      @Override
-      public boolean apply(SubmitRecord input) {
-        return input.status == OK;
-      }
-    });
-  }
-
   static String status(Change change) {
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
 
+  public Collection<ChangeData> unmergeableChanges(ChangeSet cs)
+      throws OrmException, IOException {
+    Set<ChangeData> mergeabilityMap = new HashSet<>();
+    for (ChangeData change : cs.changes()) {
+      mergeabilityMap.add(change);
+    }
+
+    Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
+    for (Branch.NameKey branch : cbb.keySet()) {
+      Collection<ChangeData> targetBranch = cbb.get(branch);
+      HashMap<Change.Id, RevCommit> commits =
+          findCommits(targetBranch, branch.getParentKey());
+
+      Set<ObjectId> allParents = Sets.newHashSetWithExpectedSize(cs.size());
+      for (RevCommit commit : commits.values()) {
+        for (RevCommit parent : commit.getParents()) {
+          allParents.add(parent.getId());
+        }
+      }
+
+      for (ChangeData change : targetBranch) {
+        RevCommit commit = commits.get(change.getId());
+        boolean isMergeCommit = commit.getParentCount() > 1;
+        boolean isLastInChain = !allParents.contains(commit.getId());
+
+        // Recheck mergeability rather than using value stored in the index,
+        // which may be stale.
+        // TODO(dborowitz): This is ugly; consider providing a way to not read
+        // stored fields from the index in the first place.
+        change.setMergeable(null);
+        Boolean mergeable = change.isMergeable();
+        if (mergeable == null) {
+          // Skip whole check, cannot determine if mergeable
+          return null;
+        }
+        if (mergeable) {
+          mergeabilityMap.remove(change);
+        }
+
+        if (isLastInChain && isMergeCommit && mergeable) {
+          for (ChangeData c : targetBranch) {
+            mergeabilityMap.remove(c);
+          }
+          break;
+        }
+      }
+    }
+    return mergeabilityMap;
+  }
+
+  private HashMap<Change.Id, RevCommit> findCommits(
+      Collection<ChangeData> changes, Project.NameKey project)
+          throws IOException, OrmException {
+    HashMap<Change.Id, RevCommit> commits = new HashMap<>();
+    if (!changes.isEmpty()) {
+      try (Repository repo = repoManager.openRepository(project);
+          RevWalk walk = new RevWalk(repo)) {
+        for (ChangeData change : changes) {
+          PatchSet patchSet = dbProvider.get().patchSets()
+              .get(change.change().currentPatchSetId());
+          String commitId = patchSet.getRevision().get();
+          RevCommit commit = walk.parseCommit(ObjectId.fromString(commitId));
+          commits.put(change.getId(), commit);
+        }
+      }
+    }
+    return commits;
+  }
+
   private RevisionResource onBehalfOf(RevisionResource rsrc, SubmitInput in)
       throws AuthException, UnprocessableEntityException, OrmException {
     ChangeControl caller = rsrc.getControl();
@@ -654,16 +504,28 @@
     return new RevisionResource(changes.parse(target), rsrc.getPatchSet());
   }
 
+  public static boolean wholeTopicEnabled(Config config) {
+    return config.getBoolean("change", null, "submitWholeTopic" , false);
+  }
+
+  private List<ChangeData> getChangesByTopic(String topic) {
+    try {
+      return queryProvider.get().byTopicOpen(topic);
+    } catch (OrmException e) {
+      throw new OrmRuntimeException(e);
+    }
+  }
+
   public static class CurrentRevision implements
       RestModifyView<ChangeResource, SubmitInput> {
     private final Provider<ReviewDb> dbProvider;
     private final Submit submit;
-    private final ChangeJson json;
+    private final ChangeJson.Factory json;
 
     @Inject
     CurrentRevision(Provider<ReviewDb> dbProvider,
         Submit submit,
-        ChangeJson json) {
+        ChangeJson.Factory json) {
       this.dbProvider = dbProvider;
       this.submit = submit;
       this.json = json;
@@ -681,8 +543,9 @@
       } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
         throw new AuthException("current revision not accessible");
       }
+
       Output out = submit.apply(new RevisionResource(rsrc, ps), input);
-      return json.format(out.change);
+      return json.create(ChangeJson.NO_OPTIONS).format(out.change);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
new file mode 100644
index 0000000..7fe738d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2015 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.server.change;
+
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.WalkSorter.PatchSetData;
+import com.google.gerrit.server.git.ChangeSet;
+import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+
+@Singleton
+public class SubmittedTogether implements RestReadView<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(
+      SubmittedTogether.class);
+
+  private final ChangeJson.Factory json;
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final MergeSuperSet mergeSuperSet;
+  private final Provider<WalkSorter> sorter;
+
+  @Inject
+  SubmittedTogether(ChangeJson.Factory json,
+      Provider<ReviewDb> dbProvider,
+      Provider<InternalChangeQuery> queryProvider,
+      MergeSuperSet mergeSuperSet,
+      Provider<WalkSorter> sorter) {
+    this.json = json;
+    this.dbProvider = dbProvider;
+    this.queryProvider = queryProvider;
+    this.mergeSuperSet = mergeSuperSet;
+    this.sorter = sorter;
+  }
+
+  @Override
+  public List<ChangeInfo> apply(ChangeResource resource)
+      throws AuthException, BadRequestException,
+      ResourceConflictException, Exception {
+    try {
+      Change c = resource.getChange();
+      List<ChangeData> cds;
+      if (c.getStatus().isOpen()) {
+        cds = getForOpenChange(c);
+      } else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
+        cds = getForMergedChange(c);
+      } else {
+        cds = getForAbandonedChange();
+      }
+
+      if (cds.size() <= 1) {
+        cds = Collections.emptyList();
+      } else {
+        // Skip sorting for singleton lists, to avoid WalkSorter opening the
+        // repo just to fill out the commit field in PatchSetData.
+        cds = sort(cds);
+      }
+
+      return json.create(EnumSet.of(
+          ListChangesOption.CURRENT_REVISION,
+          ListChangesOption.CURRENT_COMMIT))
+        .formatChangeDatas(cds);
+    } catch (OrmException | IOException e) {
+      log.error("Error on getting a ChangeSet", e);
+      throw e;
+    }
+  }
+
+  private List<ChangeData> getForOpenChange(Change c)
+      throws OrmException, IOException {
+    ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(), c);
+    return cs.changes().asList();
+  }
+
+  private List<ChangeData> getForMergedChange(Change c) throws OrmException {
+    return queryProvider.get().bySubmissionId(c.getSubmissionId());
+  }
+
+  private List<ChangeData> getForAbandonedChange() {
+    return Collections.emptyList();
+  }
+
+  private List<ChangeData> sort(List<ChangeData> cds)
+      throws OrmException, IOException {
+    List<ChangeData> sorted = new ArrayList<>(cds.size());
+    for (PatchSetData psd : sorter.get().sort(cds)) {
+      sorted.add(psd.data());
+    }
+    return sorted;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
new file mode 100644
index 0000000..46fbe67
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2016 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.server.change;
+
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.ReviewersUtil;
+import com.google.gerrit.server.ReviewersUtil.VisibilityControl;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SuggestChangeReviewers extends SuggestReviewers
+    implements RestReadView<ChangeResource> {
+  @Inject
+  SuggestChangeReviewers(AccountVisibility av,
+      GenericFactory identifiedUserFactory,
+      Provider<ReviewDb> dbProvider,
+      @GerritServerConfig Config cfg,
+      ReviewersUtil reviewersUtil) {
+    super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
+  }
+
+  @Override
+  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
+      throws BadRequestException, OrmException, IOException {
+    return reviewersUtil.suggestReviewers(this,
+        rsrc.getControl().getProjectControl(), getVisibility(rsrc));
+  }
+
+  private VisibilityControl getVisibility(final ChangeResource rsrc) {
+    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account.Id account) throws OrmException {
+          return true;
+        }
+      };
+    } else {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account.Id account) throws OrmException {
+          IdentifiedUser who =
+              identifiedUserFactory.create(dbProvider, account);
+          // we can't use changeControl directly as it won't suggest reviewers
+          // to drafts
+          return rsrc.getControl().forUser(who).isRefVisible();
+        }
+      };
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index 4561ae4..3b61033 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -14,89 +14,33 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Function;
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.GroupBaseInfo;
-import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.ReviewersUtil;
 import com.google.gerrit.server.account.AccountVisibility;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class SuggestReviewers implements RestReadView<ChangeResource> {
-  private static final String MAX_SUFFIX = "\u9fa5";
+public class SuggestReviewers {
   private static final int DEFAULT_MAX_SUGGESTED = 10;
   private static final int DEFAULT_MAX_MATCHES = 100;
-  private static final Ordering<SuggestedReviewerInfo> ORDERING =
-      Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
-        @Nullable
-        @Override
-        public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
-          if (suggestedReviewerInfo == null) {
-            return null;
-          }
-          return suggestedReviewerInfo.account != null
-              ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
-              Strings.nullToEmpty(suggestedReviewerInfo.account.name))
-              : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
-        }
-      });
 
-  private final AccountLoader accountLoader;
-  private final AccountControl accountControl;
-  private final GroupMembers.Factory groupMembersFactory;
-  private final AccountCache accountCache;
-  private final Provider<ReviewDb> dbProvider;
-  private final Provider<CurrentUser> currentUser;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final GroupBackend groupBackend;
+  protected final Provider<ReviewDb> dbProvider;
+  protected final IdentifiedUser.GenericFactory identifiedUserFactory;
+  protected final ReviewersUtil reviewersUtil;
+
   private final boolean suggestAccounts;
   private final int suggestFrom;
   private final int maxAllowed;
-  private int limit;
-  private String query;
+  protected int limit;
+  protected String query;
   private boolean useFullTextSearch;
   private final int fullTextMaxMatches;
-  private final int maxSuggestedReviewers;
-  private final ReviewerSuggestionCache reviewerSuggestionCache;
+  protected final int maxSuggestedReviewers;
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
       usage = "maximum number of reviewers to list")
@@ -112,27 +56,43 @@
     this.query = q;
   }
 
+  public String getQuery() {
+    return query;
+  }
+
+  public boolean getSuggestAccounts() {
+    return suggestAccounts;
+  }
+
+  public int getSuggestFrom() {
+    return suggestFrom;
+  }
+
+  public boolean getUseFullTextSearch() {
+    return useFullTextSearch;
+  }
+
+  public int getFullTextMaxMatches() {
+    return fullTextMaxMatches;
+  }
+
+  public int getLimit() {
+    return limit;
+  }
+
+  public int getMaxAllowed() {
+    return maxAllowed;
+  }
+
   @Inject
-  SuggestReviewers(AccountVisibility av,
-      AccountLoader.Factory accountLoaderFactory,
-      AccountControl.Factory accountControlFactory,
-      AccountCache accountCache,
-      GroupMembers.Factory groupMembersFactory,
+  public SuggestReviewers(AccountVisibility av,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      Provider<CurrentUser> currentUser,
       Provider<ReviewDb> dbProvider,
       @GerritServerConfig Config cfg,
-      GroupBackend groupBackend,
-      ReviewerSuggestionCache reviewerSuggestionCache) {
-    this.accountLoader = accountLoaderFactory.create(true);
-    this.accountControl = accountControlFactory.get();
-    this.accountCache = accountCache;
-    this.groupMembersFactory = groupMembersFactory;
+      ReviewersUtil reviewersUtil) {
     this.dbProvider = dbProvider;
     this.identifiedUserFactory = identifiedUserFactory;
-    this.currentUser = currentUser;
-    this.groupBackend = groupBackend;
-    this.reviewerSuggestionCache = reviewerSuggestionCache;
+    this.reviewersUtil = reviewersUtil;
     this.maxSuggestedReviewers =
         cfg.getInt("suggest", "maxSuggestedReviewers", DEFAULT_MAX_SUGGESTED);
     this.limit = this.maxSuggestedReviewers;
@@ -152,196 +112,4 @@
     this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
         PostReviewers.DEFAULT_MAX_REVIEWERS);
   }
-
-  private interface VisibilityControl {
-    boolean isVisibleTo(Account.Id account) throws OrmException;
-  }
-
-  @Override
-  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws BadRequestException, OrmException, IOException {
-    if (Strings.isNullOrEmpty(query)) {
-      throw new BadRequestException("missing query field");
-    }
-
-    if (!suggestAccounts || query.length() < suggestFrom) {
-      return Collections.emptyList();
-    }
-
-    VisibilityControl visibilityControl = getVisibility(rsrc);
-    List<AccountInfo> suggestedAccounts;
-    if (useFullTextSearch) {
-      suggestedAccounts = suggestAccountFullTextSearch(visibilityControl);
-    } else {
-      suggestedAccounts = suggestAccount(visibilityControl);
-    }
-
-    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
-    for (AccountInfo a : suggestedAccounts) {
-      SuggestedReviewerInfo info = new SuggestedReviewerInfo();
-      info.account = a;
-      reviewer.add(info);
-    }
-
-    Project p = rsrc.getControl().getProject();
-    for (GroupReference g : suggestAccountGroup(
-        rsrc.getControl().getProjectControl())) {
-      if (suggestGroupAsReviewer(p, g, visibilityControl)) {
-        GroupBaseInfo info = new GroupBaseInfo();
-        info.id = Url.encode(g.getUUID().get());
-        info.name = g.getName();
-        SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
-        suggestedReviewerInfo.group = info;
-        reviewer.add(suggestedReviewerInfo);
-      }
-    }
-
-    reviewer = ORDERING.immutableSortedCopy(reviewer);
-    if (reviewer.size() <= limit) {
-      return reviewer;
-    } else {
-      return reviewer.subList(0, limit);
-    }
-  }
-
-  private VisibilityControl getVisibility(final ChangeResource rsrc) {
-    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
-      return new VisibilityControl() {
-        @Override
-        public boolean isVisibleTo(Account.Id account) throws OrmException {
-          return true;
-        }
-      };
-    } else {
-      return new VisibilityControl() {
-        @Override
-        public boolean isVisibleTo(Account.Id account) throws OrmException {
-          IdentifiedUser who =
-              identifiedUserFactory.create(dbProvider, account);
-          // we can't use changeControl directly as it won't suggest reviewers
-          // to drafts
-          return rsrc.getControl().forUser(who).isRefVisible();
-        }
-      };
-    }
-  }
-
-  private List<GroupReference> suggestAccountGroup(ProjectControl ctl) {
-    return Lists.newArrayList(
-        Iterables.limit(groupBackend.suggest(query, ctl), limit));
-  }
-
-  private List<AccountInfo> suggestAccount(VisibilityControl visibilityControl)
-      throws OrmException {
-    String a = query;
-    String b = a + MAX_SUFFIX;
-
-    Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
-    Map<Account.Id, String> queryEmail = new HashMap<>();
-
-    for (Account p : dbProvider.get().accounts()
-        .suggestByFullName(a, b, limit)) {
-      if (p.isActive()) {
-        addSuggestion(r, p.getId(), visibilityControl);
-      }
-    }
-
-    if (r.size() < limit) {
-      for (Account p : dbProvider.get().accounts()
-          .suggestByPreferredEmail(a, b, limit - r.size())) {
-        if (p.isActive()) {
-          addSuggestion(r, p.getId(), visibilityControl);
-        }
-      }
-    }
-
-    if (r.size() < limit) {
-      for (AccountExternalId e : dbProvider.get().accountExternalIds()
-          .suggestByEmailAddress(a, b, limit - r.size())) {
-        if (!r.containsKey(e.getAccountId())) {
-          Account p = accountCache.get(e.getAccountId()).getAccount();
-          if (p.isActive()) {
-            if (addSuggestion(r, p.getId(), visibilityControl)) {
-              queryEmail.put(e.getAccountId(), e.getEmailAddress());
-            }
-          }
-        }
-      }
-    }
-
-    accountLoader.fill();
-    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
-      AccountInfo info = r.get(p.getKey());
-      if (info != null) {
-        info.email = p.getValue();
-      }
-    }
-    return new ArrayList<>(r.values());
-  }
-
-  private List<AccountInfo> suggestAccountFullTextSearch(
-      VisibilityControl visibilityControl) throws IOException, OrmException {
-    List<AccountInfo> results = reviewerSuggestionCache.search(
-        query, fullTextMaxMatches);
-
-    Iterator<AccountInfo> it = results.iterator();
-    while (it.hasNext()) {
-      Account.Id accountId = new Account.Id(it.next()._accountId);
-      if (!(visibilityControl.isVisibleTo(accountId)
-          && accountControl.canSee(accountId))) {
-        it.remove();
-      }
-    }
-
-    return results;
-  }
-
-  private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
-      Account.Id account, VisibilityControl visibilityControl)
-      throws OrmException {
-    if (!map.containsKey(account)
-        // Can the suggestion see the change?
-        && visibilityControl.isVisibleTo(account)
-        // Can the account see the current user?
-        && accountControl.canSee(account)) {
-      map.put(account, accountLoader.get(account));
-      return true;
-    }
-    return false;
-  }
-
-  private boolean suggestGroupAsReviewer(Project project,
-      GroupReference group, VisibilityControl visibilityControl)
-      throws OrmException, IOException {
-    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
-      return false;
-    }
-
-    try {
-      Set<Account> members = groupMembersFactory
-          .create(currentUser.get())
-          .listAccounts(group.getUUID(), project.getNameKey());
-
-      if (members.isEmpty()) {
-        return false;
-      }
-
-      if (maxAllowed > 0 && members.size() > maxAllowed) {
-        return false;
-      }
-
-      // require that at least one member in the group can see the change
-      for (Account account : members) {
-        if (visibilityControl.isVisibleTo(account.getId())) {
-          return true;
-        }
-      }
-    } catch (NoSuchGroupException e) {
-      return false;
-    } catch (NoSuchProjectException e) {
-      return false;
-    }
-
-    return false;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index d3c4132..95a701e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -70,7 +69,7 @@
 
   @Override
   public List<Record> apply(RevisionResource rsrc, Input input)
-      throws AuthException, BadRequestException, OrmException {
+      throws AuthException, OrmException {
     if (input == null) {
       input = new Input();
     }
@@ -106,7 +105,7 @@
     Map<String, None> need;
     Map<String, AccountInfo> may;
     Map<String, None> impossible;
-    Integer prologReductionCount;
+    Long prologReductionCount;
 
     Record(SubmitRecord r, AccountLoader accounts) {
       this.status = r.status;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
new file mode 100644
index 0000000..d31805d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
@@ -0,0 +1,279 @@
+// Copyright (C) 2015 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.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper to sort {@link ChangeData}s based on {@link RevWalk} ordering.
+ * <p>
+ * Split changes by project, and map each change to a single commit based on the
+ * latest patch set. The set of patch sets considered may be limited by calling
+ * {@link #includePatchSets(Iterable)}. Perform a standard {@link RevWalk} on
+ * each project repository, do an approximate topo sort, and record the order in
+ * which each change's commit is seen.
+ * <p>
+ * Once an order within each project is determined, groups of changes are sorted
+ * based on the project name. This is slightly more stable than sorting on
+ * something like the commit or change timestamp, as it will not unexpectedly
+ * reorder large groups of changes on subsequent calls if one of the changes was
+ * updated.
+ */
+class WalkSorter {
+  private static final Logger log =
+      LoggerFactory.getLogger(WalkSorter.class);
+
+  private static final Ordering<List<PatchSetData>> PROJECT_LIST_SORTER =
+      Ordering.natural().nullsFirst()
+          .onResultOf(
+            new Function<List<PatchSetData>, Project.NameKey>() {
+              @Override
+              public Project.NameKey apply(List<PatchSetData> in) {
+                if (in == null || in.isEmpty()) {
+                  return null;
+                }
+                try {
+                  return in.get(0).data().change().getProject();
+                } catch (OrmException e) {
+                  throw new IllegalStateException(e);
+                }
+              }
+            });
+
+  private final GitRepositoryManager repoManager;
+  private final Set<PatchSet.Id> includePatchSets;
+  private boolean retainBody;
+
+  @Inject
+  WalkSorter(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+    includePatchSets = new HashSet<>();
+  }
+
+  public WalkSorter includePatchSets(Iterable<PatchSet.Id> patchSets) {
+    Iterables.addAll(includePatchSets, patchSets);
+    return this;
+  }
+
+  public WalkSorter setRetainBody(boolean retainBody) {
+    this.retainBody = retainBody;
+    return this;
+  }
+
+  public Iterable<PatchSetData> sort(Iterable<ChangeData> in)
+      throws OrmException, IOException {
+    Multimap<Project.NameKey, ChangeData> byProject =
+        ArrayListMultimap.create();
+    for (ChangeData cd : in) {
+      byProject.put(cd.change().getProject(), cd);
+    }
+
+    List<List<PatchSetData>> sortedByProject =
+        new ArrayList<>(byProject.keySet().size());
+    for (Map.Entry<Project.NameKey, Collection<ChangeData>> e
+        : byProject.asMap().entrySet()) {
+      sortedByProject.add(sortProject(e.getKey(), e.getValue()));
+    }
+    Collections.sort(sortedByProject, PROJECT_LIST_SORTER);
+    return Iterables.concat(sortedByProject);
+  }
+
+  private List<PatchSetData> sortProject(Project.NameKey project,
+      Collection<ChangeData> in) throws OrmException, IOException {
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      rw.setRetainBody(retainBody);
+      Multimap<RevCommit, PatchSetData> byCommit = byCommit(rw, in);
+      if (byCommit.isEmpty()) {
+        return ImmutableList.of();
+      } else if (byCommit.size() == 1) {
+        return ImmutableList.of(byCommit.values().iterator().next());
+      }
+
+      // Walk from all patch set SHA-1s, and terminate as soon as we've found
+      // everything we're looking for. This is equivalent to just sorting the
+      // list of commits by the RevWalk's configured order.
+      //
+      // Partially topo sort the list, ensuring no parent is emitted before a
+      // direct child that is also in the input set. This preserves the stable,
+      // expected sort in the case where many commits share the same timestamp,
+      // e.g. a quick rebase. It also avoids JGit's topo sort, which slurps all
+      // interesting commits at the beginning, which is a problem since we don't
+      // know which commits to mark as uninteresting. Finding a reasonable set
+      // of commits to mark uninteresting (the "rootmost" set) is at least as
+      // difficult as just implementing this partial topo sort ourselves.
+      //
+      // (This is slightly less efficient than JGit's topo sort, which uses a
+      // private in-degree field in RevCommit rather than multimaps. We assume
+      // the input size is small enough that this is not an issue.)
+
+      Set<RevCommit> commits = byCommit.keySet();
+      Multimap<RevCommit, RevCommit> children = collectChildren(commits);
+      Multimap<RevCommit, RevCommit> pending = ArrayListMultimap.create();
+      Deque<RevCommit> todo = new ArrayDeque<>();
+
+      RevFlag done = rw.newFlag("done");
+      markStart(rw, commits);
+      int expected = commits.size();
+      int found = 0;
+      RevCommit c;
+      List<PatchSetData> result = new ArrayList<>(expected);
+      while (found < expected && (c = rw.next()) != null) {
+        if (!commits.contains(c)) {
+          continue;
+        }
+        todo.clear();
+        todo.add(c);
+        int i = 0;
+        while (!todo.isEmpty()) {
+          // Sanity check: we can't pop more than N pending commits, otherwise
+          // we have an infinite loop due to programmer error or something.
+          checkState(++i <= commits.size(),
+              "Too many pending steps while sorting %s", commits);
+          RevCommit t = todo.removeFirst();
+          if (t.has(done)) {
+            continue;
+          }
+          boolean ready = true;
+          for (RevCommit child : children.get(t)) {
+            if (!child.has(done)) {
+              pending.put(child, t);
+              ready = false;
+            }
+          }
+          if (ready) {
+            found += emit(t, byCommit, result, done);
+            todo.addAll(pending.get(t));
+          }
+        }
+      }
+      return result;
+    }
+  }
+
+  private static Multimap<RevCommit, RevCommit> collectChildren(
+      Set<RevCommit> commits) {
+    Multimap<RevCommit, RevCommit> children = ArrayListMultimap.create();
+    for (RevCommit c : commits) {
+      for (RevCommit p : c.getParents()) {
+        if (commits.contains(p)) {
+          children.put(p, c);
+        }
+      }
+    }
+    return children;
+  }
+
+  private static int emit(RevCommit c, Multimap<RevCommit, PatchSetData> byCommit,
+      List<PatchSetData> result, RevFlag done) {
+    if (c.has(done)) {
+      return 0;
+    }
+    c.add(done);
+    Collection<PatchSetData> psds = byCommit.get(c);
+    if (!psds.isEmpty()) {
+      result.addAll(psds);
+      return 1;
+    }
+    return 0;
+  }
+
+  private Multimap<RevCommit, PatchSetData> byCommit(RevWalk rw,
+      Collection<ChangeData> in) throws OrmException, IOException {
+    Multimap<RevCommit, PatchSetData> byCommit =
+        ArrayListMultimap.create(in.size(), 1);
+    for (ChangeData cd : in) {
+      PatchSet maxPs = null;
+      for (PatchSet ps : cd.patchSets()) {
+        if (shouldInclude(ps)
+            && (maxPs == null || ps.getId().get() > maxPs.getId().get())) {
+          maxPs = ps;
+        }
+      }
+      if (maxPs == null) {
+       continue; // No patch sets matched.
+      }
+      ObjectId id = ObjectId.fromString(maxPs.getRevision().get());
+      try {
+        RevCommit c = rw.parseCommit(id);
+        byCommit.put(c, PatchSetData.create(cd, maxPs, c));
+      } catch (MissingObjectException | IncorrectObjectTypeException e) {
+        log.warn(
+            "missing commit " + id.name() + " for patch set " + maxPs.getId(),
+            e);
+      }
+    }
+    return byCommit;
+  }
+
+  private boolean shouldInclude(PatchSet ps) {
+    return includePatchSets.isEmpty() || includePatchSets.contains(ps.getId());
+  }
+
+  private static void markStart(RevWalk rw, Iterable<RevCommit> commits)
+      throws IOException {
+    for (RevCommit c : commits) {
+      rw.markStart(c);
+    }
+  }
+
+  @AutoValue
+  abstract static class PatchSetData {
+    @VisibleForTesting
+    static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
+      return new AutoValue_WalkSorter_PatchSetData(cd, ps, commit);
+    }
+
+    abstract ChangeData data();
+    abstract PatchSet patchSet();
+    abstract RevCommit commit();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
deleted file mode 100644
index 67e70c1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ /dev/null
@@ -1,399 +0,0 @@
-// Copyright (C) 2012 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.server.changedetail;
-
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.TimeZone;
-
-@Singleton
-public class RebaseChange {
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager gitManager;
-  private final TimeZone serverTimeZone;
-  private final MergeUtil.Factory mergeUtilFactory;
-  private final PatchSetInserter.Factory patchSetInserterFactory;
-
-  @Inject
-  RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
-      final Provider<ReviewDb> db,
-      @GerritPersonIdent final PersonIdent myIdent,
-      final GitRepositoryManager gitManager,
-      final MergeUtil.Factory mergeUtilFactory,
-      final PatchSetInserter.Factory patchSetInserterFactory) {
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.gitManager = gitManager;
-    this.serverTimeZone = myIdent.getTimeZone();
-    this.mergeUtilFactory = mergeUtilFactory;
-    this.patchSetInserterFactory = patchSetInserterFactory;
-  }
-
-  /**
-   * Rebases the change of the given patch set.
-   *
-   * It is verified that the current user is allowed to do the rebase.
-   *
-   * If the patch set has no dependency to an open change, then the change is
-   * rebased on the tip of the destination branch.
-   *
-   * If the patch set depends on an open change, it is rebased on the latest
-   * patch set of this change.
-   *
-   * The rebased commit is added as new patch set to the change.
-   *
-   * E-mail notification and triggering of hooks happens for the creation of the
-   * new patch set.
-   *
-   * @param change the change to perform the rebase for
-   * @param patchSetId the id of the patch set
-   * @param uploader the user that creates the rebased patch set
-   * @param newBaseRev the commit that should be the new base
-   * @throws NoSuchChangeException thrown if the change to which the patch set
-   *         belongs does not exist or is not visible to the user
-   * @throws EmailException thrown if sending the e-mail to notify about the new
-   *         patch set fails
-   * @throws OrmException thrown in case accessing the database fails
-   * @throws IOException thrown if rebase is not possible or not needed
-   * @throws InvalidChangeOperationException thrown if rebase is not allowed
-   */
-  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader,
-      final String newBaseRev) throws NoSuchChangeException, EmailException, OrmException,
-      IOException, InvalidChangeOperationException {
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl changeControl =
-        changeControlFactory.validateFor(change, uploader);
-    if (!changeControl.canRebase()) {
-      throw new InvalidChangeOperationException(
-          "Cannot rebase: New patch sets are not allowed to be added to change: "
-              + changeId.toString());
-    }
-    try (Repository git = gitManager.openRepository(change.getProject());
-        RevWalk rw = new RevWalk(git);
-        ObjectInserter inserter = git.newObjectInserter()) {
-      String baseRev = newBaseRev;
-      if (baseRev == null) {
-          baseRev = findBaseRevision(patchSetId, db.get(),
-              change.getDest(), git, null, null, null);
-      }
-      ObjectId baseObjectId = git.resolve(baseRev);
-      if (baseObjectId == null) {
-        throw new InvalidChangeOperationException(
-          "Cannot rebase: Failed to resolve baseRev: " + baseRev);
-      }
-      final RevCommit baseCommit = rw.parseCommit(baseObjectId);
-
-      PersonIdent committerIdent =
-          uploader.newCommitterIdent(TimeUtil.nowTs(),
-              serverTimeZone);
-
-      rebase(git, rw, inserter, patchSetId, change,
-          uploader, baseCommit, mergeUtilFactory.create(
-              changeControl.getProjectControl().getProjectState(), true),
-          committerIdent, true, ValidatePolicy.GERRIT);
-    } catch (MergeConflictException e) {
-      throw new IOException(e.getMessage());
-    }
-  }
-
-  /**
-   * Finds the revision of commit on which the given patch set should be based.
-   *
-   * @param patchSetId the id of the patch set for which the new base commit
-   *        should be found
-   * @param db the ReviewDb
-   * @param destBranch the destination branch
-   * @param git the repository
-   * @param patchSetAncestors the original PatchSetAncestor of the given patch
-   *        set that should be based
-   * @param depPatchSetList the original patch set list on which the rebased
-   *        patch set depends
-   * @param depChangeList the original change list on whose patch set the
-   *        rebased patch set depends
-   * @return the revision of commit on which the given patch set should be based
-   * @throws IOException thrown if rebase is not possible or not needed
-   * @throws OrmException thrown in case accessing the database fails
-   */
-    private static String findBaseRevision(final PatchSet.Id patchSetId,
-        final ReviewDb db, final Branch.NameKey destBranch, final Repository git,
-        List<PatchSetAncestor> patchSetAncestors, List<PatchSet> depPatchSetList,
-        List<Change> depChangeList) throws IOException, OrmException {
-
-      String baseRev = null;
-
-      if (patchSetAncestors == null) {
-        patchSetAncestors =
-            db.patchSetAncestors().ancestorsOf(patchSetId).toList();
-      }
-
-      if (patchSetAncestors.size() > 1) {
-        throw new IOException(
-            "Cannot rebase a change with multiple parents. Parent commits: "
-                + patchSetAncestors.toString());
-      }
-      if (patchSetAncestors.size() == 0) {
-        throw new IOException(
-            "Cannot rebase a change without any parents (is this the initial commit?).");
-      }
-
-      RevId ancestorRev = patchSetAncestors.get(0).getAncestorRevision();
-      if (depPatchSetList == null || depPatchSetList.size() != 1 ||
-          !depPatchSetList.get(0).getRevision().equals(ancestorRev)) {
-        depPatchSetList = db.patchSets().byRevision(ancestorRev).toList();
-      }
-
-      for (PatchSet depPatchSet : depPatchSetList) {
-
-        Change.Id depChangeId = depPatchSet.getId().getParentKey();
-        Change depChange;
-        if (depChangeList == null || depChangeList.size() != 1 ||
-            !depChangeList.get(0).getId().equals(depChangeId)) {
-          depChange = db.changes().get(depChangeId);
-        } else {
-          depChange = depChangeList.get(0);
-        }
-        if (!depChange.getDest().equals(destBranch)) {
-          continue;
-        }
-
-        if (depChange.getStatus() == Status.ABANDONED) {
-          throw new IOException("Cannot rebase a change with an abandoned parent: "
-              + depChange.getKey().toString());
-        }
-
-        if (depChange.getStatus().isOpen()) {
-          if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
-            throw new IOException(
-                "Change is already based on the latest patch set of the dependent change.");
-          }
-          PatchSet latestDepPatchSet =
-              db.patchSets().get(depChange.currentPatchSetId());
-          baseRev = latestDepPatchSet.getRevision().get();
-        }
-        break;
-      }
-
-      if (baseRev == null) {
-        // We are dependent on a merged PatchSet or have no PatchSet
-        // dependencies at all.
-        Ref destRef = git.getRef(destBranch.get());
-        if (destRef == null) {
-          throw new IOException(
-              "The destination branch does not exist: "
-                  + destBranch.get());
-        }
-        baseRev = destRef.getObjectId().getName();
-        if (baseRev.equals(ancestorRev.get())) {
-          throw new IOException("Change is already up to date.");
-        }
-      }
-      return baseRev;
-    }
-
-  /**
-   * Rebases the change of the given patch set on the given base commit.
-   *
-   * The rebased commit is added as new patch set to the change.
-   *
-   * E-mail notification and triggering of hooks is only done for the creation of
-   * the new patch set if `sendEmail` and `runHooks` are set to true.
-   *
-   * @param git the repository
-   * @param revWalk the RevWalk
-   * @param inserter the object inserter
-   * @param patchSetId the id of the patch set
-   * @param change the change that should be rebased
-   * @param uploader the user that creates the rebased patch set
-   * @param baseCommit the commit that should be the new base
-   * @param mergeUtil merge utilities for the destination project
-   * @param committerIdent the committer's identity
-   * @param runHooks if hooks should be run for the new patch set
-   * @param validate if commit validation should be run for the new patch set
-   * @return the new patch set which is based on the given base commit
-   * @throws NoSuchChangeException thrown if the change to which the patch set
-   *         belongs does not exist or is not visible to the user
-   * @throws OrmException thrown in case accessing the database fails
-   * @throws IOException thrown if rebase is not possible or not needed
-   * @throws InvalidChangeOperationException thrown if rebase is not allowed
-   */
-  public PatchSet rebase(final Repository git, final RevWalk revWalk,
-      final ObjectInserter inserter, final PatchSet.Id patchSetId,
-      final Change change, final IdentifiedUser uploader, final RevCommit baseCommit,
-      final MergeUtil mergeUtil, PersonIdent committerIdent,
-      boolean runHooks, ValidatePolicy validate)
-          throws NoSuchChangeException,
-      OrmException, IOException, InvalidChangeOperationException,
-      MergeConflictException {
-    if (!change.currentPatchSetId().equals(patchSetId)) {
-      throw new InvalidChangeOperationException("patch set is not current");
-    }
-    final PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
-
-    final RevCommit rebasedCommit;
-    ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
-    ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId),
-        baseCommit, mergeUtil, committerIdent);
-
-    rebasedCommit = revWalk.parseCommit(newId);
-
-    final ChangeControl changeControl =
-        changeControlFactory.validateFor(change, uploader);
-
-    PatchSetInserter patchSetInserter = patchSetInserterFactory
-        .create(git, revWalk, changeControl, rebasedCommit)
-        .setValidatePolicy(validate)
-        .setDraft(originalPatchSet.isDraft())
-        .setUploader(uploader.getAccountId())
-        .setSendMail(false)
-        .setRunHooks(runHooks);
-
-    final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
-    final ChangeMessage cmsg = new ChangeMessage(
-        new ChangeMessage.Key(change.getId(),
-            ChangeUtil.messageUUID(db.get())), uploader.getAccountId(),
-            TimeUtil.nowTs(), patchSetId);
-
-    cmsg.setMessage("Patch Set " + newPatchSetId.get()
-        + ": Patch Set " + patchSetId.get() + " was rebased");
-
-    Change newChange = patchSetInserter
-        .setMessage(cmsg)
-        .insert();
-
-    return db.get().patchSets().get(newChange.currentPatchSetId());
-  }
-
-  /**
-   * Rebase a commit.
-   *
-   * @param git repository to find commits in.
-   * @param inserter inserter to handle new trees and blobs.
-   * @param original the commit to rebase.
-   * @param base base to rebase against.
-   * @param mergeUtil merge utilities for the destination project.
-   * @param committerIdent committer identity.
-   * @return the id of the rebased commit.
-   * @throws MergeConflictException the rebase failed due to a merge conflict.
-   * @throws IOException the merge failed for another reason.
-   */
-  private ObjectId rebaseCommit(Repository git, ObjectInserter inserter,
-      RevCommit original, RevCommit base, MergeUtil mergeUtil,
-      PersonIdent committerIdent) throws MergeConflictException, IOException {
-    RevCommit parentCommit = original.getParent(0);
-
-    if (base.equals(parentCommit)) {
-      throw new IOException("Change is already up to date.");
-    }
-
-    ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
-    merger.setBase(parentCommit);
-    merger.merge(original, base);
-
-    if (merger.getResultTreeId() == null) {
-      throw new MergeConflictException(
-          "The change could not be rebased due to a conflict during merge.");
-    }
-
-    CommitBuilder cb = new CommitBuilder();
-    cb.setTreeId(merger.getResultTreeId());
-    cb.setParentId(base);
-    cb.setAuthor(original.getAuthorIdent());
-    cb.setMessage(original.getFullMessage());
-    cb.setCommitter(committerIdent);
-    ObjectId objectId = inserter.insert(cb);
-    inserter.flush();
-    return objectId;
-  }
-
-  public boolean canRebase(ChangeResource r) {
-    Change c = r.getChange();
-    return canRebase(c.getProject(), c.currentPatchSetId(), c.getDest());
-  }
-
-  public boolean canRebase(RevisionResource r) {
-    return canRebase(r.getChange().getProject(),
-        r.getPatchSet().getId(), r.getChange().getDest());
-  }
-
-  public boolean canRebase(Project.NameKey project,
-      PatchSet.Id patchSetId, Branch.NameKey branch) {
-    Repository git;
-    try {
-      git = gitManager.openRepository(project);
-    } catch (RepositoryNotFoundException err) {
-      return false;
-    } catch (IOException err) {
-      return false;
-    }
-    try {
-      findBaseRevision(
-          patchSetId,
-          db.get(),
-          branch,
-          git,
-          null,
-          null,
-          null);
-      return true;
-    } catch (IOException e) {
-      return false;
-    } catch (OrmException e) {
-      return false;
-    } finally {
-      git.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java
index ffa90b6..b44affa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java
@@ -20,6 +20,7 @@
 import org.eclipse.jgit.lib.Config;
 
 public class AnonymousCowardNameProvider implements Provider<String> {
+  public static final String DEFAULT = "Anonymous Coward";
 
   private final String anonymousCoward;
 
@@ -27,7 +28,7 @@
   public AnonymousCowardNameProvider(@GerritServerConfig final Config cfg) {
     String anonymousCoward = cfg.getString("user", null, "anonymousCoward");
     if (anonymousCoward == null) {
-      anonymousCoward = "Anonymous Coward";
+      anonymousCoward = DEFAULT;
     }
     this.anonymousCoward = anonymousCoward;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index d7138b3..c3bd519 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -40,12 +40,19 @@
   private final String httpEmailHeader;
   private final String httpExternalIdHeader;
   private final String registerPageUrl;
+  private final String registerUrl;
+  private final String registerText;
   private final boolean trustContainerAuth;
   private final boolean enableRunAs;
   private final boolean userNameToLowerCase;
   private final boolean gitBasicAuth;
+  private final boolean useContributorAgreements;
   private final String loginUrl;
+  private final String loginText;
   private final String logoutUrl;
+  private final String switchAccountUrl;
+  private final String editFullNameUrl;
+  private final String httpPasswordUrl;
   private final String openIdSsoUrl;
   private final List<String> openIdDomains;
   private final List<OpenIdProviderPattern> trustedOpenIDs;
@@ -53,7 +60,7 @@
   private final String cookiePath;
   private final boolean cookieSecure;
   private final SignedToken emailReg;
-  private final SignedToken restToken;
+  private final boolean allowRegisterNewEmail;
 
   @Inject
   AuthConfig(@GerritServerConfig final Config cfg)
@@ -64,8 +71,14 @@
     httpEmailHeader = cfg.getString("auth", null, "httpemailheader");
     httpExternalIdHeader = cfg.getString("auth", null, "httpexternalidheader");
     loginUrl = cfg.getString("auth", null, "loginurl");
+    loginText = cfg.getString("auth", null, "logintext");
     logoutUrl = cfg.getString("auth", null, "logouturl");
+    switchAccountUrl = cfg.getString("auth", null, "switchAccountUrl");
+    editFullNameUrl = cfg.getString("auth", null, "editFullNameUrl");
+    httpPasswordUrl = cfg.getString("auth", null, "httpPasswordUrl");
     registerPageUrl = cfg.getString("auth", null, "registerPageUrl");
+    registerUrl = cfg.getString("auth", null, "registerUrl");
+    registerText = cfg.getString("auth", null, "registerText");
     openIdSsoUrl = cfg.getString("auth", null, "openidssourl");
     openIdDomains = Arrays.asList(cfg.getStringList("auth", null, "openIdDomain"));
     trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
@@ -75,8 +88,10 @@
     trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
     enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
     gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
+    useContributorAgreements =
+        cfg.getBoolean("auth", "contributoragreements", false);
     userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
-
+    allowRegisterNewEmail = cfg.getBoolean("auth", "allowRegisterNewEmail", true);
 
     String key = cfg.getString("auth", null, "registerEmailPrivateKey");
     if (key != null && !key.isEmpty()) {
@@ -88,15 +103,6 @@
     } else {
       emailReg = null;
     }
-
-    key = cfg.getString("auth", null, "restTokenPrivateKey");
-    if (key != null && !key.isEmpty()) {
-      int age = (int) ConfigUtil.getTimeUnit(cfg,
-          "auth", null, "maxRestTokenAge", 60, TimeUnit.SECONDS);
-      restToken = new SignedToken(age, key);
-    } else {
-      restToken = null;
-    }
   }
 
   private static List<OpenIdProviderPattern> toPatterns(Config cfg, String name) {
@@ -141,10 +147,26 @@
     return loginUrl;
   }
 
+  public String getLoginText() {
+    return loginText;
+  }
+
   public String getLogoutURL() {
     return logoutUrl;
   }
 
+  public String getSwitchAccountUrl() {
+    return switchAccountUrl;
+  }
+
+  public String getEditFullNameUrl() {
+    return editFullNameUrl;
+  }
+
+  public String getHttpPasswordUrl() {
+    return httpPasswordUrl;
+  }
+
   public String getOpenIdSsoUrl() {
     return openIdSsoUrl;
   }
@@ -165,10 +187,6 @@
     return emailReg;
   }
 
-  public SignedToken getRestToken() {
-    return restToken;
-  }
-
   /** OpenID identities which the server permits for authentication. */
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
@@ -194,6 +212,11 @@
     return gitBasicAuth;
   }
 
+  /** Whether contributor agreements are used. */
+  public boolean isUseContributorAgreements() {
+    return useContributorAgreements;
+  }
+
   public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
     switch (getAuthType()) {
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
@@ -263,8 +286,20 @@
     return registerPageUrl;
   }
 
+  public String getRegisterUrl() {
+    return registerUrl;
+  }
+
+  public String getRegisterText() {
+    return registerText;
+  }
+
   public boolean isLdapAuthType() {
     return authType == AuthType.LDAP ||
         authType == AuthType.LDAP_BIND;
   }
+
+  public boolean isAllowRegisterNewEmail() {
+    return allowRegisterNewEmail;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
index 5bff20c..7b567e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
+
 import com.google.common.cache.Cache;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -31,7 +33,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-@RequiresCapability(GlobalCapability.VIEW_CACHES)
+@RequiresAnyCapability({VIEW_CACHES, MAINTAIN_SERVER})
 @Singleton
 public class CachesCollection implements
     ChildCollection<ConfigResource, CacheResource>, AcceptsPost<ConfigResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
index 99a4f52..5025892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -31,6 +31,7 @@
   public String emailReviewers;
   public String flushCaches;
   public String killTask;
+  public String maintainServer;
   public String modifyAccount;
   public String priority;
   public String queryLimit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
new file mode 100644
index 0000000..b4b1865
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2015 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.server.config;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+public class ChangeCleanupConfig {
+  private static String SECTION = "changeCleanup";
+  private static String KEY_ABANDON_AFTER = "abandonAfter";
+  private static String KEY_ABANDON_IF_MERGEABLE = "abandonIfMergeable";
+  private static String KEY_ABANDON_MESSAGE = "abandonMessage";
+  private static String DEFAULT_ABANDON_MESSAGE =
+      "Auto-Abandoned due to inactivity, see "
+      + "${URL}Documentation/user-change-cleanup.html#auto-abandon\n"
+      + "\n"
+      + "If this change is still wanted it should be restored.";
+
+  private final ScheduleConfig scheduleConfig;
+  private final long abandonAfter;
+  private final boolean abandonIfMergeable;
+  private final String abandonMessage;
+
+  @Inject
+  ChangeCleanupConfig(@GerritServerConfig Config cfg,
+      @CanonicalWebUrl @Nullable String canonicalWebUrl) {
+    scheduleConfig = new ScheduleConfig(cfg, SECTION);
+    abandonAfter = readAbandonAfter(cfg);
+    abandonIfMergeable =
+        cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true);
+    abandonMessage = readAbandonMessage(cfg, canonicalWebUrl);
+  }
+
+  private long readAbandonAfter(Config cfg) {
+    long abandonAfter =
+        ConfigUtil.getTimeUnit(cfg, SECTION, null, KEY_ABANDON_AFTER, 0,
+            TimeUnit.MILLISECONDS);
+    return abandonAfter >= 0 ? abandonAfter : 0;
+  }
+
+  private String readAbandonMessage(Config cfg, String webUrl) {
+    String abandonMessage = cfg.getString(SECTION, null, KEY_ABANDON_MESSAGE);
+    if (Strings.isNullOrEmpty(abandonMessage)) {
+      abandonMessage = DEFAULT_ABANDON_MESSAGE;
+    }
+    if (!Strings.isNullOrEmpty(webUrl)) {
+      abandonMessage = abandonMessage.replaceAll("\\$\\{URL\\}", webUrl);
+    }
+    return abandonMessage;
+  }
+
+  public ScheduleConfig getScheduleConfig() {
+    return scheduleConfig;
+  }
+
+  public long getAbandonAfter() {
+    return abandonAfter;
+  }
+
+  public boolean getAbandonIfMergeable() {
+    return abandonIfMergeable;
+  }
+
+  public String getAbandonMessage() {
+    return abandonMessage;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index dd4ba79..be9d984 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.server.config;
 
-import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -31,15 +35,9 @@
   private static <T> T[] allValuesOf(final T defaultValue) {
     try {
       return (T[]) defaultValue.getClass().getMethod("values").invoke(null);
-    } catch (IllegalArgumentException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (SecurityException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (IllegalAccessException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (InvocationTargetException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    } catch (NoSuchMethodException e) {
+    } catch (IllegalArgumentException | NoSuchMethodException
+        | InvocationTargetException | IllegalAccessException
+        | SecurityException e) {
       throw new IllegalArgumentException("Cannot obtain enumeration values", e);
     }
   }
@@ -62,7 +60,7 @@
 
     String n = valueString.replace(' ', '_').replace('-', '_');
     for (final T e : all) {
-      if (equalsIgnoreCase(e.name(), n)) {
+      if (e.name().equalsIgnoreCase(n)) {
         return e;
       }
     }
@@ -257,9 +255,142 @@
     return v;
   }
 
+  /**
+   * Store section by inspecting Java class attributes.
+   * <p>
+   * Optimize the storage by unsetting a variable if it is
+   * being set to default value by the server.
+   * <p>
+   * Fields marked with final or transient modifiers are skipped.
+   *
+   * @param cfg config in which the values should be stored
+   * @param section section
+   * @param sub subsection
+   * @param s instance of class with config values
+   * @param defaults instance of class with default values
+   * @throws ConfigInvalidException
+   */
+  public static <T> void storeSection(Config cfg, String section, String sub,
+      T s, T defaults) throws ConfigInvalidException {
+    try {
+      for (Field f : s.getClass().getDeclaredFields()) {
+        if (skipField(f)) {
+          continue;
+        }
+        Class<?> t = f.getType();
+        String n = f.getName();
+        f.setAccessible(true);
+        Object c = f.get(s);
+        Object d = f.get(defaults);
+        Preconditions.checkNotNull(d, "Default cannot be null");
+        if (c == null || c.equals(d)) {
+          cfg.unset(section, sub, n);
+        } else {
+          if (isString(t)) {
+            cfg.setString(section, sub, n, (String) c);
+          } else if (isInteger(t)) {
+            cfg.setInt(section, sub, n, (Integer) c);
+          } else if (isLong(t)) {
+            cfg.setLong(section, sub, n, (Long) c);
+          } else if (isBoolean(t)) {
+            cfg.setBoolean(section, sub, n, (Boolean) c);
+          } else if (t.isEnum()) {
+            cfg.setEnum(section, sub, n, (Enum<?>) c);
+          } else {
+            throw new ConfigInvalidException("type is unknown: " + t.getName());
+          }
+        }
+      }
+    } catch (SecurityException | IllegalArgumentException
+        | IllegalAccessException e) {
+      throw new ConfigInvalidException("cannot save values", e);
+    }
+  }
+
+  /**
+   * Load section by inspecting Java class attributes.
+   * <p>
+   * Config values are stored optimized: no default values are stored.
+   * The loading is performed eagerly: all values are set.
+   * <p>
+   * Fields marked with final or transient modifiers are skipped.
+   *
+   * @param cfg config from which the values are loaded
+   * @param section section
+   * @param sub subsection
+   * @param s instance of class in which the values are set
+   * @param defaults instance of class with default values
+   * @param i instance to merge during the load. When present, the
+   * boolean fields are not nullified when their values are false
+   * @return loaded instance
+   * @throws ConfigInvalidException
+   */
+  public static <T> T loadSection(Config cfg, String section, String sub,
+      T s, T defaults, T i) throws ConfigInvalidException {
+    try {
+      for (Field f : s.getClass().getDeclaredFields()) {
+        if (skipField(f)) {
+          continue;
+        }
+        Class<?> t = f.getType();
+        String n = f.getName();
+        f.setAccessible(true);
+        Object d = f.get(defaults);
+        Preconditions.checkNotNull(d, "Default cannot be null");
+        if (isString(t)) {
+          f.set(s, MoreObjects.firstNonNull(cfg.getString(section, sub, n), d));
+        } else if (isInteger(t)) {
+          f.set(s, cfg.getInt(section, sub, n, (Integer) d));
+        } else if (isLong(t)) {
+          f.set(s, cfg.getLong(section, sub, n, (Long) d));
+        } else if (isBoolean(t)) {
+          boolean b = cfg.getBoolean(section, sub, n, (Boolean) d);
+          if (b || i != null) {
+            f.set(s, b);
+          }
+        } else if (t.isEnum()) {
+          f.set(s, cfg.getEnum(section, sub, n, (Enum<?>) d));
+        } else {
+          throw new ConfigInvalidException("type is unknown: " + t.getName());
+        }
+        if (i != null) {
+          Object o = f.get(i);
+          if (o != null) {
+            f.set(s, o);
+          }
+        }
+      }
+    } catch (SecurityException | IllegalArgumentException
+        | IllegalAccessException e) {
+      throw new ConfigInvalidException("cannot load values", e);
+    }
+    return s;
+  }
+
+  private static boolean skipField(Field field) {
+    int modifiers = field.getModifiers();
+    return Modifier.isFinal(modifiers) || Modifier.isTransient(modifiers);
+  }
+
+  private static boolean isString(Class<?> t) {
+    return String.class == t;
+  }
+
+  private static boolean isBoolean(Class<?> t) {
+    return Boolean.class == t || boolean.class == t;
+  }
+
+  private static boolean isLong(Class<?> t) {
+    return Long.class == t || long.class == t;
+  }
+
+  private static boolean isInteger(Class<?> t) {
+    return Integer.class == t || int.class == t;
+  }
+
   private static boolean match(final String a, final String... cases) {
     for (final String b : cases) {
-      if (equalsIgnoreCase(a, b)) {
+      if (b != null && b.equalsIgnoreCase(a)) {
         return true;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
new file mode 100644
index 0000000..df3c37f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2015 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.server.config;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.config.ConfirmEmail.Input;
+import com.google.gerrit.server.mail.EmailTokenVerifier;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
+  public static class Input {
+    @DefaultInput
+    public String token;
+  }
+
+  private final Provider<CurrentUser> self;
+  private final EmailTokenVerifier emailTokenVerifier;
+  private final AccountManager accountManager;
+
+  @Inject
+  public ConfirmEmail(Provider<CurrentUser> self,
+      EmailTokenVerifier emailTokenVerifier,
+      AccountManager accountManager) {
+    this.self = self;
+    this.emailTokenVerifier = emailTokenVerifier;
+    this.accountManager = accountManager;
+  }
+
+  @Override
+  public Response<?> apply(ConfigResource rsrc, Input input)
+      throws AuthException, UnprocessableEntityException, AccountException,
+      OrmException {
+    CurrentUser user = self.get();
+    if (!user.isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.token == null) {
+      throw new UnprocessableEntityException("missing token");
+    }
+
+    try {
+      EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(input.token);
+      Account.Id accId = user.getAccountId();
+      if (accId.equals(token.getAccountId())) {
+        accountManager.link(accId, token.toAuthRequest());
+        return Response.none();
+      } else {
+        throw new UnprocessableEntityException("invalid token");
+      }
+    } catch (EmailTokenVerifier.InvalidTokenException e) {
+      throw new UnprocessableEntityException("invalid token");
+    } catch (AccountException e) {
+      throw new UnprocessableEntityException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java
index b029060..a22f52d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.config.DeleteTask.Input;
 import com.google.inject.Singleton;
 
 @Singleton
-@RequiresCapability(GlobalCapability.KILL_TASK)
+@RequiresAnyCapability({KILL_TASK, MAINTAIN_SERVER})
 public class DeleteTask implements RestModifyView<TaskResource, Input> {
   public static class Input {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
index 2d9f21a..f435a2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -14,46 +14,107 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
+import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 
-import java.util.Collections;
-import java.util.HashSet;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
-import java.util.Set;
 
-/** Download protocol from {@code gerrit.config}. */
+/**
+ * Download protocol from {@code gerrit.config}.
+ * <p>
+ * Only used to configure the built-in set of schemes and commands in the core
+ * download-commands plugin; not used by other plugins.
+ */
 @Singleton
 public class DownloadConfig {
-  private final Set<DownloadScheme> downloadSchemes;
-  private final Set<DownloadCommand> downloadCommands;
+  private final ImmutableSet<String> downloadSchemes;
+  private final ImmutableSet<DownloadCommand> downloadCommands;
+  private final ImmutableSet<ArchiveFormat> archiveFormats;
 
   @Inject
   DownloadConfig(@GerritServerConfig final Config cfg) {
-    List<DownloadScheme> allSchemes =
-        ConfigUtil.getEnumList(cfg, "download", null, "scheme",
-            DownloadScheme.DEFAULT_DOWNLOADS);
-    downloadSchemes =
-        Collections.unmodifiableSet(new HashSet<>(allSchemes));
+    String[] allSchemes = cfg.getStringList("download", null, "scheme");
+    if (allSchemes.length == 0) {
+      downloadSchemes = ImmutableSet.of(
+          CoreDownloadSchemes.SSH,
+          CoreDownloadSchemes.HTTP,
+          CoreDownloadSchemes.ANON_HTTP);
+    } else {
+      List<String> normalized = new ArrayList<>(allSchemes.length);
+      for (String s : allSchemes) {
+        String core = toCoreScheme(s);
+        if (core == null) {
+          throw new IllegalArgumentException(
+              "not a core download scheme: " + s);
+        }
+        normalized.add(core);
+      }
+      downloadSchemes = ImmutableSet.copyOf(normalized);
+    }
 
+    DownloadCommand[] downloadCommandValues = DownloadCommand.values();
     List<DownloadCommand> allCommands =
         ConfigUtil.getEnumList(cfg, "download", null, "command",
-            DownloadCommand.DEFAULT_DOWNLOADS);
-    downloadCommands =
-        Collections.unmodifiableSet(new HashSet<>(allCommands));
+            downloadCommandValues, null);
+    if (isOnlyNull(allCommands)) {
+      downloadCommands = ImmutableSet.copyOf(downloadCommandValues);
+    } else {
+      downloadCommands = ImmutableSet.copyOf(allCommands);
+    }
+
+    String v = cfg.getString("download", null, "archive");
+    if (v == null) {
+      archiveFormats = ImmutableSet.copyOf(EnumSet.allOf(ArchiveFormat.class));
+    } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
+      archiveFormats = ImmutableSet.of();
+    } else {
+      archiveFormats = ImmutableSet.copyOf(ConfigUtil.getEnumList(cfg,
+          "download", null, "archive",
+          ArchiveFormat.TGZ));
+    }
+  }
+
+  private static boolean isOnlyNull(List<?> list) {
+    return list.size() == 1 && list.get(0) == null;
+  }
+
+  private static String toCoreScheme(String s) {
+    try {
+      Field f = CoreDownloadSchemes.class.getField(s.toUpperCase());
+      int m = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;
+      if ((f.getModifiers() & m) == m && f.getType() == String.class) {
+        return (String) f.get(null);
+      } else {
+        return null;
+      }
+    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException
+        | IllegalAccessException e) {
+      return null;
+    }
   }
 
   /** Scheme used to download. */
-  public Set<DownloadScheme> getDownloadSchemes() {
+  public ImmutableSet<String> getDownloadSchemes() {
     return downloadSchemes;
   }
 
   /** Command used to download. */
-  public Set<DownloadCommand> getDownloadCommands() {
+  public ImmutableSet<DownloadCommand> getDownloadCommands() {
     return downloadCommands;
   }
+
+  /** Archive formats for downloading. */
+  public ImmutableSet<ArchiveFormat> getArchiveFormats() {
+    return archiveFormats;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
index a7c03be..eb5ef22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -25,7 +27,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-@RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@RequiresAnyCapability({FLUSH_CACHES, MAINTAIN_SERVER})
 @Singleton
 public class FlushCache implements RestModifyView<CacheResource, Input> {
   public static class Input {
@@ -44,9 +46,9 @@
   public Response<String> apply(CacheResource rsrc, Input input)
       throws AuthException {
     if (WEB_SESSIONS.equals(rsrc.getName())
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canMaintainServer()) {
       throw new AuthException(String.format(
-          "only site administrators can flush %s", WEB_SESSIONS));
+          "only site maintainers can flush %s", WEB_SESSIONS));
     }
 
     rsrc.getCache().invalidateAll();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java
index 54096bb..572b56e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GcConfig.java
@@ -23,14 +23,19 @@
 @Singleton
 public class GcConfig {
   private final ScheduleConfig scheduleConfig;
+  private final boolean aggressive;
 
   @Inject
   GcConfig(@GerritServerConfig Config cfg) {
     scheduleConfig = new ScheduleConfig(cfg, ConfigConstants.CONFIG_GC_SECTION);
+    aggressive = cfg.getBoolean(ConfigConstants.CONFIG_GC_SECTION, "aggressive", false);
   }
 
   public ScheduleConfig getScheduleConfig() {
     return scheduleConfig;
   }
 
+  public boolean isAggressive() {
+    return aggressive;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 7bdccaa..90249c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -20,8 +20,12 @@
 import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.config.ExternalIncludedIn;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -34,11 +38,11 @@
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.extensions.webui.TopMenu;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AnonymousUser;
@@ -63,19 +67,18 @@
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupInfoCacheFactory;
 import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.server.account.PerformRenameGroup;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.ChangeMergeQueue;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.EmailMerge;
 import com.google.gerrit.server.git.GitModule;
-import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
@@ -92,6 +95,7 @@
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupModule;
 import com.google.gerrit.server.index.ReindexAfterUpdate;
+import com.google.gerrit.server.mail.AddKeySender;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.EmailModule;
@@ -113,7 +117,6 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.CommentLinkInfo;
 import com.google.gerrit.server.project.CommentLinkProvider;
-import com.google.gerrit.server.project.PerformCreateProject;
 import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectControl;
@@ -125,6 +128,7 @@
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.HashtagValidationListener;
@@ -139,7 +143,6 @@
 import org.eclipse.jgit.transport.PreUploadHook;
 
 import java.util.List;
-import java.util.Set;
 
 
 /** Starts global state with standard dependencies. */
@@ -185,31 +188,31 @@
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(AddReviewerSender.Factory.class);
+    factory(AddKeySender.Factory.class);
+    factory(BatchUpdate.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
+    factory(ChangeJson.Factory.class);
     factory(CreateChangeSender.Factory.class);
     factory(GroupDetailFactory.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
     factory(GroupMembers.Factory.class);
+    factory(EmailMerge.Factory.class);
     factory(MergedSender.Factory.class);
     factory(MergeFailSender.Factory.class);
     factory(MergeUtil.Factory.class);
     factory(PatchScriptFactory.Factory.class);
-    factory(PerformCreateGroup.Factory.class);
-    factory(PerformRenameGroup.Factory.class);
     factory(PluginUser.Factory.class);
     factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
     factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
-    factory(PerformCreateProject.Factory.class);
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class)
         .toProvider(AccountVisibilityProvider.class)
         .in(SINGLETON);
-    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
-        .annotatedWith(ProjectOwnerGroups.class)
-        .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
+    factory(ProjectOwnerGroupsProvider.Factory.class);
+    bind(RepositoryConfig.class);
 
     bind(AuthBackend.class).to(UniversalAuthBackend.class).in(SINGLETON);
     DynamicSet.setOf(binder(), AuthBackend.class);
@@ -221,16 +224,15 @@
     bind(ToolsCatalog.class);
     bind(EventFactory.class);
     bind(TransferConfig.class);
+    bind(GitwebConfig.class);
 
     bind(GcConfig.class);
+    bind(ChangeCleanupConfig.class);
 
     bind(ApprovalsUtil.class);
-    bind(ChangeMergeQueue.class).in(SINGLETON);
-    bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
 
     bind(RuntimeInstance.class)
-        .toProvider(VelocityRuntimeProvider.class)
-        .in(SINGLETON);
+        .toProvider(VelocityRuntimeProvider.class);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
     bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
@@ -261,6 +263,7 @@
     DynamicSet.setOf(binder(), PreUploadHook.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
+    DynamicSet.setOf(binder(), GarbageCollectorListener.class);
     DynamicSet.setOf(binder(), HeadUpdatedListener.class);
     DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class);
@@ -280,9 +283,12 @@
     DynamicSet.setOf(binder(), MessageOfTheDay.class);
     DynamicMap.mapOf(binder(), DownloadScheme.class);
     DynamicMap.mapOf(binder(), DownloadCommand.class);
+    DynamicMap.mapOf(binder(), CloneCommand.class);
+    DynamicMap.mapOf(binder(), ExternalIncludedIn.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
     DynamicSet.setOf(binder(), FileWebLink.class);
+    DynamicSet.setOf(binder(), FileHistoryWebLink.class);
     DynamicSet.setOf(binder(), DiffWebLink.class);
     DynamicSet.setOf(binder(), ProjectWebLink.class);
     DynamicSet.setOf(binder(), BranchWebLink.class);
@@ -297,9 +303,9 @@
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
+    factory(SubmoduleSectionParser.Factory.class);
 
     bind(AccountManager.class);
-    bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
 
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index cdfad8d..5d88ec0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -16,10 +16,9 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.servlet.RequestScoped;
@@ -34,8 +33,5 @@
 
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ProjectControl.Factory.class).in(SINGLETON);
-
-    factory(SubmoduleOp.Factory.class);
-    factory(MergeOp.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index 89331fd..281600d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -29,12 +29,12 @@
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 /** Creates {@link GerritServerConfig}. */
 public class GerritServerConfigModule extends AbstractModule {
-  public static String getSecureStoreClassName(final File sitePath) {
+  public static String getSecureStoreClassName(final Path sitePath) {
     if (sitePath != null) {
       return getSecureStoreFromGerritConfig(sitePath);
     }
@@ -43,17 +43,17 @@
     return nullToDefault(secureStoreProperty);
   }
 
-  private static String getSecureStoreFromGerritConfig(final File sitePath) {
+  private static String getSecureStoreFromGerritConfig(final Path sitePath) {
     AbstractModule m = new AbstractModule() {
       @Override
       protected void configure() {
-        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
         bind(SitePaths.class);
       }
     };
     Injector injector = Guice.createInjector(m);
     SitePaths site = injector.getInstance(SitePaths.class);
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config.toFile(), FS.DETECTED);
     if (!cfg.getFile().exists()) {
       return DefaultSecureStore.class.getName();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index aa699c5..8be42db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
@@ -44,19 +44,18 @@
 
   @Override
   public Config get() {
-    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    FileBasedConfig cfg =
+        new FileBasedConfig(site.gerrit_config.toFile(), FS.DETECTED);
 
     if (!cfg.getFile().exists()) {
-      log.info("No " + site.gerrit_config.getAbsolutePath()
+      log.info("No " + site.gerrit_config.toAbsolutePath()
           + "; assuming defaults");
       return new GerritConfig(cfg, secureStore);
     }
 
     try {
       cfg.load();
-    } catch (IOException e) {
-      throw new ProvisionException(e.getMessage(), e);
-    } catch (ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException e) {
       throw new ProvisionException(e.getMessage(), e);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
index e915427..d120275 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetPreferences.java
@@ -41,14 +41,11 @@
   @Override
   public PreferenceInfo apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
-    Repository git = gitMgr.openRepository(allUsersName);
-    try {
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
       VersionedAccountPreferences p =
           VersionedAccountPreferences.forDefault();
       p.load(git);
       return new PreferenceInfo(null, p, git);
-    } finally {
-      git.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
new file mode 100644
index 0000000..9eca842
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -0,0 +1,398 @@
+// Copyright (C) 2015 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.server.config;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GitwebType;
+import com.google.gerrit.extensions.config.CloneCommand;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.EnableSignedPush;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.GetArchive;
+import com.google.gerrit.server.change.Submit;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class GetServerInfo implements RestReadView<ConfigResource> {
+  private static final String URL_ALIAS = "urlAlias";
+  private static final String KEY_MATCH = "match";
+  private static final String KEY_TOKEN = "token";
+
+  private final Config config;
+  private final AuthConfig authConfig;
+  private final Realm realm;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
+  private final DynamicMap<DownloadCommand> downloadCommands;
+  private final DynamicMap<CloneCommand> cloneCommands;
+  private final GetArchive.AllowedFormats archiveFormats;
+  private final AllProjectsName allProjectsName;
+  private final AllUsersName allUsersName;
+  private final String anonymousCowardName;
+  private final GitwebConfig gitwebConfig;
+  private final DynamicItem<AvatarProvider> avatar;
+  private final boolean enableSignedPush;
+
+  @Inject
+  public GetServerInfo(
+      @GerritServerConfig Config config,
+      AuthConfig authConfig,
+      Realm realm,
+      DynamicMap<DownloadScheme> downloadSchemes,
+      DynamicMap<DownloadCommand> downloadCommands,
+      DynamicMap<CloneCommand> cloneCommands,
+      GetArchive.AllowedFormats archiveFormats,
+      AllProjectsName allProjectsName,
+      AllUsersName allUsersName,
+      @AnonymousCowardName String anonymousCowardName,
+      GitwebConfig gitwebConfig,
+      DynamicItem<AvatarProvider> avatar,
+      @EnableSignedPush boolean enableSignedPush) {
+    this.config = config;
+    this.authConfig = authConfig;
+    this.realm = realm;
+    this.downloadSchemes = downloadSchemes;
+    this.downloadCommands = downloadCommands;
+    this.cloneCommands = cloneCommands;
+    this.archiveFormats = archiveFormats;
+    this.allProjectsName = allProjectsName;
+    this.allUsersName = allUsersName;
+    this.anonymousCowardName = anonymousCowardName;
+    this.gitwebConfig = gitwebConfig;
+    this.avatar = avatar;
+    this.enableSignedPush = enableSignedPush;
+  }
+
+  @Override
+  public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
+    ServerInfo info = new ServerInfo();
+    info.auth = getAuthInfo(authConfig, realm);
+    info.change = getChangeInfo(config);
+    info.download =
+        getDownloadInfo(downloadSchemes, downloadCommands, cloneCommands,
+            archiveFormats);
+    info.gerrit = getGerritInfo(config, allProjectsName, allUsersName);
+    info.gitweb = getGitwebInfo(gitwebConfig);
+    info.plugin = getPluginInfo();
+    info.sshd = getSshdInfo(config);
+    info.suggest = getSuggestInfo(config);
+
+    Map<String, String> urlAliases = getUrlAliasesInfo(config);
+    info.urlAliases = !urlAliases.isEmpty() ? urlAliases : null;
+
+    info.user = getUserInfo(anonymousCowardName);
+    info.receive = getReceiveInfo();
+    return info;
+  }
+
+  private AuthInfo getAuthInfo(AuthConfig cfg, Realm realm) {
+    AuthInfo info = new AuthInfo();
+    info.authType = cfg.getAuthType();
+    info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
+    info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
+    info.switchAccountUrl = cfg.getSwitchAccountUrl();
+
+    switch (info.authType) {
+      case LDAP:
+      case LDAP_BIND:
+        info.registerUrl = cfg.getRegisterUrl();
+        info.registerText = cfg.getRegisterText();
+        info.editFullNameUrl = cfg.getEditFullNameUrl();
+        info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
+        break;
+
+      case CUSTOM_EXTENSION:
+        info.registerUrl = cfg.getRegisterUrl();
+        info.registerText = cfg.getRegisterText();
+        info.editFullNameUrl = cfg.getEditFullNameUrl();
+        info.httpPasswordUrl = cfg.getHttpPasswordUrl();
+        break;
+
+      case HTTP:
+      case HTTP_LDAP:
+        info.loginUrl = cfg.getLoginUrl();
+        info.loginText = cfg.getLoginText();
+        break;
+
+      case CLIENT_SSL_CERT_LDAP:
+      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+      case OAUTH:
+      case OPENID:
+      case OPENID_SSO:
+        break;
+    }
+    return info;
+  }
+
+  private ChangeConfigInfo getChangeInfo(Config cfg) {
+    ChangeConfigInfo info = new ChangeConfigInfo();
+    info.allowDrafts = toBoolean(cfg.getBoolean("change", "allowDrafts", true));
+    info.largeChange = cfg.getInt("change", "largeChange", 500);
+    info.replyTooltip =
+        Optional.fromNullable(cfg.getString("change", null, "replyTooltip"))
+            .or("Reply and score") + " (Shortcut: a)";
+    info.replyLabel =
+        Optional.fromNullable(cfg.getString("change", null, "replyLabel"))
+            .or("Reply") + "\u2026";
+    info.updateDelay = (int) ConfigUtil.getTimeUnit(
+        cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS);
+    info.submitWholeTopic = Submit.wholeTopicEnabled(cfg);
+    return info;
+  }
+
+  private DownloadInfo getDownloadInfo(
+      DynamicMap<DownloadScheme> downloadSchemes,
+      DynamicMap<DownloadCommand> downloadCommands,
+      DynamicMap<CloneCommand> cloneCommands,
+      GetArchive.AllowedFormats archiveFormats) {
+    DownloadInfo info = new DownloadInfo();
+    info.schemes = new HashMap<>();
+    for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
+      DownloadScheme scheme = e.getProvider().get();
+      if (scheme.isEnabled() && scheme.getUrl("${project}") != null) {
+        info.schemes.put(e.getExportName(),
+            getDownloadSchemeInfo(scheme, downloadCommands, cloneCommands));
+      }
+    }
+    info.archives = Lists.newArrayList(Iterables.transform(
+        archiveFormats.getAllowed(),
+        new Function<ArchiveFormat, String>() {
+          @Override
+          public String apply(ArchiveFormat in) {
+            return in.getShortName();
+          }
+        }));
+    return info;
+  }
+
+  private DownloadSchemeInfo getDownloadSchemeInfo(DownloadScheme scheme,
+      DynamicMap<DownloadCommand> downloadCommands,
+      DynamicMap<CloneCommand> cloneCommands) {
+    DownloadSchemeInfo info = new DownloadSchemeInfo();
+    info.url = scheme.getUrl("${project}");
+    info.isAuthRequired = toBoolean(scheme.isAuthRequired());
+    info.isAuthSupported = toBoolean(scheme.isAuthSupported());
+
+    info.commands = new HashMap<>();
+    for (DynamicMap.Entry<DownloadCommand> e : downloadCommands) {
+      String commandName = e.getExportName();
+      DownloadCommand command = e.getProvider().get();
+      String c = command.getCommand(scheme, "${project}", "${ref}");
+      if (c != null) {
+        info.commands.put(commandName, c);
+      }
+    }
+
+    info.cloneCommands = new HashMap<>();
+    for (DynamicMap.Entry<CloneCommand> e : cloneCommands) {
+      String commandName = e.getExportName();
+      CloneCommand command = e.getProvider().get();
+      String c = command.getCommand(scheme, "${project-path}/${project-base-name}");
+      if (c != null) {
+        c = c.replaceAll("\\$\\{project-path\\}/\\$\\{project-base-name\\}",
+            "\\$\\{project\\}");
+        info.cloneCommands.put(commandName, c);
+      }
+    }
+
+    return info;
+  }
+
+  private GerritInfo getGerritInfo(Config cfg, AllProjectsName allProjectsName,
+      AllUsersName allUsersName) {
+    GerritInfo info = new GerritInfo();
+    info.allProjects = allProjectsName.get();
+    info.allUsers = allUsersName.get();
+    info.reportBugUrl = cfg.getString("gerrit", null, "reportBugUrl");
+    info.reportBugText = cfg.getString("gerrit", null, "reportBugText");
+    info.docUrl = getDocUrl(cfg);
+    info.editGpgKeys = toBoolean(enableSignedPush
+        && cfg.getBoolean("gerrit", null, "editGpgKeys", true));
+    return info;
+  }
+
+  private String getDocUrl(Config cfg) {
+    String docUrl = cfg.getString("gerrit", null, "docUrl");
+    if (Strings.isNullOrEmpty(docUrl)) {
+      return null;
+    }
+    return CharMatcher.is('/').trimTrailingFrom(docUrl) + '/';
+  }
+
+  private GitwebInfo getGitwebInfo(GitwebConfig cfg) {
+    if (cfg.getUrl() == null || cfg.getGitwebType() == null) {
+      return null;
+    }
+
+    GitwebInfo info = new GitwebInfo();
+    info.url = cfg.getUrl();
+    info.type = cfg.getGitwebType();
+    return info;
+  }
+
+  private PluginConfigInfo getPluginInfo() {
+    PluginConfigInfo info = new PluginConfigInfo();
+    info.hasAvatars = toBoolean(avatar.get() != null);
+    return info;
+  }
+
+  private Map<String, String> getUrlAliasesInfo(Config cfg) {
+    Map<String, String> urlAliases = new HashMap<>();
+    for (String subsection : cfg.getSubsections(URL_ALIAS)) {
+      urlAliases.put(cfg.getString(URL_ALIAS, subsection, KEY_MATCH),
+         cfg.getString(URL_ALIAS, subsection, KEY_TOKEN));
+    }
+    return urlAliases;
+  }
+
+  private SshdInfo getSshdInfo(Config cfg) {
+    String[] addr = cfg.getStringList("sshd", null, "listenAddress");
+    if (addr.length == 1 && isOff(addr[0])) {
+      return null;
+    }
+    return new SshdInfo();
+  }
+
+  private static boolean isOff(String listenHostname) {
+    return "off".equalsIgnoreCase(listenHostname)
+        || "none".equalsIgnoreCase(listenHostname)
+        || "no".equalsIgnoreCase(listenHostname);
+  }
+
+  private SuggestInfo getSuggestInfo(Config cfg) {
+    SuggestInfo info = new SuggestInfo();
+    info.from = cfg.getInt("suggest", "from", 0);
+    return info;
+  }
+
+  private UserConfigInfo getUserInfo(String anonymousCowardName) {
+    UserConfigInfo info = new UserConfigInfo();
+    info.anonymousCowardName = anonymousCowardName;
+    return info;
+  }
+
+  private ReceiveInfo getReceiveInfo() {
+    ReceiveInfo info = new ReceiveInfo();
+    info.enableSignedPush = enableSignedPush;
+    return info;
+  }
+
+  private static Boolean toBoolean(boolean v) {
+    return v ? v : null;
+  }
+
+  public static class ServerInfo {
+    public AuthInfo auth;
+    public ChangeConfigInfo change;
+    public DownloadInfo download;
+    public GerritInfo gerrit;
+    public GitwebInfo gitweb;
+    public PluginConfigInfo plugin;
+    public SshdInfo sshd;
+    public SuggestInfo suggest;
+    public Map<String, String> urlAliases;
+    public UserConfigInfo user;
+    public ReceiveInfo receive;
+  }
+
+  public static class AuthInfo {
+    public AuthType authType;
+    public Boolean useContributorAgreements;
+    public List<Account.FieldName> editableAccountFields;
+    public String loginUrl;
+    public String loginText;
+    public String switchAccountUrl;
+    public String registerUrl;
+    public String registerText;
+    public String editFullNameUrl;
+    public String httpPasswordUrl;
+    public Boolean isGitBasicAuth;
+  }
+
+  public static class ChangeConfigInfo {
+    public Boolean allowDrafts;
+    public int largeChange;
+    public String replyLabel;
+    public String replyTooltip;
+    public int updateDelay;
+    public Boolean submitWholeTopic;
+  }
+
+  public static class DownloadInfo {
+    public Map<String, DownloadSchemeInfo> schemes;
+    public List<String> archives;
+  }
+
+  public static class DownloadSchemeInfo {
+    public String url;
+    public Boolean isAuthRequired;
+    public Boolean isAuthSupported;
+    public Map<String, String> commands;
+    public Map<String, String> cloneCommands;
+  }
+
+  public static class GerritInfo {
+    public String allProjects;
+    public String allUsers;
+    public String docUrl;
+    public String reportBugUrl;
+    public String reportBugText;
+    public Boolean editGpgKeys;
+  }
+
+  public static class GitwebInfo {
+    public String url;
+    public GitwebType type;
+  }
+
+  public static class PluginConfigInfo {
+    public Boolean hasAvatars;
+  }
+
+  public static class SshdInfo {
+  }
+
+  public static class SuggestInfo {
+    public int from;
+  }
+
+  public static class UserConfigInfo {
+    public String anonymousCowardName;
+  }
+
+  public static class ReceiveInfo {
+    public Boolean enableSignedPush;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
index 9aa8590..722eb91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
@@ -24,7 +24,6 @@
 import org.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
 import org.kohsuke.args4j.Option;
 
-import java.io.File;
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
@@ -33,17 +32,19 @@
 import java.lang.management.ThreadMXBean;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
 public class GetSummary implements RestReadView<ConfigResource> {
 
   private final WorkQueue workQueue;
-  private final File sitePath;
+  private final Path sitePath;
 
   @Option(name = "--gc", usage = "perform Java GC before retrieving memory stats")
   private boolean gc;
@@ -62,7 +63,7 @@
   }
 
   @Inject
-  public GetSummary(WorkQueue workQueue, @SitePath File sitePath) {
+  public GetSummary(WorkQueue workQueue, @SitePath Path sitePath) {
     this.workQueue = workQueue;
     this.sitePath = sitePath;
   }
@@ -88,12 +89,17 @@
   private TaskSummaryInfo getTaskSummary() {
     Collection<Task<?>> pending = workQueue.getTasks();
     int tasksTotal = pending.size();
-    int tasksRunning = 0, tasksReady = 0, tasksSleeping = 0;
+    int tasksRunning = 0;
+    int tasksReady = 0;
+    int tasksSleeping = 0;
     for (Task<?> task : pending) {
       switch (task.getState()) {
-        case RUNNING: tasksRunning++; break;
-        case READY: tasksReady++; break;
-        case SLEEPING: tasksSleeping++; break;
+        case RUNNING: tasksRunning++;
+          break;
+        case READY: tasksReady++;
+          break;
+        case SLEEPING: tasksSleeping++;
+          break;
         case CANCELLED:
         case DONE:
         case OTHER:
@@ -184,9 +190,11 @@
     try {
       jvmSummary.host = InetAddress.getLocalHost().getHostName();
     } catch (UnknownHostException e) {
+      // Ignored
     }
 
-    jvmSummary.currentWorkingDirectory = path(new File(".").getAbsoluteFile().getParentFile());
+    jvmSummary.currentWorkingDirectory =
+        path(Paths.get(".").toAbsolutePath().getParent());
     jvmSummary.site = path(sitePath);
     return jvmSummary;
   }
@@ -210,11 +218,11 @@
     return String.format("%1$6.2f%2$s", value, suffix).trim();
   }
 
-  private static String path(File file) {
+  private static String path(Path path) {
     try {
-      return file.getCanonicalPath();
+      return path.toRealPath().normalize().toString();
     } catch (IOException err) {
-      return file.getAbsolutePath();
+      return path.toAbsolutePath().normalize().toString();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 5e2f71f..5dd2784 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -30,7 +30,8 @@
       @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx) {
-    super(gb, config, threadContext, serverCtx, "receive", null, "allowGroup");
+    super(gb, threadContext, serverCtx, config.getStringList("receive", null,
+        "allowGroup"));
 
     // If no group was set, default to "registered users"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index 79cfd88..545f48b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -29,7 +29,8 @@
       @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
       ServerRequestContext serverCtx) {
-    super(gb, config, threadContext, serverCtx, "upload", null, "allowGroup");
+    super(gb, threadContext, serverCtx, config.getStringList("upload", null,
+        "allowGroup"));
 
     // If no group was set, default to "registered users" and "anonymous"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java
new file mode 100644
index 0000000..4a58f73
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebCgiConfig.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2015 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.server.config;
+
+import static java.nio.file.Files.isExecutable;
+import static java.nio.file.Files.isRegularFile;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@Singleton
+public class GitwebCgiConfig {
+  private static final Logger log =
+      LoggerFactory.getLogger(GitwebCgiConfig.class);
+
+  public GitwebCgiConfig disabled() {
+    return new GitwebCgiConfig();
+  }
+
+  private final Path cgi;
+  private final Path css;
+  private final Path js;
+  private final Path logoPng;
+
+  @Inject
+  GitwebCgiConfig(SitePaths sitePaths, @GerritServerConfig Config cfg) {
+    if (GitwebConfig.isDisabled(cfg)) {
+      cgi = null;
+      css = null;
+      js = null;
+      logoPng = null;
+      return;
+    }
+
+    String cfgCgi = cfg.getString("gitweb", null, "cgi");
+    Path pkgCgi = Paths.get("/usr/lib/cgi-bin/gitweb.cgi");
+    String[] resourcePaths = {"/usr/share/gitweb/static", "/usr/share/gitweb",
+        "/var/www/static", "/var/www"};
+    Path cgi;
+
+    if (cfgCgi != null) {
+      // Use the CGI script configured by the administrator, failing if it
+      // cannot be used as specified.
+      //
+      cgi = sitePaths.resolve(cfgCgi);
+      if (!isRegularFile(cgi)) {
+        throw new IllegalStateException("Cannot find gitweb.cgi: " + cgi);
+      }
+      if (!isExecutable(cgi)) {
+        throw new IllegalStateException("Cannot execute gitweb.cgi: " + cgi);
+      }
+
+      if (!cgi.equals(pkgCgi)) {
+        // Assume the administrator pointed us to the distribution,
+        // which also has the corresponding CSS and logo file.
+        //
+        String absPath = cgi.getParent().toAbsolutePath().toString();
+        resourcePaths = new String[] {absPath + "/static", absPath};
+      }
+
+    } else if (cfg.getString("gitweb", null, "url") != null) {
+      // Use an externally managed gitweb instance, and not an internal one.
+      //
+      cgi = null;
+      resourcePaths = new String[] {};
+
+    } else if (isRegularFile(pkgCgi) && isExecutable(pkgCgi)) {
+      // Use the OS packaged CGI.
+      //
+      log.debug("Assuming gitweb at " + pkgCgi);
+      cgi = pkgCgi;
+
+    } else {
+      log.warn("gitweb not installed (no " + pkgCgi + " found)");
+      cgi = null;
+      resourcePaths = new String[] {};
+    }
+
+    Path css = null;
+    Path js = null;
+    Path logo = null;
+    for (String path : resourcePaths) {
+      Path dir = Paths.get(path);
+      css = dir.resolve("gitweb.css");
+      js = dir.resolve("gitweb.js");
+      logo = dir.resolve("git-logo.png");
+      if (isRegularFile(css) && isRegularFile(logo)) {
+        break;
+      }
+    }
+
+    this.cgi = cgi;
+    this.css = css;
+    this.js = js;
+    this.logoPng = logo;
+  }
+
+  private GitwebCgiConfig() {
+    this.cgi = null;
+    this.css = null;
+    this.js = null;
+    this.logoPng = null;
+  }
+
+  /** @return local path to the CGI executable; null if we shouldn't execute. */
+  public Path getGitwebCgi() {
+    return cgi;
+  }
+
+  /** @return local path of the {@code gitweb.css} matching the CGI. */
+  public Path getGitwebCss() {
+    return css;
+  }
+
+  /** @return local path of the {@code gitweb.js} for the CGI. */
+  public Path getGitwebJs() {
+    return js;
+  }
+
+  /** @return local path of the {@code git-logo.png} for the CGI. */
+  public Path getGitLogoPng() {
+    return logoPng;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java
new file mode 100644
index 0000000..10b9fb0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -0,0 +1,229 @@
+// 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.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.base.Strings.nullToEmpty;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GitwebType;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GitwebConfig {
+  private static final Logger log = LoggerFactory.getLogger(GitwebConfig.class);
+
+  public static boolean isDisabled(Config cfg) {
+    return isEmptyString(cfg, "gitweb", null, "url")
+        || isEmptyString(cfg, "gitweb", null, "cgi");
+  }
+
+  private static boolean isEmptyString(Config cfg, String section,
+      String subsection, String name) {
+    // This is currently the only way to check for the empty string in a JGit
+    // config. Fun!
+    String[] values = cfg.getStringList(section, subsection, name);
+    return values.length > 0 && Strings.isNullOrEmpty(values[0]);
+  }
+
+  /**
+   * Get a GitwebType based on the given config.
+   *
+   * @param cfg Gerrit config.
+   * @return GitwebType from the given name, else null if not found.
+   */
+  public static GitwebType typeFromConfig(Config cfg) {
+    GitwebType defaultType = defaultType(cfg.getString("gitweb", null, "type"));
+    if (defaultType == null) {
+      return null;
+    }
+    GitwebType type = new GitwebType();
+
+    type.setLinkName(firstNonNull(
+        cfg.getString("gitweb", null, "linkname"),
+        defaultType.getLinkName()));
+    type.setBranch(firstNonNull(
+        cfg.getString("gitweb", null, "branch"),
+        defaultType.getBranch()));
+    type.setProject(firstNonNull(
+        cfg.getString("gitweb", null, "project"),
+        defaultType.getProject()));
+    type.setRevision(firstNonNull(
+        cfg.getString("gitweb", null, "revision"),
+        defaultType.getRevision()));
+    type.setRootTree(firstNonNull(
+        cfg.getString("gitweb", null, "roottree"),
+        defaultType.getRootTree()));
+    type.setFile(firstNonNull(
+        cfg.getString("gitweb", null, "file"),
+        defaultType.getFile()));
+    type.setFileHistory(firstNonNull(
+        cfg.getString("gitweb", null, "filehistory"),
+        defaultType.getFileHistory()));
+    type.setLinkDrafts(
+        cfg.getBoolean("gitweb", null, "linkdrafts",
+            defaultType.getLinkDrafts()));
+    type.setUrlEncode(
+        cfg.getBoolean("gitweb", null, "urlencode",
+            defaultType.getUrlEncode()));
+    String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
+    if (pathSeparator != null) {
+      if (pathSeparator.length() == 1) {
+        char c = pathSeparator.charAt(0);
+        if (isValidPathSeparator(c)) {
+          type.setPathSeparator(
+              firstNonNull(c, defaultType.getPathSeparator()));
+        } else {
+          log.warn("Invalid gitweb.pathSeparator: " + c);
+        }
+      } else {
+        log.warn(
+            "gitweb.pathSeparator is not a single character: " + pathSeparator);
+      }
+    }
+    return type;
+  }
+
+  private static GitwebType defaultType(String typeName) {
+    GitwebType type = new GitwebType();
+    switch (nullToEmpty(typeName)) {
+      case "":
+      case "gitweb":
+        type.setLinkName("gitweb");
+        type.setProject("?p=${project}.git;a=summary");
+        type.setRevision("?p=${project}.git;a=commit;h=${commit}");
+        type.setBranch("?p=${project}.git;a=shortlog;h=${branch}");
+        type.setRootTree("?p=${project}.git;a=tree;hb=${commit}");
+        type.setFile("?p=${project}.git;hb=${commit};f=${file}");
+        type.setFileHistory(
+            "?p=${project}.git;a=history;hb=${branch};f=${file}");
+        break;
+      case "cgit":
+        type.setLinkName("cgit");
+        type.setProject("${project}.git/summary");
+        type.setRevision("${project}.git/commit/?id=${commit}");
+        type.setBranch("${project}.git/log/?h=${branch}");
+        type.setRootTree("${project}.git/tree/?h=${commit}");
+        type.setFile("${project}.git/tree/${file}?h=${commit}");
+        type.setFileHistory("${project}.git/log/${file}?h=${branch}");
+        break;
+      case "custom":
+        // For a custom type with no explicit link name, just reuse "gitweb".
+        type.setLinkName("gitweb");
+        type.setProject("");
+        type.setRevision("");
+        type.setBranch("");
+        type.setRootTree("");
+        type.setFile("");
+        type.setFileHistory("");
+        break;
+      default:
+        return null;
+    }
+    return type;
+  }
+
+  private final String url;
+  private final GitwebType type;
+
+  @Inject
+  GitwebConfig(GitwebCgiConfig cgiConfig, @GerritServerConfig Config cfg) {
+    if (isDisabled(cfg)) {
+      type = null;
+      url = null;
+      return;
+    }
+
+    String cfgUrl = cfg.getString("gitweb", null, "url");
+    GitwebType type = typeFromConfig(cfg);
+    if (type == null) {
+      this.type = null;
+      url = null;
+      return;
+    } else if (cgiConfig.getGitwebCgi() == null) {
+      // Use an externally managed gitweb instance, and not an internal one.
+      url = cfgUrl;
+    } else {
+      url = firstNonNull(cfgUrl, "gitweb");
+    }
+
+    if (isNullOrEmpty(type.getBranch())) {
+      log.warn("No Pattern specified for gitweb.branch, disabling.");
+      this.type = null;
+    } else if (isNullOrEmpty(type.getProject())) {
+      log.warn("No Pattern specified for gitweb.project, disabling.");
+      this.type = null;
+    } else if (isNullOrEmpty(type.getRevision())) {
+      log.warn("No Pattern specified for gitweb.revision, disabling.");
+      this.type = null;
+    } else if (isNullOrEmpty(type.getRootTree())) {
+      log.warn("No Pattern specified for gitweb.roottree, disabling.");
+      this.type = null;
+    } else if (isNullOrEmpty(type.getFile())) {
+      log.warn("No Pattern specified for gitweb.file, disabling.");
+      this.type = null;
+    } else if (isNullOrEmpty(type.getFileHistory())) {
+      log.warn("No Pattern specified for gitweb.filehistory, disabling.");
+      this.type = null;
+    } else {
+      this.type = type;
+    }
+  }
+
+  /** @return GitwebType for gitweb viewer. */
+  public GitwebType getGitwebType() {
+    return type;
+  }
+
+  /**
+   * @return URL of the entry point into gitweb. This URL may be relative to our
+   *         context if gitweb is hosted by ourselves; or absolute if its hosted
+   *         elsewhere; or null if gitweb has not been configured.
+   */
+  public String getUrl() {
+    return url;
+  }
+
+  /**
+   * Determines if a given character can be used unencoded in an URL as a
+   * replacement for the path separator '/'.
+   *
+   * Reasoning: http://www.ietf.org/rfc/rfc1738.txt § 2.2:
+   *
+   * ... only alphanumerics, the special characters "$-_.+!*'(),", and
+   *  reserved characters used for their reserved purposes may be used
+   * unencoded within a URL.
+   *
+   * The following characters might occur in file names, however:
+   *
+   * alphanumeric characters,
+   *
+   * "$-_.+!',"
+   */
+  static boolean isValidPathSeparator(char c) {
+    switch (c) {
+      case '*':
+      case '(':
+      case ')':
+        return true;
+      default:
+        return false;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 5c3ec39..9e55a7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -25,7 +25,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -40,13 +39,10 @@
 
   @Inject
   protected GroupSetProvider(GroupBackend groupBackend,
-      @GerritServerConfig Config config,
       ThreadLocalRequestContext threadContext,
-      ServerRequestContext serverCtx, String section,
-      String subsection, String name) {
+      ServerRequestContext serverCtx, String[] groupNames) {
     RequestContext ctx = threadContext.setContext(serverCtx);
     try {
-      String[] groupNames = config.getStringList(section, subsection, name);
       ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
       for (String n : groupNames) {
         GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
index df6d86b..382c8fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
@@ -14,14 +14,15 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
 import static com.google.gerrit.server.config.CacheResource.cacheNameOf;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Joiner;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheStats;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -36,12 +37,12 @@
 import java.util.Map;
 import java.util.TreeMap;
 
-@RequiresCapability(GlobalCapability.VIEW_CACHES)
+@RequiresAnyCapability({VIEW_CACHES, MAINTAIN_SERVER})
 public class ListCaches implements RestReadView<ConfigResource> {
   private final DynamicMap<Cache<?, ?>> cacheMap;
 
   public static enum OutputFormat {
-    LIST, TEXT_LIST;
+    LIST, TEXT_LIST
   }
 
   @Option(name = "--format", usage = "output format")
@@ -77,7 +78,7 @@
         return BinaryResult.create(Joiner.on('\n').join(cacheNames))
             .base64()
             .setContentType("text/plain")
-            .setCharacterEncoding(UTF_8.name());
+            .setCharacterEncoding(UTF_8);
       } else {
         return cacheNames;
       }
@@ -85,7 +86,7 @@
   }
 
   public enum CacheType {
-    MEM, DISK;
+    MEM, DISK
   }
 
   public static class CacheInfo {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
index b01ae2f1..c6ba605 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
@@ -80,7 +80,7 @@
   }
 
   private static boolean isPluginNameSane(String pluginName) {
-    return CharMatcher.JAVA_LETTER_OR_DIGIT
+    return CharMatcher.javaLetterOrDigit()
         .or(CharMatcher.is('-'))
         .matchesAllOf(pluginName);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
deleted file mode 100644
index a7360de..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.config;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.git.ReloadSubmitQueueOp;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-
-import java.util.concurrent.ScheduledFuture;
-
-/** Configuration for a master node in a cluster of servers. */
-public class MasterNodeStartup extends LifecycleModule {
-  @Override
-  public void configure() {
-    listener().to(Lifecycle.class);
-  }
-
-  @Singleton
-  static class Lifecycle implements LifecycleListener {
-    private static final int INITIAL_DELAY_S = 15;
-
-    private final ReloadSubmitQueueOp submit;
-    private final long delay;
-    private volatile ScheduledFuture<?> handle;
-
-    @Inject
-    Lifecycle(ReloadSubmitQueueOp submit,
-        @GerritServerConfig Config config) {
-      this.submit = submit;
-      this.delay = ConfigUtil.getTimeUnit(config,
-          "changeMerge", null, "checkFrequency",
-          SECONDS.convert(5, MINUTES), SECONDS);
-    }
-
-    @Override
-    public void start() {
-      if (delay > 0) {
-        handle = submit.startWithFixedDelay(INITIAL_DELAY_S, delay, SECONDS);
-      } else {
-        handle = submit.start(INITIAL_DELAY_S, SECONDS);
-      }
-    }
-
-    @Override
-    public void stop() {
-      ScheduledFuture<?> f = handle;
-      if (f != null) {
-        handle = null;
-        f.cancel(true);
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 64848ba..e909f17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -35,7 +35,9 @@
     delete(TASK_KIND).to(DeleteTask.class);
     child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
     get(CONFIG_KIND, "version").to(GetVersion.class);
+    get(CONFIG_KIND, "info").to(GetServerInfo.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
+    put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 76f5323..a2aa937 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -35,6 +35,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Map;
 
 @Singleton
@@ -61,7 +62,7 @@
     this.projectStateFactory = projectStateFactory;
     this.pluginConfigs = Maps.newHashMap();
 
-    this.cfgSnapshot = FileSnapshot.save(site.gerrit_config);
+    this.cfgSnapshot = FileSnapshot.save(site.gerrit_config.toFile());
     this.cfg = cfgProvider.get();
   }
 
@@ -103,8 +104,9 @@
    * @return the plugin configuration from the 'gerrit.config' file
    */
   public PluginConfig getFromGerritConfig(String pluginName, boolean refresh) {
-    if (refresh && cfgSnapshot.isModified(site.gerrit_config)) {
-      cfgSnapshot = FileSnapshot.save(site.gerrit_config);
+    File configFile = site.gerrit_config.toFile();
+    if (refresh && cfgSnapshot.isModified(configFile)) {
+      cfgSnapshot = FileSnapshot.save(configFile);
       cfg = cfgProvider.get();
     }
     return new PluginConfig(pluginName, cfg);
@@ -237,33 +239,33 @@
 
   /**
    * Returns the configuration for the specified plugin that is stored in the
-   * plugin configuration file 'etc/<plugin-name>.config'.
+   * plugin configuration file '{@code etc/<plugin-name>.config}'.
    *
    * The plugin configuration is only loaded once and is then cached.
    *
    * @param pluginName the name of the plugin for which the configuration should
    *        be returned
-   * @return the plugin configuration from the 'etc/<plugin-name>.config' file
+   * @return the plugin configuration from the
+   *         '{@code etc/<plugin-name>.config}' file
    */
   public synchronized Config getGlobalPluginConfig(String pluginName) {
     if (pluginConfigs.containsKey(pluginName)) {
       return pluginConfigs.get(pluginName);
     }
 
-    File pluginConfigFile = new File(site.etc_dir, pluginName + ".config");
-    FileBasedConfig cfg = new FileBasedConfig(pluginConfigFile, FS.DETECTED);
+    Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
+    FileBasedConfig cfg =
+        new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
     pluginConfigs.put(pluginName, cfg);
     if (!cfg.getFile().exists()) {
-      log.info("No " + pluginConfigFile.getAbsolutePath() + "; assuming defaults");
+      log.info("No " + pluginConfigFile.toAbsolutePath() + "; assuming defaults");
       return cfg;
     }
 
     try {
       cfg.load();
-    } catch (IOException e) {
-      log.warn("Failed to load " + pluginConfigFile.getAbsolutePath(), e);
-    } catch (ConfigInvalidException e) {
-      log.warn("Failed to load " + pluginConfigFile.getAbsolutePath(), e);
+    } catch (IOException | ConfigInvalidException e) {
+      log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
     }
 
     return cfg;
@@ -271,15 +273,15 @@
 
   /**
    * Returns the configuration for the specified plugin that is stored in the
-   * '<plugin-name>.config' file in the 'refs/meta/config' branch of the
-   * specified project.
+   * '{@code <plugin-name>.config}' file in the 'refs/meta/config' branch of
+   * the specified project.
    *
    * @param projectName the name of the project for which the plugin
    *        configuration should be returned
    * @param pluginName the name of the plugin for which the configuration should
    *        be returned
-   * @return the plugin configuration from the '<plugin-name>.config' file of
-   *         the specified project
+   * @return the plugin configuration from the '{@code <plugin-name>.config}'
+   *         file of the specified project
    * @throws NoSuchProjectException thrown if the specified project does not
    *         exist
    */
@@ -290,15 +292,15 @@
 
   /**
    * Returns the configuration for the specified plugin that is stored in the
-   * '<plugin-name>.config' file in the 'refs/meta/config' branch of the
-   * specified project.
+   * '{@code <plugin-name>.config}' file in the 'refs/meta/config' branch of
+   * the specified project.
    *
    * @param projectState the project for which the plugin configuration should
    *        be returned
    * @param pluginName the name of the plugin for which the configuration should
    *        be returned
-   * @return the plugin configuration from the '<plugin-name>.config' file of
-   *         the specified project
+   * @return the plugin configuration from the '{@code <plugin-name>.config}'
+   *         file of the specified project
    */
   public Config getProjectPluginConfig(ProjectState projectState,
       String pluginName) {
@@ -307,10 +309,10 @@
 
   /**
    * Returns the configuration for the specified plugin that is stored in the
-   * '<plugin-name>.config' file in the 'refs/meta/config' branch of the
-   * specified project. Parameters which are not set in the
-   * '<plugin-name>.config' of this project are inherited from the parent
-   * project's '<plugin-name>.config' files.
+   * '{@code <plugin-name>.config}' file in the 'refs/meta/config' branch of
+   * the specified project. Parameters which are not set in the
+   * '{@code <plugin-name>.config}' of this project are inherited from the
+   * parent project's '{@code <plugin-name>.config}' files.
    *
    * E.g.: child project: [mySection "mySubsection"] myKey = childValue
    *
@@ -324,9 +326,9 @@
    *        configuration should be returned
    * @param pluginName the name of the plugin for which the configuration should
    *        be returned
-   * @return the plugin configuration from the '<plugin-name>.config' file of
-   *         the specified project with inheriting non-set parameters from the
-   *         parent projects
+   * @return the plugin configuration from the '{@code <plugin-name>.config}'
+   *         file of the specified project with inheriting non-set parameters
+   *         from the parent projects
    * @throws NoSuchProjectException thrown if the specified project does not
    *         exist
    */
@@ -337,10 +339,10 @@
 
   /**
    * Returns the configuration for the specified plugin that is stored in the
-   * '<plugin-name>.config' file in the 'refs/meta/config' branch of the
-   * specified project. Parameters which are not set in the
-   * '<plugin-name>.config' of this project are inherited from the parent
-   * project's '<plugin-name>.config' files.
+   * '{@code <plugin-name>.config}' file in the 'refs/meta/config' branch of
+   * the specified project. Parameters which are not set in the
+   * '{@code <plugin-name>.config}' of this project are inherited from the
+   * parent project's '{@code <plugin-name>.config}' files.
    *
    * E.g.: child project: [mySection "mySubsection"] myKey = childValue
    *
@@ -354,9 +356,9 @@
    *        be returned
    * @param pluginName the name of the plugin for which the configuration should
    *        be returned
-   * @return the plugin configuration from the '<plugin-name>.config' file of
-   *         the specified project with inheriting non-set parameters from the
-   *         parent projects
+   * @return the plugin configuration from the '{@code <plugin-name>.config}'
+   *         file of the specified project with inheriting non-set parameters
+   *         from the parent projects
    */
   public Config getProjectPluginConfigWithInheritance(ProjectState projectState,
       String pluginName) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
index 7302ea1..5178210 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
 import com.google.common.cache.Cache;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -31,7 +32,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-@RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@RequiresAnyCapability({FLUSH_CACHES, MAINTAIN_SERVER})
 @Singleton
 public class PostCaches implements RestModifyView<ConfigResource, Input> {
   public static class Input {
@@ -52,7 +53,7 @@
   }
 
   public static enum Operation {
-    FLUSH_ALL, FLUSH;
+    FLUSH_ALL, FLUSH
   }
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
@@ -66,9 +67,8 @@
   }
 
   @Override
-  public Object apply(ConfigResource rsrc, Input input) throws AuthException,
-      ResourceNotFoundException, BadRequestException,
-      UnprocessableEntityException {
+  public Object apply(ConfigResource rsrc, Input input)
+      throws AuthException, BadRequestException, UnprocessableEntityException {
     if (input == null || input.operation == null) {
       throw new BadRequestException("operation must be specified");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 5943801..b907866 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
@@ -202,6 +203,46 @@
   }
 
   /**
+   * Called before the project config is updated. To modify the value before the
+   * project config is updated, override this method and return the modified
+   * value. Default implementation returns the same value.
+   *
+   * @param configValue the original configValue that was entered.
+   * @return the modified configValue.
+   */
+  public ConfigValue preUpdate(ConfigValue configValue) {
+    return configValue;
+  }
+
+  /**
+   * Called after reading the project config value. To modify the value before
+   * returning it to the client, override this method and return the modified
+   * value. Default implementation returns the same value.
+   *
+   * @param project the project.
+   * @param value the actual value of the config entry (computed out of the
+   *        configured value, the inherited value and the default value).
+   * @return the modified value.
+   */
+  public String onRead(ProjectState project, String value) {
+    return value;
+  }
+
+  /**
+   * Called after reading the project config value of type ARRAY. To modify the
+   * values before returning it to the client, override this method and return
+   * the modified values. Default implementation returns the same values.
+   *
+   * @param project the project.
+   * @param values the actual values of the config entry (computed out of the
+   *        configured value, the inherited value and the default value).
+   * @return the modified values.
+   */
+  public List<String> onRead(ProjectState project, List<String> values) {
+    return values;
+  }
+
+  /**
    * Called after a project config is updated.
    *
    * @param project project name.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 0189de3..23615d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,30 +14,37 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.util.ServerRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.Config;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 /**
  * Provider of the group(s) which should become owners of a newly created
- * project. Currently only supports {@code ownerGroup} declarations in the
- * {@code "*"} repository, like so:
+ * project. The only matching patterns supported are exact match or wildcard
+ * matching which can be specified by ending the name with a {@code *}.
  *
  * <pre>
  * [repository &quot;*&quot;]
  *     ownerGroup = Registered Users
  *     ownerGroup = Administrators
+ * [repository &quot;project/*&quot;]
+ *     ownerGroup = Administrators
  * </pre>
  */
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
-  @Inject
+
+  public interface Factory {
+    public ProjectOwnerGroupsProvider create(Project.NameKey project);
+  }
+
+  @AssistedInject
   public ProjectOwnerGroupsProvider(GroupBackend gb,
-      @GerritServerConfig final Config config,
-      ThreadLocalRequestContext context,
-      ServerRequestContext serverCtx) {
-    super(gb, config, context, serverCtx, "repository", "*", "ownerGroup");
+      ThreadLocalRequestContext context, ServerRequestContext serverCtx,
+      RepositoryConfig repositoryCfg,
+      @Assisted Project.NameKey project) {
+    super(gb, context, serverCtx, repositoryCfg.getOwnerGroups(project));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
new file mode 100644
index 0000000..c34b8a6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2014 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.server.config;
+
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class RepositoryConfig {
+
+  static final String SECTION_NAME = "repository";
+  static final String OWNER_GROUP_NAME = "ownerGroup";
+  static final String DEFAULT_SUBMIT_TYPE_NAME = "defaultSubmitType";
+
+  private final Config cfg;
+
+  @Inject
+  public RepositoryConfig(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+  }
+
+  public SubmitType getDefaultSubmitType(Project.NameKey project) {
+    return cfg.getEnum(SECTION_NAME, findSubSection(project.get()),
+        DEFAULT_SUBMIT_TYPE_NAME, SubmitType.MERGE_IF_NECESSARY);
+  }
+
+  public String[] getOwnerGroups(Project.NameKey project) {
+    return cfg.getStringList(SECTION_NAME, findSubSection(project.get()),
+        OWNER_GROUP_NAME);
+  }
+
+  /**
+   * Find the subSection to get repository configuration from.
+   * <p>
+   * SubSection can use the * pattern so if project name matches more than one
+   * section, return the more precise one. E.g if the following subSections are
+   * defined:
+   *
+   * <pre>
+   * [repository "somePath/*"]
+   *   name = value
+   * [repository "somePath/somePath/*"]
+   *   name = value
+   * </pre>
+   *
+   * and this method is called with "somePath/somePath/someProject" as project
+   * name, it will return the subSection "somePath/somePath/*"
+   *
+   * @param project Name of the project
+   * @return the name of the subSection, null if none is found
+   */
+  private String findSubSection(String project) {
+    String subSectionFound = null;
+    for (String subSection : cfg.getSubsections(SECTION_NAME)) {
+      if (isMatch(subSection, project)
+          && (subSectionFound == null || subSectionFound.length() < subSection
+              .length())) {
+        subSectionFound = subSection;
+      }
+    }
+    return subSectionFound;
+  }
+
+  private boolean isMatch(String subSection, String project) {
+    return project.equals(subSection)
+        || (subSection.endsWith("*") && project.startsWith(subSection
+            .substring(0, subSection.length() - 1)));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
index 4ada7b1..c2c002e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
@@ -37,6 +37,7 @@
     this.cleanup = cleanup;
   }
 
+  @SuppressWarnings("resource")
   @Override
   public ReviewDb get() {
     if (db == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
index d19f063..a2835a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -28,6 +28,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.text.MessageFormat;
+import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
 public class ScheduleConfig {
@@ -38,6 +39,11 @@
   private static final String KEY_INTERVAL = "interval";
   private static final String KEY_STARTTIME = "startTime";
 
+  private final Config rc;
+  private final String section;
+  private final String subsection;
+  private final String keyInterval;
+  private final String keyStartTime;
   private final long initialDelay;
   private final long interval;
 
@@ -62,6 +68,11 @@
   @VisibleForTesting
   ScheduleConfig(Config rc, String section, String subsection,
       String keyInterval, String keyStartTime, DateTime now) {
+    this.rc = rc;
+    this.section = section;
+    this.subsection = subsection;
+    this.keyInterval = keyInterval;
+    this.keyStartTime = keyStartTime;
     this.interval = interval(rc, section, subsection, keyInterval);
     if (interval > 0) {
       this.initialDelay = initialDelay(rc, section, subsection, keyStartTime, now,
@@ -122,7 +133,8 @@
           startTime.hourOfDay().set(firstStartTime.getHourOfDay());
           startTime.minuteOfHour().set(firstStartTime.getMinuteOfHour());
         } catch (IllegalArgumentException e1) {
-          formatter = DateTimeFormat.forPattern("E HH:mm");
+          formatter = DateTimeFormat.forPattern("E HH:mm")
+              .withLocale(Locale.US);
           LocalDateTime firstStartDateTime = formatter.parseLocalDateTime(start);
           startTime.dayOfWeek().set(firstStartDateTime.getDayOfWeek());
           startTime.hourOfDay().set(firstStartDateTime.getHourOfDay());
@@ -150,4 +162,31 @@
     return delay;
   }
 
+  @Override
+  public String toString() {
+    StringBuilder b = new StringBuilder();
+    b.append(formatValue(keyInterval));
+    b.append(", ");
+    b.append(formatValue(keyStartTime));
+    return b.toString();
+  }
+
+  private String formatValue(String key) {
+    StringBuilder b = new StringBuilder();
+    b.append(section);
+    if (subsection != null) {
+      b.append(".");
+      b.append(subsection);
+    }
+    b.append(".");
+    b.append(key);
+    String value = rc.getString(section, subsection, key);
+    if (value != null) {
+      b.append(" = ");
+      b.append(value);
+    } else {
+      b.append(": NA");
+    }
+    return b.toString();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index d97499c..519a4a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.account.VersionedAccountPreferences;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -32,11 +33,11 @@
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
 public class SetPreferences implements RestModifyView<ConfigResource, Input> {
-  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
 
   @Inject
-  SetPreferences(MetaDataUpdate.User metaDataUpdateFactory,
+  SetPreferences(Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       AllUsersName allUsersName) {
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
@@ -58,7 +59,7 @@
     }
 
     VersionedAccountPreferences p;
-    MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
+    MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
     try {
       p = VersionedAccountPreferences.forDefault();
       p.load(md);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index fbff7c4..e41fb30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 
 /** Important paths within a {@link SitePath}. */
 @Singleton
@@ -28,88 +31,82 @@
   public static final String HEADER_FILENAME = "GerritSiteHeader.html";
   public static final String FOOTER_FILENAME = "GerritSiteFooter.html";
 
-  public final File site_path;
-  public final File bin_dir;
-  public final File etc_dir;
-  public final File lib_dir;
-  public final File tmp_dir;
-  public final File logs_dir;
-  public final File plugins_dir;
-  public final File data_dir;
-  public final File mail_dir;
-  public final File hooks_dir;
-  public final File static_dir;
-  public final File themes_dir;
-  public final File index_dir;
+  public final Path site_path;
+  public final Path bin_dir;
+  public final Path etc_dir;
+  public final Path lib_dir;
+  public final Path tmp_dir;
+  public final Path logs_dir;
+  public final Path plugins_dir;
+  public final Path data_dir;
+  public final Path mail_dir;
+  public final Path hooks_dir;
+  public final Path static_dir;
+  public final Path themes_dir;
+  public final Path index_dir;
 
-  public final File gerrit_sh;
-  public final File gerrit_war;
+  public final Path gerrit_sh;
+  public final Path gerrit_war;
 
-  public final File gerrit_config;
-  public final File secure_config;
-  public final File contact_information_pub;
+  public final Path gerrit_config;
+  public final Path secure_config;
 
-  public final File ssl_keystore;
-  public final File ssh_key;
-  public final File ssh_rsa;
-  public final File ssh_dsa;
-  public final File peer_keys;
+  public final Path ssl_keystore;
+  public final Path ssh_key;
+  public final Path ssh_rsa;
+  public final Path ssh_dsa;
+  public final Path peer_keys;
 
-  public final File site_css;
-  public final File site_header;
-  public final File site_footer;
-  public final File site_gitweb;
+  public final Path site_css;
+  public final Path site_header;
+  public final Path site_footer;
+  public final Path site_gitweb;
 
   /** {@code true} if {@link #site_path} has not been initialized. */
   public final boolean isNew;
 
   @Inject
-  public SitePaths(final @SitePath File sitePath) throws FileNotFoundException {
+  public SitePaths(@SitePath Path sitePath) throws IOException {
     site_path = sitePath;
+    Path p = sitePath;
 
-    bin_dir = new File(site_path, "bin");
-    etc_dir = new File(site_path, "etc");
-    lib_dir = new File(site_path, "lib");
-    tmp_dir = new File(site_path, "tmp");
-    plugins_dir = new File(site_path, "plugins");
-    data_dir = new File(site_path, "data");
-    logs_dir = new File(site_path, "logs");
-    mail_dir = new File(etc_dir, "mail");
-    hooks_dir = new File(site_path, "hooks");
-    static_dir = new File(site_path, "static");
-    themes_dir = new File(site_path, "themes");
-    index_dir = new File(site_path, "index");
+    bin_dir = p.resolve("bin");
+    etc_dir = p.resolve("etc");
+    lib_dir = p.resolve("lib");
+    tmp_dir = p.resolve("tmp");
+    plugins_dir = p.resolve("plugins");
+    data_dir = p.resolve("data");
+    logs_dir = p.resolve("logs");
+    mail_dir = etc_dir.resolve("mail");
+    hooks_dir = p.resolve("hooks");
+    static_dir = p.resolve("static");
+    themes_dir = p.resolve("themes");
+    index_dir = p.resolve("index");
 
-    gerrit_sh = new File(bin_dir, "gerrit.sh");
-    gerrit_war = new File(bin_dir, "gerrit.war");
+    gerrit_sh = bin_dir.resolve("gerrit.sh");
+    gerrit_war = bin_dir.resolve("gerrit.war");
 
-    gerrit_config = new File(etc_dir, "gerrit.config");
-    secure_config = new File(etc_dir, "secure.config");
-    contact_information_pub = new File(etc_dir, "contact_information.pub");
+    gerrit_config = etc_dir.resolve("gerrit.config");
+    secure_config = etc_dir.resolve("secure.config");
 
-    ssl_keystore = new File(etc_dir, "keystore");
-    ssh_key = new File(etc_dir, "ssh_host_key");
-    ssh_rsa = new File(etc_dir, "ssh_host_rsa_key");
-    ssh_dsa = new File(etc_dir, "ssh_host_dsa_key");
-    peer_keys = new File(etc_dir, "peer_keys");
+    ssl_keystore = etc_dir.resolve("keystore");
+    ssh_key = etc_dir.resolve("ssh_host_key");
+    ssh_rsa = etc_dir.resolve("ssh_host_rsa_key");
+    ssh_dsa = etc_dir.resolve("ssh_host_dsa_key");
+    peer_keys = etc_dir.resolve("peer_keys");
 
-    site_css = new File(etc_dir, CSS_FILENAME);
-    site_header = new File(etc_dir, HEADER_FILENAME);
-    site_footer = new File(etc_dir, FOOTER_FILENAME);
-    site_gitweb = new File(etc_dir, "gitweb_config.perl");
+    site_css = etc_dir.resolve(CSS_FILENAME);
+    site_header = etc_dir.resolve(HEADER_FILENAME);
+    site_footer = etc_dir.resolve(FOOTER_FILENAME);
+    site_gitweb = etc_dir.resolve("gitweb_config.perl");
 
-    if (site_path.exists()) {
-      final String[] contents = site_path.list();
-      if (contents != null) {
-        isNew = contents.length == 0;
-      } else if (site_path.isDirectory()) {
-        throw new FileNotFoundException("Cannot access " + site_path);
-      } else {
-        throw new FileNotFoundException("Not a directory: " + site_path);
-      }
-    } else {
+    boolean isNew;
+    try (DirectoryStream<Path> files = Files.newDirectoryStream(site_path)) {
+      isNew = Iterables.isEmpty(files);
+    } catch (NoSuchFileException e) {
       isNew = true;
     }
+    this.isNew = isNew;
   }
 
   /**
@@ -120,16 +117,13 @@
    * @param path the path string to resolve. May be null.
    * @return the resolved path; null if {@code path} was null or empty.
    */
-  public File resolve(final String path) {
+  public Path resolve(String path) {
     if (path != null && !path.isEmpty()) {
-      File loc = new File(path);
-      if (!loc.isAbsolute()) {
-        loc = new File(site_path, path);
-      }
+      Path loc = site_path.resolve(path).normalize();
       try {
-        return loc.getCanonicalFile();
+        return loc.toRealPath();
       } catch (IOException e) {
-        return loc.getAbsoluteFile();
+        return loc.toAbsolutePath();
       }
     }
     return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java
deleted file mode 100644
index 0ab21f9..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.contact;
-
-import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ContactInformation;
-
-public interface ContactStore {
-  boolean isEnabled();
-
-  void store(Account account, ContactInformation info)
-      throws ContactInformationStoreException;
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreConnection.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreConnection.java
deleted file mode 100644
index 24c9e66..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreConnection.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2011 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.server.contact;
-
-import java.io.IOException;
-import java.net.URL;
-
-/** Single connection to a {@link ContactStore}. */
-public interface ContactStoreConnection {
-  public static interface Factory {
-    /**
-     * Open a new connection to a {@link ContactStore}.
-     *
-     * @param url contact store URL.
-     * @return a new connection to the store.
-     *
-     * @throws IOException the URL couldn't be opened.
-     */
-    ContactStoreConnection open(URL url) throws IOException;
-  }
-
-  /**
-   * Store a blob of contact data in the store.
-   *
-   * @param body protocol-specific body data.
-   *
-   * @throws IOException an error occurred storing the contact data.
-   */
-  public void store(byte[] body) throws IOException;
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
deleted file mode 100644
index 6b195de..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.contact;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
-
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.openpgp.PGPPublicKey;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.util.StringUtils;
-
-import java.io.File;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.security.Security;
-
-/** Creates the {@link ContactStore} based on the configuration. */
-public class ContactStoreModule extends AbstractModule {
-  @Override
-  protected void configure() {
-  }
-
-  @Nullable
-  @Provides
-  public ContactStore provideContactStore(@GerritServerConfig final Config config,
-      final SitePaths site, final SchemaFactory<ReviewDb> schema,
-      final ContactStoreConnection.Factory connFactory) {
-    final String url = config.getString("contactstore", null, "url");
-    if (StringUtils.isEmptyOrNull(url)) {
-      return new NoContactStore();
-    }
-
-    if (!havePGP()) {
-      throw new ProvisionException("BouncyCastle PGP not installed; "
-          + " needed to encrypt contact information");
-    }
-
-    final URL storeUrl;
-    try {
-      storeUrl = new URL(url);
-    } catch (MalformedURLException e) {
-      throw new ProvisionException("Invalid contactstore.url: " + url, e);
-    }
-
-    final String storeAPPSEC = config.getString("contactstore", null, "appsec");
-    final File pubkey = site.contact_information_pub;
-    if (!pubkey.exists()) {
-      throw new ProvisionException("PGP public key file \""
-          + pubkey.getAbsolutePath() + "\" not found");
-    }
-    return new EncryptedContactStore(storeUrl, storeAPPSEC, pubkey, schema,
-        connFactory);
-  }
-
-  private static boolean havePGP() {
-    try {
-      Class.forName(PGPPublicKey.class.getName());
-      addBouncyCastleProvider();
-      return true;
-    } catch (NoClassDefFoundError | ClassNotFoundException | SecurityException
-        | NoSuchMethodException | InstantiationException
-        | IllegalAccessException | InvocationTargetException
-        | ClassCastException noBouncyCastle) {
-      return false;
-    }
-  }
-
-  private static void addBouncyCastleProvider() throws ClassNotFoundException,
-          SecurityException, NoSuchMethodException, InstantiationException,
-          IllegalAccessException, InvocationTargetException {
-    Class<?> clazz = Class.forName(BouncyCastleProvider.class.getName());
-    Constructor<?> constructor = clazz.getConstructor();
-    Security.addProvider((java.security.Provider) constructor.newInstance());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
deleted file mode 100644
index 4048748..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
+++ /dev/null
@@ -1,282 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.contact;
-
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.UrlEncoded;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-import org.bouncycastle.bcpg.ArmoredOutputStream;
-import org.bouncycastle.openpgp.PGPCompressedData;
-import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
-import org.bouncycastle.openpgp.PGPEncryptedData;
-import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
-import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPLiteralData;
-import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
-import org.bouncycastle.openpgp.PGPPublicKey;
-import org.bouncycastle.openpgp.PGPPublicKeyRing;
-import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
-import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRingCollection;
-import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder;
-import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.sql.Timestamp;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.TimeZone;
-
-/** Encrypts {@link ContactInformation} instances and saves them. */
-@Singleton
-class EncryptedContactStore implements ContactStore {
-  private static final Logger log =
-      LoggerFactory.getLogger(EncryptedContactStore.class);
-  private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
-
-  private final SchemaFactory<ReviewDb> schema;
-  private final PGPPublicKey dest;
-  private final SecureRandom prng;
-  private final URL storeUrl;
-  private final String storeAPPSEC;
-  private final ContactStoreConnection.Factory connFactory;
-
-  EncryptedContactStore(final URL storeUrl, final String storeAPPSEC,
-      final File pubKey, final SchemaFactory<ReviewDb> schema,
-      final ContactStoreConnection.Factory connFactory) {
-    this.storeUrl = storeUrl;
-    this.storeAPPSEC = storeAPPSEC;
-    this.schema = schema;
-    this.dest = selectKey(readPubRing(pubKey));
-    this.connFactory = connFactory;
-
-    final String prngName = "SHA1PRNG";
-    try {
-      prng = SecureRandom.getInstance(prngName);
-    } catch (NoSuchAlgorithmException e) {
-      throw new ProvisionException("Cannot create " + prngName, e);
-    }
-
-    // Run a test encryption to verify the proper algorithms exist in
-    // our JVM and we are able to invoke them. This helps to identify
-    // a system configuration problem early at startup, rather than a
-    // lot later during runtime.
-    //
-    try {
-      encrypt("test", new Date(0), "test".getBytes("UTF-8"));
-    } catch (PGPException | IOException e) {
-      throw new ProvisionException("PGP encryption not available", e);
-    }
-  }
-
-  @Override
-  public boolean isEnabled() {
-    return true;
-  }
-
-  private static PGPPublicKeyRingCollection readPubRing(final File pub) {
-    try (InputStream fin = new FileInputStream(pub);
-        InputStream in = PGPUtil.getDecoderStream(fin)) {
-        return new BcPGPPublicKeyRingCollection(in);
-    } catch (IOException | PGPException e) {
-      throw new ProvisionException("Cannot read " + pub, e);
-    }
-  }
-
-  private static PGPPublicKey selectKey(final PGPPublicKeyRingCollection rings) {
-    for (final Iterator<?> ri = rings.getKeyRings(); ri.hasNext();) {
-      final PGPPublicKeyRing currRing = (PGPPublicKeyRing) ri.next();
-      for (final Iterator<?> ki = currRing.getPublicKeys(); ki.hasNext();) {
-        final PGPPublicKey k = (PGPPublicKey) ki.next();
-        if (k.isEncryptionKey()) {
-          return k;
-        }
-      }
-    }
-    return null;
-  }
-
-  @Override
-  public void store(final Account account, final ContactInformation info)
-      throws ContactInformationStoreException {
-    try {
-      final byte[] plainText = format(account, info).getBytes("UTF-8");
-      final String fileName = "account-" + account.getId();
-      final Date fileDate = account.getContactFiledOn();
-      final byte[] encText = encrypt(fileName, fileDate, plainText);
-      final String encStr = new String(encText, "UTF-8");
-
-      final Timestamp filedOn = account.getContactFiledOn();
-      final UrlEncoded u = new UrlEncoded();
-      if (storeAPPSEC != null) {
-        u.put("APPSEC", storeAPPSEC);
-      }
-      if (account.getPreferredEmail() != null) {
-        u.put("email", account.getPreferredEmail());
-      }
-      if (filedOn != null) {
-        u.put("filed", String.valueOf(filedOn.getTime() / 1000L));
-      }
-      u.put("account_id", String.valueOf(account.getId().get()));
-      u.put("data", encStr);
-      connFactory.open(storeUrl).store(u.toString().getBytes("UTF-8"));
-    } catch (IOException | PGPException e) {
-      log.error("Cannot store encrypted contact information", e);
-      throw new ContactInformationStoreException(e);
-    }
-  }
-
-  private final PGPEncryptedDataGenerator cpk() {
-    final BcPGPDataEncryptorBuilder builder =
-        new BcPGPDataEncryptorBuilder(PGPEncryptedData.CAST5)
-            .setSecureRandom(prng);
-    PGPEncryptedDataGenerator cpk =
-        new PGPEncryptedDataGenerator(builder, true);
-    final BcPublicKeyKeyEncryptionMethodGenerator methodGenerator =
-        new BcPublicKeyKeyEncryptionMethodGenerator(dest);
-    cpk.addMethod(methodGenerator);
-    return cpk;
-  }
-
-  private byte[] encrypt(final String name, final Date date,
-      final byte[] rawText) throws PGPException,
-      IOException {
-    final byte[] zText = compress(name, date, rawText);
-
-    final ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    final ArmoredOutputStream aout = new ArmoredOutputStream(buf);
-    final OutputStream cout = cpk().open(aout, zText.length);
-    cout.write(zText);
-    cout.close();
-    aout.close();
-
-    return buf.toByteArray();
-  }
-
-  private static byte[] compress(final String fileName, Date fileDate,
-      final byte[] plainText) throws IOException {
-    final ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    final PGPCompressedDataGenerator comdg;
-    final int len = plainText.length;
-    if (fileDate == null) {
-      fileDate = PGPLiteralData.NOW;
-    }
-
-    comdg = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
-    final OutputStream out =
-        new PGPLiteralDataGenerator().open(comdg.open(buf),
-            PGPLiteralData.BINARY, fileName, len, fileDate);
-    out.write(plainText);
-    out.close();
-    comdg.close();
-    return buf.toByteArray();
-  }
-
-  private String format(final Account account, final ContactInformation info)
-      throws ContactInformationStoreException {
-    Timestamp on = account.getContactFiledOn();
-    if (on == null) {
-      on = TimeUtil.nowTs();
-    }
-
-    final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-    df.setTimeZone(UTC);
-
-    final StringBuilder b = new StringBuilder();
-    field(b, "Account-Id", account.getId().toString());
-    field(b, "Date", df.format(on) + " " + UTC.getID());
-    field(b, "Full-Name", account.getFullName());
-    field(b, "Preferred-Email", account.getPreferredEmail());
-
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        for (final AccountExternalId e : db.accountExternalIds().byAccount(
-            account.getId())) {
-          final StringBuilder oistr = new StringBuilder();
-          if (e.getEmailAddress() != null && e.getEmailAddress().length() > 0) {
-            if (oistr.length() > 0) {
-              oistr.append(' ');
-            }
-            oistr.append(e.getEmailAddress());
-          }
-          if (e.isScheme(AccountExternalId.SCHEME_MAILTO)) {
-            if (oistr.length() > 0) {
-              oistr.append(' ');
-            }
-            oistr.append('<');
-            oistr.append(e.getExternalId());
-            oistr.append('>');
-          }
-          field(b, "Identity", oistr.toString());
-        }
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      throw new ContactInformationStoreException(e);
-    }
-
-    field(b, "Address", info.getAddress());
-    field(b, "Country", info.getCountry());
-    field(b, "Phone-Number", info.getPhoneNumber());
-    field(b, "Fax-Number", info.getFaxNumber());
-    return b.toString();
-  }
-
-  private static void field(final StringBuilder b, final String name,
-      String value) {
-    if (value == null) {
-      return;
-    }
-    value = value.trim();
-    if (value.length() == 0) {
-      return;
-    }
-
-    b.append(name);
-    b.append(':');
-    if (value.indexOf('\n') == -1) {
-      b.append(' ');
-      b.append(value);
-    } else {
-      value = value.replaceAll("\r\n", "\n");
-      value = value.replaceAll("\n", "\n\t");
-      b.append("\n\t");
-      b.append(value);
-    }
-    b.append('\n');
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java
deleted file mode 100644
index 471f6a2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2011 Google Inc. All Rights Reserved.
-
-package com.google.gerrit.server.contact;
-
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
-
-import org.eclipse.jgit.util.IO;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
-
-/** {@link ContactStoreConnection} with an underlying {@literal @HttpURLConnection}. */
-public class HttpContactStoreConnection implements ContactStoreConnection {
-  public static Module module() {
-    return new AbstractModule() {
-      @Override
-      protected void configure() {
-        install(new FactoryModuleBuilder()
-            .implement(ContactStoreConnection.class, HttpContactStoreConnection.class)
-            .build(ContactStoreConnection.Factory.class));
-      }
-    };
-  }
-
-  private final HttpURLConnection conn;
-
-  @Inject
-  HttpContactStoreConnection(@Assisted final URL url) throws IOException {
-    final URLConnection urlConn = url.openConnection();
-    if (!(urlConn instanceof HttpURLConnection)) {
-      throw new IllegalArgumentException("Non-HTTP URL not supported: " + urlConn);
-    }
-    conn = (HttpURLConnection) urlConn;
-  }
-
-  @Override
-  public void store(final byte[] body) throws IOException {
-    conn.setRequestMethod("POST");
-    conn.setRequestProperty("Content-Type",
-        "application/x-www-form-urlencoded; charset=UTF-8");
-    conn.setDoOutput(true);
-    conn.setFixedLengthStreamingMode(body.length);
-    try (OutputStream out = conn.getOutputStream()) {
-      out.write(body);
-    }
-    if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
-      throw new IOException("Connection failed: " + conn.getResponseCode());
-    }
-    final byte[] dst = new byte[2];
-    final InputStream in = conn.getInputStream();
-    try {
-      IO.readFully(in, dst, 0, 2);
-    } finally {
-      in.close();
-    }
-    if (dst[0] != 'O' || dst[1] != 'K') {
-      throw new IOException("Store failed: " + dst[0] + dst[1]);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
deleted file mode 100644
index 98001bb..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.contact;
-
-import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ContactInformation;
-
-public class NoContactStore implements ContactStore {
-  @Override
-  public boolean isEnabled() {
-    return false;
-  }
-
-  @Override
-  public void store(Account account, ContactInformation info)
-      throws ContactInformationStoreException {
-    throw new ContactInformationStoreException(new IllegalStateException(
-        "ContactStore not configured"));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index 03441e7..1d9c795 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.documentation;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.pegdown.Extensions.ALL;
 import static org.pegdown.Extensions.HARDWRAPS;
 import static org.pegdown.Extensions.SUPPRESS_ALL_HTML;
@@ -156,17 +157,10 @@
       throw new FileNotFoundException("Resource " + name);
     }
     file.set("file".equals(url.getProtocol()));
-    InputStream in = url.openStream();
-    try {
-      TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024);
-      try {
-        tmp.copy(in);
-        return new String(tmp.toByteArray(), "UTF-8");
-      } finally {
-        tmp.close();
-      }
-    } finally {
-      in.close();
+    try (InputStream in = url.openStream();
+        TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024)) {
+      tmp.copy(in);
+      return new String(tmp.toByteArray(), UTF_8);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index 188e95b..446013e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -94,9 +94,7 @@
         out.add(result);
       }
       return out;
-    } catch (IOException e) {
-      throw new DocQueryException(e);
-    } catch (ParseException e) {
+    } catch (IOException | ParseException e) {
       throw new DocQueryException(e);
     }
   }
@@ -110,19 +108,16 @@
       return null;
     }
 
-    ZipInputStream zip = new ZipInputStream(index);
-    try {
+    try (ZipInputStream zip = new ZipInputStream(index)) {
       ZipEntry entry;
       while ((entry = zip.getNextEntry()) != null) {
-        IndexOutput out = dir.createOutput(entry.getName(), null);
-        int count;
-        while ((count = zip.read(buffer)) != -1) {
-          out.writeBytes(buffer, count);
+        try (IndexOutput out = dir.createOutput(entry.getName(), null)) {
+          int count;
+          while ((count = zip.read(buffer)) != -1) {
+            out.writeBytes(buffer, count);
+          }
         }
-        out.close();
       }
-    } finally {
-      zip.close();
     }
     // We must NOT call dir.close() here, as DirectoryReader.open() expects an opened directory.
     return dir;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 8f73793..e44c810 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -29,10 +29,12 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -54,6 +56,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ThreeWayMerger;
@@ -86,13 +89,19 @@
   }
   private final TimeZone tz;
   private final GitRepositoryManager gitManager;
+  private final ChangeIndexer indexer;
+  private final Provider<ReviewDb> reviewDb;
   private final Provider<CurrentUser> currentUser;
 
   @Inject
   ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent,
       GitRepositoryManager gitManager,
+      ChangeIndexer indexer,
+      Provider<ReviewDb> reviewDb,
       Provider<CurrentUser> currentUser) {
     this.gitManager = gitManager;
+    this.indexer = indexer;
+    this.reviewDb = reviewDb;
     this.currentUser = currentUser;
     this.tz = gerritIdent.getTimeZone();
   }
@@ -114,7 +123,7 @@
       throw new AuthException("Authentication required");
     }
 
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId());
 
     try (Repository repo = gitManager.openRepository(change.getProject())) {
@@ -127,7 +136,10 @@
         ObjectId revision = ObjectId.fromString(ps.getRevision().get());
         String editRefName = RefNames.refsEdit(me.getAccountId(), change.getId(),
             ps.getId());
-        return update(repo, me, editRefName, rw, ObjectId.zeroId(), revision);
+        Result res =
+            update(repo, me, editRefName, rw, ObjectId.zeroId(), revision);
+        indexer.index(reviewDb.get(), change);
+        return res;
       }
     }
   }
@@ -150,7 +162,7 @@
     }
 
     Change change = edit.getChange();
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     String refName = RefNames.refsEdit(me.getAccountId(), change.getId(),
         current.getId());
     try (Repository repo = gitManager.openRepository(change.getProject());
@@ -226,7 +238,7 @@
       throw new UnchangedCommitMessageException();
     }
 
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     Project.NameKey project = edit.getChange().getProject();
     try (Repository repo = gitManager.openRepository(project);
         RevWalk rw = new RevWalk(repo);
@@ -312,7 +324,7 @@
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    IdentifiedUser me = currentUser.get().asIdentifiedUser();
     Project.NameKey project = edit.getChange().getProject();
     try (Repository repo = gitManager.openRepository(project);
         RevWalk rw = new RevWalk(repo);
@@ -390,7 +402,7 @@
   private static ObjectId writeNewTree(TreeOperation op, RevWalk rw,
       ObjectInserter ins, RevCommit prevEdit, ObjectReader reader,
       String fileName, @Nullable String newFile,
-      final @Nullable ObjectId content) throws IOException {
+      @Nullable final ObjectId content) throws IOException {
     DirCache newTree = readTree(reader, prevEdit);
     DirCacheEditor dce = newTree.editor();
     switch (op) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 29bda1d..6a933e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -17,24 +17,30 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.base.Optional;
-import com.google.common.collect.Iterables;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeKind;
+import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,7 +56,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
-import java.util.Map;
 
 /**
  * Utility functions to manipulate change edits.
@@ -62,21 +67,33 @@
 public class ChangeEditUtil {
   private final GitRepositoryManager gitManager;
   private final PatchSetInserter.Factory patchSetInserterFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ChangeIndexer indexer;
+  private final ProjectCache projectCache;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
+  private final ChangeKindCache changeKindCache;
+  private final BatchUpdate.Factory updateFactory;
 
   @Inject
   ChangeEditUtil(GitRepositoryManager gitManager,
       PatchSetInserter.Factory patchSetInserterFactory,
-      ChangeControl.GenericFactory changeControlFactory,
+      ProjectControl.GenericFactory projectControlFactory,
+      ChangeIndexer indexer,
+      ProjectCache projectCache,
       Provider<ReviewDb> db,
-      Provider<CurrentUser> user) {
+      Provider<CurrentUser> user,
+      ChangeKindCache changeKindCache,
+      BatchUpdate.Factory updateFactory) {
     this.gitManager = gitManager;
     this.patchSetInserterFactory = patchSetInserterFactory;
-    this.changeControlFactory = changeControlFactory;
+    this.projectControlFactory = projectControlFactory;
+    this.indexer = indexer;
+    this.projectCache = projectCache;
     this.db = db;
     this.user = user;
+    this.changeKindCache = changeKindCache;
+    this.updateFactory = updateFactory;
   }
 
   /**
@@ -94,7 +111,7 @@
     if (!currentUser.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    return byChange(change, (IdentifiedUser)currentUser);
+    return byChange(change, currentUser.asIdentifiedUser());
   }
 
   /**
@@ -109,16 +126,16 @@
   public Optional<ChangeEdit> byChange(Change change, IdentifiedUser user)
       throws IOException {
     try (Repository repo = gitManager.openRepository(change.getProject())) {
-      String editRefPrefix = RefNames.refsEditPrefix(user.getAccountId(), change.getId());
-      Map<String, Ref> refs = repo.getRefDatabase().getRefs(editRefPrefix);
-      if (refs.isEmpty()) {
+      int n = change.currentPatchSetId().get();
+      String[] refNames = new String[n];
+      for (int i = n; i > 0; i--) {
+        refNames[i-1] = RefNames.refsEdit(user.getAccountId(), change.getId(),
+            new PatchSet.Id(change.getId(), i));
+      }
+      Ref ref = repo.getRefDatabase().firstExactRef(refNames);
+      if (ref == null) {
         return Optional.absent();
       }
-
-      // TODO(davido): Rather than failing when we encounter the corrupt state
-      // where there is more than one ref, we could silently delete all but the
-      // current one.
-      Ref ref = Iterables.getOnlyElement(refs.values());
       try (RevWalk rw = new RevWalk(repo)) {
         RevCommit commit = rw.parseCommit(ref.getObjectId());
         PatchSet basePs = getBasePatchSet(change, ref);
@@ -132,13 +149,14 @@
    * its parent.
    *
    * @param edit change edit to publish
-   * @throws NoSuchChangeException
+   * @throws NoSuchProjectException
    * @throws IOException
    * @throws OrmException
-   * @throws ResourceConflictException
+   * @throws UpdateException
+   * @throws RestApiException
    */
-  public void publish(ChangeEdit edit) throws NoSuchChangeException,
-      IOException, OrmException, ResourceConflictException {
+  public void publish(ChangeEdit edit) throws NoSuchProjectException,
+      IOException, OrmException, RestApiException, UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         RevWalk rw = new RevWalk(repo);
@@ -149,14 +167,12 @@
             "only edit for current patch set can be published");
       }
 
-      try {
-        insertPatchSet(edit, change, repo, rw, basePatchSet,
-            squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
-        // TODO(davido): This should happen in the same BatchRefUpdate.
-        deleteRef(repo, edit);
-      } catch (InvalidChangeOperationException e) {
-        throw new ResourceConflictException(e.getMessage());
-      }
+      Change updatedChange =
+          insertPatchSet(edit, change, repo, rw, inserter, basePatchSet,
+              squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
+      // TODO(davido): This should happen in the same BatchRefUpdate.
+      deleteRef(repo, edit);
+      indexer.index(db.get(), updatedChange);
     }
   }
 
@@ -169,12 +185,10 @@
   public void delete(ChangeEdit edit)
       throws IOException {
     Change change = edit.getChange();
-    Repository repo = gitManager.openRepository(change.getProject());
-    try {
+    try (Repository repo = gitManager.openRepository(change.getProject())) {
       deleteRef(repo, edit);
-    } finally {
-      repo.close();
     }
+    indexer.index(db.get(), change);
   }
 
   private PatchSet getBasePatchSet(Change change, Ref ref)
@@ -184,8 +198,8 @@
       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
       String psId = ref.getName().substring(pos + 1);
       return db.get().patchSets().get(new PatchSet.Id(
-          change.getId(), Integer.valueOf(psId)));
-    } catch (OrmException e) {
+          change.getId(), Integer.parseInt(psId)));
+    } catch (OrmException | NumberFormatException e) {
       throw new IOException(e);
     }
   }
@@ -202,28 +216,48 @@
     return writeSquashedCommit(rw, inserter, parent, edit);
   }
 
-  private void insertPatchSet(ChangeEdit edit, Change change,
-      Repository repo, RevWalk rw, PatchSet basePatchSet, RevCommit squashed)
-      throws NoSuchChangeException, InvalidChangeOperationException,
-      OrmException, IOException {
-    PatchSet ps = new PatchSet(
-        ChangeUtil.nextPatchSetId(change.currentPatchSetId()));
-    ps.setRevision(new RevId(ObjectId.toString(squashed)));
-    ps.setUploader(edit.getUser().getAccountId());
-    ps.setCreatedOn(TimeUtil.nowTs());
+  private Change insertPatchSet(ChangeEdit edit, Change change,
+      Repository repo, RevWalk rw, ObjectInserter oi, PatchSet basePatchSet,
+      RevCommit squashed) throws NoSuchProjectException, RestApiException,
+      UpdateException, IOException {
+    RefControl ctl = projectControlFactory
+        .controlFor(change.getProject(), edit.getUser())
+        .controlForRef(change.getDest());
+    PatchSet.Id psId =
+        ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+    PatchSetInserter inserter =
+        patchSetInserterFactory.create(ctl, psId, squashed);
 
-    PatchSetInserter insr =
-        patchSetInserterFactory.create(repo, rw,
-            changeControlFactory.controlFor(change, edit.getUser()),
-            squashed);
-    insr.setPatchSet(ps)
+    inserter.setUploader(edit.getUser().getAccountId());
+
+    StringBuilder message = new StringBuilder("Patch Set ")
+      .append(inserter.getPatchSetId().get())
+      .append(": ");
+
+    ProjectState project = projectCache.get(change.getDest().getParentKey());
+    // Previously checked that the base patch set is the current patch set.
+    ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
+    ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed);
+    if (kind == ChangeKind.NO_CODE_CHANGE) {
+      message.append("Commit message was updated.");
+    } else {
+      message.append("Published edit on patch set ")
+        .append(basePatchSet.getPatchSetId())
+        .append(".");
+    }
+
+    try (BatchUpdate bu = updateFactory.create(
+        db.get(), change.getProject(), ctl.getUser(),
+        TimeUtil.nowTs())) {
+      bu.setRepository(repo, rw, oi);
+      bu.addOp(change.getId(), inserter
         .setDraft(change.getStatus() == Status.DRAFT ||
             basePatchSet.isDraft())
-        .setMessage(
-            String.format("Patch Set %d: Published edit on patch set %d",
-                ps.getPatchSetId(),
-                basePatchSet.getPatchSetId()))
-        .insert();
+        .setMessage(message.toString()));
+      bu.execute();
+    }
+
+    return inserter.getChange();
   }
 
   private static void deleteRef(Repository repo, ChangeEdit edit)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
index 9a5ad82..f083b7a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.events;
 
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.data.ChangeAttribute;
 
 public abstract class ChangeEvent extends RefEvent {
@@ -34,7 +33,7 @@
 
   @Override
   public String getRefName() {
-    return R_HEADS + change.branch;
+    return RefNames.fullName(change.branch);
   }
 
   public Change.Key getChangeKey() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index f0c0bc1..d52f831 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.server.events;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Function;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -27,9 +31,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -57,56 +59,59 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 @Singleton
 public class EventFactory {
   private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
+
   private final AccountCache accountCache;
   private final Provider<String> urlProvider;
   private final PatchListCache patchListCache;
-  private final SchemaFactory<ReviewDb> schema;
   private final PatchSetInfoFactory psInfoFactory;
   private final PersonIdent myIdent;
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeKindCache changeKindCache;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       PatchSetInfoFactory psif,
-      PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
+      PatchListCache patchListCache,
       @GerritPersonIdent PersonIdent myIdent,
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
-      ChangeKindCache changeKindCache) {
+      ChangeKindCache changeKindCache,
+      Provider<InternalChangeQuery> queryProvider) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.patchListCache = patchListCache;
-    this.schema = schema;
     this.psInfoFactory = psif;
     this.myIdent = myIdent;
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
     this.changeKindCache = changeKindCache;
+    this.queryProvider = queryProvider;
   }
 
   /**
@@ -116,7 +121,7 @@
    * @param change
    * @return object suitable for serialization to JSON
    */
-  public ChangeAttribute asChangeAttribute(final Change change) {
+  public ChangeAttribute asChangeAttribute(ReviewDb db, Change change) {
     ChangeAttribute a = new ChangeAttribute();
     a.project = change.getProject().get();
     a.branch = change.getDest().getShortName();
@@ -125,8 +130,7 @@
     a.number = change.getId().toString();
     a.subject = change.getSubject();
     try {
-      a.commitMessage =
-          changeDataFactory.create(db.get(), change).commitMessage();
+      a.commitMessage = changeDataFactory.create(db, change).commitMessage();
     } catch (Exception e) {
       log.error("Error while getting full commit message for"
           + " change " + a.number);
@@ -146,12 +150,13 @@
    * @param refName
    * @return object suitable for serialization to JSON
    */
-  public RefUpdateAttribute asRefUpdateAttribute(final ObjectId oldId, final ObjectId newId, final Branch.NameKey refName) {
+  public RefUpdateAttribute asRefUpdateAttribute(ObjectId oldId, ObjectId newId,
+      Branch.NameKey refName) {
     RefUpdateAttribute ru = new RefUpdateAttribute();
     ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
     ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
     ru.project = refName.getParentKey().get();
-    ru.refName = refName.getShortName();
+    ru.refName = refName.get();
     return ru;
   }
 
@@ -173,10 +178,10 @@
    * @param a
    * @param notes
    */
-  public void addAllReviewers(ChangeAttribute a, ChangeNotes notes)
+  public void addAllReviewers(ReviewDb db, ChangeAttribute a, ChangeNotes notes)
       throws OrmException {
     Collection<Account.Id> reviewers =
-        approvalsUtil.getReviewers(db.get(), notes).values();
+        approvalsUtil.getReviewers(db, notes).values();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
       for (Account.Id id : reviewers) {
@@ -226,44 +231,17 @@
     }
   }
 
-  public void addDependencies(ChangeAttribute ca, Change change) {
+  public void addDependencies(RevWalk rw, ChangeAttribute ca, Change change,
+      PatchSet currentPs) {
+    if (change == null || currentPs == null) {
+      return;
+    }
     ca.dependsOn = new ArrayList<>();
     ca.neededBy = new ArrayList<>();
     try {
-      final ReviewDb db = schema.open();
-      try {
-        final PatchSet.Id psId = change.currentPatchSetId();
-        for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(psId)) {
-          for (PatchSet p :
-              db.patchSets().byRevision(a.getAncestorRevision())) {
-            Change c = db.changes().get(p.getId().getParentKey());
-            ca.dependsOn.add(newDependsOn(c, p));
-          }
-        }
-
-        final PatchSet ps = db.patchSets().get(psId);
-        if (ps == null) {
-          log.error("Error while generating the list of descendants for"
-              + " PatchSet " + psId + ": Cannot find PatchSet entry in"
-              + " database.");
-        } else {
-          final RevId revId = ps.getRevision();
-          for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(revId)) {
-            final PatchSet p = db.patchSets().get(a.getPatchSet());
-            if (p == null) {
-              log.error("Error while generating the list of descendants for"
-                  + " revision " + revId.get() + ": Cannot find PatchSet entry in"
-                  + " database for " + a.getPatchSet());
-              continue;
-            }
-            final Change c = db.changes().get(p.getId().getParentKey());
-            ca.neededBy.add(newNeededBy(c, p));
-          }
-        }
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
+      addDependsOn(rw, ca, change, currentPs);
+      addNeededBy(rw, ca, change, currentPs);
+    } catch (OrmException | IOException e) {
       // Squash DB exceptions and leave dependency lists partially filled.
     }
     // Remove empty lists so a confusing label won't be displayed in the output.
@@ -275,6 +253,67 @@
     }
   }
 
+  private void addDependsOn(RevWalk rw, ChangeAttribute ca, Change change,
+      PatchSet currentPs) throws OrmException, IOException {
+    RevCommit commit =
+        rw.parseCommit(ObjectId.fromString(currentPs.getRevision().get()));
+    final List<String> parentNames = new ArrayList<>(commit.getParentCount());
+    for (RevCommit p : commit.getParents()) {
+      parentNames.add(p.name());
+    }
+
+    // Find changes in this project having a patch set matching any parent of
+    // this patch set's revision.
+    for (ChangeData cd : queryProvider.get().byProjectCommits(
+        change.getProject(), parentNames)) {
+      for (PatchSet ps : cd.patchSets()) {
+        for (String p : parentNames) {
+          if (!ps.getRevision().get().equals(p)) {
+            continue;
+          }
+          ca.dependsOn.add(newDependsOn(checkNotNull(cd.change()), ps));
+        }
+      }
+    }
+    // Sort by original parent order.
+    Collections.sort(ca.dependsOn, Ordering.natural().onResultOf(
+        new Function<DependencyAttribute, Integer>() {
+          @Override
+          public Integer apply(DependencyAttribute d) {
+            for (int i = 0; i < parentNames.size(); i++) {
+              if (parentNames.get(i).equals(d.revision)) {
+                return i;
+              }
+            }
+            return parentNames.size() + 1;
+          }
+        }));
+  }
+
+  private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change,
+      PatchSet currentPs) throws OrmException, IOException {
+    if (currentPs.getGroups() == null || currentPs.getGroups().isEmpty()) {
+      return;
+    }
+    String rev = currentPs.getRevision().get();
+    // Find changes in the same related group as this patch set, having a patch
+    // set whose parent matches this patch set's revision.
+    for (ChangeData cd : queryProvider.get().byProjectGroups(
+        change.getProject(), currentPs.getGroups())) {
+      patchSets: for (PatchSet ps : cd.patchSets()) {
+        RevCommit commit =
+            rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+        for (RevCommit p : commit.getParents()) {
+          if (!p.name().equals(rev)) {
+            continue;
+          }
+          ca.neededBy.add(newNeededBy(checkNotNull(cd.change()), ps));
+          continue patchSets;
+        }
+      }
+    }
+  }
+
   private DependencyAttribute newDependsOn(Change c, PatchSet ps) {
     DependencyAttribute d = newDependencyAttribute(c, ps);
     d.isCurrentPatchSet = ps.getId().equals(c.currentPatchSetId());
@@ -312,24 +351,21 @@
     a.commitMessage = commitMessage;
   }
 
-  public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps,
-      LabelTypes labelTypes) {
-    addPatchSets(a, ps, null, false, null, labelTypes);
-  }
-
-  public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
+  public void addPatchSets(ReviewDb db, RevWalk revWalk, ChangeAttribute ca,
+      Collection<PatchSet> ps,
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       LabelTypes labelTypes) {
-    addPatchSets(ca, ps, approvals, false, null, labelTypes);
+    addPatchSets(db, revWalk, ca, ps, approvals, false, null, labelTypes);
   }
 
-  public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
+  public void addPatchSets(ReviewDb db, RevWalk revWalk, ChangeAttribute ca,
+      Collection<PatchSet> ps,
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       boolean includeFiles, Change change, LabelTypes labelTypes) {
     if (!ps.isEmpty()) {
       ca.patchSets = new ArrayList<>(ps.size());
       for (PatchSet p : ps) {
-        PatchSetAttribute psa = asPatchSetAttribute(p);
+        PatchSetAttribute psa = asPatchSetAttribute(db, revWalk, p);
         if (approvals != null) {
           addApprovals(psa, p.getId(), approvals, labelTypes);
         }
@@ -372,6 +408,7 @@
         patchSetAttribute.files.add(p);
       }
     } catch (PatchListNotAvailableException e) {
+      log.warn("Cannot get patch list", e);
     }
   }
 
@@ -392,7 +429,8 @@
    * @param patchSet
    * @return object suitable for serialization to JSON
    */
-  public PatchSetAttribute asPatchSetAttribute(final PatchSet patchSet) {
+  public PatchSetAttribute asPatchSetAttribute(ReviewDb db, RevWalk revWalk,
+      PatchSet patchSet) {
     PatchSetAttribute p = new PatchSetAttribute();
     p.revision = patchSet.getRevision().get();
     p.number = Integer.toString(patchSet.getPatchSetId());
@@ -400,40 +438,35 @@
     p.uploader = asAccountAttribute(patchSet.getUploader());
     p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
     p.isDraft = patchSet.isDraft();
-    final PatchSet.Id pId = patchSet.getId();
+    PatchSet.Id pId = patchSet.getId();
     try {
-      final ReviewDb db = schema.open();
-      try {
-        p.parents = new ArrayList<>();
-        for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(
-            patchSet.getId())) {
-          p.parents.add(a.getAncestorRevision().get());
-        }
-
-        UserIdentity author = psInfoFactory.get(db, pId).getAuthor();
-        if (author.getAccount() == null) {
-          p.author = new AccountAttribute();
-          p.author.email = author.getEmail();
-          p.author.name = author.getName();
-          p.author.username = "";
-        } else {
-          p.author = asAccountAttribute(author.getAccount());
-        }
-
-        Change change = db.changes().get(pId.getParentKey());
-        List<Patch> list =
-            patchListCache.get(change, patchSet).toPatchList(pId);
-        for (Patch pe : list) {
-          if (!Patch.COMMIT_MSG.equals(pe.getFileName())) {
-            p.sizeDeletions -= pe.getDeletions();
-            p.sizeInsertions += pe.getInsertions();
-          }
-        }
-        p.kind = changeKindCache.getChangeKind(db, change, patchSet);
-      } finally {
-        db.close();
+      p.parents = new ArrayList<>();
+      RevCommit c = revWalk.parseCommit(ObjectId.fromString(p.revision));
+      for (RevCommit parent : c.getParents()) {
+        p.parents.add(parent.name());
       }
-    } catch (OrmException e) {
+
+      UserIdentity author = psInfoFactory.get(db, pId).getAuthor();
+      if (author.getAccount() == null) {
+        p.author = new AccountAttribute();
+        p.author.email = author.getEmail();
+        p.author.name = author.getName();
+        p.author.username = "";
+      } else {
+        p.author = asAccountAttribute(author.getAccount());
+      }
+
+      Change change = db.changes().get(pId.getParentKey());
+      List<Patch> list =
+          patchListCache.get(change, patchSet).toPatchList(pId);
+      for (Patch pe : list) {
+        if (!Patch.COMMIT_MSG.equals(pe.getFileName())) {
+          p.sizeDeletions -= pe.getDeletions();
+          p.sizeInsertions += pe.getInsertions();
+        }
+      }
+      p.kind = changeKindCache.getChangeKind(db, change, patchSet);
+    } catch (OrmException | IOException e) {
       log.error("Cannot load patch set data for " + patchSet.getId(), e);
     } catch (PatchSetInfoNotAvailableException e) {
       log.error(String.format("Cannot get authorEmail for %s.", pId), e);
@@ -488,7 +521,11 @@
    * @param account
    * @return object suitable for serialization to JSON
    */
-  public AccountAttribute asAccountAttribute(final Account account) {
+  public AccountAttribute asAccountAttribute(Account account) {
+    if (account == null) {
+      return null;
+    }
+
     AccountAttribute who = new AccountAttribute();
     who.name = account.getFullName();
     who.email = account.getPreferredEmail();
@@ -553,9 +590,9 @@
   }
 
   /** Get a link to the change; null if the server doesn't know its own address. */
-  private String getChangeUrl(final Change change) {
+  private String getChangeUrl(Change change) {
     if (change != null && urlProvider.get() != null) {
-      final StringBuilder r = new StringBuilder();
+      StringBuilder r = new StringBuilder();
       r.append(urlProvider.get());
       r.append(change.getChangeId());
       return r.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
index 908fd0a..9b37c38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
@@ -35,6 +35,7 @@
     registerClass(new ReviewerAddedEvent());
     registerClass(new PatchSetCreatedEvent());
     registerClass(new TopicChangedEvent());
+    registerClass(new ProjectCreatedEvent());
   }
 
   /** Register an event.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectCreatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectCreatedEvent.java
new file mode 100644
index 0000000..c1534df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectCreatedEvent.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2015 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.server.events;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+public class ProjectCreatedEvent extends ProjectEvent {
+  public String projectName;
+  public String headName;
+
+  public ProjectCreatedEvent() {
+    super("project-created");
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(projectName);
+  }
+
+  public String getHeadName() {
+    return headName;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 5327448..7eef0ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -46,16 +46,22 @@
     this.listeners = listeners;
   }
 
-  public void fire(Project.NameKey project, RefUpdate refUpdate) {
+  public void fire(Project.NameKey project, RefUpdate refUpdate,
+      ReceiveCommand.Type type) {
     fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
-        refUpdate.getNewObjectId());
+        refUpdate.getNewObjectId(), type);
   }
 
-  public void fire(Project.NameKey project, String ref,
-      ObjectId oldObjectId, ObjectId newObjectId) {
+  public void fire(Project.NameKey project, RefUpdate refUpdate) {
+    fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
+        refUpdate.getNewObjectId(), ReceiveCommand.Type.UPDATE);
+  }
+
+  public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
+      ObjectId newObjectId, ReceiveCommand.Type type) {
     ObjectId o = oldObjectId != null ? oldObjectId : ObjectId.zeroId();
     ObjectId n = newObjectId != null ? newObjectId : ObjectId.zeroId();
-    Event event = new Event(project, ref, o.name(), n.name());
+    Event event = new Event(project, ref, o.name(), n.name(), type);
     for (GitReferenceUpdatedListener l : listeners) {
       try {
         l.onGitReferenceUpdated(event);
@@ -65,10 +71,19 @@
     }
   }
 
+  public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
+      ObjectId newObjectId) {
+    fire(project, ref, oldObjectId, newObjectId, ReceiveCommand.Type.UPDATE);
+  }
+
+  public void fire(Project.NameKey project, ReceiveCommand cmd) {
+    fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId(), cmd.getType());
+  }
+
   public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate) {
     for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
       if (cmd.getResult() == ReceiveCommand.Result.OK) {
-        fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId());
+        fire(project, cmd);
       }
     }
   }
@@ -78,13 +93,16 @@
     private final String ref;
     private final String oldObjectId;
     private final String newObjectId;
+    private final ReceiveCommand.Type type;
 
     Event(Project.NameKey project, String ref,
-        String oldObjectId, String newObjectId) {
+        String oldObjectId, String newObjectId,
+        ReceiveCommand.Type type) {
       this.projectName = project.get();
       this.ref = ref;
       this.oldObjectId = oldObjectId;
       this.newObjectId = newObjectId;
+      this.type = type;
     }
 
     @Override
@@ -108,6 +126,21 @@
     }
 
     @Override
+    public boolean isCreate() {
+      return type == ReceiveCommand.Type.CREATE;
+    }
+
+    @Override
+    public boolean isDelete() {
+      return type == ReceiveCommand.Type.DELETE;
+    }
+
+    @Override
+    public boolean isNonFastForward() {
+      return type == ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
+    }
+
+    @Override
     public String toString() {
       return String.format("%s[%s,%s: %s -> %s]", getClass().getSimpleName(),
           projectName, ref, oldObjectId, newObjectId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
index 5952568..899b06a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
@@ -72,7 +72,7 @@
     long getTimeoutMillis(@GerritServerConfig final Config cfg) {
       return ConfigUtil.getTimeUnit(
           cfg, "receive", null, "timeout",
-          TimeUnit.MINUTES.toMillis(2),
+          TimeUnit.MINUTES.toMillis(4),
           TimeUnit.MILLISECONDS);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index d8e56b3..be17776 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_REJECT_COMMITS;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.client.Project;
@@ -41,7 +42,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
@@ -59,7 +59,7 @@
   public static NoteMap loadRejectCommitsMap(Repository repo, RevWalk walk)
       throws IOException {
     try {
-      Ref ref = repo.getRef(RefNames.REFS_REJECT_COMMITS);
+      Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_REJECT_COMMITS);
       if (ref == null) {
         return NoteMap.newEmptyMap();
       }
@@ -137,12 +137,12 @@
   }
 
   private ObjectId createNoteContent(String reason, ObjectInserter inserter)
-      throws UnsupportedEncodingException, IOException {
+      throws IOException {
     String noteContent = reason != null ? reason : "";
     if (noteContent.length() > 0 && !noteContent.endsWith("\n")) {
       noteContent = noteContent + "\n";
     }
-    return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
+    return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes(UTF_8));
   }
 
   private PersonIdent createPersonIdent() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
new file mode 100644
index 0000000..c54fe26
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -0,0 +1,389 @@
+// Copyright (C) 2015 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.server.git;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Context for a set of updates that should be applied for a site.
+ * <p>
+ * An update operation can be divided into three phases:
+ * <ol>
+ * <li>Git reference updates</li>
+ * <li>Database updates</li>
+ * <li>Post-update steps<li>
+ * </ol>
+ * A single conceptual operation, such as a REST API call or a merge operation,
+ * may make multiple changes at each step, which all need to be serialized
+ * relative to each other. Moreover, for consistency, <em>all</em> git ref
+ * updates must be performed before <em>any</em> database updates, since
+ * database updates might refer to newly-created patch set refs. And all
+ * post-update steps, such as hooks, should run only after all storage
+ * mutations have completed.
+ * <p>
+ * Depending on the backend used, each step might support batching, for example
+ * in a {@code BatchRefUpdate} or one or more database transactions. All
+ * operations in one phase must complete successfully before proceeding to the
+ * next phase.
+ */
+public class BatchUpdate implements AutoCloseable {
+  public interface Factory {
+    public BatchUpdate create(ReviewDb db, Project.NameKey project,
+        CurrentUser user, Timestamp when);
+  }
+
+  public class Context {
+    public Project.NameKey getProject() {
+      return project;
+    }
+
+    public Timestamp getWhen() {
+      return when;
+    }
+
+    public ReviewDb getDb() {
+      return db;
+    }
+
+    public CurrentUser getUser() {
+      return user;
+    }
+  }
+
+  public class RepoContext extends Context {
+    public Repository getRepository() throws IOException {
+      initRepository();
+      return repo;
+    }
+
+    public RevWalk getRevWalk() throws IOException {
+      initRepository();
+      return revWalk;
+    }
+
+    public ObjectInserter getInserter() throws IOException {
+      initRepository();
+      return inserter;
+    }
+
+    public BatchRefUpdate getBatchRefUpdate() throws IOException {
+      initRepository();
+      if (batchRefUpdate == null) {
+        batchRefUpdate = repo.getRefDatabase().newBatchUpdate();
+      }
+      return batchRefUpdate;
+    }
+
+    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
+      getBatchRefUpdate().addCommand(cmd);
+    }
+
+    public TimeZone getTimeZone() {
+      return tz;
+    }
+  }
+
+  public class ChangeContext extends Context {
+    private final ChangeControl ctl;
+    private final ChangeUpdate update;
+
+    private ChangeContext(ChangeControl ctl) {
+      this.ctl = ctl;
+      this.update = changeUpdateFactory.create(ctl, when);
+    }
+
+    public ChangeUpdate getChangeUpdate() {
+      return update;
+    }
+
+    public ChangeNotes getChangeNotes() {
+      return update.getChangeNotes();
+    }
+
+    public ChangeControl getChangeControl() {
+      return ctl;
+    }
+
+    public Change getChange() {
+      return update.getChange();
+    }
+  }
+
+  public static class Op {
+    @SuppressWarnings("unused")
+    public void updateRepo(RepoContext ctx) throws Exception {
+    }
+
+    @SuppressWarnings("unused")
+    public void updateChange(ChangeContext ctx) throws Exception {
+    }
+
+    // TODO(dborowitz): Support async operations?
+    @SuppressWarnings("unused")
+    public void postUpdate(Context ctx) throws Exception {
+    }
+  }
+
+  public abstract static class InsertChangeOp extends Op {
+    public abstract Change getChange();
+  }
+
+  private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
+  private final ChangeIndexer indexer;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ChangeUpdate.Factory changeUpdateFactory;
+  private final GitReferenceUpdated gitRefUpdated;
+
+  private final Project.NameKey project;
+  private final CurrentUser user;
+  private final Timestamp when;
+  private final TimeZone tz;
+
+  private final ListMultimap<Change.Id, Op> ops = ArrayListMultimap.create();
+  private final Map<Change.Id, Change> newChanges = new HashMap<>();
+  private final List<CheckedFuture<?, IOException>> indexFutures =
+      new ArrayList<>();
+
+  private Repository repo;
+  private ObjectInserter inserter;
+  private RevWalk revWalk;
+  private BatchRefUpdate batchRefUpdate;
+  private boolean closeRepo;
+
+  @AssistedInject
+  BatchUpdate(GitRepositoryManager repoManager,
+      ChangeIndexer indexer,
+      ChangeControl.GenericFactory changeControlFactory,
+      ChangeUpdate.Factory changeUpdateFactory,
+      GitReferenceUpdated gitRefUpdated,
+      @GerritPersonIdent PersonIdent serverIdent,
+      @Assisted ReviewDb db,
+      @Assisted Project.NameKey project,
+      @Assisted CurrentUser user,
+      @Assisted Timestamp when) {
+    this.db = db;
+    this.repoManager = repoManager;
+    this.indexer = indexer;
+    this.changeControlFactory = changeControlFactory;
+    this.changeUpdateFactory = changeUpdateFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.project = project;
+    this.user = user;
+    this.when = when;
+    tz = serverIdent.getTimeZone();
+  }
+
+  @Override
+  public void close() {
+    if (closeRepo) {
+      revWalk.close();
+      inserter.close();
+      repo.close();
+    }
+  }
+
+  public BatchUpdate setRepository(Repository repo, RevWalk revWalk,
+      ObjectInserter inserter) {
+    checkState(this.repo == null, "repo already set");
+    closeRepo = false;
+    this.repo = checkNotNull(repo, "repo");
+    this.revWalk = checkNotNull(revWalk, "revWalk");
+    this.inserter = checkNotNull(inserter, "inserter");
+    return this;
+  }
+
+  private void initRepository() throws IOException {
+    if (repo == null) {
+      this.repo = repoManager.openRepository(project);
+      closeRepo = true;
+      inserter = repo.newObjectInserter();
+      revWalk = new RevWalk(inserter.newReader());
+    }
+  }
+
+  public CurrentUser getUser() {
+    return user;
+  }
+
+  public Repository getRepository() throws IOException {
+    initRepository();
+    return repo;
+  }
+
+  public RevWalk getRevWalk() throws IOException {
+    initRepository();
+    return revWalk;
+  }
+
+  public ObjectInserter getObjectInserter() throws IOException {
+    initRepository();
+    return inserter;
+  }
+
+  public BatchUpdate addOp(Change.Id id, Op op) {
+    checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
+    ops.put(id, op);
+    return this;
+  }
+
+  public BatchUpdate insertChange(InsertChangeOp op) {
+    Change c = op.getChange();
+    checkArgument(!newChanges.containsKey(c.getId()),
+        "only one op allowed to create change %s", c.getId());
+    newChanges.put(c.getId(), c);
+    ops.get(c.getId()).add(0, op);
+    return this;
+  }
+
+  public void execute() throws UpdateException, RestApiException {
+    try {
+      executeRefUpdates();
+      executeChangeOps();
+      reindexChanges();
+
+      if (batchRefUpdate != null) {
+        // Fire ref update events only after all mutations are finished, since
+        // callers may assume a patch set ref being created means the change was
+        // created, or a branch advancing meaning some changes were closed.
+        gitRefUpdated.fire(project, batchRefUpdate);
+      }
+
+      executePostOps();
+    } catch (UpdateException | RestApiException e) {
+      // Propagate REST API exceptions thrown by operations; they commonly throw
+      // exceptions like ResourceConflictException to indicate an atomic update
+      // failure.
+      throw e;
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e);
+      throw new UpdateException(e);
+    }
+  }
+
+  private void executeRefUpdates()
+      throws IOException, UpdateException, RestApiException {
+    try {
+      RepoContext ctx = new RepoContext();
+      for (Op op : ops.values()) {
+        op.updateRepo(ctx);
+      }
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e, RestApiException.class);
+      throw new UpdateException(e);
+    }
+
+    if (repo == null || batchRefUpdate == null
+        || batchRefUpdate.getCommands().isEmpty()) {
+      return;
+    }
+    inserter.flush();
+    batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
+    boolean ok = true;
+    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+      if (cmd.getResult() != ReceiveCommand.Result.OK) {
+        ok = false;
+        break;
+      }
+    }
+    if (!ok) {
+      throw new UpdateException("BatchRefUpdate failed: " + batchRefUpdate);
+    }
+  }
+
+  private void executeChangeOps() throws UpdateException, RestApiException {
+    try {
+      for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
+        Change.Id id = e.getKey();
+        db.changes().beginTransaction(id);
+        ChangeContext ctx;
+        try {
+          ctx = newChangeContext(id);
+          for (Op op : e.getValue()) {
+            op.updateChange(ctx);
+          }
+          db.commit();
+        } finally {
+          db.rollback();
+        }
+        ctx.getChangeUpdate().commit();
+        indexFutures.add(indexer.indexAsync(id));
+      }
+    } catch (Exception e) {
+      Throwables.propagateIfPossible(e, RestApiException.class);
+      throw new UpdateException(e);
+    }
+  }
+
+  private ChangeContext newChangeContext(Change.Id id) throws Exception {
+    Change c = newChanges.get(id);
+    if (c == null) {
+      c = db.changes().get(id);
+    }
+    // Pass in preloaded change to controlFor, to avoid:
+    //  - reading from a db that does not belong to this update
+    //  - attempting to read a change that doesn't exist yet
+    return new ChangeContext(
+      changeControlFactory.controlFor(c, user));
+  }
+
+  private void reindexChanges() throws IOException {
+    ChangeIndexer.allAsList(indexFutures).checkedGet();
+  }
+
+  private void executePostOps() throws Exception {
+    Context ctx = new Context();
+    for (Op op : ops.values()) {
+      op.postUpdate(ctx);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
index c447d31..0ef9e0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
@@ -15,8 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.ImmutableList;
-
-import org.eclipse.jgit.lib.Constants;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 import java.util.List;
 
@@ -35,26 +34,18 @@
     } else {
       ImmutableList.Builder<String> builder = ImmutableList.builder();
       for (String b : order) {
-        builder.add(fullName(b));
+        builder.add(RefNames.fullName(b));
       }
       this.order = builder.build();
     }
   }
 
-  private static String fullName(String branch) {
-    if (branch.startsWith(Constants.R_HEADS)) {
-      return branch;
-    } else {
-      return Constants.R_HEADS + branch;
-    }
-  }
-
-  public List<String> getMoreStable(String branch) {
-    int i = order.indexOf(fullName(branch));
+  public String[] getMoreStable(String branch) {
+    int i = order.indexOf(RefNames.fullName(branch));
     if (0 <= i) {
-      return order.subList(i + 1, order.size());
-    } else {
-      return ImmutableList.of();
+      List<String> r = order.subList(i + 1, order.size());
+      return r.toArray(new String[r.size()]);
     }
+    return new String[] {};
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
deleted file mode 100644
index be3902c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ /dev/null
@@ -1,277 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.git;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.RemotePeer;
-import com.google.gerrit.server.config.GerritRequestModule;
-import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
-import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-import com.google.inject.servlet.RequestScoped;
-
-import com.jcraft.jsch.HostKey;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.net.SocketAddress;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-
-@Singleton
-public class ChangeMergeQueue implements MergeQueue {
-  private static final Logger log =
-      LoggerFactory.getLogger(ChangeMergeQueue.class);
-
-  private final Map<Branch.NameKey, MergeEntry> active = new HashMap<>();
-  private final Map<Branch.NameKey, RecheckJob> recheck = new HashMap<>();
-
-  private final WorkQueue workQueue;
-  private final Provider<MergeOp.Factory> bgFactory;
-  private final PerThreadRequestScope.Scoper threadScoper;
-
-  @Inject
-  ChangeMergeQueue(final WorkQueue wq, Injector parent) {
-    workQueue = wq;
-
-    Injector child = parent.createChildInjector(new AbstractModule() {
-      @Override
-      protected void configure() {
-        bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
-        bind(RequestScopePropagator.class)
-            .to(PerThreadRequestScope.Propagator.class);
-        bind(PerThreadRequestScope.Propagator.class);
-        install(new GerritRequestModule());
-
-        bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
-            new Provider<SocketAddress>() {
-              @Override
-              public SocketAddress get() {
-                throw new OutOfScopeException("No remote peer on merge thread");
-              }
-            });
-        bind(SshInfo.class).toInstance(new SshInfo() {
-          @Override
-          public List<HostKey> getHostKeys() {
-            return Collections.emptyList();
-          }
-        });
-      }
-
-      @Provides
-      public PerThreadRequestScope.Scoper provideScoper(
-          final PerThreadRequestScope.Propagator propagator,
-          final Provider<RequestScopedReviewDbProvider> dbProvider) {
-        final RequestContext requestContext = new RequestContext() {
-          @Override
-          public CurrentUser getCurrentUser() {
-            throw new OutOfScopeException("No user on merge thread");
-          }
-
-          @Override
-          public Provider<ReviewDb> getReviewDbProvider() {
-            return dbProvider.get();
-          }
-        };
-        return new PerThreadRequestScope.Scoper() {
-          @Override
-          public <T> Callable<T> scope(Callable<T> callable) {
-            return propagator.scope(requestContext, callable);
-          }
-        };
-      }
-    });
-    bgFactory = child.getProvider(MergeOp.Factory.class);
-    threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
-  }
-
-  @Override
-  public void merge(Branch.NameKey branch) {
-    if (start(branch)) {
-      mergeImpl(branch);
-    }
-  }
-
-  private synchronized boolean start(final Branch.NameKey branch) {
-    final MergeEntry e = active.get(branch);
-    if (e == null) {
-      // Let the caller attempt this merge, its the only one interested
-      // in processing this branch right now.
-      //
-      active.put(branch, new MergeEntry(branch));
-      return true;
-    } else {
-      // Request that the job queue handle this merge later.
-      //
-      e.needMerge = true;
-      return false;
-    }
-  }
-
-  @Override
-  public synchronized void schedule(final Branch.NameKey branch) {
-    MergeEntry e = active.get(branch);
-    if (e == null) {
-      e = new MergeEntry(branch);
-      active.put(branch, e);
-      e.needMerge = true;
-      scheduleJob(e);
-    } else {
-      e.needMerge = true;
-    }
-  }
-
-  @Override
-  public synchronized void recheckAfter(final Branch.NameKey branch,
-      final long delay, final TimeUnit delayUnit) {
-    final long now = TimeUtil.nowMs();
-    final long at = now + MILLISECONDS.convert(delay, delayUnit);
-    RecheckJob e = recheck.get(branch);
-    if (e == null) {
-      e = new RecheckJob(branch);
-      workQueue.getDefaultQueue().schedule(e, now - at, MILLISECONDS);
-      recheck.put(branch, e);
-    }
-    e.recheckAt = Math.max(at, e.recheckAt);
-  }
-
-  private synchronized void finish(final Branch.NameKey branch) {
-    final MergeEntry e = active.get(branch);
-    if (e == null) {
-      // Not registered? Shouldn't happen but ignore it.
-      //
-      return;
-    }
-
-    if (!e.needMerge) {
-      // No additional merges are in progress, we can delete it.
-      //
-      active.remove(branch);
-      return;
-    }
-
-    scheduleJob(e);
-  }
-
-  private void scheduleJob(final MergeEntry e) {
-    if (!e.jobScheduled) {
-      // No job has been scheduled to execute this branch, but it needs
-      // to run a merge again.
-      //
-      e.jobScheduled = true;
-      workQueue.getDefaultQueue().schedule(e, 0, TimeUnit.SECONDS);
-    }
-  }
-
-  private synchronized void unschedule(final MergeEntry e) {
-    e.jobScheduled = false;
-    e.needMerge = false;
-  }
-
-  private void mergeImpl(final Branch.NameKey branch) {
-    try {
-      threadScoper.scope(new Callable<Void>(){
-        @Override
-        public Void call() throws Exception {
-          bgFactory.get().create(branch).merge();
-          return null;
-        }
-      }).call();
-    } catch (Throwable e) {
-      log.error("Merge attempt for " + branch + " failed", e);
-    } finally {
-      finish(branch);
-    }
-  }
-
-  private synchronized void recheck(final RecheckJob e) {
-    final long remainingDelay = e.recheckAt - TimeUtil.nowMs();
-    if (MILLISECONDS.convert(10, SECONDS) < remainingDelay) {
-      // Woke up too early, the job deadline was pushed back.
-      // Reschedule for the new deadline. We allow for a small
-      // amount of fuzz due to multiple reschedule attempts in
-      // a short period of time being caused by MergeOp.
-      //
-      workQueue.getDefaultQueue().schedule(e, remainingDelay, MILLISECONDS);
-    } else {
-      // Schedule a merge attempt on this branch to see if we can
-      // actually complete it this time.
-      //
-      schedule(e.dest);
-    }
-  }
-
-  private class MergeEntry implements Runnable {
-    final Branch.NameKey dest;
-    boolean needMerge;
-    boolean jobScheduled;
-
-    MergeEntry(final Branch.NameKey d) {
-      dest = d;
-    }
-
-    @Override
-    public void run() {
-      unschedule(this);
-      mergeImpl(dest);
-    }
-
-    @Override
-    public String toString() {
-      final Project.NameKey project = dest.getParentKey();
-      return "submit " + project.get() + " " + dest.getShortName();
-    }
-  }
-
-  private class RecheckJob implements Runnable {
-    final Branch.NameKey dest;
-    long recheckAt;
-
-    RecheckJob(final Branch.NameKey d) {
-      dest = d;
-    }
-
-    @Override
-    public void run() {
-      recheck(this);
-    }
-
-    @Override
-    public String toString() {
-      final Project.NameKey project = dest.getParentKey();
-      return "recheck " + project.get() + " " + dest.getShortName();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
new file mode 100644
index 0000000..fdf9b34
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2015 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.server.git;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A set of changes grouped together to be submitted atomically.
+ * <p>
+ * This class is not thread safe.
+ */
+public class ChangeSet {
+  private final ImmutableCollection<ChangeData> changeData;
+
+  public ChangeSet(Iterable<ChangeData> changes) {
+    Set<Change.Id> ids = new HashSet<>();
+    ImmutableSet.Builder<ChangeData> cdb = ImmutableSet.builder();
+    for (ChangeData cd : changes) {
+      if (ids.add(cd.getId())) {
+        cdb.add(cd);
+      }
+    }
+    changeData = cdb.build();
+  }
+
+  public ChangeSet(ChangeData change) {
+    this(ImmutableList.of(change));
+  }
+
+  public ImmutableSet<Change.Id> ids() {
+    ImmutableSet.Builder<Change.Id> ret = ImmutableSet.builder();
+    for (ChangeData cd : changeData) {
+      ret.add(cd.getId());
+    }
+    return ret.build();
+  }
+
+  public Set<PatchSet.Id> patchIds() throws OrmException {
+    Set<PatchSet.Id> ret = new HashSet<>();
+    for (ChangeData cd : changeData) {
+      ret.add(cd.change().currentPatchSetId());
+    }
+    return ret;
+  }
+
+  public SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject()
+      throws OrmException {
+    SetMultimap<Project.NameKey, Branch.NameKey> ret =
+        HashMultimap.create();
+    for (ChangeData cd : changeData) {
+      ret.put(cd.change().getProject(), cd.change().getDest());
+    }
+    return ret;
+  }
+
+  public Multimap<Project.NameKey, Change.Id> changesByProject()
+      throws OrmException {
+    ListMultimap<Project.NameKey, Change.Id> ret =
+        ArrayListMultimap.create();
+    for (ChangeData cd : changeData) {
+      ret.put(cd.change().getProject(), cd.getId());
+    }
+    return ret;
+  }
+
+  public Multimap<Branch.NameKey, ChangeData> changesByBranch()
+      throws OrmException {
+    ListMultimap<Branch.NameKey, ChangeData> ret =
+        ArrayListMultimap.create();
+    for (ChangeData cd : changeData) {
+      ret.put(cd.change().getDest(), cd);
+    }
+    return ret;
+  }
+
+  public ImmutableCollection<ChangeData> changes() {
+    return changeData;
+  }
+
+  public int size() {
+    return changeData.size();
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + ids();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 1ff37b0..37a0886 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.base.Function;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.reviewdb.client.Change;
@@ -21,6 +23,8 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -28,6 +32,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
+import java.io.IOException;
 import java.util.List;
 
 /** Extended commit entity with code review specific metadata. */
@@ -50,11 +55,11 @@
         }
       }).nullsFirst();
 
-  public static RevWalk newRevWalk(Repository repo) {
+  public static CodeReviewRevWalk newRevWalk(Repository repo) {
     return new CodeReviewRevWalk(repo);
   }
 
-  public static RevWalk newRevWalk(ObjectReader reader) {
+  public static CodeReviewRevWalk newRevWalk(ObjectReader reader) {
     return new CodeReviewRevWalk(reader);
   }
 
@@ -85,7 +90,7 @@
     return r;
   }
 
-  private static class CodeReviewRevWalk extends RevWalk {
+  public static class CodeReviewRevWalk extends RevWalk {
     private CodeReviewRevWalk(Repository repo) {
       super(repo);
     }
@@ -95,9 +100,42 @@
     }
 
     @Override
-    protected RevCommit createCommit(AnyObjectId id) {
+    protected CodeReviewCommit createCommit(AnyObjectId id) {
       return new CodeReviewCommit(id);
     }
+
+    @Override
+    public CodeReviewCommit next() throws MissingObjectException,
+         IncorrectObjectTypeException, IOException {
+      return (CodeReviewCommit) super.next();
+    }
+
+    @Override
+    public void markStart(RevCommit c) throws MissingObjectException,
+        IncorrectObjectTypeException, IOException {
+      checkArgument(c instanceof CodeReviewCommit);
+      super.markStart(c);
+    }
+
+    @Override
+    public void markUninteresting(final RevCommit c)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      checkArgument(c instanceof CodeReviewCommit);
+      super.markUninteresting(c);
+    }
+
+    @Override
+    public CodeReviewCommit lookupCommit(AnyObjectId id) {
+      return (CodeReviewCommit) super.lookupCommit(id);
+    }
+
+    @Override
+    public CodeReviewCommit parseCommit(AnyObjectId id)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      return (CodeReviewCommit) super.parseCommit(id);
+    }
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 3d3d9b1..cc9e977 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -16,7 +16,7 @@
 
 public enum CommitMergeStatus {
   /** */
-  CLEAN_MERGE("Change has been successfully merged into the git repository"),
+  CLEAN_MERGE("Change has been successfully merged"),
 
   /** */
   CLEAN_PICK("Change has been successfully cherry-picked"),
@@ -39,7 +39,7 @@
           + "Please rebase the change locally and upload the rebased commit for review."),
 
   /** */
-  MISSING_DEPENDENCY(""),
+  MISSING_DEPENDENCY("Missing dependency"),
 
   /** */
   NO_PATCH_SET(""),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
new file mode 100644
index 0000000..ca1f705c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2015 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.server.git;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+public class DestinationList extends TabFile {
+  public static final String DIR_NAME = "destinations";
+  private SetMultimap<String, Branch.NameKey> destinations = HashMultimap.create();
+
+  public Set<Branch.NameKey> getDestinations(String label) {
+    return destinations.get(label);
+  }
+
+  public void parseLabel(String label, String text,
+      ValidationError.Sink errors) throws IOException {
+    destinations.replaceValues(label,
+        toSet(parse(text, DIR_NAME + label, TRIM, null, errors)));
+  }
+
+  public String asText(String label) {
+    Set<Branch.NameKey> dests = destinations.get(label);
+    if (dests == null) {
+      return null;
+    }
+    List<Row> rows = Lists.newArrayListWithCapacity(dests.size());
+    for (Branch.NameKey dest : sort(dests)) {
+      rows.add(new Row(dest.get(), dest.getParentKey().get()));
+    }
+    return asText("Ref", "Project", rows);
+  }
+
+  protected static Set<Branch.NameKey> toSet(List<Row> destRows) {
+    Set<Branch.NameKey> dests = Sets.newHashSetWithExpectedSize(destRows.size());
+    for(Row row : destRows) {
+      dests.add(new Branch.NameKey(new Project.NameKey(row.right), row.left));
+    }
+    return dests;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
new file mode 100644
index 0000000..66742bc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2015 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.server.git;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.MergedSender;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ExecutorService;
+
+public class EmailMerge implements Runnable, RequestContext {
+  private static final Logger log = LoggerFactory.getLogger(EmailMerge.class);
+
+  public interface Factory {
+    EmailMerge create(Change.Id changeId, Account.Id submitter);
+  }
+
+  private final ExecutorService sendEmailsExecutor;
+  private final MergedSender.Factory mergedSenderFactory;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ThreadLocalRequestContext requestContext;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  private final Change.Id changeId;
+  private final Account.Id submitter;
+  private ReviewDb db;
+
+  @Inject
+  EmailMerge(@SendEmailExecutor ExecutorService executor,
+      MergedSender.Factory mergedSenderFactory,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext requestContext,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      @Assisted Change.Id changeId,
+      @Assisted @Nullable Account.Id submitter) {
+    this.sendEmailsExecutor = executor;
+    this.mergedSenderFactory = mergedSenderFactory;
+    this.schemaFactory = schemaFactory;
+    this.requestContext = requestContext;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.changeId = changeId;
+    this.submitter = submitter;
+  }
+
+  void sendAsync() {
+    sendEmailsExecutor.submit(this);
+  }
+
+  @Override
+  public void run() {
+    RequestContext old = requestContext.setContext(this);
+    try {
+      MergedSender cm = mergedSenderFactory.create(changeId);
+      if (submitter != null) {
+        cm.setFrom(submitter);
+      }
+      cm.send();
+    } catch (Exception e) {
+      log.error("Cannot email merged notification for " + changeId, e);
+    } finally {
+      requestContext.setContext(old);
+      if (db != null) {
+        db.close();
+        db = null;
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "send-email merged";
+  }
+
+  @Override
+  public CurrentUser getUser() {
+    if (submitter != null) {
+      return identifiedUserFactory.create(
+          getReviewDbProvider(), submitter).getRealUser();
+    }
+    throw new OutOfScopeException("No user on email thread");
+  }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return new Provider<ReviewDb>() {
+      @Override
+      public ReviewDb get() {
+        if (db == null) {
+          try {
+            db = schemaFactory.open();
+          } catch (OrmException e) {
+            throw new ProvisionException("Cannot open ReviewDb", e);
+          }
+        }
+        return db;
+      }
+    };
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
index 24af14d..cfdedd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -16,7 +16,11 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.extensions.events.GarbageCollectorListener;
+import com.google.gerrit.extensions.events.GarbageCollectorListener.Event;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GcConfig;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.api.GarbageCollectCommand;
@@ -46,15 +50,21 @@
 
   private final GitRepositoryManager repoManager;
   private final GarbageCollectionQueue gcQueue;
+  private final GcConfig gcConfig;
+  private final DynamicSet<GarbageCollectorListener> listeners;
 
   public interface Factory {
     GarbageCollection create();
   }
 
   @Inject
-  GarbageCollection(GitRepositoryManager repoManager, GarbageCollectionQueue gcQueue) {
+  GarbageCollection(GitRepositoryManager repoManager,
+      GarbageCollectionQueue gcQueue, GcConfig config,
+      DynamicSet<GarbageCollectorListener> listeners) {
     this.repoManager = repoManager;
     this.gcQueue = gcQueue;
+    this.gcConfig = config;
+    this.listeners = listeners;
   }
 
   public GarbageCollectionResult run(List<Project.NameKey> projectNames) {
@@ -63,6 +73,11 @@
 
   public GarbageCollectionResult run(List<Project.NameKey> projectNames,
       PrintWriter writer) {
+    return run(projectNames, gcConfig.isAggressive(), writer);
+  }
+
+  public GarbageCollectionResult run(List<Project.NameKey> projectNames,
+      boolean aggressive, PrintWriter writer) {
     GarbageCollectionResult result = new GarbageCollectionResult();
     Set<Project.NameKey> projectsToGc = gcQueue.addAll(projectNames);
     for (Project.NameKey projectName : Sets.difference(
@@ -71,18 +86,18 @@
           GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED, projectName));
     }
     for (Project.NameKey p : projectsToGc) {
-      Repository repo = null;
-      try {
-        repo = repoManager.openRepository(p);
-        logGcConfiguration(p, repo);
+      try (Repository repo = repoManager.openRepository(p)) {
+        logGcConfiguration(p, repo, aggressive);
         print(writer, "collecting garbage for \"" + p + "\":\n");
         GarbageCollectCommand gc = Git.wrap(repo).gc();
+        gc.setAggressive(aggressive);
         logGcInfo(p, "before:", gc.getStatistics());
         gc.setProgressMonitor(writer != null ? new TextProgressMonitor(writer)
             : NullProgressMonitor.INSTANCE);
         Properties statistics = gc.call();
         logGcInfo(p, "after: ", statistics);
         print(writer, "done.\n\n");
+        fire(p, statistics);
       } catch (RepositoryNotFoundException e) {
         logGcError(writer, p, e);
         result.addError(new GarbageCollectionResult.Error(
@@ -93,15 +108,33 @@
         result.addError(new GarbageCollectionResult.Error(
             GarbageCollectionResult.Error.Type.GC_FAILED, p));
       } finally {
-        if (repo != null) {
-          repo.close();
-        }
         gcQueue.gcFinished(p);
       }
     }
     return result;
   }
 
+  private void fire(final Project.NameKey p, final Properties statistics) {
+    Event event = new GarbageCollectorListener.Event() {
+      @Override
+      public String getProjectName() {
+        return p.get();
+      }
+
+      @Override
+      public Properties getStatistics() {
+        return statistics;
+      }
+    };
+    for (GarbageCollectorListener l : listeners) {
+      try {
+        l.onGarbageCollected(event);
+      } catch (RuntimeException e) {
+        log.warn("Failure in GarbageCollectorListener", e);
+      }
+    }
+  }
+
   private static void logGcInfo(Project.NameKey projectName, String msg) {
     logGcInfo(projectName, msg, null);
   }
@@ -123,9 +156,10 @@
   }
 
   private static void logGcConfiguration(Project.NameKey projectName,
-      Repository repo) {
+      Repository repo, boolean aggressive) {
     StringBuilder b = new StringBuilder();
     Config cfg = repo.getConfig();
+    b.append("gc.aggressive=").append(aggressive).append("; ");
     b.append(formatConfigValues(cfg, ConfigConstants.CONFIG_GC_SECTION, null));
     for (String subsection : cfg.getSubsections(ConfigConstants.CONFIG_GC_SECTION)) {
       b.append(formatConfigValues(cfg, ConfigConstants.CONFIG_GC_SECTION,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
index 2b0d3e9..b2a4311 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
@@ -23,7 +23,7 @@
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
 
-import java.io.File;
+import java.nio.file.Path;
 
 public class GarbageCollectionLogFile implements LifecycleListener {
 
@@ -43,7 +43,7 @@
     LogManager.getLogger(GarbageCollection.LOG_NAME).removeAllAppenders();
   }
 
-  private static void initLogSystem(File logdir) {
+  private static void initLogSystem(Path logdir) {
     Logger gcLogger = LogManager.getLogger(GarbageCollection.LOG_NAME);
     gcLogger.removeAllAppenders();
     gcLogger.addAppender(SystemLog.createAppender(logdir,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
index 68d25d9..e01924e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
@@ -54,7 +54,8 @@
       if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
         gcLog.info("Ignoring missing gc schedule configuration");
       } else if (delay < 0 || interval <= 0) {
-        gcLog.warn("Ignoring invalid gc schedule configuration");
+        gcLog.warn(String.format(
+            "Ignoring invalid gc schedule configuration: %s", scheduleConfig));
       } else {
         queue.getDefaultQueue().scheduleAtFixedRate(gcRunner, delay,
             interval, TimeUnit.MILLISECONDS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
index 9ef7256..aa0fc55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
 /** Configures the Git support. */
 public class GitModule extends FactoryModule {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
new file mode 100644
index 0000000..51125b4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
@@ -0,0 +1,303 @@
+// Copyright (C) 2015 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.server.git;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.eclipse.jgit.revwalk.RevFlag.UNINTERESTING;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Helper for assigning groups to commits during {@link ReceiveCommits}.
+ * <p>
+ * For each commit encountered along a walk between the branch tip and the tip
+ * of the push, the group of a commit is defined as follows:
+ * <ul>
+ *   <li>If the commit is an existing patch set of a change, the group is read
+ *   from the group field in the corresponding {@link PatchSet} record.</li>
+ *   <li>If all of a commit's parents are merged into the branch, then its group
+ *   is its own SHA-1.</li>
+ *   <li>If the commit has a single parent that is not yet merged into the
+ *   branch, then its group is the same as the parent's group.<li>
+ *   <li>For a merge commit, choose a parent and use that parent's group. If one
+ *   of the parents has a group from a patch set, use that group, otherwise, use
+ *   the group from the first parent. In addition to setting this merge commit's
+ *   group, use the chosen group for all commits that would otherwise use a
+ *   group from the parents that were not chosen.</li>
+ *   <li>If a merge commit has multiple parents whose group comes from separate
+ *   patch sets, concatenate the groups from those parents together. This
+ *   indicates two side branches were pushed separately, followed by the merge.
+ *   <li>
+ * </ul>
+ * <p>
+ * Callers must call {@link #visit(RevCommit)} on all commits between the
+ * current branch tip and the tip of a push, in reverse topo order (parents
+ * before children). Once all commits have been visited, call {@link
+ * #getGroups()} for the result.
+ */
+public class GroupCollector {
+  private static final Logger log =
+      LoggerFactory.getLogger(GroupCollector.class);
+
+  public static List<String> getCurrentGroups(ReviewDb db, Change c)
+      throws OrmException {
+    PatchSet ps = db.patchSets().get(c.currentPatchSetId());
+    return ps != null ? ps.getGroups() : null;
+  }
+
+  public static List<String> getDefaultGroups(PatchSet ps) {
+    return ImmutableList.of(ps.getRevision().get());
+  }
+
+  public static List<String> getGroups(RevisionResource rsrc) {
+    if (rsrc.getEdit().isPresent()) {
+      // Groups for an edit are just the base revision's groups, since they have
+      // the same parent.
+      return rsrc.getEdit().get().getBasePatchSet().getGroups();
+    }
+    return rsrc.getPatchSet().getGroups();
+  }
+
+  private static interface Lookup {
+    List<String> lookup(PatchSet.Id psId) throws OrmException;
+  }
+
+  private final Multimap<ObjectId, PatchSet.Id> patchSetsBySha;
+  private final Multimap<ObjectId, String> groups;
+  private final SetMultimap<String, String> groupAliases;
+  private final Lookup groupLookup;
+
+  private boolean done;
+
+  private GroupCollector(
+      Multimap<ObjectId, PatchSet.Id> patchSetsBySha,
+      Lookup groupLookup) {
+    this.patchSetsBySha = patchSetsBySha;
+    this.groupLookup = groupLookup;
+    groups = ArrayListMultimap.create();
+    groupAliases = HashMultimap.create();
+  }
+
+  public GroupCollector(
+      Multimap<ObjectId, Ref> changeRefsById,
+      final ReviewDb db) {
+    this(
+        Multimaps.transformValues(
+            changeRefsById,
+            new Function<Ref, PatchSet.Id>() {
+              @Override
+              public PatchSet.Id apply(Ref in) {
+                return PatchSet.Id.fromRef(in.getName());
+              }
+            }),
+        new Lookup() {
+          @Override
+          public List<String> lookup(PatchSet.Id psId) throws OrmException {
+            PatchSet ps = db.patchSets().get(psId);
+            return ps != null ? ps.getGroups() : null;
+          }
+        });
+  }
+
+  @VisibleForTesting
+  GroupCollector(
+      Multimap<ObjectId, PatchSet.Id> patchSetsBySha,
+      final ListMultimap<PatchSet.Id, String> groupLookup) {
+    this(
+        patchSetsBySha,
+        new Lookup() {
+          @Override
+          public List<String> lookup(PatchSet.Id psId) {
+            List<String> groups = groupLookup.get(psId);
+            return !groups.isEmpty() ? groups : null;
+          }
+        });
+  }
+
+  public void visit(RevCommit c) {
+    checkState(!done, "visit() called after getGroups()");
+    Set<RevCommit> interestingParents = getInterestingParents(c);
+
+    if (interestingParents.size() == 0) {
+      // All parents are uninteresting: treat this commit as the root of a new
+      // group of related changes.
+      groups.put(c, c.name());
+      return;
+    } else if (interestingParents.size() == 1) {
+      // Only one parent is new in this push. If it is the only parent, just use
+      // that parent's group. If there are multiple parents, perhaps this commit
+      // is a merge of a side branch. This commit belongs in that parent's group
+      // in that case.
+      groups.putAll(c, groups.get(interestingParents.iterator().next()));
+      return;
+    }
+
+    // Multiple parents, merging at least two branches containing new commits in
+    // this push.
+    Set<String> thisCommitGroups = new TreeSet<>();
+    Set<String> parentGroupsNewInThisPush =
+        Sets.newLinkedHashSetWithExpectedSize(interestingParents.size());
+    for (RevCommit p : interestingParents) {
+      Collection<String> parentGroups = groups.get(p);
+      if (parentGroups.isEmpty()) {
+        throw new IllegalStateException(String.format(
+            "no group assigned to parent %s of commit %s", p.name(), c.name()));
+      }
+
+      for (String parentGroup : parentGroups) {
+        if (isGroupFromExistingPatchSet(p, parentGroup)) {
+          // This parent's group is from an existing patch set, i.e. the parent
+          // not new in this push. Use this group for the commit.
+          thisCommitGroups.add(parentGroup);
+        } else {
+          // This parent's group is new in this push.
+          parentGroupsNewInThisPush.add(parentGroup);
+        }
+      }
+    }
+
+    Iterable<String> toAlias;
+    if (thisCommitGroups.isEmpty()) {
+      // All parent groups were new in this push. Pick the first one and alias
+      // other parents' groups to this first parent.
+      String firstParentGroup = parentGroupsNewInThisPush.iterator().next();
+      thisCommitGroups = ImmutableSet.of(firstParentGroup);
+      toAlias = Iterables.skip(parentGroupsNewInThisPush, 1);
+    } else {
+      // For each parent group that was new in this push, alias it to the actual
+      // computed group(s) for this commit.
+      toAlias = parentGroupsNewInThisPush;
+    }
+    groups.putAll(c, thisCommitGroups);
+    for (String pg : toAlias) {
+      groupAliases.putAll(pg, thisCommitGroups);
+    }
+  }
+
+  public SetMultimap<ObjectId, String> getGroups() throws OrmException {
+    done = true;
+    SetMultimap<ObjectId, String> result = MultimapBuilder
+        .hashKeys(groups.keySet().size())
+        .treeSetValues()
+        .build();
+    for (Map.Entry<ObjectId, Collection<String>> e
+        : groups.asMap().entrySet()) {
+      ObjectId id = e.getKey();
+      result.putAll(id.copy(), resolveGroups(id, e.getValue()));
+    }
+    return result;
+  }
+
+  private Set<RevCommit> getInterestingParents(RevCommit commit) {
+    Set<RevCommit> result =
+        Sets.newLinkedHashSetWithExpectedSize(commit.getParentCount());
+    for (RevCommit p : commit.getParents()) {
+      if (!p.has(UNINTERESTING)) {
+        result.add(p);
+      }
+    }
+    return result;
+  }
+
+  private boolean isGroupFromExistingPatchSet(RevCommit commit, String group) {
+    ObjectId id = parseGroup(commit, group);
+    return id != null && patchSetsBySha.containsKey(id);
+  }
+
+  private Set<String> resolveGroups(ObjectId forCommit,
+      Collection<String> candidates) throws OrmException {
+    Set<String> actual = Sets.newTreeSet();
+    Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size());
+    Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size());
+    Deque<String> todo = new ArrayDeque<>(candidates);
+    // BFS through all aliases to find groups that are not aliased to anything
+    // else.
+    while (!todo.isEmpty()) {
+      String g = todo.removeFirst();
+      if (!seen.add(g)) {
+        continue;
+      }
+      Set<String> aliases = groupAliases.get(g);
+      if (aliases.isEmpty()) {
+        if (!done.contains(g)) {
+          Iterables.addAll(actual, resolveGroup(forCommit, g));
+          done.add(g);
+        }
+      } else {
+        todo.addAll(aliases);
+      }
+    }
+    return actual;
+  }
+
+  private ObjectId parseGroup(ObjectId forCommit, String group) {
+    try {
+      return ObjectId.fromString(group);
+    } catch (IllegalArgumentException e) {
+      // Shouldn't happen; some sort of corruption or manual tinkering?
+      log.warn("group for commit {} is not a SHA-1: {}",
+          forCommit.name(), group);
+      return null;
+    }
+  }
+
+  private Iterable<String> resolveGroup(ObjectId forCommit, String group)
+      throws OrmException {
+    ObjectId id = parseGroup(forCommit, group);
+    if (id != null) {
+      PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null);
+      if (psId != null) {
+        List<String> groups = groupLookup.lookup(psId);
+        // Group for existing patch set may be missing, e.g. if group has not
+        // been migrated yet.
+        if (groups != null && !groups.isEmpty()) {
+          return groups;
+        }
+      }
+    }
+    return ImmutableList.of(group);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
index 29948c2..1477f6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -18,18 +18,15 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-public class GroupList {
+public class GroupList extends TabFile {
   public static final String FILE_NAME = "groups";
   private final Map<AccountGroup.UUID, GroupReference> byUUID;
 
@@ -37,24 +34,14 @@
         this.byUUID = byUUID;
   }
 
-  public static GroupList parse(String text, ValidationError.Sink errors) throws IOException {
-    Map<AccountGroup.UUID, GroupReference> groupsByUUID = new HashMap<>();
-
-    BufferedReader br = new BufferedReader(new StringReader(text));
-    String s;
-    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
-      if (s.isEmpty() || s.startsWith("#")) {
-        continue;
-      }
-
-      int tab = s.indexOf('\t');
-      if (tab < 0) {
-        errors.error(new ValidationError(FILE_NAME, lineNumber, "missing tab delimiter"));
-        continue;
-      }
-
-      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
-      String name = s.substring(tab + 1).trim();
+  public static GroupList parse(String text, ValidationError.Sink errors)
+      throws IOException {
+    List<Row> rows = parse(text, FILE_NAME, TRIM, TRIM, errors);
+    Map<AccountGroup.UUID, GroupReference> groupsByUUID =
+        new HashMap<>(rows.size());
+    for(Row row : rows) {
+      AccountGroup.UUID uuid = new AccountGroup.UUID(row.left);
+      String name = row.right;
       GroupReference ref = new GroupReference(uuid, name);
 
       groupsByUUID.put(uuid, ref);
@@ -90,49 +77,19 @@
     byUUID.put(uuid, reference);
   }
 
-  private static String pad(int len, String src) {
-    if (len <= src.length()) {
-      return src;
-    }
-
-    StringBuilder r = new StringBuilder(len);
-    r.append(src);
-    while (r.length() < len) {
-      r.append(' ');
-    }
-    return r.toString();
-  }
-
-  private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
-    ArrayList<T> r = new ArrayList<>(m);
-    Collections.sort(r);
-    return r;
-  }
-
   public String asText() {
     if (byUUID.isEmpty()) {
       return null;
     }
 
-    final int uuidLen = 40;
-    StringBuilder buf = new StringBuilder();
-    buf.append(pad(uuidLen, "# UUID"));
-    buf.append('\t');
-    buf.append("Group Name");
-    buf.append('\n');
-
-    buf.append('#');
-    buf.append('\n');
-
+    List<Row> rows = new ArrayList<>(byUUID.size());
     for (GroupReference g : sort(byUUID.values())) {
       if (g.getUUID() != null && g.getName() != null) {
-        buf.append(pad(uuidLen, g.getUUID().get()));
-        buf.append('\t');
-        buf.append(g.getName());
-        buf.append('\n');
+        rows.add(new Row(g.getUUID().get(), g.getName()));
       }
     }
-    return buf.toString();
+
+    return asText("UUID", "Group Name", rows);
   }
 
   public void retainUUIDs(Collection<AccountGroup.UUID> toBeRetained) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/HackPushNegotiateHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/HackPushNegotiateHook.java
new file mode 100644
index 0000000..5cd7dc3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/HackPushNegotiateHook.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2010 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.server.git;
+
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+
+import com.google.common.collect.Sets;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.BaseReceivePack;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.UploadPack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Advertises part of history to git push clients.
+ * <p>
+ * This is a hack to work around the lack of negotiation in the
+ * send-pack/receive-pack wire protocol.
+ * <p>
+ * When the server is frequently advancing master by creating merge commits, the
+ * client may not be able to discover a common ancestor during push. Attempting
+ * to push will re-upload a very large amount of history. This hook hacks in a
+ * fake negotiation replacement by walking history and sending recent commits as
+ * {@code ".have"} lines in the wire protocol, allowing the client to find a
+ * common ancestor.
+ */
+public class HackPushNegotiateHook implements AdvertiseRefsHook {
+  private static final Logger log = LoggerFactory
+      .getLogger(HackPushNegotiateHook.class);
+
+  /** Size of an additional ".have" line. */
+  private static final int HAVE_LINE_LEN = 4
+      + Constants.OBJECT_ID_STRING_LENGTH
+      + 1 + 5 + 1;
+
+  /**
+   * Maximum number of bytes to "waste" in the advertisement with a peek at this
+   * repository's current reachable history.
+   */
+  private static final int MAX_EXTRA_BYTES = 8192;
+
+  /**
+   * Number of recent commits to advertise immediately, hoping to show a client
+   * a nearby merge base.
+   */
+  private static final int BASE_COMMITS = 64;
+
+  /** Number of commits to skip once base has already been shown. */
+  private static final int STEP_COMMITS = 16;
+
+  /** Total number of commits to extract from the history. */
+  private static final int MAX_HISTORY = MAX_EXTRA_BYTES / HAVE_LINE_LEN;
+
+  @Override
+  public void advertiseRefs(UploadPack us) {
+    throw new UnsupportedOperationException(
+        "HackPushNegotiateHook cannot be used for UploadPack");
+  }
+
+  @Override
+  public void advertiseRefs(BaseReceivePack rp)
+      throws ServiceMayNotContinueException {
+    Map<String, Ref> r = rp.getAdvertisedRefs();
+    if (r == null) {
+      try {
+        r = rp.getRepository().getRefDatabase().getRefs(ALL);
+      } catch (ServiceMayNotContinueException e) {
+        throw e;
+      } catch (IOException e) {
+        ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
+        ex.initCause(e);
+        throw ex;
+      }
+    }
+    rp.setAdvertisedRefs(r, history(r.values(), rp));
+  }
+
+  private Set<ObjectId> history(Collection<Ref> refs, BaseReceivePack rp) {
+    Set<ObjectId> alreadySending = rp.getAdvertisedObjects();
+    if (alreadySending.isEmpty()) {
+      alreadySending = idsOf(refs);
+    }
+
+    int max = MAX_HISTORY - Math.max(0, alreadySending.size() - refs.size());
+    if (max <= 0) {
+      return Collections.emptySet();
+    }
+
+    // Scan history until the advertisement is full.
+    RevWalk rw = rp.getRevWalk();
+    try {
+      for (Ref ref : refs) {
+        try {
+          if (ref.getObjectId() != null) {
+            rw.markStart(rw.parseCommit(ref.getObjectId()));
+          }
+        } catch (IOException badCommit) {
+          continue;
+        }
+      }
+
+      Set<ObjectId> history = Sets.newHashSetWithExpectedSize(max);
+      try {
+        int stepCnt = 0;
+        for (RevCommit c; history.size() < max && (c = rw.next()) != null;) {
+          if (c.getParentCount() <= 1
+              && !alreadySending.contains(c)
+              && (history.size() < BASE_COMMITS || (++stepCnt % STEP_COMMITS) == 0)) {
+            history.add(c);
+          }
+        }
+      } catch (IOException err) {
+        log.error("error trying to advertise history", err);
+      }
+      return history;
+    } finally {
+      rw.reset();
+    }
+  }
+
+  private static Set<ObjectId> idsOf(Collection<Ref> refs) {
+    Set<ObjectId> r = Sets.newHashSetWithExpectedSize(refs.size());
+    for (Ref ref : refs) {
+      if (ref.getObjectId() != null) {
+        r.add(ref.getObjectId());
+      }
+    }
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java
new file mode 100644
index 0000000..4c7f637
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/InMemoryInserter.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2016 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.server.git;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PackParser;
+
+public class InMemoryInserter extends ObjectInserter {
+  private final ObjectReader reader;
+  private final Map<ObjectId, InsertedObject> inserted = new LinkedHashMap<>();
+  private final boolean closeReader;
+
+  public InMemoryInserter(ObjectReader reader) {
+    this.reader = checkNotNull(reader);
+    closeReader = false;
+  }
+
+  public InMemoryInserter(Repository repo) {
+    this.reader = repo.newObjectReader();
+    closeReader = true;
+  }
+
+  @Override
+  public ObjectId insert(int type, long length, InputStream in) throws IOException {
+    return insert(InsertedObject.create(type, in));
+  }
+
+  @Override
+  public ObjectId insert(int type, byte[] data) {
+    return insert(type, data, 0, data.length);
+  }
+
+  @Override
+  public ObjectId insert(int type, byte[] data, int off, int len) {
+    return insert(InsertedObject.create(type, data, off, len));
+  }
+
+  public ObjectId insert(InsertedObject obj) {
+    inserted.put(obj.id(), obj);
+    return obj.id();
+  }
+
+  @Override
+  public PackParser newPackParser(InputStream in) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ObjectReader newReader() {
+    return new Reader();
+  }
+
+  @Override
+  public void flush() {
+    // Do nothing; objects are not written to the repo.
+  }
+
+  @Override
+  public void close() {
+    if (closeReader) {
+      reader.close();
+    }
+  }
+
+  public ImmutableList<InsertedObject> getInsertedObjects() {
+    return ImmutableList.copyOf(inserted.values());
+  }
+
+  public void clear() {
+    inserted.clear();
+  }
+
+  private class Reader extends ObjectReader {
+    @Override
+    public ObjectReader newReader() {
+      return new Reader();
+    }
+
+    @Override
+    public Collection<ObjectId> resolve(AbbreviatedObjectId id) throws IOException {
+      Set<ObjectId> result = new HashSet<>();
+      for (ObjectId insId : inserted.keySet()) {
+        if (id.prefixCompare(insId) == 0) {
+          result.add(insId);
+        }
+      }
+      result.addAll(reader.resolve(id));
+      return result;
+    }
+
+    @Override
+    public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
+      InsertedObject obj = inserted.get(objectId);
+      if (obj == null) {
+        return reader.open(objectId, typeHint);
+      }
+      if (typeHint != OBJ_ANY && obj.type() != typeHint) {
+        throw new IncorrectObjectTypeException(objectId.copy(), typeHint);
+      }
+      return obj.newLoader();
+    }
+
+    @Override
+    public Set<ObjectId> getShallowCommits() throws IOException {
+      return reader.getShallowCommits();
+    }
+
+    @Override
+    public void close() {
+      // Do nothing; this class owns no open resources.
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/InsertException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/InsertException.java
deleted file mode 100644
index 575ad52..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/InsertException.java
+++ /dev/null
@@ -1,30 +0,0 @@
-//Copyright (C) 2014 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.server.git;
-
-/**
- * Thrown in inserting change or patchset, e.g. OrmException or IOException.
- */
-public class InsertException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  InsertException(final String msg) {
-    super(msg, null);
-  }
-
-  InsertException(final String msg, final Throwable why) {
-    super(msg, why);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/InsertedObject.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/InsertedObject.java
new file mode 100644
index 0000000..8a766af
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/InsertedObject.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2016 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.server.git;
+
+import com.google.auto.value.AutoValue;
+import com.google.protobuf.ByteString;
+import java.io.IOException;
+import java.io.InputStream;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+
+@AutoValue
+public abstract class InsertedObject {
+  static InsertedObject create(int type, InputStream in) throws IOException {
+    return create(type, ByteString.readFrom(in));
+  }
+
+  static InsertedObject create(int type, ByteString bs) {
+    ObjectId id;
+    try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
+      id = fmt.idFor(type, bs.size(), bs.newInput());
+    } catch (IOException e) {
+      throw new IllegalStateException(e);
+    }
+    return new AutoValue_InsertedObject(id, type, bs);
+  }
+
+  static InsertedObject create(int type, byte[] src, int off, int len) {
+    return create(type, ByteString.copyFrom(src, off, len));
+  }
+
+  public abstract ObjectId id();
+
+  public abstract int type();
+
+  public abstract ByteString data();
+
+  ObjectLoader newLoader() {
+    return new ObjectLoader.SmallObject(type(), data().toByteArray());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/IntegrationException.java
similarity index 73%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/IntegrationException.java
index d3ebb95..58d4e6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/IntegrationException.java
@@ -14,19 +14,19 @@
 
 package com.google.gerrit.server.git;
 
-/** Indicates the current branch's queue cannot be processed at this time. */
-public class MergeException extends Exception {
+/** Indicates an integration operation (see {@link MergeOp}) failed. */
+public class IntegrationException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public MergeException(String msg) {
+  public IntegrationException(String msg) {
     super(msg);
   }
 
-  public MergeException(Throwable why) {
+  public IntegrationException(Throwable why) {
     super(why);
   }
 
-  public MergeException(String msg, Throwable why) {
+  public IntegrationException(String msg, Throwable why) {
     super(msg, why);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 633c3bb..2d82445 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -27,8 +27,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import com.jcraft.jsch.Session;
-
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.Config;
@@ -39,9 +37,6 @@
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig;
-import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -51,7 +46,14 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.concurrent.locks.Lock;
@@ -84,15 +86,6 @@
 
     @Override
     public void start() {
-      // Install our own factory which always runs in batch mode, as we
-      // have no UI available for interactive prompting.
-      SshSessionFactory.setInstance(new JschConfigSessionFactory() {
-        @Override
-        protected void configure(OpenSshConfig.Host hc, Session session) {
-          // Default configuration is batch mode.
-        }
-      });
-
       WindowCacheConfig cfg = new WindowCacheConfig();
       cfg.fromConfig(serverConfig);
       if (serverConfig.getString("core", null, "streamFileThreshold") == null) {
@@ -128,8 +121,8 @@
     }
   }
 
-  private final File basePath;
-  private final File noteDbPath;
+  private final Path basePath;
+  private final Path noteDbPath;
   private final Lock namesUpdateLock;
   private volatile SortedSet<Project.NameKey> names;
 
@@ -153,7 +146,7 @@
   }
 
   /** @return base directory under which all projects are stored. */
-  public File getBasePath() {
+  public Path getBasePath() {
     return basePath;
   }
 
@@ -163,12 +156,12 @@
     return openRepository(basePath, name);
   }
 
-  private Repository openRepository(File path, Project.NameKey name)
+  private Repository openRepository(Path path, Project.NameKey name)
       throws RepositoryNotFoundException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
-    File gitDir = new File(path, name.get());
+    File gitDir = path.resolve(name.get()).toFile();
     if (!names.contains(name)) {
       // The this.names list does not hold the project-name but it can still exist
       // on disk; for instance when the project has been created directly on the
@@ -208,19 +201,19 @@
   public Repository createRepository(Project.NameKey name)
       throws RepositoryNotFoundException, RepositoryCaseMismatchException {
     Repository repo = createRepository(basePath, name);
-    if (noteDbPath != null) {
+    if (noteDbPath != null && !noteDbPath.equals(basePath)) {
       createRepository(noteDbPath, name);
     }
     return repo;
   }
 
-  private Repository createRepository(File path, Project.NameKey name)
+  private Repository createRepository(Path path, Project.NameKey name)
       throws RepositoryNotFoundException, RepositoryCaseMismatchException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
 
-    File dir = FileKey.resolve(new File(path, name.get()), FS.DETECTED);
+    File dir = FileKey.resolve(path.resolve(name.get()).toFile(), FS.DETECTED);
     FileKey loc;
     if (dir != null) {
       // Already exists on disk, use the repository we found.
@@ -235,7 +228,7 @@
       // of the repository name, so prefer the standard bare name.
       //
       String n = name.get() + Constants.DOT_GIT_EXT;
-      loc = FileKey.exact(new File(path, n), FS.DETECTED);
+      loc = FileKey.exact(path.resolve(n).toFile(), FS.DETECTED);
     }
 
     try {
@@ -295,11 +288,8 @@
   @Override
   public String getProjectDescription(final Project.NameKey name)
       throws RepositoryNotFoundException, IOException {
-    final Repository e = openRepository(name);
-    try {
+    try (Repository e = openRepository(name)) {
       return getProjectDescription(e);
-    } finally {
-      e.close();
     }
   }
 
@@ -326,38 +316,30 @@
   }
 
   @Override
-  public void setProjectDescription(final Project.NameKey name,
-      final String description) {
+  public void setProjectDescription(Project.NameKey name, String description) {
     // Update git's description file, in case gitweb is being used
     //
-    try {
-      final Repository e = openRepository(name);
-      try {
-        final String old = getProjectDescription(e);
-        if ((old == null && description == null)
-            || (old != null && old.equals(description))) {
-          return;
-        }
-
-        final LockFile f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED);
-        if (f.lock()) {
-          String d = description;
-          if (d != null) {
-            d = d.trim();
-            if (d.length() > 0) {
-              d += "\n";
-            }
-          } else {
-            d = "";
-          }
-          f.write(Constants.encode(d));
-          f.commit();
-        }
-      } finally {
-        e.close();
+    try (Repository e = openRepository(name)) {
+      String old = getProjectDescription(e);
+      if ((old == null && description == null)
+          || (old != null && old.equals(description))) {
+        return;
       }
-    } catch (RepositoryNotFoundException e) {
-      log.error("Cannot update description for " + name, e);
+
+      LockFile f = new LockFile(new File(e.getDirectory(), "description"));
+      if (f.lock()) {
+        String d = description;
+        if (d != null) {
+          d = d.trim();
+          if (d.length() > 0) {
+            d += "\n";
+          }
+        } else {
+          d = "";
+        }
+        f.write(Constants.encode(d));
+        f.commit();
+      }
     } catch (IOException e) {
       log.error("Cannot update description for " + name, e);
     }
@@ -366,28 +348,25 @@
   private boolean isUnreasonableName(final Project.NameKey nameKey) {
     final String name = nameKey.get();
 
-    if (name.length() == 0) return true; // no empty paths
-    if (name.charAt(name.length() -1) == '/') return true; // no suffix
-
-    if (name.indexOf('\\') >= 0) return true; // no windows/dos style paths
-    if (name.charAt(0) == '/') return true; // no absolute paths
-    if (new File(name).isAbsolute()) return true; // no absolute paths
-
-    if (name.startsWith("../")) return true; // no "l../etc/passwd"
-    if (name.contains("/../")) return true; // no "foo/../etc/passwd"
-    if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
-    if (name.contains("//")) return true; // windows UNC path can be "//..."
-    if (name.contains("?")) return true; // common unix wildcard
-    if (name.contains("%")) return true; // wildcard or string parameter
-    if (name.contains("*")) return true; // wildcard
-    if (name.contains(":")) return true; // Could be used for absolute paths in windows?
-    if (name.contains("<")) return true; // redirect input
-    if (name.contains(">")) return true; // redirect output
-    if (name.contains("|")) return true; // pipe
-    if (name.contains("$")) return true; // dollar sign
-    if (name.contains("\r")) return true; // carriage return
-
-    return false; // is a reasonable name
+    return name.length() == 0  // no empty paths
+      || name.charAt(name.length() -1) == '/' // no suffix
+      || name.indexOf('\\') >= 0 // no windows/dos style paths
+      || name.charAt(0) == '/' // no absolute paths
+      || new File(name).isAbsolute() // no absolute paths
+      || name.startsWith("../") // no "l../etc/passwd"
+      || name.contains("/../") // no "foo/../etc/passwd"
+      || name.contains("/./") // "foo/./foo" is insane to ask
+      || name.contains("//") // windows UNC path can be "//..."
+      || name.contains(".git/") // no path segments that end with '.git' as "foo.git/bar"
+      || name.contains("?") // common unix wildcard
+      || name.contains("%") // wildcard or string parameter
+      || name.contains("*") // wildcard
+      || name.contains(":") // Could be used for absolute paths in windows?
+      || name.contains("<") // redirect input
+      || name.contains(">") // redirect output
+      || name.contains("|") // pipe
+      || name.contains("$") // dollar sign
+      || name.contains("\r"); // carriage return
   }
 
   @Override
@@ -397,51 +376,60 @@
     // scanning the filesystem. Don't rely on the cached names collection.
     namesUpdateLock.lock();
     try {
-      SortedSet<Project.NameKey> n = new TreeSet<>();
-      scanProjects(basePath, "", n);
-      names = Collections.unmodifiableSortedSet(n);
-      return n;
+      ProjectVisitor visitor = new ProjectVisitor();
+      try {
+        Files.walkFileTree(basePath, EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+            Integer.MAX_VALUE, visitor);
+      } catch (IOException e) {
+        log.error("Error walking repository tree " + basePath.toAbsolutePath(),
+            e);
+      }
+      return Collections.unmodifiableSortedSet(visitor.found);
     } finally {
       namesUpdateLock.unlock();
     }
   }
 
-  private void scanProjects(final File dir, final String prefix,
-      final SortedSet<Project.NameKey> names) {
-    final File[] ls = dir.listFiles();
-    if (ls == null) {
-      return;
+  private class ProjectVisitor extends SimpleFileVisitor<Path> {
+    private final SortedSet<Project.NameKey> found = new TreeSet<>();
+
+    @Override
+    public FileVisitResult preVisitDirectory(Path dir,
+        BasicFileAttributes attrs) throws IOException {
+      if (!dir.equals(basePath) && isRepo(dir)) {
+        addProject(dir);
+        return FileVisitResult.SKIP_SUBTREE;
+      }
+      return FileVisitResult.CONTINUE;
     }
 
-    for (File f : ls) {
-      String fileName = f.getName();
-      if (fileName.equals(Constants.DOT_GIT)) {
-        // Skip repositories named only `.git`
-      } else if (FileKey.isGitRepository(f, FS.DETECTED)) {
-        Project.NameKey nameKey = getProjectName(prefix, fileName);
-        if (isUnreasonableName(nameKey)) {
-          log.warn("Ignoring unreasonably named repository " + f.getAbsolutePath());
-        } else {
-          names.add(nameKey);
-        }
+    private boolean isRepo(Path p) {
+      String name = p.getFileName().toString();
+      return !name.equals(Constants.DOT_GIT)
+          && (name.endsWith(Constants.DOT_GIT_EXT)
+              || FileKey.isGitRepository(p.toFile(), FS.DETECTED));
+    }
 
-      } else if (f.isDirectory()) {
-        scanProjects(f, prefix + f.getName() + "/", names);
+    private void addProject(Path p) {
+      Project.NameKey nameKey = getProjectName(p);
+      if (isUnreasonableName(nameKey)) {
+        log.warn(
+            "Ignoring unreasonably named repository " + p.toAbsolutePath());
+      } else {
+        found.add(nameKey);
       }
     }
-  }
 
-  private Project.NameKey getProjectName(final String prefix,
-      final String fileName) {
-    final String projectName;
-    if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
-      int newLen = fileName.length() - Constants.DOT_GIT_EXT.length();
-      projectName = prefix + fileName.substring(0, newLen);
-
-    } else {
-      projectName = prefix + fileName;
+    private Project.NameKey getProjectName(Path p) {
+      String projectName = basePath.relativize(p).toString();
+      if (File.separatorChar != '/') {
+        projectName = projectName.replace(File.separatorChar, '/');
+      }
+      if (projectName.endsWith(Constants.DOT_GIT_EXT)) {
+        int newLen = projectName.length() - Constants.DOT_GIT_EXT.length();
+        projectName = projectName.substring(0, newLen);
+      }
+      return new Project.NameKey(projectName);
     }
-
-    return new Project.NameKey(projectName);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 05e864b..496b386 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -14,46 +14,52 @@
 
 package com.google.gerrit.server.git;
 
-import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static com.google.common.base.Preconditions.checkState;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Table;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.mail.MergeFailSender;
-import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -66,16 +72,14 @@
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -86,16 +90,17 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -116,22 +121,8 @@
  * be merged cleanly.
  */
 public class MergeOp {
-  public interface Factory {
-    MergeOp create(Branch.NameKey branch);
-  }
-
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
-  /** Amount of time to wait between submit and checking for missing deps. */
-  private static final long DEPENDENCY_DELAY =
-      MILLISECONDS.convert(15, MINUTES);
-
-  private static final long LOCK_FAILURE_RETRY_DELAY =
-      MILLISECONDS.convert(15, SECONDS);
-
-  private static final long MAX_SUBMIT_WINDOW =
-      MILLISECONDS.convert(12, HOURS);
-
   private final AccountCache accountCache;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeControl.GenericFactory changeControlFactory;
@@ -139,41 +130,48 @@
   private final ChangeHooks hooks;
   private final ChangeIndexer indexer;
   private final ChangeMessagesUtil cmUtil;
-  private final ChangeNotes.Factory notesFactory;
   private final ChangeUpdate.Factory updateFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final GitRepositoryManager repoManager;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final MergedSender.Factory mergedSenderFactory;
-  private final MergeFailSender.Factory mergeFailSenderFactory;
-  private final MergeQueue mergeQueue;
+  private final LabelNormalizer labelNormalizer;
+  private final EmailMerge.Factory mergedSenderFactory;
+  private final MergeSuperSet mergeSuperSet;
   private final MergeValidators.Factory mergeValidatorsFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ProjectCache projectCache;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final RequestScopePropagator requestScopePropagator;
-  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final InternalChangeQuery internalChangeQuery;
+  private final PersonIdent serverIdent;
   private final SubmitStrategyFactory submitStrategyFactory;
-  private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<SubmoduleOp> subOpProvider;
   private final TagCache tagCache;
-  private final WorkQueue workQueue;
 
-  private final String logPrefix;
-  private final Branch.NameKey destBranch;
-  private final ListMultimap<SubmitType, CodeReviewCommit> toMerge;
-  private final List<CodeReviewCommit> potentiallyStillSubmittable;
+  private final Map<Change.Id, List<SubmitRecord>> records;
   private final Map<Change.Id, CodeReviewCommit> commits;
-  private final List<Change> toUpdate;
+
+  private static final String MACHINE_ID;
+  static {
+    String id;
+    try {
+      id = InetAddress.getLocalHost().getHostAddress();
+    } catch (UnknownHostException e) {
+      id = "unknown";
+    }
+    MACHINE_ID = id;
+  }
+  private String staticSubmissionId;
+  private String submissionId;
 
   private ProjectState destProject;
   private ReviewDb db;
   private Repository repo;
-  private RevWalk rw;
+  private CodeReviewRevWalk rw;
   private RevFlag canMergeFlag;
-  private CodeReviewCommit branchTip;
-  private MergeTip mergeTip;
   private ObjectInserter inserter;
   private PersonIdent refLogIdent;
+  private Map<Branch.NameKey, RefUpdate> pendingRefUpdates;
+  private Map<Branch.NameKey, CodeReviewCommit> openBranches;
+  private Map<Branch.NameKey, MergeTip> mergeTips;
 
   @Inject
   MergeOp(AccountCache accountCache,
@@ -183,25 +181,21 @@
       ChangeHooks hooks,
       ChangeIndexer indexer,
       ChangeMessagesUtil cmUtil,
-      ChangeNotes.Factory notesFactory,
       ChangeUpdate.Factory updateFactory,
       GitReferenceUpdated gitRefUpdated,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      MergedSender.Factory mergedSenderFactory,
-      MergeFailSender.Factory mergeFailSenderFactory,
-      MergeQueue mergeQueue,
+      LabelNormalizer labelNormalizer,
+      EmailMerge.Factory mergedSenderFactory,
+      MergeSuperSet mergeSuperSet,
       MergeValidators.Factory mergeValidatorsFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       ProjectCache projectCache,
-      Provider<InternalChangeQuery> queryProvider,
-      RequestScopePropagator requestScopePropagator,
-      SchemaFactory<ReviewDb> schemaFactory,
+      InternalChangeQuery internalChangeQuery,
+      @GerritPersonIdent PersonIdent serverIdent,
       SubmitStrategyFactory submitStrategyFactory,
-      SubmoduleOp.Factory subOpFactory,
-      TagCache tagCache,
-      WorkQueue workQueue,
-      @Assisted Branch.NameKey branch) {
+      Provider<SubmoduleOp> subOpProvider,
+      TagCache tagCache) {
     this.accountCache = accountCache;
     this.approvalsUtil = approvalsUtil;
     this.changeControlFactory = changeControlFactory;
@@ -209,266 +203,369 @@
     this.hooks = hooks;
     this.indexer = indexer;
     this.cmUtil = cmUtil;
-    this.notesFactory = notesFactory;
     this.updateFactory = updateFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.repoManager = repoManager;
     this.identifiedUserFactory = identifiedUserFactory;
+    this.labelNormalizer = labelNormalizer;
     this.mergedSenderFactory = mergedSenderFactory;
-    this.mergeFailSenderFactory = mergeFailSenderFactory;
-    this.mergeQueue = mergeQueue;
+    this.mergeSuperSet = mergeSuperSet;
     this.mergeValidatorsFactory = mergeValidatorsFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.projectCache = projectCache;
-    this.queryProvider = queryProvider;
-    this.requestScopePropagator = requestScopePropagator;
-    this.schemaFactory = schemaFactory;
+    this.internalChangeQuery = internalChangeQuery;
+    this.serverIdent = serverIdent;
     this.submitStrategyFactory = submitStrategyFactory;
-    this.subOpFactory = subOpFactory;
+    this.subOpProvider = subOpProvider;
     this.tagCache = tagCache;
-    this.workQueue = workQueue;
-    logPrefix = String.format("[%s@%s]: ", branch.toString(),
-        ISODateTimeFormat.hourMinuteSecond().print(TimeUtil.nowMs()));
-    destBranch = branch;
-    toMerge = ArrayListMultimap.create();
-    potentiallyStillSubmittable = new ArrayList<>();
+
     commits = new HashMap<>();
-    toUpdate = Lists.newArrayList();
+    pendingRefUpdates = new HashMap<>();
+    openBranches = new HashMap<>();
+    pendingRefUpdates = new HashMap<>();
+    records = new HashMap<>();
+    mergeTips = new HashMap<>();
   }
 
-  private void setDestProject() throws MergeException {
+  private void setDestProject(Branch.NameKey destBranch)
+      throws IntegrationException {
     destProject = projectCache.get(destBranch.getParentKey());
     if (destProject == null) {
-      throw new MergeException("No such project: " + destBranch.getParentKey());
+      throw new IntegrationException(
+          "No such project: " + destBranch.getParentKey());
     }
   }
 
-  private void openSchema() throws OrmException {
-    if (db == null) {
-      db = schemaFactory.open();
-    }
+  private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
+    return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
+      @Override
+      public boolean apply(SubmitRecord input) {
+        return input.status == SubmitRecord.Status.OK;
+      }
+    });
   }
 
-  public void merge()
-      throws MergeException, NoSuchChangeException, IOException {
-    logDebug("Beginning merge attempt on {}", destBranch);
-    setDestProject();
-    try {
-      openSchema();
-      openRepository();
+  public static List<SubmitRecord> checkSubmitRule(ChangeData cd)
+      throws ResourceConflictException, OrmException {
+    PatchSet patchSet = cd.currentPatchSet();
+    if (patchSet == null) {
+      throw new ResourceConflictException(
+          "missing current patch set for change " + cd.getId());
+    }
+    List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
+        .setPatchSet(patchSet)
+        .evaluate();
+    Optional<SubmitRecord> ok = findOkRecord(results);
+    if (ok.isPresent()) {
+      // Rules supplied a valid solution.
+      return ImmutableList.of(ok.get());
+    } else if (results.isEmpty()) {
+      throw new IllegalStateException(String.format(
+          "SubmitRuleEvaluator.evaluate for change %s " +
+          "returned empty list for %s in %s",
+          cd.getId(),
+          patchSet.getId(),
+          cd.change().getProject().get()));
+    }
 
-      RefUpdate branchUpdate = openBranch();
-      boolean reopen = false;
+    for (SubmitRecord record : results) {
+      switch (record.status) {
+        case CLOSED:
+          throw new ResourceConflictException(String.format(
+              "change %s is closed", cd.getId()));
 
-      ListMultimap<SubmitType, Change> toSubmit =
-          validateChangeList(queryProvider.get().submitted(destBranch));
-      ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
-          ArrayListMultimap.create();
-      List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
-          new ArrayList<>();
-      while (!toMerge.isEmpty()) {
-        logDebug("Beginning merge iteration with {} left to merge",
-            toMerge.size());
-        toMergeNextTurn.clear();
-        Set<SubmitType> submitTypes = new HashSet<>(toMerge.keySet());
-        for (SubmitType submitType : submitTypes) {
-          if (reopen) {
-            logDebug("Reopening branch");
-            branchUpdate = openBranch();
-          }
-          SubmitStrategy strategy = createStrategy(submitType);
-          MergeTip mergeTip = preMerge(strategy, toMerge.get(submitType));
-          RefUpdate update = updateBranch(strategy, branchUpdate);
-          reopen = true;
+        case RULE_ERROR:
+          throw new ResourceConflictException(String.format(
+              "rule error for change %s: %s",
+              cd.getId(), record.errorMessage));
 
-          updateChangeStatus(toSubmit.get(submitType), mergeTip);
-          updateSubscriptions(toSubmit.get(submitType));
-          if (update != null) {
-            fireRefUpdated(update);
-          }
+        case NOT_READY:
+          StringBuilder msg = new StringBuilder();
+          msg.append(cd.getId() + ":");
+          for (SubmitRecord.Label lbl : record.labels) {
+            switch (lbl.status) {
+              case OK:
+              case MAY:
+                continue;
 
-          for (Iterator<CodeReviewCommit> it =
-              potentiallyStillSubmittable.iterator(); it.hasNext();) {
-            CodeReviewCommit commit = it.next();
-            if (containsMissingCommits(toMerge, commit)
-                || containsMissingCommits(toMergeNextTurn, commit)) {
-              // change has missing dependencies, but all commits which are
-              // missing are still attempted to be merged with another submit
-              // strategy, retry to merge this commit in the next turn
-              logDebug("Revision {} of patch set {} has missing dependencies"
-                  + " with different submit types, reconsidering on next run",
-                  commit.name(), commit.getPatchsetId());
-              it.remove();
-              commit.setStatusCode(null);
-              commit.missing = null;
-              toMergeNextTurn.put(submitType, commit);
+              case REJECT:
+                msg.append(" blocked by ").append(lbl.label);
+                msg.append(";");
+                continue;
+
+              case NEED:
+                msg.append(" needs ").append(lbl.label);
+                msg.append(";");
+                continue;
+
+              case IMPOSSIBLE:
+                msg.append(" needs ").append(lbl.label)
+                .append(" (check project access)");
+                msg.append(";");
+                continue;
+
+              default:
+                throw new IllegalStateException(String.format(
+                    "Unsupported SubmitRecord.Label %s for %s in %s in %s",
+                    lbl.toString(),
+                    patchSet.getId(),
+                    cd.getId(),
+                    cd.change().getProject().get()));
             }
           }
-          logDebug("Adding {} changes potentially submittable on next run",
-              potentiallyStillSubmittable.size());
-          potentiallyStillSubmittableOnNextRun.addAll(
-              potentiallyStillSubmittable);
-          potentiallyStillSubmittable.clear();
-        }
-        toMerge.clear();
-        toMerge.putAll(toMergeNextTurn);
-        logDebug("Adding {} changes to merge on next run", toMerge.size());
-      }
+          throw new ResourceConflictException(msg.toString());
 
-      updateChangeStatus(toUpdate, mergeTip);
-
-      for (CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
-        Capable capable = isSubmitStillPossible(commit);
-        if (capable != Capable.OK) {
-          sendMergeFail(commit.notes(),
-              message(commit.change(), capable.getMessage()), false);
-        }
-      }
-    } catch (NoSuchProjectException noProject) {
-      logWarn("Project " + destBranch.getParentKey() + " no longer exists,"
-          + " abandoning open changes");
-      abandonAllOpenChanges();
-    } catch (OrmException e) {
-      throw new MergeException("Cannot query the database", e);
-    } finally {
-      if (inserter != null) {
-        inserter.close();
-      }
-      if (rw != null) {
-        rw.close();
-      }
-      if (repo != null) {
-        repo.close();
-      }
-      if (db != null) {
-        db.close();
+        default:
+          throw new IllegalStateException(String.format(
+              "Unsupported SubmitRecord %s for %s in %s",
+              record,
+              patchSet.getId().getId(),
+              cd.change().getProject().get()));
       }
     }
+    throw new IllegalStateException();
   }
 
-  private boolean containsMissingCommits(
-      ListMultimap<SubmitType, CodeReviewCommit> map, CodeReviewCommit commit) {
-    if (!isSubmitForMissingCommitsStillPossible(commit)) {
-      return false;
-    }
+  private void checkSubmitRulesAndState(ChangeSet cs)
+      throws ResourceConflictException, OrmException {
 
-    for (CodeReviewCommit missingCommit : commit.missing) {
-      if (!map.containsValue(missingCommit)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  private boolean isSubmitForMissingCommitsStillPossible(
-      CodeReviewCommit commit) {
-    PatchSet.Id psId = commit.getPatchsetId();
-    if (commit.missing == null || commit.missing.isEmpty()) {
-      logDebug("Patch set {} is not submittable: no list of missing commits",
-          psId);
-      return false;
-    }
-
-    for (CodeReviewCommit missingCommit : commit.missing) {
+    StringBuilder msgbuf = new StringBuilder();
+    List<Change.Id> problemChanges = new ArrayList<>();
+    for (Change.Id id : cs.ids()) {
       try {
-        loadChangeInfo(missingCommit);
-      } catch (NoSuchChangeException | OrmException e) {
-        logError("Cannot check if missing commits can be submitted", e);
-        return false;
-      }
-
-      if (missingCommit.getPatchsetId() == null) {
-        // The commit doesn't have a patch set, so it cannot be
-        // submitted to the branch.
-        //
-        logDebug("Patch set {} is not submittable: dependency {} has no"
-            + " associated patch set", psId, missingCommit.name());
-        return false;
-      }
-
-      if (!missingCommit.change().currentPatchSetId().equals(
-          missingCommit.getPatchsetId())) {
-        PatchSet.Id missingId = missingCommit.getPatchsetId();
-        // If the missing commit is not the current patch set,
-        // the change must be rebased to use the proper parent.
-        //
-        logDebug("Patch set {} is not submittable: depends on patch set {} of"
-            + " change {}, but current patch set is {}", psId, missingId,
-            missingId.getParentKey(),
-            missingCommit.change().currentPatchSetId());
-        return false;
+        ChangeData cd = changeDataFactory.create(db, id);
+        if (cd.change().getStatus() != Change.Status.NEW){
+          throw new ResourceConflictException("Change " +
+              cd.change().getChangeId() + " is in state " +
+              cd.change().getStatus());
+        } else {
+          records.put(cd.change().getId(), checkSubmitRule(cd));
+        }
+      } catch (ResourceConflictException e) {
+        msgbuf.append(e.getMessage() + "\n");
+        problemChanges.add(id);
       }
     }
+    String reason = msgbuf.toString();
+    if (!reason.isEmpty()) {
+        throw new ResourceConflictException("The change could not be " +
+            "submitted because it depends on change(s) " +
+            problemChanges.toString() + ", which could not be submitted " +
+            "because:\n" + reason);
+    }
+  }
 
-    return true;
+  private void updateSubmissionId(Change change) {
+    Hasher h = Hashing.sha1().newHasher();
+    h.putLong(Thread.currentThread().getId())
+        .putUnencodedChars(MACHINE_ID);
+    staticSubmissionId = h.hash().toString().substring(0, 8);
+    submissionId = change.getId().get() + "-" + TimeUtil.nowMs() +
+        "-" + staticSubmissionId;
+  }
+
+  public void merge(ReviewDb db, Change change, IdentifiedUser caller,
+      boolean checkSubmitRules) throws NoSuchChangeException,
+      OrmException, ResourceConflictException {
+    updateSubmissionId(change);
+    this.db = db;
+    logDebug("Beginning integration of {}", change);
+    try {
+      ChangeSet cs = mergeSuperSet.completeChangeSet(db, change);
+      logDebug("Calculated to merge {}", cs);
+      if (checkSubmitRules) {
+        logDebug("Checking submit rules and state");
+        checkSubmitRulesAndState(cs);
+      }
+      try {
+        integrateIntoHistory(cs, caller);
+      } catch (IntegrationException e) {
+        logError("Merge Conflict", e);
+        throw new ResourceConflictException(e.getMessage());
+      }
+    } catch (IOException e) {
+      // Anything before the merge attempt is an error
+      throw new OrmException(e);
+    }
+  }
+
+  private void integrateIntoHistory(ChangeSet cs, IdentifiedUser caller)
+      throws IntegrationException, NoSuchChangeException,
+      ResourceConflictException {
+    logDebug("Beginning merge attempt on {}", cs);
+    Map<Branch.NameKey, ListMultimap<SubmitType, ChangeData>> toSubmit =
+        new HashMap<>();
+    logDebug("Perform the merges");
+    try {
+      Multimap<Project.NameKey, Branch.NameKey> br = cs.branchesByProject();
+      Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
+      for (Project.NameKey project : br.keySet()) {
+        openRepository(project);
+        for (Branch.NameKey branch : br.get(project)) {
+          setDestProject(branch);
+
+          ListMultimap<SubmitType, ChangeData> submitting =
+              validateChangeList(cbb.get(branch), caller);
+          toSubmit.put(branch, submitting);
+
+          Set<SubmitType> submitTypes = new HashSet<>(submitting.keySet());
+          for (SubmitType submitType : submitTypes) {
+            SubmitStrategy strategy = createStrategy(branch, submitType,
+                getBranchTip(branch), caller);
+
+            MergeTip mergeTip = preMerge(strategy, submitting.get(submitType),
+                getBranchTip(branch));
+            mergeTips.put(branch, mergeTip);
+            updateChangeStatus(submitting.get(submitType), branch,
+                true, caller);
+          }
+          inserter.flush();
+        }
+        closeRepository();
+      }
+      logDebug("Write out the new branch tips");
+      SubmoduleOp subOp = subOpProvider.get();
+      for (Project.NameKey project : br.keySet()) {
+        openRepository(project);
+        for (Branch.NameKey branch : br.get(project)) {
+
+          RefUpdate update = updateBranch(branch);
+          pendingRefUpdates.remove(branch);
+
+          setDestProject(branch);
+          ListMultimap<SubmitType, ChangeData> submitting = toSubmit.get(branch);
+          for (SubmitType submitType : submitting.keySet()) {
+            updateChangeStatus(submitting.get(submitType), branch,
+                false, caller);
+            updateSubmoduleSubscriptions(subOp, branch, getBranchTip(branch));
+          }
+          if (update != null) {
+            fireRefUpdated(branch, update);
+          }
+        }
+        closeRepository();
+      }
+
+      updateSuperProjects(subOp, br.values());
+      checkState(pendingRefUpdates.isEmpty(), "programmer error: "
+          + "pending ref update list not emptied");
+    } catch (NoSuchProjectException noProject) {
+      logWarn("Project " + noProject.project() + " no longer exists, "
+          + "abandoning open changes");
+      abandonAllOpenChanges(noProject.project());
+    } catch (OrmException e) {
+      throw new IntegrationException("Cannot query the database", e);
+    } catch (IOException e) {
+      throw new IntegrationException("Cannot query the database", e);
+    } finally {
+      closeRepository();
+    }
   }
 
   private MergeTip preMerge(SubmitStrategy strategy,
-      List<CodeReviewCommit> toMerge) throws MergeException {
-    logDebug("Running submit strategy {} for {} commits",
-        strategy.getClass().getSimpleName(), toMerge.size());
-    mergeTip = strategy.run(branchTip, toMerge);
+      List<ChangeData> submitted, CodeReviewCommit branchTip)
+      throws IntegrationException, OrmException {
+    logDebug("Running submit strategy {} for {} commits {}",
+        strategy.getClass().getSimpleName(), submitted.size(), submitted);
+    List<CodeReviewCommit> toMerge = new ArrayList<>(submitted.size());
+    for (ChangeData cd : submitted) {
+      CodeReviewCommit commit = commits.get(cd.change().getId());
+      checkState(commit != null,
+          "commit for %s not found by validateChangeList", cd.change().getId());
+      toMerge.add(commit);
+    }
+    MergeTip mergeTip = strategy.run(branchTip, toMerge);
     refLogIdent = strategy.getRefLogIdent();
     logDebug("Produced {} new commits", strategy.getNewCommits().size());
     commits.putAll(strategy.getNewCommits());
     return mergeTip;
   }
 
-  private SubmitStrategy createStrategy(SubmitType submitType)
-      throws MergeException, NoSuchProjectException {
+  private SubmitStrategy createStrategy(Branch.NameKey destBranch,
+      SubmitType submitType, CodeReviewCommit branchTip, IdentifiedUser caller)
+      throws IntegrationException, NoSuchProjectException {
     return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
-        canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
+        canMergeFlag, getAlreadyAccepted(branchTip), destBranch, caller);
   }
 
-  private void openRepository() throws MergeException, NoSuchProjectException {
-    Project.NameKey name = destBranch.getParentKey();
+  private void openRepository(Project.NameKey name)
+      throws IntegrationException, NoSuchProjectException {
     try {
       repo = repoManager.openRepository(name);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(name, notFound);
     } catch (IOException err) {
       String m = "Error opening repository \"" + name.get() + '"';
-      throw new MergeException(m, err);
+      throw new IntegrationException(m, err);
     }
 
     rw = CodeReviewCommit.newRevWalk(repo);
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
+    rw.setRetainBody(false);
     canMergeFlag = rw.newFlag("CAN_MERGE");
 
     inserter = repo.newObjectInserter();
   }
 
-  private RefUpdate openBranch()
-      throws MergeException, OrmException, NoSuchChangeException {
+  private void closeRepository() {
+    if (inserter != null) {
+      inserter.close();
+      inserter = null;
+    }
+    if (rw != null) {
+      rw.close();
+      rw = null;
+    }
+    if (repo != null) {
+      repo.close();
+      repo = null;
+    }
+  }
+
+  private RefUpdate getPendingRefUpdate(Branch.NameKey destBranch)
+      throws IntegrationException {
+
+    if (pendingRefUpdates.containsKey(destBranch)) {
+      logDebug("Access cached open branch {}: {}", destBranch.get(),
+          openBranches.get(destBranch));
+      return pendingRefUpdates.get(destBranch);
+    }
+
     try {
       RefUpdate branchUpdate = repo.updateRef(destBranch.get());
+      CodeReviewCommit branchTip;
       if (branchUpdate.getOldObjectId() != null) {
-        branchTip =
-            (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
-      } else if (repo.getFullBranch().equals(destBranch.get())) {
+        branchTip = rw.parseCommit(branchUpdate.getOldObjectId());
+      } else if (Objects.equals(repo.getFullBranch(), destBranch.get())) {
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
       } else {
-        for (ChangeData cd : queryProvider.get().submitted(destBranch)) {
-          try {
-            Change c = cd.change();
-            setNew(c, message(c, "Change could not be merged, "
-                + "because the destination branch does not exist anymore."));
-          } catch (OrmException e) {
-            log.error("Error setting change new", e);
-          }
-        }
+        throw new IntegrationException("The destination branch "
+            + destBranch.get() + " does not exist anymore.");
       }
+
       logDebug("Opened branch {}: {}", destBranch.get(), branchTip);
+      pendingRefUpdates.put(destBranch, branchUpdate);
+      openBranches.put(destBranch, branchTip);
       return branchUpdate;
     } catch (IOException e) {
-      throw new MergeException("Cannot open branch", e);
+      throw new IntegrationException("Cannot open branch", e);
+    }
+  }
+
+  private CodeReviewCommit getBranchTip(Branch.NameKey destBranch)
+      throws IntegrationException {
+    if (openBranches.containsKey(destBranch)) {
+      return openBranches.get(destBranch);
+    } else {
+      getPendingRefUpdate(destBranch);
+      return openBranches.get(destBranch);
     }
   }
 
   private Set<RevCommit> getAlreadyAccepted(CodeReviewCommit branchTip)
-      throws MergeException {
+      throws IntegrationException {
     Set<RevCommit> alreadyAccepted = new HashSet<>();
 
     if (branchTip != null) {
@@ -478,13 +575,16 @@
     try {
       for (Ref r : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
         try {
-          alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
+          CodeReviewCommit aac = rw.parseCommit(r.getObjectId());
+          if (!commits.values().contains(aac)) {
+            alreadyAccepted.add(aac);
+          }
         } catch (IncorrectObjectTypeException iote) {
           // Not a commit? Skip over it.
         }
       }
     } catch (IOException e) {
-      throw new MergeException(
+      throw new IntegrationException(
           "Failed to determine already accepted commits.", e);
     }
 
@@ -492,16 +592,18 @@
     return alreadyAccepted;
   }
 
-  private ListMultimap<SubmitType, Change> validateChangeList(
-      List<ChangeData> submitted) throws MergeException {
+  private ListMultimap<SubmitType, ChangeData> validateChangeList(
+      Collection<ChangeData> submitted, IdentifiedUser caller)
+      throws IntegrationException, ResourceConflictException,
+      NoSuchChangeException, OrmException {
     logDebug("Validating {} changes", submitted.size());
-    ListMultimap<SubmitType, Change> toSubmit = ArrayListMultimap.create();
+    ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create();
 
     Map<String, Ref> allRefs;
     try {
       allRefs = repo.getRefDatabase().getRefs(ALL);
     } catch (IOException e) {
-      throw new MergeException(e.getMessage(), e);
+      throw new IntegrationException(e.getMessage(), e);
     }
 
     Set<ObjectId> tips = new HashSet<>();
@@ -517,31 +619,30 @@
         // Reload change in case index was stale.
         chg = cd.reloadChange();
       } catch (OrmException e) {
-        throw new MergeException("Failed to validate changes", e);
+        throw new IntegrationException("Failed to validate changes", e);
       }
       Change.Id changeId = cd.getId();
-      if (chg.getStatus() != Change.Status.SUBMITTED) {
-        logDebug("Change {} is not submitted: {}", changeId, chg.getStatus());
+      if (chg.getStatus() != Change.Status.NEW) {
+        logDebug("Change {} is not new: {}", changeId, chg.getStatus());
         continue;
       }
       if (chg.currentPatchSetId() == null) {
         logError("Missing current patch set on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
       PatchSet ps;
+      Branch.NameKey destBranch = chg.getDest();
       try {
         ps = cd.currentPatchSet();
       } catch (OrmException e) {
-        throw new MergeException("Cannot query the database", e);
+        throw new IntegrationException("Cannot query the database", e);
       }
       if (ps == null || ps.getRevision() == null
           || ps.getRevision().get() == null) {
         logError("Missing patch set or revision on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -552,7 +653,6 @@
       } catch (IllegalArgumentException iae) {
         logError("Invalid revision on patch set " + ps.getId());
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -569,18 +669,15 @@
         logError("Revision " + idstr + " of patch set " + ps.getId()
             + " is not contained in any ref");
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
       CodeReviewCommit commit;
       try {
-        commit = (CodeReviewCommit) rw.parseCommit(id);
+        commit = rw.parseCommit(id);
       } catch (IOException e) {
-        logError(
-            "Invalid commit " + idstr + " on patch set " + ps.getId(), e);
+        logError("Invalid commit " + idstr + " on patch set " + ps.getId(), e);
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -592,51 +689,31 @@
       MergeValidators mergeValidators = mergeValidatorsFactory.create();
       try {
         mergeValidators.validatePreMerge(
-            repo, commit, destProject, destBranch, ps.getId());
+            repo, commit, destProject, destBranch, ps.getId(), caller);
       } catch (MergeValidationException mve) {
         logDebug("Revision {} of patch set {} failed validation: {}",
             idstr, ps.getId(), mve.getStatus());
         commit.setStatusCode(mve.getStatus());
-        toUpdate.add(chg);
         continue;
       }
 
-      if (branchTip != null) {
-        // If this commit is already merged its a bug in the queuing code
-        // that we got back here. Just mark it complete and move on. It's
-        // merged and that is all that mattered to the requestor.
-        //
-        try {
-          if (rw.isMergedInto(commit, branchTip)) {
-            logDebug("Revision {} of patch set {} is already merged",
-                idstr, ps.getId());
-            commit.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
-            try {
-              setMerged(chg, null, commit);
-            } catch (OrmException e) {
-              logError("Cannot mark change " + chg.getId() + " merged", e);
-            }
-            continue;
-          }
-        } catch (IOException err) {
-          throw new MergeException("Cannot perform merge base test", err);
-        }
-      }
-
       SubmitType submitType;
       submitType = getSubmitType(commit.getControl(), ps);
       if (submitType == null) {
         logError("No submit type for revision " + idstr + " of patch set "
             + ps.getId());
         commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE);
-        toUpdate.add(chg);
         continue;
       }
 
       commit.add(canMergeFlag);
-      toMerge.put(submitType, commit);
-      toSubmit.put(submitType, chg);
+      toSubmit.put(submitType, cd);
     }
+
+    List<ChangeData> notSubmittable = new ArrayList<>(submitted);
+    notSubmittable.removeAll(toSubmit.values());
+    updateChangeStatus(notSubmittable, null, false, caller);
+
     logDebug("Submitting on this run: {}", toSubmit);
     return toSubmit;
   }
@@ -657,13 +734,22 @@
     }
   }
 
-  private RefUpdate updateBranch(SubmitStrategy strategy,
-      RefUpdate branchUpdate) throws MergeException {
+  private RefUpdate updateBranch(Branch.NameKey destBranch)
+      throws IntegrationException {
+    RefUpdate branchUpdate = getPendingRefUpdate(destBranch);
+    CodeReviewCommit branchTip = getBranchTip(destBranch);
+
+    MergeTip mergeTip = mergeTips.get(destBranch);
+
     CodeReviewCommit currentTip =
         mergeTip != null ? mergeTip.getCurrentTip() : null;
     if (Objects.equals(branchTip, currentTip)) {
-      logDebug("Branch already at merge tip {}, no update to perform",
-          currentTip.name());
+      if (currentTip != null) {
+        logDebug("Branch already at merge tip {}, no update to perform",
+            currentTip.name());
+      } else {
+        logDebug("Both branch and merge tip are nonexistent, no update");
+      }
       return null;
     } else if (currentTip == null) {
       logDebug("No merge tip, no update to perform");
@@ -677,7 +763,7 @@
             new ProjectConfig(destProject.getProject().getNameKey());
         cfg.load(repo, currentTip);
       } catch (Exception e) {
-        throw new MergeException("Submit would store invalid"
+        throw new IntegrationException("Submit would store invalid"
             + " project configuration " + currentTip.name() + " for "
             + destProject.getProject().getName(), e);
       }
@@ -713,31 +799,22 @@
           return branchUpdate;
 
         case LOCK_FAILURE:
-          String msg;
-          if (strategy.retryOnLockFailure()) {
-            mergeQueue.recheckAfter(destBranch, LOCK_FAILURE_RETRY_DELAY,
-                MILLISECONDS);
-            msg = "will retry";
-          } else {
-            msg = "will not retry";
-          }
-          // TODO(dborowitz): Implement RefUpdate.toString().
-          throw new IOException(branchUpdate.getResult().name() + ", " + msg
-              + '\n' + branchUpdate);
+          throw new IntegrationException("Failed to lock " + branchUpdate.getName());
         default:
           throw new IOException(branchUpdate.getResult().name()
               + '\n' + branchUpdate);
       }
     } catch (IOException e) {
-      throw new MergeException("Cannot update " + branchUpdate.getName(), e);
+      throw new IntegrationException("Cannot update " + branchUpdate.getName(), e);
     }
   }
 
-  private void fireRefUpdated(RefUpdate branchUpdate) {
+  private void fireRefUpdated(Branch.NameKey destBranch,
+      RefUpdate branchUpdate) {
     logDebug("Firing ref updated hooks for {}", branchUpdate.getName());
     gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate);
     hooks.doRefUpdatedHook(destBranch, branchUpdate,
-        getAccount(mergeTip.getCurrentTip()));
+        getAccount(mergeTips.get(destBranch).getCurrentTip()));
   }
 
   private Account getAccount(CodeReviewCommit codeReviewCommit) {
@@ -758,10 +835,19 @@
     return "";
   }
 
-  private void updateChangeStatus(List<Change> submitted, MergeTip mergeTip)
-      throws NoSuchChangeException {
-    logDebug("Updating change status for {} changes", submitted.size());
-    for (Change c : submitted) {
+  private void updateChangeStatus(List<ChangeData> changes,
+      Branch.NameKey destBranch, boolean dryRun, IdentifiedUser caller)
+      throws NoSuchChangeException, IntegrationException, ResourceConflictException,
+      OrmException {
+    if (!dryRun) {
+      logDebug("Updating change status for {} changes", changes.size());
+    } else {
+      logDebug("Checking change state for {} changes in a dry run",
+          changes.size());
+    }
+    MergeTip mergeTip = destBranch != null ? mergeTips.get(destBranch) : null;
+    for (ChangeData cd : changes) {
+      Change c = cd.change();
       CodeReviewCommit commit = commits.get(c.getId());
       CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
       if (s == null) {
@@ -773,6 +859,14 @@
         continue;
       }
 
+      if (!dryRun) {
+        try {
+          setApproval(cd, caller);
+        } catch (IOException e) {
+          throw new OrmException(e);
+        }
+      }
+
       String txt = s.getMessage();
       logDebug("Status of change {} ({}) on {}: {}", c.getId(), commit.name(),
           c.getDest(), s);
@@ -780,23 +874,31 @@
       ObjectId mergeResultRev =
           mergeTip != null ? mergeTip.getMergeResults().get(commit) : null;
       try {
+        ChangeMessage msg;
         switch (s) {
           case CLEAN_MERGE:
-            setMerged(c, message(c, txt + getByAccountName(commit)),
-                mergeResultRev);
+            if (!dryRun) {
+              setMerged(c, message(c, txt + getByAccountName(commit)),
+                  mergeResultRev);
+            }
             break;
 
           case CLEAN_REBASE:
           case CLEAN_PICK:
-            setMerged(c, message(c, txt + " as " + commit.name()
-                + getByAccountName(commit)), mergeResultRev);
+            if (!dryRun) {
+              setMerged(c, message(c, txt + " as " + commit.name()
+                  + getByAccountName(commit)), mergeResultRev);
+            }
             break;
 
           case ALREADY_MERGED:
-            setMerged(c, null, mergeResultRev);
+            if (!dryRun) {
+              setMerged(c, null, mergeResultRev);
+            }
             break;
 
           case PATH_CONFLICT:
+          case REBASE_MERGE_CONFLICT:
           case MANUAL_RECURSIVE_MERGE:
           case CANNOT_CHERRY_PICK_ROOT:
           case NOT_FAST_FORWARD:
@@ -806,131 +908,62 @@
           case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND:
           case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT:
           case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN:
-            setNew(commit, message(c, txt));
-            break;
+            setNew(commit.notes(), message(c, txt));
+            throw new ResourceConflictException("Cannot merge " + commit.name()
+                + "\n" + s.getMessage());
 
           case MISSING_DEPENDENCY:
             logDebug("Change {} is missing dependency", c.getId());
-            potentiallyStillSubmittable.add(commit);
-            break;
+            throw new IntegrationException(
+                "Cannot merge " + commit.name() + "\n" + s.getMessage());
+
+          case REVISION_GONE:
+            logDebug("Commit not found for change {}", c.getId());
+            msg = new ChangeMessage(
+                new ChangeMessage.Key(
+                    c.getId(),
+                    ChangeUtil.messageUUID(db)),
+                null,
+                TimeUtil.nowTs(),
+                c.currentPatchSetId());
+            msg.setMessage("Failed to read commit for this patch set");
+            setNew(commit.notes(), msg);
+            throw new IntegrationException(msg.getMessage());
 
           default:
-            setNew(commit,
-                message(c, "Unspecified merge failure: " + s.name()));
-            break;
+            msg = message(c, "Unspecified merge failure: " + s.name());
+            setNew(commit.notes(), msg);
+            throw new IntegrationException(msg.getMessage());
         }
-      } catch (OrmException err) {
-        logWarn("Error updating change status for " + c.getId(), err);
-      } catch (IOException err) {
+      } catch (OrmException | IOException err) {
         logWarn("Error updating change status for " + c.getId(), err);
       }
     }
   }
 
-  private void updateSubscriptions(List<Change> submitted) {
+  private void updateSubmoduleSubscriptions(SubmoduleOp subOp,
+      Branch.NameKey destBranch, CodeReviewCommit branchTip) {
+    MergeTip mergeTip = mergeTips.get(destBranch);
     if (mergeTip != null
         && (branchTip == null || branchTip != mergeTip.getCurrentTip())) {
-      logDebug("Updating submodule subscriptions for {} changes",
-          submitted.size());
-      SubmoduleOp subOp =
-          subOpFactory.create(destBranch, mergeTip.getCurrentTip(), rw, repo,
-              destProject.getProject(), submitted, commits,
-              getAccount(mergeTip.getCurrentTip()));
+      logDebug("Updating submodule subscriptions for branch {}", destBranch);
       try {
-        subOp.update();
+        subOp.updateSubmoduleSubscriptions(db, destBranch);
       } catch (SubmoduleException e) {
-        logError(
-            "The gitLinks were not updated according to the subscriptions" , e);
+        logError("The submodule subscriptions were not updated according"
+            + "to the .gitmodules files", e);
       }
     }
   }
 
-  private Capable isSubmitStillPossible(CodeReviewCommit commit) {
-    Capable capable;
-    Change c = commit.change();
-    boolean submitStillPossible =
-        isSubmitForMissingCommitsStillPossible(commit);
-    long now = TimeUtil.nowMs();
-    long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
-    if (submitStillPossible && now < waitUntil) {
-      long recheckIn = waitUntil - now;
-      logDebug("Submit for {} is still possible; rechecking in {}ms",
-          c.getId(), recheckIn);
-      // If we waited a short while we might still be able to get
-      // this change submitted. Reschedule an attempt in a bit.
-      //
-      mergeQueue.recheckAfter(destBranch, recheckIn, MILLISECONDS);
-      capable = Capable.OK;
-    } else if (submitStillPossible) {
-      // It would be possible to submit the change if the missing
-      // dependencies are also submitted. Perhaps the user just
-      // forgot to submit those.
-      //
-      logDebug("Submit for {} is still possible after missing dependencies",
-          c.getId());
-      StringBuilder m = new StringBuilder();
-      m.append("Change could not be merged because of a missing dependency.");
-      m.append("\n");
-
-      m.append("\n");
-
-      m.append("The following changes must also be submitted:\n");
-      m.append("\n");
-      for (CodeReviewCommit missingCommit : commit.missing) {
-        m.append("* ");
-        m.append(missingCommit.change().getKey().get());
-        m.append("\n");
-      }
-      capable = new Capable(m.toString());
-    } else {
-      // It is impossible to submit this change as-is. The author
-      // needs to rebase it in order to work around the missing
-      // dependencies.
-      //
-      logDebug("Submit for {} is not possible", c.getId());
-      StringBuilder m = new StringBuilder();
-      m.append("Change cannot be merged due to unsatisfiable dependencies.\n");
-      m.append("\n");
-      m.append("The following dependency errors were found:\n");
-      m.append("\n");
-      for (CodeReviewCommit missingCommit : commit.missing) {
-        PatchSet.Id missingPsId = missingCommit.getPatchsetId();
-        if (missingPsId != null) {
-          m.append("* Depends on patch set ");
-          m.append(missingPsId.get());
-          m.append(" of ");
-          m.append(missingCommit.change().getKey().abbreviate());
-          PatchSet.Id currPsId = missingCommit.change().currentPatchSetId();
-          if (!missingPsId.equals(currPsId)) {
-            m.append(", however the current patch set is ");
-            m.append(currPsId.get());
-          }
-          m.append(".\n");
-
-        } else {
-          m.append("* Depends on commit ");
-          m.append(missingCommit.name());
-          m.append(" which has no change associated with it.\n");
-        }
-      }
-      m.append("\n");
-      m.append("Please rebase the change and upload a replacement commit.");
-      capable = new Capable(m.toString());
-    }
-
-    return capable;
-  }
-
-  private void loadChangeInfo(CodeReviewCommit commit)
-      throws NoSuchChangeException, OrmException {
-    if (commit.getControl() == null) {
-      List<PatchSet> matches =
-          db.patchSets().byRevision(new RevId(commit.name())).toList();
-      if (matches.size() == 1) {
-        PatchSet.Id psId = matches.get(0).getId();
-        commit.setPatchsetId(psId);
-        commit.setControl(changeControl(db.changes().get(psId.getParentKey())));
-      }
+  private void updateSuperProjects(SubmoduleOp subOp,
+      Collection<Branch.NameKey> branches) {
+    logDebug("Updating superprojects");
+    try {
+      subOp.updateSuperProjects(db, branches);
+    } catch (SubmoduleException e) {
+      logError("The gitlinks were not updated according to the "
+          + "subscriptions", e);
     }
   }
 
@@ -951,7 +984,7 @@
       throws OrmException, IOException {
     logDebug("Setting change {} merged", c.getId());
     ChangeUpdate update = null;
-    PatchSetApproval submitter;
+    final PatchSetApproval submitter;
     PatchSet merged;
     try {
       db.changes().beginTransaction(c.getId());
@@ -978,8 +1011,15 @@
       db.rollback();
     }
     update.commit();
-    sendMergedEmail(c, submitter);
     indexer.index(db, c);
+
+    try {
+      mergedSenderFactory.create(
+          c.getId(),
+          submitter != null ? submitter.getAccountId() : null).sendAsync();
+    } catch (Exception e) {
+      log.error("Cannot email merged notification for " + c.getId(), e);
+    }
     if (submitter != null && mergeResultRev != null) {
       try {
         hooks.doChangeMergedHook(c,
@@ -997,6 +1037,7 @@
       @Override
       public Change update(Change c) {
         c.setStatus(Change.Status.MERGED);
+        c.setSubmissionId(submissionId);
         if (!merged.equals(c.currentPatchSetId())) {
           // Uncool; the patch set changed after we merged it.
           // Go back to the patch set that was actually merged.
@@ -1013,41 +1054,139 @@
     });
   }
 
-  private void sendMergedEmail(final Change c, final PatchSetApproval from) {
-    workQueue.getDefaultQueue()
-        .submit(requestScopePropagator.wrap(new Runnable() {
-      @Override
-      public void run() {
-        PatchSet patchSet;
-        try {
-          ReviewDb reviewDb = schemaFactory.open();
-          try {
-            patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
-          } finally {
-            reviewDb.close();
-          }
-        } catch (Exception e) {
-          logError("Cannot send email for submitted patch set " + c.getId(), e);
-          return;
-        }
+  private void setApproval(ChangeData cd, IdentifiedUser user)
+      throws OrmException, IOException {
+    Timestamp timestamp = TimeUtil.nowTs();
+    ChangeControl control = cd.changeControl();
+    PatchSet.Id psId = cd.currentPatchSet().getId();
+    PatchSet.Id psIdNewRev = commits.get(cd.change().getId())
+        .change().currentPatchSetId();
 
-        try {
-          MergedSender cm = mergedSenderFactory.create(changeControl(c));
-          if (from != null) {
-            cm.setFrom(from.getAccountId());
-          }
-          cm.setPatchSet(patchSet);
-          cm.send();
-        } catch (Exception e) {
-          logError("Cannot send email for submitted patch set " + c.getId(), e);
-        }
-      }
+    logDebug("Add approval for " + cd + " from user " + user);
+    ChangeUpdate update = updateFactory.create(control, timestamp);
+    List<SubmitRecord> record = records.get(cd.change().getId());
+    if (record != null) {
+      update.merge(record);
+    }
+    db.changes().beginTransaction(cd.change().getId());
+    try {
+      BatchMetaDataUpdate batch = approve(control, psId, user,
+          update, timestamp);
+      batch.write(update, new CommitBuilder());
 
-      @Override
-      public String toString() {
-        return "send-email merged";
+      // If the submit strategy created a new revision (rebase, cherry-pick)
+      // approve that as well
+      if (!psIdNewRev.equals(psId)) {
+        batch = approve(control, psIdNewRev, user,
+            update, timestamp);
+        // Write update commit after all normalized label commits.
+        batch.write(update, new CommitBuilder());
       }
-    }));
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    indexer.index(db, cd.change());
+  }
+
+  private BatchMetaDataUpdate approve(ChangeControl control, PatchSet.Id psId,
+      IdentifiedUser user, ChangeUpdate update, Timestamp timestamp)
+          throws OrmException {
+    Map<PatchSetApproval.Key, PatchSetApproval> byKey = Maps.newHashMap();
+    for (PatchSetApproval psa :
+      approvalsUtil.byPatchSet(db, control, psId)) {
+      if (!byKey.containsKey(psa.getKey())) {
+        byKey.put(psa.getKey(), psa);
+      }
+    }
+
+    PatchSetApproval submit = new PatchSetApproval(
+          new PatchSetApproval.Key(
+              psId,
+              user.getAccountId(),
+              LabelId.SUBMIT),
+              (short) 1, TimeUtil.nowTs());
+    byKey.put(submit.getKey(), submit);
+    submit.setValue((short) 1);
+    submit.setGranted(timestamp);
+
+    // Flatten out existing approvals for this patch set based upon the current
+    // permissions. Once the change is closed the approvals are not updated at
+    // presentation view time, except for zero votes used to indicate a reviewer
+    // was added. So we need to make sure votes are accurate now. This way if
+    // permissions get modified in the future, historical records stay accurate.
+    LabelNormalizer.Result normalized =
+        labelNormalizer.normalize(control, byKey.values());
+
+    // TODO(dborowitz): Don't use a label in notedb; just check when status
+    // change happened.
+    update.putApproval(submit.getLabel(), submit.getValue());
+    logDebug("Adding submit label " + submit);
+
+    db.patchSetApprovals().upsert(normalized.getNormalized());
+    db.patchSetApprovals().update(zero(normalized.deleted()));
+
+    try {
+      return saveToBatch(control, update, normalized, timestamp);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  private static Iterable<PatchSetApproval> zero(
+      Iterable<PatchSetApproval> approvals) {
+    return Iterables.transform(approvals,
+        new Function<PatchSetApproval, PatchSetApproval>() {
+          @Override
+          public PatchSetApproval apply(PatchSetApproval in) {
+            PatchSetApproval copy = new PatchSetApproval(in.getPatchSetId(), in);
+            copy.setValue((short) 0);
+            return copy;
+          }
+        });
+  }
+
+
+  private BatchMetaDataUpdate saveToBatch(ChangeControl ctl,
+      ChangeUpdate callerUpdate, LabelNormalizer.Result normalized,
+      Timestamp timestamp) throws IOException {
+    Table<Account.Id, String, Optional<Short>> byUser = HashBasedTable.create();
+    for (PatchSetApproval psa : normalized.updated()) {
+      byUser.put(psa.getAccountId(), psa.getLabel(),
+          Optional.of(psa.getValue()));
+    }
+    for (PatchSetApproval psa : normalized.deleted()) {
+      byUser.put(psa.getAccountId(), psa.getLabel(), Optional.<Short> absent());
+    }
+
+    BatchMetaDataUpdate batch = callerUpdate.openUpdate();
+    for (Account.Id accountId : byUser.rowKeySet()) {
+      if (!accountId.equals(callerUpdate.getUser().getAccountId())) {
+        ChangeUpdate update = updateFactory.create(
+            ctl.forUser(identifiedUserFactory.create(accountId)), timestamp);
+        update.setSubject("Finalize approvals at submit");
+        putApprovals(update, byUser.row(accountId));
+
+        CommitBuilder commit = new CommitBuilder();
+        commit.setCommitter(new PersonIdent(serverIdent, timestamp));
+        batch.write(update, commit);
+      }
+    }
+
+    putApprovals(callerUpdate,
+        byUser.row(callerUpdate.getUser().getAccountId()));
+    return batch;
+  }
+
+  private static void putApprovals(ChangeUpdate update,
+      Map<String, Optional<Short>> approvals) {
+    for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) {
+      if (e.getValue().isPresent()) {
+        update.putApproval(e.getKey(), e.getValue().get());
+      } else {
+        update.removeApproval(e.getKey());
+      }
+    }
   }
 
   private ChangeControl changeControl(Change c) throws NoSuchChangeException {
@@ -1055,88 +1194,10 @@
         c, identifiedUserFactory.create(c.getOwner()));
   }
 
-  private void setNew(CodeReviewCommit c, ChangeMessage msg)
+  private void setNew(ChangeNotes notes, final ChangeMessage msg)
       throws NoSuchChangeException, IOException {
-    sendMergeFail(c.notes(), msg, true);
-  }
+    Change c = notes.getChange();
 
-  private void setNew(Change c, ChangeMessage msg)
-      throws NoSuchChangeException, IOException {
-    sendMergeFail(notesFactory.create(c), msg, true);
-  }
-
-  private enum RetryStatus {
-    UNSUBMIT, RETRY_NO_MESSAGE, RETRY_ADD_MESSAGE
-  }
-
-  private RetryStatus getRetryStatus(
-      @Nullable PatchSetApproval submitter,
-      ChangeMessage msg,
-      ChangeNotes notes) {
-    Change.Id id = notes.getChangeId();
-    if (submitter != null) {
-      long sinceMs = TimeUtil.nowMs() - submitter.getGranted().getTime();
-      if (sinceMs > MAX_SUBMIT_WINDOW) {
-        logDebug("Change {} submitted {}ms ago, unsubmitting", id, sinceMs);
-        return RetryStatus.UNSUBMIT;
-      } else {
-        logDebug("Change {} submitted {}ms ago, within window", id, sinceMs);
-      }
-    } else {
-      logDebug("No submitter for change {}", id);
-    }
-
-    try {
-      ChangeMessage last = Iterables.getLast(cmUtil.byChange(db, notes));
-      if (last != null) {
-        if (Objects.equals(last.getAuthor(), msg.getAuthor())
-            && Objects.equals(last.getMessage(), msg.getMessage())) {
-          long lastMs = last.getWrittenOn().getTime();
-          long msgMs = msg.getWrittenOn().getTime();
-          long sinceMs = msgMs - lastMs;
-          if (sinceMs > MAX_SUBMIT_WINDOW) {
-            logDebug("Last message for change {} was {}ms ago, unsubmitting",
-                id, sinceMs);
-            return RetryStatus.UNSUBMIT;
-          } else {
-            logDebug("Last message for change {} was {}ms ago, within window",
-                id, sinceMs);
-            return RetryStatus.RETRY_NO_MESSAGE;
-          }
-        } else {
-          logDebug("Last message for change {} differed, adding message", id);
-        }
-      }
-      return RetryStatus.RETRY_ADD_MESSAGE;
-    } catch (OrmException err) {
-      logWarn("Cannot check previous merge failure, unsubmitting", err);
-      return RetryStatus.UNSUBMIT;
-    }
-  }
-
-  private void sendMergeFail(ChangeNotes notes, final ChangeMessage msg,
-      boolean makeNew) throws NoSuchChangeException, IOException {
-    logDebug("Possibly sending merge failure notification for {}",
-        notes.getChangeId());
-    PatchSetApproval submitter = null;
-    try {
-      submitter = approvalsUtil.getSubmitter(
-          db, notes, notes.getChange().currentPatchSetId());
-    } catch (Exception e) {
-      logError("Cannot get submitter for change " + notes.getChangeId(), e);
-    }
-
-    if (!makeNew) {
-      RetryStatus retryStatus = getRetryStatus(submitter, msg, notes);
-      if (retryStatus == RetryStatus.RETRY_NO_MESSAGE) {
-        return;
-      } else if (retryStatus == RetryStatus.UNSUBMIT) {
-        makeNew = true;
-      }
-    }
-
-    final boolean setStatusNew = makeNew;
-    final Change c = notes.getChange();
     Change change = null;
     ChangeUpdate update = null;
     try {
@@ -1148,9 +1209,7 @@
           @Override
           public Change update(Change c) {
             if (c.getStatus().isOpen()) {
-              if (setStatusNew) {
-                c.setStatus(Change.Status.NEW);
-              }
+              c.setStatus(Change.Status.NEW);
               ChangeUtil.updated(c);
             }
             return c;
@@ -1173,58 +1232,15 @@
     if (update != null) {
       update.commit();
     }
+    indexer.index(db, change);
 
-    CheckedFuture<?, IOException> indexFuture;
-    if (change != null) {
-      indexFuture = indexer.indexAsync(change.getId());
-    } else {
-      indexFuture = null;
+    PatchSetApproval submitter = null;
+    try {
+      submitter = approvalsUtil.getSubmitter(
+          db, notes, notes.getChange().currentPatchSetId());
+    } catch (Exception e) {
+      logError("Cannot get submitter for change " + notes.getChangeId(), e);
     }
-    final PatchSetApproval from = submitter;
-    workQueue.getDefaultQueue()
-        .submit(requestScopePropagator.wrap(new Runnable() {
-      @Override
-      public void run() {
-        PatchSet patchSet;
-        try {
-          ReviewDb reviewDb = schemaFactory.open();
-          try {
-            patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
-          } finally {
-            reviewDb.close();
-          }
-        } catch (Exception e) {
-          logError("Cannot send email notifications about merge failure", e);
-          return;
-        }
-
-        try {
-          MergeFailSender cm = mergeFailSenderFactory.create(c);
-          if (from != null) {
-            cm.setFrom(from.getAccountId());
-          }
-          cm.setPatchSet(patchSet);
-          cm.setChangeMessage(msg);
-          cm.send();
-        } catch (Exception e) {
-          logError("Cannot send email notifications about merge failure", e);
-        }
-      }
-
-      @Override
-      public String toString() {
-        return "send-email merge-failed";
-      }
-    }));
-
-    if (indexFuture != null) {
-      try {
-        indexFuture.checkedGet();
-      } catch (IOException e) {
-        logError("Failed to index new change message", e);
-      }
-    }
-
     if (submitter != null) {
       try {
         hooks.doMergeFailedHook(c,
@@ -1236,24 +1252,14 @@
     }
   }
 
-  private void abandonAllOpenChanges() throws NoSuchChangeException {
-    Exception err = null;
+  private void abandonAllOpenChanges(Project.NameKey destProject)
+      throws NoSuchChangeException {
     try {
-      openSchema();
-      for (ChangeData cd
-          : queryProvider.get().byProjectOpen(destBranch.getParentKey())) {
+      for (ChangeData cd : internalChangeQuery.byProjectOpen(destProject)) {
         abandonOneChange(cd.change());
       }
-      db.close();
-      db = null;
-    } catch (IOException e) {
-      err = e;
-    } catch (OrmException e) {
-      err = e;
-    }
-    if (err != null) {
-      logWarn("Cannot abandon changes for deleted project "
-          + destBranch.getParentKey().get(), err);
+    } catch (IOException | OrmException e) {
+      logWarn("Cannot abandon changes for deleted project ", e);
     }
   }
 
@@ -1302,31 +1308,33 @@
 
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug(logPrefix + msg, args);
+      log.debug("[" + submissionId + "]" + msg, args);
     }
   }
 
   private void logWarn(String msg, Throwable t) {
     if (log.isWarnEnabled()) {
-      log.warn(logPrefix + msg, t);
+      log.warn("[" + submissionId + "]" + msg, t);
     }
   }
 
   private void logWarn(String msg) {
     if (log.isWarnEnabled()) {
-      log.warn(logPrefix + msg);
+      log.warn("[" + submissionId + "]" + msg);
     }
   }
 
   private void logError(String msg, Throwable t) {
     if (log.isErrorEnabled()) {
-      log.error(logPrefix + msg, t);
+      if (t != null) {
+        log.error("[" + submissionId + "]" + msg, t);
+      } else {
+        log.error("[" + submissionId + "]" + msg);
+      }
     }
   }
 
   private void logError(String msg) {
-    if (log.isErrorEnabled()) {
-      log.error(logPrefix + msg);
-    }
+    logError(msg, null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
deleted file mode 100644
index a53b04c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Branch;
-
-import java.util.concurrent.TimeUnit;
-
-public interface MergeQueue {
-  void merge(Branch.NameKey branch);
-  void schedule(Branch.NameKey branch);
-  void recheckAfter(Branch.NameKey branch, long delay, TimeUnit delayUnit);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
index 4985390..aa55751 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevCommitList;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -27,12 +28,12 @@
 import java.util.Set;
 
 public class MergeSorter {
-  private final RevWalk rw;
+  private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
 
-  public MergeSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
-      final RevFlag canMergeFlag) {
+  public MergeSorter(CodeReviewRevWalk rw, Set<RevCommit> alreadyAccepted,
+      RevFlag canMergeFlag) {
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.accepted = alreadyAccepted;
@@ -51,8 +52,8 @@
         rw.markUninteresting(c);
       }
 
-      RevCommit c;
-      final RevCommitList<RevCommit> contents = new RevCommitList<>();
+      CodeReviewCommit c;
+      RevCommitList<RevCommit> contents = new RevCommitList<>();
       while ((c = rw.next()) != null) {
         if (!c.has(canMergeFlag) || !incoming.contains(c)) {
           // We cannot merge n as it would bring something we
@@ -62,7 +63,7 @@
             n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY);
             n.missing = new ArrayList<>();
           }
-          n.missing.add((CodeReviewCommit) c);
+          n.missing.add(c);
         } else {
           contents.add(c);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
new file mode 100644
index 0000000..c9a5c3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2015 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.server.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Calculates the minimal superset of changes required to be merged.
+ * <p>
+ * This includes all parents between a change and the tip of its target
+ * branch for the merging/rebasing submit strategies. For the cherry-pick
+ * strategy no additional changes are included.
+ * <p>
+ * If change.submitWholeTopic is enabled, also all changes of the topic
+ * and their parents are included.
+ */
+@Singleton
+public class MergeSuperSet {
+  private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
+
+  private final ChangeData.Factory changeDataFactory;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final GitRepositoryManager repoManager;
+  private final Config cfg;
+
+  @Inject
+  MergeSuperSet(@GerritServerConfig Config cfg,
+      ChangeData.Factory changeDataFactory,
+      Provider<InternalChangeQuery> queryProvider,
+      GitRepositoryManager repoManager) {
+    this.cfg = cfg;
+    this.changeDataFactory = changeDataFactory;
+    this.queryProvider = queryProvider;
+    this.repoManager = repoManager;
+  }
+
+  public ChangeSet completeChangeSet(ReviewDb db, Change change)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      OrmException {
+    ChangeData cd = changeDataFactory.create(db, change.getId());
+    if (Submit.wholeTopicEnabled(cfg)) {
+      return completeChangeSetIncludingTopics(db, new ChangeSet(cd));
+    } else {
+      return completeChangeSetWithoutTopic(db, new ChangeSet(cd));
+    }
+  }
+
+  private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      OrmException {
+    List<ChangeData> ret = new ArrayList<>();
+
+    Multimap<Project.NameKey, Change.Id> pc = changes.changesByProject();
+    for (Project.NameKey project : pc.keySet()) {
+      try (Repository repo = repoManager.openRepository(project);
+           RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+        for (Change.Id cId : pc.get(project)) {
+          ChangeData cd = changeDataFactory.create(db, cId);
+
+          SubmitTypeRecord r = new SubmitRuleEvaluator(cd).getSubmitType();
+          if (r.status != SubmitTypeRecord.Status.OK) {
+            logErrorAndThrow("Failed to get submit type for " + cd.getId());
+          }
+          if (r.type == SubmitType.CHERRY_PICK) {
+            ret.add(cd);
+            continue;
+          }
+
+          // Get the underlying git commit object
+          PatchSet ps = cd.currentPatchSet();
+          String objIdStr = ps.getRevision().get();
+          RevCommit commit = rw.parseCommit(ObjectId.fromString(objIdStr));
+
+          // Collect unmerged ancestors
+          Branch.NameKey destBranch = cd.change().getDest();
+          repo.getRefDatabase().refresh();
+          Ref ref = repo.getRefDatabase().getRef(destBranch.get());
+
+          rw.reset();
+          rw.sort(RevSort.TOPO);
+          rw.markStart(commit);
+          if (ref != null) {
+            RevCommit head = rw.parseCommit(ref.getObjectId());
+            rw.markUninteresting(head);
+          }
+
+          List<String> hashes = new ArrayList<>();
+          for (RevCommit c : rw) {
+            hashes.add(c.name());
+          }
+
+          if (!hashes.isEmpty()) {
+            // Merged changes are ok to exclude
+            Iterable<ChangeData> destChanges = queryProvider.get()
+                .byCommitsOnBranchNotMerged(
+                  repo, db, cd.change().getDest(), hashes);
+            for (ChangeData chd : destChanges) {
+              ret.add(chd);
+            }
+          }
+        }
+      }
+    }
+
+    return new ChangeSet(ret);
+  }
+
+  private ChangeSet completeChangeSetIncludingTopics(
+      ReviewDb db, ChangeSet changes) throws MissingObjectException,
+      IncorrectObjectTypeException, IOException, OrmException {
+    Set<String> topicsTraversed = new HashSet<>();
+    boolean done = false;
+    ChangeSet newCs = completeChangeSetWithoutTopic(db, changes);
+    while (!done) {
+      List<ChangeData> chgs = new ArrayList<>();
+      done = true;
+      for (ChangeData cd : newCs.changes()) {
+        chgs.add(cd);
+        String topic = cd.change().getTopic();
+        if (!Strings.isNullOrEmpty(topic) && !topicsTraversed.contains(topic)) {
+          chgs.addAll(queryProvider.get().byTopicOpen(topic));
+          done = false;
+          topicsTraversed.add(topic);
+        }
+      }
+      changes = new ChangeSet(chgs);
+      newCs = completeChangeSetWithoutTopic(db, changes);
+    }
+    return newCs;
+  }
+
+  private void logError(String msg) {
+    if (log.isErrorEnabled()) {
+      log.error(msg);
+    }
+  }
+
+  private void logErrorAndThrow(String msg) throws OrmException {
+    logError(msg);
+    throw new OrmException(msg);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
index ba92651..7be7014 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
@@ -33,21 +34,23 @@
  * merge failed or another error state.
  */
 public class MergeTip {
+  private CodeReviewCommit initialTip;
   private CodeReviewCommit branchTip;
   private Map<ObjectId, ObjectId> mergeResults;
 
   /**
-   * @param initial Tip before the merge operation; may be null, indicating an
-   *     unborn branch.
-   * @param toMerge List of CodeReview commits to be merged in merge operation;
-   *     may not be null or empty.
+   * @param initialTip tip before the merge operation; may be null, indicating
+   *     an unborn branch.
+   * @param toMerge list of commits to be merged in merge operation; may not be
+   *     null or empty.
    */
-  public MergeTip(@Nullable CodeReviewCommit initial,
+  public MergeTip(@Nullable CodeReviewCommit initialTip,
       Collection<CodeReviewCommit> toMerge) {
-    checkArgument(toMerge != null && !toMerge.isEmpty(),
-        "toMerge may not be null or empty: %s", toMerge);
+    checkNotNull(toMerge, "toMerge may not be null");
+    checkArgument(!toMerge.isEmpty(), "toMerge may not be empty");
+    this.initialTip = initialTip;
+    this.branchTip = initialTip;
     this.mergeResults = Maps.newHashMap();
-    this.branchTip = initial;
     // Assume fast-forward merge until opposite is proven.
     for (CodeReviewCommit commit : toMerge) {
       mergeResults.put(commit.copy(), commit.copy());
@@ -55,6 +58,14 @@
   }
 
   /**
+   * @return the initial tip of the branch before the merge operation started;
+   *     may be null, indicating a previously unborn branch.
+   */
+  public CodeReviewCommit getInitialTip() {
+    return initialTip;
+  }
+
+  /**
    * Moves this MergeTip to newTip and appends mergeResult.
    *
    * @param newTip The new tip; may not be null.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 7cbf9fa..13476ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -23,15 +23,20 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
@@ -61,23 +66,20 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.PackParser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
-import java.sql.Timestamp;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
-import java.util.TimeZone;
 
 public class MergeUtil {
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
@@ -137,7 +139,7 @@
 
   public CodeReviewCommit getFirstFastForward(
       final CodeReviewCommit mergeTip, final RevWalk rw,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+      final List<CodeReviewCommit> toMerge) throws IntegrationException {
     for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext();) {
       try {
         final CodeReviewCommit n = i.next();
@@ -146,31 +148,29 @@
           return n;
         }
       } catch (IOException e) {
-        throw new MergeException("Cannot fast-forward test during merge", e);
+        throw new IntegrationException(
+            "Cannot fast-forward test during merge", e);
       }
     }
     return mergeTip;
   }
 
   public List<CodeReviewCommit> reduceToMinimalMerge(MergeSorter mergeSorter,
-      Collection<CodeReviewCommit> toSort) throws MergeException {
+      Collection<CodeReviewCommit> toSort) throws IntegrationException {
     List<CodeReviewCommit> result = new ArrayList<>();
     try {
       result.addAll(mergeSorter.sort(toSort));
     } catch (IOException e) {
-      throw new MergeException("Branch head sorting failed", e);
+      throw new IntegrationException("Branch head sorting failed", e);
     }
     Collections.sort(result, CodeReviewCommit.ORDER);
     return result;
   }
 
-  public PatchSetApproval getSubmitter(CodeReviewCommit c) {
-    return approvalsUtil.getSubmitter(db.get(), c.notes(), c.getPatchsetId());
-  }
-
-  public RevCommit createCherryPickFromCommit(Repository repo,
+  public CodeReviewCommit createCherryPickFromCommit(Repository repo,
       ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
-      PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
+      PersonIdent cherryPickCommitterIdent, String commitMsg,
+      CodeReviewRevWalk rw)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
       MergeIdenticalTreeException, MergeConflictException {
 
@@ -195,7 +195,9 @@
     }
   }
 
-  public String createCherryPickCommitMessage(final CodeReviewCommit n) {
+  public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl,
+      PatchSet.Id psId) {
+    Change c = ctl.getChange();
     final List<FooterLine> footers = n.getFooterLines();
     final StringBuilder msgbuf = new StringBuilder();
     msgbuf.append(n.getFullMessage());
@@ -215,16 +217,16 @@
       msgbuf.append('\n');
     }
 
-    if (!contains(footers, FooterConstants.CHANGE_ID, n.change().getKey().get())) {
+    if (!contains(footers, FooterConstants.CHANGE_ID, c.getKey().get())) {
       msgbuf.append(FooterConstants.CHANGE_ID.getName());
       msgbuf.append(": ");
-      msgbuf.append(n.change().getKey().get());
+      msgbuf.append(c.getKey().get());
       msgbuf.append('\n');
     }
 
     final String siteUrl = urlProvider.get();
     if (siteUrl != null) {
-      final String url = siteUrl + n.getPatchsetId().getParentKey().get();
+      final String url = siteUrl + c.getId().get();
       if (!contains(footers, FooterConstants.REVIEWED_ON, url)) {
         msgbuf.append(FooterConstants.REVIEWED_ON.getName());
         msgbuf.append(": ");
@@ -235,7 +237,7 @@
 
     PatchSetApproval submitAudit = null;
 
-    for (final PatchSetApproval a : safeGetApprovals(n)) {
+    for (final PatchSetApproval a : safeGetApprovals(ctl, psId)) {
       if (a.getValue() <= 0) {
         // Negative votes aren't counted.
         continue;
@@ -301,6 +303,10 @@
     return msgbuf.toString();
   }
 
+  public String createCherryPickCommitMessage(final CodeReviewCommit n) {
+    return createCherryPickCommitMessage(n, n.getControl(), n.getPatchsetId());
+  }
+
   private static boolean isCodeReview(LabelId id) {
     return "Code-Review".equalsIgnoreCase(id.get());
   }
@@ -309,11 +315,12 @@
     return "Verified".equalsIgnoreCase(id.get());
   }
 
-  private Iterable<PatchSetApproval> safeGetApprovals(CodeReviewCommit n) {
+  private Iterable<PatchSetApproval> safeGetApprovals(
+      ChangeControl ctl, PatchSet.Id psId) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), n.getControl(), n.getPatchsetId());
+      return approvalsUtil.byPatchSet(db.get(), ctl, psId);
     } catch (OrmException e) {
-      log.error("Can't read approval records for " + n.getPatchsetId(), e);
+      log.error("Can't read approval records for " + psId, e);
       return Collections.emptyList();
     }
   }
@@ -337,76 +344,30 @@
     return false;
   }
 
-  public PersonIdent computeMergeCommitAuthor(final PersonIdent myIdent,
-      final RevWalk rw, final List<CodeReviewCommit> codeReviewCommits) {
-    PatchSetApproval submitter = null;
-    for (final CodeReviewCommit c : codeReviewCommits) {
-      PatchSetApproval s = getSubmitter(c);
-      if (submitter == null
-          || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) {
-        submitter = s;
-      }
-    }
-
-    // Try to use the submitter's identity for the merge commit author.
-    // If all of the commits being merged are created by the submitter,
-    // prefer the identity line they used in the commits rather than the
-    // preferred identity stored in the user account. This way the Git
-    // commit records are more consistent internally.
-    //
-    PersonIdent authorIdent;
-    if (submitter != null) {
-      IdentifiedUser who =
-          identifiedUserFactory.create(submitter.getAccountId());
-      Set<String> emails = new HashSet<>();
-      for (RevCommit c : codeReviewCommits) {
-        try {
-          rw.parseBody(c);
-        } catch (IOException e) {
-          log.warn("Cannot parse commit " + c.name(), e);
-          continue;
-        }
-        emails.add(c.getAuthorIdent().getEmailAddress());
-      }
-
-      final Timestamp dt = submitter.getGranted();
-      final TimeZone tz = myIdent.getTimeZone();
-      if (emails.size() == 1 && who.hasEmailAddress(emails.iterator().next())) {
-        authorIdent =
-            new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz);
-      } else {
-        authorIdent = who.newCommitterIdent(dt, tz);
-      }
-    } else {
-      authorIdent = myIdent;
-    }
-    return authorIdent;
-  }
-
   public boolean canMerge(final MergeSorter mergeSorter,
       final Repository repo, final CodeReviewCommit mergeTip,
       final CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     if (hasMissingDependencies(mergeSorter, toMerge)) {
       return false;
     }
 
-    ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
-    try {
-      return m.merge(new AnyObjectId[] {mergeTip, toMerge});
+    try (ObjectInserter ins = new InMemoryInserter(repo)) {
+      return newThreeWayMerger(repo, ins)
+          .merge(new AnyObjectId[] {mergeTip, toMerge});
     } catch (LargeObjectException e) {
       log.warn("Cannot merge due to LargeObjectException: " + toMerge.name());
       return false;
     } catch (NoMergeBaseException e) {
       return false;
     } catch (IOException e) {
-      throw new MergeException("Cannot merge " + toMerge.name(), e);
+      throw new IntegrationException("Cannot merge " + toMerge.name(), e);
     }
   }
 
-  public boolean canFastForward(final MergeSorter mergeSorter,
-      final CodeReviewCommit mergeTip, final RevWalk rw,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean canFastForward(MergeSorter mergeSorter,
+      CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge)
+      throws IntegrationException {
     if (hasMissingDependencies(mergeSorter, toMerge)) {
       return false;
     }
@@ -414,13 +375,13 @@
     try {
       return mergeTip == null || rw.isMergedInto(mergeTip, toMerge);
     } catch (IOException e) {
-      throw new MergeException("Cannot fast-forward test during merge", e);
+      throw new IntegrationException("Cannot fast-forward test during merge", e);
     }
   }
 
-  public boolean canCherryPick(final MergeSorter mergeSorter,
-      final Repository repo, final CodeReviewCommit mergeTip, final RevWalk rw,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean canCherryPick(MergeSorter mergeSorter, Repository repo,
+      CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge)
+      throws IntegrationException {
     if (mergeTip == null) {
       // The branch is unborn. Fast-forward is possible.
       //
@@ -439,12 +400,12 @@
       // taking the delta relative to that one parent and redoing
       // that on the current merge tip.
       //
-      try {
-        ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
+      try (ObjectInserter ins = new InMemoryInserter(repo)) {
+        ThreeWayMerger m = newThreeWayMerger(repo, ins);
         m.setBase(toMerge.getParent(0));
         return m.merge(mergeTip, toMerge);
       } catch (IOException e) {
-        throw new MergeException("Cannot merge " + toMerge.name(), e);
+        throw new IntegrationException("Cannot merge " + toMerge.name(), e);
       }
     }
 
@@ -459,42 +420,24 @@
   }
 
   public boolean hasMissingDependencies(final MergeSorter mergeSorter,
-      final CodeReviewCommit toMerge) throws MergeException {
+      final CodeReviewCommit toMerge) throws IntegrationException {
     try {
       return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
     } catch (IOException e) {
-      throw new MergeException("Branch head sorting failed", e);
+      throw new IntegrationException("Branch head sorting failed", e);
     }
   }
 
-  public static ObjectInserter createDryRunInserter(Repository db) {
-    final ObjectInserter delegate = db.newObjectInserter();
-    return new ObjectInserter.Filter() {
-      @Override
-      protected ObjectInserter delegate() {
-        return delegate;
-      }
-      @Override
-      public PackParser newPackParser(InputStream in) throws IOException {
-        throw new UnsupportedOperationException();
-      }
-      @Override
-      public void flush() throws IOException {
-        // Do nothing.
-      }
-    };
-  }
-
-  public CodeReviewCommit mergeOneCommit(final PersonIdent myIdent,
-      final Repository repo, final RevWalk rw, final ObjectInserter inserter,
-      final RevFlag canMergeFlag, final Branch.NameKey destBranch,
-      final CodeReviewCommit mergeTip, final CodeReviewCommit n)
-      throws MergeException {
+  public CodeReviewCommit mergeOneCommit(PersonIdent author,
+      PersonIdent committer, Repository repo, CodeReviewRevWalk rw,
+      ObjectInserter inserter, RevFlag canMergeFlag, Branch.NameKey destBranch,
+      CodeReviewCommit mergeTip, CodeReviewCommit n)
+      throws IntegrationException {
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
     try {
       if (m.merge(new AnyObjectId[] {mergeTip, n})) {
-        return writeMergeCommit(myIdent, rw, inserter, canMergeFlag, destBranch,
-            mergeTip, m.getResultTreeId(), n);
+        return writeMergeCommit(author, committer, rw, inserter, canMergeFlag,
+            destBranch, mergeTip, m.getResultTreeId(), n);
       } else {
         failed(rw, canMergeFlag, mergeTip, n, CommitMergeStatus.PATH_CONFLICT);
       }
@@ -503,10 +446,10 @@
         failed(rw, canMergeFlag, mergeTip, n,
             getCommitMergeStatus(e.getReason()));
       } catch (IOException e2) {
-        throw new MergeException("Cannot merge " + n.name(), e);
+        throw new IntegrationException("Cannot merge " + n.name(), e);
       }
     } catch (IOException e) {
-      throw new MergeException("Cannot merge " + n.name(), e);
+      throw new IntegrationException("Cannot merge " + n.name(), e);
     }
     return mergeTip;
   }
@@ -523,32 +466,32 @@
     }
   }
 
-  private static CodeReviewCommit failed(final RevWalk rw,
-      final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
-      final CodeReviewCommit n, final CommitMergeStatus failure)
+  private static CodeReviewCommit failed(CodeReviewRevWalk rw,
+      RevFlag canMergeFlag, CodeReviewCommit mergeTip, CodeReviewCommit n,
+      CommitMergeStatus failure)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
     rw.resetRetain(canMergeFlag);
     rw.markStart(n);
     rw.markUninteresting(mergeTip);
     CodeReviewCommit failed;
-    while ((failed = (CodeReviewCommit) rw.next()) != null) {
+    while ((failed = rw.next()) != null) {
       failed.setStatusCode(failure);
     }
     return failed;
   }
 
-  public CodeReviewCommit writeMergeCommit(final PersonIdent myIdent,
-      final RevWalk rw, final ObjectInserter inserter,
-      final RevFlag canMergeFlag, final Branch.NameKey destBranch,
-      final CodeReviewCommit mergeTip, final ObjectId treeId,
-      final CodeReviewCommit n) throws IOException,
-      MissingObjectException, IncorrectObjectTypeException {
+  public CodeReviewCommit writeMergeCommit(PersonIdent author,
+      PersonIdent committer, CodeReviewRevWalk rw, ObjectInserter inserter,
+      RevFlag canMergeFlag, Branch.NameKey destBranch,
+      CodeReviewCommit mergeTip, ObjectId treeId, CodeReviewCommit n)
+      throws IOException, MissingObjectException,
+      IncorrectObjectTypeException {
     final List<CodeReviewCommit> merged = new ArrayList<>();
     rw.resetRetain(canMergeFlag);
     rw.markStart(n);
     rw.markUninteresting(mergeTip);
-    for (final RevCommit c : rw) {
-      final CodeReviewCommit crc = (CodeReviewCommit) c;
+    CodeReviewCommit crc;
+    while ((crc = rw.next()) != null) {
       if (crc.getPatchsetId() != null) {
         merged.add(crc);
       }
@@ -570,17 +513,15 @@
       }
     }
 
-    PersonIdent authorIdent = computeMergeCommitAuthor(myIdent, rw, merged);
-
     final CommitBuilder mergeCommit = new CommitBuilder();
     mergeCommit.setTreeId(treeId);
     mergeCommit.setParentIds(mergeTip, n);
-    mergeCommit.setAuthor(authorIdent);
-    mergeCommit.setCommitter(myIdent);
+    mergeCommit.setAuthor(author);
+    mergeCommit.setCommitter(committer);
     mergeCommit.setMessage(msgbuf.toString());
 
     CodeReviewCommit mergeResult =
-        (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit));
+        rw.parseCommit(commit(inserter, mergeCommit));
     mergeResult.setControl(n.getControl());
     return mergeResult;
   }
@@ -679,41 +620,37 @@
     return id;
   }
 
-  public PatchSetApproval markCleanMerges(final RevWalk rw,
+  public void markCleanMerges(final RevWalk rw,
       final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
-      final Set<RevCommit> alreadyAccepted) throws MergeException {
+      final Set<RevCommit> alreadyAccepted) throws IntegrationException {
     if (mergeTip == null) {
       // If mergeTip is null here, branchTip was null, indicating a new branch
       // at the start of the merge process. We also elected to merge nothing,
       // probably due to missing dependencies. Nothing was cleanly merged.
       //
-      return null;
+      return;
     }
 
     try {
-      PatchSetApproval submitApproval = null;
-
       rw.resetRetain(canMergeFlag);
       rw.sort(RevSort.TOPO);
       rw.sort(RevSort.REVERSE, true);
       rw.markStart(mergeTip);
       for (RevCommit c : alreadyAccepted) {
-        rw.markUninteresting(c);
+        // If branch was not created by this submit.
+        if (!Objects.equals(c, mergeTip)) {
+          rw.markUninteresting(c);
+        }
       }
 
       CodeReviewCommit c;
       while ((c = (CodeReviewCommit) rw.next()) != null) {
         if (c.getPatchsetId() != null) {
           c.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-          if (submitApproval == null) {
-            submitApproval = getSubmitter(c);
-          }
         }
       }
-
-      return submitApproval;
     } catch (IOException e) {
-      throw new MergeException("Cannot mark clean merges", e);
+      throw new IntegrationException("Cannot mark clean merges", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index fb96a6b..2c5e512 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -189,7 +189,7 @@
   }
 
   private void loadBase(String notesBranch) throws IOException {
-    Ref branch = db.getRef(notesBranch);
+    Ref branch = db.getRefDatabase().exactRef(notesBranch);
     if (branch != null) {
       baseCommit = revWalk.parseCommit(branch.getObjectId());
       base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
@@ -240,7 +240,7 @@
           }
         } else {
           throw new ConcurrentRefUpdateException("Failed to lock the ref: "
-              + notesBranch, db.getRef(notesBranch), result);
+              + notesBranch, refUpdate.getRef(), result);
         }
 
       } else if (result == Result.REJECTED) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index f8ee4a7..1519d84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -97,7 +97,6 @@
 
   private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
   private static final String KEY_ACCEPTED = "accepted";
-  private static final String KEY_REQUIRE_CONTACT_INFORMATION = "requireContactInformation";
   private static final String KEY_AUTO_VERIFY = "autoVerify";
   private static final String KEY_AGREEMENT_URL = "agreementUrl";
 
@@ -118,6 +117,8 @@
   private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
       "requireContributorAgreement";
   private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
+  private static final String KEY_ENABLE_SIGNED_PUSH = "enableSignedPush";
+  private static final String KEY_REQUIRE_SIGNED_PUSH = "requireSignedPush";
 
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
@@ -305,6 +306,10 @@
     return notifySections.values();
   }
 
+  public void putNotifyConfig(String name, NotifyConfig nc) {
+    notifySections.put(name, nc);
+  }
+
   public Map<String, LabelType> getLabelSections() {
     return labelSections;
   }
@@ -414,6 +419,10 @@
     p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, InheritableBoolean.INHERIT));
     p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, InheritableBoolean.INHERIT));
     p.setCreateNewChangeForAllNotInTarget(getEnum(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, InheritableBoolean.INHERIT));
+    p.setEnableSignedPush(getEnum(rc, RECEIVE, null,
+          KEY_ENABLE_SIGNED_PUSH, InheritableBoolean.INHERIT));
+    p.setRequireSignedPush(getEnum(rc, RECEIVE, null,
+          KEY_REQUIRE_SIGNED_PUSH, InheritableBoolean.INHERIT));
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
@@ -448,8 +457,6 @@
     for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
       ContributorAgreement ca = getContributorAgreement(name, true);
       ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
-      ca.setRequireContactInformation(
-          rc.getBoolean(CONTRIBUTOR_AGREEMENT, name, KEY_REQUIRE_CONTACT_INFORMATION, false));
       ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
       ca.setAccepted(loadPermissionRules(
           rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, groupsByName, false));
@@ -639,7 +646,7 @@
 
   private static LabelValue parseLabelValue(String src) {
     List<String> parts = ImmutableList.copyOf(
-        Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings().limit(2)
+        Splitter.on(CharMatcher.whitespace()).omitEmptyStrings().limit(2)
         .split(src));
     if (parts.isEmpty()) {
       throw new IllegalArgumentException("empty value");
@@ -771,6 +778,17 @@
       Config pluginConfig = new Config();
       pluginConfigs.put(plugin, pluginConfig);
       for (String name : rc.getNames(PLUGIN, plugin)) {
+        String value = rc.getString(PLUGIN, plugin, name);
+        if (value.startsWith("Group[")) {
+          GroupReference refFromString = GroupReference.fromString(value);
+          GroupReference ref = groupList.byUUID(refFromString.getUUID());
+          if (ref == null) {
+            ref = refFromString;
+            error(new ValidationError(PROJECT_CONFIG,
+                "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
+          }
+          rc.setString(PLUGIN, plugin, name, ref.toString());
+        }
         pluginConfig.setStringList(PLUGIN, plugin, name,
             Arrays.asList(rc.getStringList(PLUGIN, plugin, name)));
       }
@@ -822,6 +840,10 @@
     set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, p.getCreateNewChangeForAllNotInTarget(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
+    set(rc, RECEIVE, null, KEY_ENABLE_SIGNED_PUSH,
+        p.getEnableSignedPush(), InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_PUSH,
+        p.getRequireSignedPush(), InheritableBoolean.INHERIT);
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
@@ -836,9 +858,9 @@
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
+    savePluginSections(rc, keepGroups);
     groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
-    savePluginSections(rc);
 
     saveConfig(PROJECT_CONFIG, rc);
     saveGroupList();
@@ -885,7 +907,6 @@
       Config rc, Set<AccountGroup.UUID> keepGroups) {
     for (ContributorAgreement ca : sort(contributorAgreements.values())) {
       set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription());
-      set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_REQUIRE_CONTACT_INFORMATION, ca.isRequireContactInformation());
       set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl());
 
       if (ca.getAutoVerify() != null) {
@@ -1091,7 +1112,7 @@
     }
   }
 
-  private void savePluginSections(Config rc) {
+  private void savePluginSections(Config rc, Set<AccountGroup.UUID> keepGroups) {
     List<String> existing = Lists.newArrayList(rc.getSubsections(PLUGIN));
     for (String name : existing) {
       rc.unsetSection(PLUGIN, name);
@@ -1101,6 +1122,14 @@
       String plugin = e.getKey();
       Config pluginConfig = e.getValue();
       for (String name : pluginConfig.getNames(PLUGIN, plugin)) {
+        String value = pluginConfig.getString(PLUGIN, plugin, name);
+        if (value.startsWith("Group[")) {
+          GroupReference ref = resolve(GroupReference.fromString(value));
+          if (ref.getUUID() != null) {
+            keepGroups.add(ref.getUUID());
+            pluginConfig.setString(PLUGIN, plugin, name, ref.toString());
+          }
+        }
         rc.setStringList(PLUGIN, plugin, name,
             Arrays.asList(pluginConfig.getStringList(PLUGIN, plugin, name)));
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
new file mode 100644
index 0000000..dffb18a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 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.server.git;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public class QueryList extends TabFile {
+  public static final String FILE_NAME = "queries";
+  protected final Map<String, String> queriesByName;
+
+  private QueryList(List<Row> queriesByName) {
+    this.queriesByName = toMap(queriesByName);
+  }
+
+  public static QueryList parse(String text, ValidationError.Sink errors)
+      throws IOException {
+    return new QueryList(parse(text, FILE_NAME, TRIM, TRIM, errors));
+  }
+
+  public String getQuery(String name) {
+    return queriesByName.get(name);
+  }
+
+  public String asText() {
+    return asText("Name", "Query", queriesByName);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
index 8595582..fdf7c40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -28,16 +29,17 @@
 import java.util.Set;
 
 public class RebaseSorter {
-
-  private final RevWalk rw;
+  private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
-  private final Set<RevCommit> accepted;
+  private final RevCommit initialTip;
+  private final Set<RevCommit> alreadyAccepted;
 
-  public RebaseSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
-      final RevFlag canMergeFlag) {
+  public RebaseSorter(CodeReviewRevWalk rw, RevCommit initialTip,
+      Set<RevCommit> alreadyAccepted, RevFlag canMergeFlag) {
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
-    this.accepted = alreadyAccepted;
+    this.initialTip = initialTip;
+    this.alreadyAccepted = alreadyAccepted;
   }
 
   public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> incoming)
@@ -49,14 +51,18 @@
 
       rw.resetRetain(canMergeFlag);
       rw.markStart(n);
-      for (RevCommit c : accepted) {
-        rw.markUninteresting(c);
+      if (initialTip != null) {
+        rw.markUninteresting(initialTip);
       }
 
       CodeReviewCommit c;
       final List<CodeReviewCommit> contents = new ArrayList<>();
-      while ((c = (CodeReviewCommit) rw.next()) != null) {
+      while ((c = rw.next()) != null) {
         if (!c.has(canMergeFlag) || !incoming.contains(c)) {
+          if (isAlreadyMerged(c)) {
+            rw.markUninteresting(c);
+            break;
+          }
           // We cannot merge n as it would bring something we
           // aren't permitted to merge at this time. Drop n.
           //
@@ -82,6 +88,21 @@
     return sorted;
   }
 
+  private boolean isAlreadyMerged(CodeReviewCommit commit) throws IOException {
+    try (CodeReviewRevWalk mirw =
+        CodeReviewCommit.newRevWalk(rw.getObjectReader())) {
+      mirw.reset();
+      mirw.markStart(commit);
+      for (RevCommit accepted : alreadyAccepted) {
+        if (mirw.isMergedInto(mirw.parseCommit(commit),
+            mirw.parseCommit(accepted))) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   private static <T> T removeOne(final Collection<T> c) {
     final Iterator<T> i = c.iterator();
     final T r = i.next();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index bc7b0d1..0401367f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
 import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
@@ -44,6 +45,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
@@ -60,9 +62,11 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -87,6 +91,7 @@
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -109,6 +114,7 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
@@ -133,6 +139,7 @@
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -171,6 +178,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -256,24 +264,21 @@
     }
   }
 
-  private static final Function<Exception, InsertException> INSERT_EXCEPTION =
-      new Function<Exception, InsertException>() {
+  private static final Function<Exception, RestApiException> INSERT_EXCEPTION =
+      new Function<Exception, RestApiException>() {
         @Override
-        public InsertException apply(Exception input) {
-          if (input instanceof OrmException) {
-            return new InsertException("ORM error", input);
+        public RestApiException apply(Exception input) {
+          if (input instanceof RestApiException) {
+            return (RestApiException) input;
           }
-          if (input instanceof IOException) {
-            return new InsertException("IO error", input);
-          }
-          return new InsertException("Error inserting change/patchset", input);
+          return new RestApiException("Error inserting change/patchset", input);
         }
       };
 
   private Set<Account.Id> reviewersFromCommandLine = Sets.newLinkedHashSet();
   private Set<Account.Id> ccFromCommandLine = Sets.newLinkedHashSet();
 
-  private final IdentifiedUser currentUser;
+  private final IdentifiedUser user;
   private final ReviewDb db;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeData.Factory changeDataFactory;
@@ -297,7 +302,7 @@
   private final AccountCache accountCache;
   private final ChangesCollection changes;
   private final ChangeInserter.Factory changeInserterFactory;
-  private final WorkQueue workQueue;
+  private final ExecutorService sendEmailExecutor;
   private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
   private final ChangeIndexer indexer;
@@ -305,6 +310,8 @@
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
   private final ChangeKindCache changeKindCache;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final SetHashtagsOp.Factory hashtagsFactory;
 
   private final ProjectControl projectControl;
   private final Project project;
@@ -318,15 +325,16 @@
   private List<CreateRequest> newChanges = Collections.emptyList();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
       new HashMap<>();
+  private final List<UpdateGroupsRequest> updateGroups = new ArrayList<>();
   private final Set<RevCommit> validCommits = new HashSet<>();
 
   private ListMultimap<Change.Id, Ref> refsByChange;
   private SetMultimap<ObjectId, Ref> refsById;
   private Map<String, Ref> allRefs;
 
-  private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<SubmoduleOp> subOpProvider;
   private final Provider<Submit> submitProvider;
-  private final MergeQueue mergeQueue;
+  private final Provider<MergeOp> mergeOpProvider;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final NotesMigration notesMigration;
   private final ChangeEditUtil editUtil;
@@ -365,7 +373,7 @@
       final ChangeInserter.Factory changeInserterFactory,
       final CommitValidators.Factory commitValidatorsFactory,
       @CanonicalWebUrl final String canonicalWebUrl,
-      final WorkQueue workQueue,
+      @SendEmailExecutor final ExecutorService sendEmailExecutor,
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
       final ChangeIndexer indexer,
@@ -374,14 +382,16 @@
       ReceiveConfig config,
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
-      final SubmoduleOp.Factory subOpFactory,
+      final Provider<SubmoduleOp> subOpProvider,
       final Provider<Submit> submitProvider,
-      final MergeQueue mergeQueue,
+      final Provider<MergeOp> mergeOpProvider,
       final ChangeKindCache changeKindCache,
       final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       final NotesMigration notesMigration,
-      final ChangeEditUtil editUtil) throws IOException {
-    this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
+      final ChangeEditUtil editUtil,
+      final BatchUpdate.Factory batchUpdateFactory,
+      final SetHashtagsOp.Factory hashtagsFactory) throws IOException {
+    this.user = projectControl.getUser().asIdentifiedUser();
     this.db = db;
     this.queryProvider = queryProvider;
     this.changeDataFactory = changeDataFactory;
@@ -405,7 +415,7 @@
     this.changes = changes;
     this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.workQueue = workQueue;
+    this.sendEmailExecutor = sendEmailExecutor;
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
     this.indexer = indexer;
@@ -413,6 +423,8 @@
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
     this.changeKindCache = changeKindCache;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.hashtagsFactory = hashtagsFactory;
 
     this.projectControl = projectControl;
     this.labelTypes = projectControl.getLabelTypes();
@@ -421,9 +433,9 @@
     this.rp = new ReceivePack(repo);
     this.rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
 
-    this.subOpFactory = subOpFactory;
+    this.subOpProvider = subOpProvider;
     this.submitProvider = submitProvider;
-    this.mergeQueue = mergeQueue;
+    this.mergeOpProvider = mergeOpProvider;
     this.pluginConfigEntries = pluginConfigEntries;
     this.notesMigration = notesMigration;
 
@@ -484,6 +496,7 @@
     advHooks.add(rp.getAdvertiseRefsHook());
     advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
         db, queryProvider, projectControl.getProject().getNameKey()));
+    advHooks.add(new HackPushNegotiateHook());
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
   }
 
@@ -556,6 +569,7 @@
     commandProgress = progress.beginSubTask("refs", UNKNOWN);
 
     batch = repo.getRefDatabase().newBatchUpdate();
+    batch.setPushCertificate(rp.getPushCertificate());
     batch.setRefLogIdent(rp.getRefLogIdent());
     batch.setRefLogMessage("push", true);
 
@@ -593,10 +607,11 @@
       for (Error error : errors.keySet()) {
         rp.sendMessage(buildError(error, errors.get(error)));
       }
-      rp.sendMessage(String.format("User: %s", displayName(currentUser)));
+      rp.sendMessage(String.format("User: %s", displayName(user)));
       rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
     }
 
+    Set<Branch.NameKey> branches = Sets.newHashSet();
     for (final ReceiveCommand c : commands) {
         if (c.getResult() == OK) {
           if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
@@ -612,6 +627,8 @@
               case UPDATE:
               case UPDATE_NONFASTFORWARD:
                 autoCloseChanges(c);
+                branches.add(new Branch.NameKey(project.getNameKey(),
+                    c.getRefName()));
                 break;
 
               case DELETE:
@@ -641,16 +658,25 @@
             // We only fire gitRefUpdated for direct refs updates.
             // Events for change refs are fired when they are created.
             //
-            gitRefUpdated.fire(project.getNameKey(), c.getRefName(),
-                c.getOldId(), c.getNewId());
+            gitRefUpdated.fire(project.getNameKey(), c);
             hooks.doRefUpdatedHook(
                 new Branch.NameKey(project.getNameKey(), c.getRefName()),
                 c.getOldId(),
                 c.getNewId(),
-                currentUser.getAccount());
+                user.getAccount());
           }
         }
     }
+    // Update superproject gitlinks if required.
+    SubmoduleOp op = subOpProvider.get();
+    try {
+       op.updateSubmoduleSubscriptions(db, branches);
+       op.updateSuperProjects(db, branches);
+    } catch (SubmoduleException e) {
+      log.error("Can't update submodule subscriptions "
+          + "or update the superprojects", e);
+    }
+
     closeProgress.end();
     commandProgress.end();
     progress.end();
@@ -696,7 +722,7 @@
       boolean edit = magicBranch != null && magicBranch.edit;
       for (ReplaceRequest u : updated) {
         addMessage(formatChangeUrl(canonicalWebUrl, u.change,
-            u.info.getSubject(), edit));
+            u.newCommit.getShortMessage(), edit));
       }
       addMessage("");
     }
@@ -736,7 +762,7 @@
           if (replace.insertPatchSet().checkedGet() != null) {
             replace.inputCommand.setResult(OK);
           }
-        } catch (IOException | InsertException err) {
+        } catch (IOException | RestApiException err) {
           reject(replace.inputCommand, "internal server error");
           log.error(String.format(
               "Cannot add patch set to change %d in project %s",
@@ -785,9 +811,9 @@
     }
 
     try {
-      List<CheckedFuture<?, InsertException>> futures = Lists.newArrayList();
+      List<CheckedFuture<?, RestApiException>> futures = Lists.newArrayList();
       for (ReplaceRequest replace : replaceByChange.values()) {
-        if (magicBranch != null && replace.inputCommand == magicBranch.cmd) {
+        if (replace.inputCommand == magicBranch.cmd) {
           futures.add(replace.insertPatchSet());
         }
       }
@@ -796,13 +822,23 @@
         futures.add(create.insertChange());
       }
 
-      for (CheckedFuture<?, InsertException> f : futures) {
+      for (UpdateGroupsRequest update : updateGroups) {
+        futures.add(update.updateGroups());
+      }
+
+      for (CheckedFuture<?, RestApiException> f : futures) {
         f.checkedGet();
       }
       magicBranch.cmd.setResult(OK);
-    } catch (InsertException err) {
-      log.error("Can't insert change/patchset for " + project.getName(), err);
-      reject(magicBranch.cmd, "internal server error");
+    } catch (RestApiException err) {
+      log.error("Can't insert change/patchset for " + project.getName()
+          + ". " + err.getMessage(), err);
+
+      String rejection = "internal server error";
+      if (err.getCause() != null) {
+        rejection += ": " + err.getCause().getMessage();
+      }
+      reject(magicBranch.cmd, rejection);
     } catch (IOException err) {
       log.error("Can't read commits for " + project.getName(), err);
       reject(magicBranch.cmd, "internal server error");
@@ -848,7 +884,7 @@
       }
 
       HookResult result = hooks.doRefUpdateHook(project, cmd.getRefName(),
-                              currentUser.getAccount(), cmd.getOldId(),
+                              user.getAccount(), cmd.getOldId(),
                               cmd.getNewId());
 
       if (result != null) {
@@ -919,7 +955,7 @@
                   addError("  " + err.getMessage());
                 }
                 reject(cmd, "invalid project configuration");
-                log.error("User " + currentUser.getUserName()
+                log.error("User " + user.getUserName()
                     + " tried to push invalid project configuration "
                     + cmd.getNewId().name() + " for " + project.getName());
                 continue;
@@ -934,7 +970,7 @@
                 }
               } else {
                 if (!oldParent.equals(newParent)
-                    && !currentUser.getCapabilities().canAdministrateServer()) {
+                    && !user.getCapabilities().canAdministrateServer()) {
                   reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
                   continue;
                 }
@@ -980,7 +1016,7 @@
               }
             } catch (Exception e) {
               reject(cmd, "invalid project configuration");
-              log.error("User " + currentUser.getUserName()
+              log.error("User " + user.getUserName()
                   + " tried to push invalid project configuration "
                   + cmd.getNewId().name() + " for " + project.getName(), e);
               continue;
@@ -1194,10 +1230,8 @@
 
     String parse(CmdLineParser clp, Repository repo, Set<String> refs)
         throws CmdLineException {
-      String ref = MagicBranch.getDestBranchName(cmd.getRefName());
-      if (!ref.startsWith(Constants.R_REFS)) {
-        ref = Constants.R_HEADS + ref;
-      }
+      String ref = RefNames.fullName(
+          MagicBranch.getDestBranchName(cmd.getRefName()));
 
       int optionStart = ref.indexOf('%');
       if (0 < optionStart) {
@@ -1355,7 +1389,13 @@
     } else if (newChangeForAllNotInTarget) {
       String destBranch = magicBranch.dest.get();
       try {
-        ObjectId baseHead = repo.getRef(destBranch).getObjectId();
+        Ref r = repo.getRefDatabase().exactRef(destBranch);
+        if (r == null) {
+          reject(cmd, destBranch + " not found");
+          return;
+        }
+
+        ObjectId baseHead = r.getObjectId();
         magicBranch.baseCommit =
             Collections.singletonList(walk.parseCommit(baseHead));
       } catch (IOException ex) {
@@ -1464,39 +1504,62 @@
 
   private void selectNewAndReplacedChangesFromMagicBranch() {
     newChanges = Lists.newArrayList();
-    final RevWalk walk = rp.getRevWalk();
-    walk.reset();
-    walk.sort(RevSort.TOPO);
-    walk.sort(RevSort.REVERSE, true);
+
+    SetMultimap<ObjectId, Ref> existing = changeRefsById();
+    GroupCollector groupCollector = new GroupCollector(changeRefsById(), db);
+
+    rp.getRevWalk().reset();
+    rp.getRevWalk().sort(RevSort.TOPO);
+    rp.getRevWalk().sort(RevSort.REVERSE, true);
     try {
-      Set<ObjectId> existing = Sets.newHashSet();
-      walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
+      rp.getRevWalk().markStart(
+          rp.getRevWalk().parseCommit(magicBranch.cmd.getNewId()));
       if (magicBranch.baseCommit != null) {
         for (RevCommit c : magicBranch.baseCommit) {
-          walk.markUninteresting(c);
+          rp.getRevWalk().markUninteresting(c);
         }
         Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
         if (targetRef != null) {
-          walk.markUninteresting(walk.parseCommit(targetRef.getObjectId()));
+          rp.getRevWalk().markUninteresting(
+              rp.getRevWalk().parseCommit(targetRef.getObjectId()));
         }
       } else {
         markHeadsAsUninteresting(
-            walk,
-            existing,
+            rp.getRevWalk(),
             magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
       }
 
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<>();
       final int maxBatchChanges =
-          receiveConfig.getEffectiveMaxBatchChangesLimit(currentUser);
+          receiveConfig.getEffectiveMaxBatchChangesLimit(user);
       for (;;) {
-        final RevCommit c = walk.next();
+        final RevCommit c = rp.getRevWalk().next();
         if (c == null) {
           break;
         }
-        if (existing.contains(c)) { // Commit is already tracked.
-          continue;
+        groupCollector.visit(c);
+        Collection<Ref> existingRefs = existing.get(c);
+        if (!existingRefs.isEmpty()) { // Commit is already tracked.
+          // Corner cases where an existing commit might need a new group:
+          // A) Existing commit has a null group; wasn't assigned during schema
+          //    upgrade, or schema upgrade is performed on a running server.
+          // B) Let A<-B<-C, then:
+          //      1. Push A to refs/heads/master
+          //      2. Push B to refs/for/master
+          //      3. Force push A~ to refs/heads/master
+          //      4. Push C to refs/for/master.
+          //      B will be in existing so we aren't replacing the patch set. It
+          //      used to have its own group, but now needs to to be changed to
+          //      A's group.
+          // C) Commit is a PatchSet of a pre-existing change uploaded with a
+          //    different target branch.
+          for (Ref ref : existingRefs) {
+            updateGroups.add(new UpdateGroupsRequest(ref, c));
+          }
+          if (!(newChangeForAllNotInTarget || magicBranch.base != null)) {
+            continue;
+          }
         }
 
         if (!validCommit(magicBranch.ctl, magicBranch.cmd, c)) {
@@ -1539,7 +1602,8 @@
         }
       }
 
-      for (ChangeLookup p : pending) {
+      for (Iterator<ChangeLookup> itr = pending.iterator(); itr.hasNext();) {
+        ChangeLookup p = itr.next();
         if (newChangeIds.contains(p.changeKey)) {
           reject(magicBranch.cmd, "squash commits first");
           newChanges = Collections.emptyList();
@@ -1561,6 +1625,21 @@
         if (changes.size() == 1) {
           // Schedule as a replacement to this one matching change.
           //
+
+          RevId currentPs = changes.get(0).currentPatchSet().getRevision();
+          // If Commit is already current PatchSet of target Change.
+          if (p.commit.name().equals(currentPs.get())) {
+            if (pending.size() == 1) {
+              // There are no commits left to check, all commits in pending were already
+              // current PatchSet of the corresponding target changes.
+              reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
+            } else {
+              // Commit is already current PatchSet.
+              // Remove from pending and try next commit.
+              itr.remove();
+              continue;
+            }
+          }
           if (requestReplace(
               magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
             continue;
@@ -1604,28 +1683,35 @@
       reject(magicBranch.cmd, "edit is not supported for new changes");
       return;
     }
-    for (CreateRequest create : newChanges) {
-      batch.addCommand(create.cmd);
+
+    try {
+      Multimap<ObjectId, String> groups = groupCollector.getGroups();
+      for (CreateRequest create : newChanges) {
+        batch.addCommand(create.cmd);
+        create.groups = groups.get(create.commit);
+      }
+      for (ReplaceRequest replace : replaceByChange.values()) {
+        replace.groups = groups.get(replace.newCommit);
+      }
+      for (UpdateGroupsRequest update : updateGroups) {
+        update.groups = Sets.newHashSet(groups.get(update.commit));
+      }
+    } catch (OrmException e) {
+      log.error("Error collecting groups for changes", e);
+      reject(magicBranch.cmd, "internal server error");
+      return;
     }
   }
 
-  private void markHeadsAsUninteresting(
-      final RevWalk walk,
-      Set<ObjectId> existing,
-      @Nullable String forRef) {
+  private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
     for (Ref ref : allRefs.values()) {
-      if (ref.getObjectId() == null) {
-        continue;
-      } else if (ref.getName().startsWith(REFS_CHANGES)) {
-        existing.add(ref.getObjectId());
-      } else if (ref.getName().startsWith(R_HEADS)
-          || (forRef != null && forRef.equals(ref.getName()))) {
+      if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
+          && ref.getObjectId() != null) {
         try {
-          walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
+          rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
         } catch (IOException e) {
           log.warn(String.format("Invalid ref %s in %s",
               ref.getName(), project.getName()), e);
-          continue;
         }
       }
     }
@@ -1653,38 +1739,43 @@
     final ReceiveCommand cmd;
     final ChangeInserter ins;
     boolean created;
+    Collection<String> groups;
 
     CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
         throws OrmException {
       commit = c;
       change = new Change(changeKey,
           new Change.Id(db.nextChangeId()),
-          currentUser.getAccountId(),
+          user.getAccountId(),
           magicBranch.dest,
           TimeUtil.nowTs());
       change.setTopic(magicBranch.topic);
-      ins = changeInserterFactory.create(ctl.getProjectControl(), change, c)
-          .setDraft(magicBranch.draft);
+      ins = changeInserterFactory.create(ctl, change, c)
+          .setDraft(magicBranch.draft)
+          // Changes already validated in validateNewCommits.
+          .setValidatePolicy(CommitValidators.Policy.NONE);
       cmd = new ReceiveCommand(ObjectId.zeroId(), c,
           ins.getPatchSet().getRefName());
+      ins.setUpdateRefCommand(cmd);
+      if (rp.getPushCertificate() != null) {
+        ins.setPushCertificate(rp.getPushCertificate().toTextWithSignature());
+      }
     }
 
-    CheckedFuture<Void, InsertException> insertChange() throws IOException {
+    CheckedFuture<Void, RestApiException> insertChange() throws IOException {
       rp.getRevWalk().parseBody(commit);
 
       final Thread caller = Thread.currentThread();
       ListenableFuture<Void> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<Void>() {
         @Override
-        public Void call() throws OrmException, IOException {
+        public Void call()
+            throws OrmException, RestApiException, UpdateException {
           if (caller == Thread.currentThread()) {
-            insertChange(db);
+            insertChange(ReceiveCommits.this.db);
           } else {
-            ReviewDb db = schemaFactory.open();
-            try {
-              insertChange(db);
-            } finally {
-              db.close();
+            try (ReviewDb threadLocalDb = schemaFactory.open()) {
+              insertChange(threadLocalDb);
             }
           }
           synchronized (newProgress) {
@@ -1696,35 +1787,41 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
-    private void insertChange(ReviewDb db) throws OrmException, IOException {
-      final PatchSet ps = ins.getPatchSet();
-      final Account.Id me = currentUser.getAccountId();
+    private void insertChange(ReviewDb threadLocalDb)
+        throws OrmException, RestApiException, UpdateException {
+      final PatchSet ps = ins.setGroups(groups).getPatchSet();
+      final Account.Id me = user.getAccountId();
       final List<FooterLine> footerLines = commit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
       Map<String, Short> approvals = new HashMap<>();
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
         approvals = magicBranch.labels;
-        ins.setHashtags(magicBranch.hashtags);
       }
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
-
-      ChangeMessage msg =
-          new ChangeMessage(new ChangeMessage.Key(change.getId(),
-              ChangeUtil.messageUUID(db)), me, ps.getCreatedOn(), ps.getId());
-      StringBuilder msgs = renderMessageWithApprovals(ps.getPatchSetId(),
-          approvals, Collections.<String, PatchSetApproval>emptyMap());
-      msg.setMessage(msgs.toString() + ".");
-
-      ins
-        .setReviewers(recipients.getReviewers())
-        .setExtraCC(recipients.getCcOnly())
-        .setApprovals(approvals)
-        .setMessage(msg)
-        .setRequestScopePropagator(requestScopePropagator)
-        .setSendMail(true)
-        .insert();
+      String msg = renderMessageWithApprovals(ps.getPatchSetId(), null,
+          approvals, Collections.<String, PatchSetApproval> emptyMap());
+      try (ObjectInserter oi = repo.newObjectInserter();
+          BatchUpdate bu = batchUpdateFactory.create(threadLocalDb,
+            change.getProject(), user, change.getCreatedOn())) {
+        bu.setRepository(repo, rp.getRevWalk(), oi);
+        bu.insertChange(ins
+            .setReviewers(recipients.getReviewers())
+            .setExtraCC(recipients.getCcOnly())
+            .setApprovals(approvals)
+            .setMessage(msg)
+            .setRequestScopePropagator(requestScopePropagator)
+            .setSendMail(true)
+            .setUpdateRef(true));
+        if (magicBranch != null) {
+          bu.addOp(
+              ins.getChange().getId(),
+              hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags))
+                .setRunHooks(false));
+        }
+        bu.execute();
+      }
       created = true;
 
       if (magicBranch != null && magicBranch.submit) {
@@ -1734,41 +1831,31 @@
   }
 
   private void submit(ChangeControl changeCtl, PatchSet ps)
-      throws OrmException, IOException {
+      throws OrmException, ResourceConflictException {
     Submit submit = submitProvider.get();
     RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
-    Change c;
     try {
-      // Force submit even if submit rule evaluation fails.
-      c = submit.submit(rsrc, currentUser, true);
-    } catch (ResourceConflictException e) {
-      throw new IOException(e);
+      mergeOpProvider.get().merge(db, rsrc.getChange(),
+          changeCtl.getUser().asIdentifiedUser(), false);
+    } catch (NoSuchChangeException e) {
+      throw new OrmException(e);
     }
-    if (c == null) {
-      addError("Submitting change " + changeCtl.getChange().getChangeId()
-          + " failed.");
-    } else {
-      addMessage("");
-      mergeQueue.merge(c.getDest());
-      c = db.changes().get(c.getId());
-      switch (c.getStatus()) {
-        case SUBMITTED:
-          addMessage("Change " + c.getChangeId() + " submitted.");
+    addMessage("");
+    Change c = db.changes().get(rsrc.getChange().getId());
+    switch (c.getStatus()) {
+      case MERGED:
+        addMessage("Change " + c.getChangeId() + " merged.");
+        break;
+      case NEW:
+        ChangeMessage msg = submit.getConflictMessage(rsrc);
+        if (msg != null) {
+          addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
           break;
-        case MERGED:
-          addMessage("Change " + c.getChangeId() + " merged.");
-          break;
-        case NEW:
-          ChangeMessage msg = submit.getConflictMessage(rsrc);
-          if (msg != null) {
-            addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
-            break;
-          }
-          //$FALL-THROUGH$
-        default:
-          addMessage("change " + c.getChangeId() + " is "
-              + c.getStatus().name().toLowerCase());
-      }
+        }
+        //$FALL-THROUGH$
+      default:
+        addMessage("change " + c.getChangeId() + " is "
+            + c.getStatus().name().toLowerCase());
     }
   }
 
@@ -1841,7 +1928,7 @@
     }
   }
 
-  private StringBuilder renderMessageWithApprovals(int patchSetId,
+  private String renderMessageWithApprovals(int patchSetId, String suffix,
       Map<String, Short> n, Map<String, PatchSetApproval> c) {
     StringBuilder msgs = new StringBuilder("Uploaded patch set " + patchSetId);
     if (!n.isEmpty()) {
@@ -1859,7 +1946,12 @@
             .append(LabelVote.create(e.getKey(), e.getValue()).format());
       }
     }
-    return msgs;
+
+    if (!Strings.isNullOrEmpty(suffix)) {
+      msgs.append(suffix);
+    }
+
+    return msgs.append('.').toString();
   }
 
   private class ReplaceRequest {
@@ -1878,6 +1970,7 @@
     String mergedIntoRef;
     boolean skip;
     private PatchSet.Id priorPatchSet;
+    Collection<String> groups;
 
     ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
         final ReceiveCommand cmd, final boolean checkMergedInto) {
@@ -1915,12 +2008,6 @@
       }
 
       RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-      if (newCommit == priorCommit) {
-        // Ignore requests to make the change its current state.
-        skip = true;
-        reject(inputCommand, "commit already exists (as current patchset)");
-        return false;
-      }
 
       changeCtl = projectControl.controlFor(change);
       if (!changeCtl.canAddPatchSet()) {
@@ -2007,7 +2094,7 @@
       Optional<ChangeEdit> edit = null;
 
       try {
-        edit = editUtil.byChange(change, currentUser);
+        edit = editUtil.byChange(change, user);
       } catch (IOException e) {
         log.error("Cannt retrieve edit", e);
         return false;
@@ -2041,29 +2128,35 @@
           ObjectId.zeroId(),
           newCommit,
           RefNames.refsEdit(
-              currentUser.getAccountId(),
+              user.getAccountId(),
               change.getId(),
               newPatchSet.getId()));
     }
 
-    private void newPatchSet() {
+    private void newPatchSet() throws IOException {
       PatchSet.Id id =
           ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId());
       newPatchSet = new PatchSet(id);
       newPatchSet.setCreatedOn(TimeUtil.nowTs());
-      newPatchSet.setUploader(currentUser.getAccountId());
+      newPatchSet.setUploader(user.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
+      newPatchSet.setGroups(groups);
+      if (rp.getPushCertificate() != null) {
+        newPatchSet.setPushCertificate(
+            rp.getPushCertificate().toTextWithSignature());
+      }
       if (magicBranch != null && magicBranch.draft) {
         newPatchSet.setDraft(true);
       }
-      info = patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+      info = patchSetInfoFactory.get(
+          rp.getRevWalk(), newCommit, newPatchSet.getId());
       cmd = new ReceiveCommand(
           ObjectId.zeroId(),
           newCommit,
           newPatchSet.getRefName());
     }
 
-    CheckedFuture<PatchSet.Id, InsertException> insertPatchSet()
+    CheckedFuture<PatchSet.Id, RestApiException> insertPatchSet()
         throws IOException {
       rp.getRevWalk().parseBody(newCommit);
 
@@ -2071,18 +2164,16 @@
       ListenableFuture<PatchSet.Id> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<PatchSet.Id>() {
         @Override
-        public PatchSet.Id call() throws OrmException, IOException {
+        public PatchSet.Id call() throws OrmException, IOException,
+            ResourceConflictException {
           try {
             if (magicBranch != null && magicBranch.edit) {
               return upsertEdit();
             } else if (caller == Thread.currentThread()) {
               return insertPatchSet(db);
             } else {
-              ReviewDb db = schemaFactory.open();
-              try {
+              try (ReviewDb db = schemaFactory.open()) {
                 return insertPatchSet(db);
-              } finally {
-                db.close();
               }
             }
           } catch (OrmException | IOException  e) {
@@ -2103,24 +2194,26 @@
         throws OrmException {
       msg =
           new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-              .messageUUID(db)), currentUser.getAccountId(), newPatchSet.getCreatedOn(),
+              .messageUUID(db)), user.getAccountId(), newPatchSet.getCreatedOn(),
               newPatchSet.getId());
-      StringBuilder msgs = renderMessageWithApprovals(
-          newPatchSet.getPatchSetId(), approvals, scanLabels(db, approvals));
+
+      msg.setMessage(renderMessageWithApprovals(newPatchSet.getPatchSetId(),
+          changeKindMessage(changeKind), approvals, scanLabels(db, approvals)));
+
+      return msg;
+    }
+
+    private String changeKindMessage(ChangeKind changeKind) {
       switch (changeKind) {
         case TRIVIAL_REBASE:
         case NO_CHANGE:
-          msgs.append(": Patch Set " + priorPatchSet.get() + " was rebased");
-          break;
+          return ": Patch Set " + priorPatchSet.get() + " was rebased";
         case NO_CODE_CHANGE:
-          msgs.append(": Commit message was updated");
-          break;
+          return ": Commit message was updated";
         case REWORK:
         default:
-          break;
+          return null;
       }
-      msg.setMessage(msgs.toString() + ".");
-      return msg;
     }
 
     private Map<String, PatchSetApproval> scanLabels(ReviewDb db,
@@ -2130,7 +2223,7 @@
       // We optimize here and only retrieve current when approvals provided
       if (!approvals.isEmpty()) {
         for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
-            db, changeCtl, priorPatchSet, currentUser.getAccountId())) {
+            db, changeCtl, priorPatchSet, user.getAccountId())) {
           if (a.isSubmit()) {
             continue;
           }
@@ -2151,8 +2244,9 @@
       return newPatchSet.getId();
     }
 
-    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException {
-      final Account.Id me = currentUser.getAccountId();
+    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException,
+        ResourceConflictException {
+      final Account.Id me = user.getAccountId();
       final List<FooterLine> footerLines = newCommit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
       Map<String, Short> approvals = new HashMap<>();
@@ -2182,7 +2276,9 @@
           return null;
         }
 
-        ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
+        if (newPatchSet.getGroups() == null) {
+          newPatchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
+        }
         db.patchSets().insert(Collections.singleton(newPatchSet));
 
         if (checkMergedInto) {
@@ -2266,15 +2362,14 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
-      CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
+      indexer.index(db, change);
       if (changeKind != ChangeKind.TRIVIAL_REBASE) {
-        workQueue.getDefaultQueue()
-            .submit(requestScopePropagator.wrap(new Runnable() {
+        sendEmailExecutor.submit(requestScopePropagator.wrap(new Runnable() {
           @Override
           public void run() {
             try {
               ReplacePatchSetSender cm =
-                  replacePatchSetFactory.create(change);
+                  replacePatchSetFactory.create(change.getId());
               cm.setFrom(me);
               cm.setPatchSet(newPatchSet, info);
               cm.setChangeMessage(msg);
@@ -2295,18 +2390,17 @@
           }
         }));
       }
-      f.checkedGet();
 
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
           ObjectId.zeroId(), newCommit);
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
       if (mergedIntoRef != null) {
         hooks.doChangeMergedHook(
-            change, currentUser.getAccount(), newPatchSet, db, newCommit.getName());
+            change, user.getAccount(), newPatchSet, db, newCommit.getName());
       }
 
       if (!approvals.isEmpty()) {
-        hooks.doCommentAddedHook(change, currentUser.getAccount(), newPatchSet,
+        hooks.doCommentAddedHook(change, user.getAccount(), newPatchSet,
             null, approvals, db);
       }
 
@@ -2318,6 +2412,61 @@
     }
   }
 
+  private class UpdateGroupsRequest {
+    private final PatchSet.Id psId;
+    private final RevCommit commit;
+    Set<String> groups;
+
+    UpdateGroupsRequest(Ref ref, RevCommit commit) {
+      this.psId = checkNotNull(PatchSet.Id.fromRef(ref.getName()));
+      this.commit = commit;
+    }
+
+    private void updateGroups(ReviewDb db) throws OrmException, IOException {
+      PatchSet ps = db.patchSets().atomicUpdate(psId,
+          new AtomicUpdate<PatchSet>() {
+            @Override
+            public PatchSet update(PatchSet ps) {
+              List<String> oldGroups = ps.getGroups();
+              if (oldGroups == null) {
+                if (groups == null) {
+                  return null;
+                }
+              } else if (Sets.newHashSet(oldGroups).equals(groups)) {
+                return null;
+              }
+              ps.setGroups(groups);
+              return ps;
+            }
+          });
+      if (ps != null) {
+        Change change = db.changes().get(psId.getParentKey());
+        if (change != null) {
+          indexer.index(db, change);
+        }
+      }
+    }
+
+    CheckedFuture<Void, RestApiException> updateGroups() {
+      final Thread caller = Thread.currentThread();
+      ListenableFuture<Void> future = changeUpdateExector.submit(
+          requestScopePropagator.wrap(new Callable<Void>() {
+        @Override
+        public Void call() throws OrmException, IOException {
+          if (caller == Thread.currentThread()) {
+            updateGroups(db);
+          } else {
+            try (ReviewDb db = schemaFactory.open()) {
+              updateGroups(db);
+            }
+          }
+          return null;
+        }
+      }));
+      return Futures.makeChecked(future, INSERT_EXCEPTION);
+    }
+  }
+
   private List<Ref> refs(Change.Id changeId) {
     return refsByChange().get(changeId);
   }
@@ -2428,35 +2577,33 @@
       return;
     }
 
-    boolean defaultName = Strings.isNullOrEmpty(currentUser.getAccount().getFullName());
+    boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName());
     final RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
-      Set<ObjectId> existing = Sets.newHashSet();
       RevObject parsedObject = walk.parseAny(cmd.getNewId());
       if (!(parsedObject instanceof RevCommit)) {
         return;
       }
+      SetMultimap<ObjectId, Ref> existing = changeRefsById();
       walk.markStart((RevCommit)parsedObject);
-      markHeadsAsUninteresting(walk, existing, cmd.getRefName());
-
-      RevCommit c;
-      while ((c = walk.next()) != null) {
-        if (existing.contains(c)) {
+      markHeadsAsUninteresting(walk, cmd.getRefName());
+      for (RevCommit c; (c = walk.next()) != null;) {
+        if (existing.keySet().contains(c)) {
           continue;
         } else if (!validCommit(ctl, cmd, c)) {
           break;
         }
 
-        if (defaultName && currentUser.hasEmailAddress(
+        if (defaultName && user.hasEmailAddress(
               c.getCommitterIdent().getEmailAddress())) {
           try {
-            Account a = db.accounts().get(currentUser.getAccountId());
+            Account a = db.accounts().get(user.getAccountId());
             if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
               a.setFullName(c.getCommitterIdent().getName());
               db.accounts().update(Collections.singleton(a));
-              currentUser.getAccount().setFullName(a.getFullName());
+              user.getAccount().setFullName(a.getFullName());
               accountCache.evict(a.getId());
             }
           } catch (OrmException e) {
@@ -2480,7 +2627,7 @@
     }
 
     CommitReceivedEvent receiveEvent =
-        new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, currentUser);
+        new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user);
     CommitValidators commitValidators =
         commitValidatorsFactory.create(ctl, sshInfo, repo);
 
@@ -2552,21 +2699,10 @@
           closeProgress.update(1);
         }
       }
-
-      // Update superproject gitlinks if required.
-      subOpFactory.create(
-          branch, newTip, rw, repo, project,
-          new ArrayList<Change>(),
-          new HashMap<Change.Id, CodeReviewCommit>(),
-          currentUser.getAccount()).update();
-    } catch (InsertException e) {
+    } catch (RestApiException e) {
       log.error("Can't insert patchset", e);
-    } catch (IOException e) {
+    } catch (IOException | OrmException e) {
       log.error("Can't scan for changes to close", e);
-    } catch (OrmException e) {
-      log.error("Can't scan for changes to close", e);
-    } catch (SubmoduleException e) {
-      log.error("Can't complete git links check", e);
     }
   }
 
@@ -2595,11 +2731,11 @@
     result.change = change;
     result.changeCtl = projectControl.controlFor(change);
     result.newPatchSet = ps;
-    result.info = patchSetInfoFactory.get(commit, psi);
+    result.info = patchSetInfoFactory.get(rp.getRevWalk(), commit, psi);
     result.mergedIntoRef = refName;
     markChangeMergedByPush(db, result, result.changeCtl);
     hooks.doChangeMergedHook(
-        change, currentUser.getAccount(), result.newPatchSet, db, commit.getName());
+        change, user.getAccount(), result.newPatchSet, db, commit.getName());
     sendMergedEmail(result);
     return change.getKey();
   }
@@ -2648,7 +2784,7 @@
       msgBuf.append(".");
       ChangeMessage msg = new ChangeMessage(
           new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)),
-          currentUser.getAccountId(), change.getLastUpdatedOn(),
+          user.getAccountId(), change.getLastUpdatedOn(),
           result.info.getKey());
       msg.setMessage(msgBuf.toString());
 
@@ -2664,13 +2800,13 @@
   }
 
   private void sendMergedEmail(final ReplaceRequest result) {
-    workQueue.getDefaultQueue()
-        .submit(requestScopePropagator.wrap(new Runnable() {
+    final Change.Id id = result.change.getId();
+    sendEmailExecutor.submit(requestScopePropagator.wrap(new Runnable() {
       @Override
       public void run() {
         try {
-          final MergedSender cm = mergedSenderFactory.create(result.changeCtl);
-          cm.setFrom(currentUser.getAccountId());
+          final MergedSender cm = mergedSenderFactory.create(id);
+          cm.setFrom(user.getAccountId());
           cm.setPatchSet(result.newPatchSet, result.info);
           cm.send();
         } catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index 7095552..43a4d4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -28,11 +28,8 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ServiceMayNotContinueException;
@@ -41,6 +38,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
@@ -89,15 +87,11 @@
         r.put(name, e.getValue());
       }
     }
-    rp.setAdvertisedRefs(r, advertiseHistory(r.values(), rp));
+    rp.setAdvertisedRefs(r, advertiseOpenChanges());
   }
 
-  private Set<ObjectId> advertiseHistory(
-      Iterable<Ref> sending,
-      BaseReceivePack rp) {
-    Set<ObjectId> toInclude = Sets.newHashSet();
-
-    // Advertise some recent open changes, in case a commit is based one.
+  private Set<ObjectId> advertiseOpenChanges() {
+    // Advertise some recent open changes, in case a commit is based on one.
     final int limit = 32;
     try {
       Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(limit);
@@ -110,67 +104,18 @@
           toGet.add(id);
         }
       }
+
+      Set<ObjectId> r = Sets.newHashSetWithExpectedSize(toGet.size());
       for (PatchSet ps : db.patchSets().get(toGet)) {
         if (ps.getRevision() != null && ps.getRevision().get() != null) {
-          toInclude.add(ObjectId.fromString(ps.getRevision().get()));
+          r.add(ObjectId.fromString(ps.getRevision().get()));
         }
       }
+      return r;
     } catch (OrmException err) {
       log.error("Cannot list open changes of " + projectName, err);
+      return Collections.emptySet();
     }
-
-    // Size of an additional ".have" line.
-    final int haveLineLen = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
-
-    // Maximum number of bytes to "waste" in the advertisement with
-    // a peek at this repository's current reachable history.
-    final int maxExtraSize = 8192;
-
-    // Number of recent commits to advertise immediately, hoping to
-    // show a client a nearby merge base.
-    final int base = 64;
-
-    // Number of commits to skip once base has already been shown.
-    final int step = 16;
-
-    // Total number of commits to extract from the history.
-    final int max = maxExtraSize / haveLineLen;
-
-    // Scan history until the advertisement is full.
-    Set<ObjectId> alreadySending = Sets.newHashSet();
-    RevWalk rw = rp.getRevWalk();
-    for (Ref ref : sending) {
-      try {
-        if (ref.getObjectId() != null) {
-          alreadySending.add(ref.getObjectId());
-          rw.markStart(rw.parseCommit(ref.getObjectId()));
-        }
-      } catch (IOException badCommit) {
-        continue;
-      }
-    }
-
-    int stepCnt = 0;
-    RevCommit c;
-    try {
-      while ((c = rw.next()) != null && toInclude.size() < max) {
-        if (alreadySending.contains(c)) {
-        } else if (toInclude.contains(c)) {
-        } else if (c.getParentCount() > 1) {
-        } else if (toInclude.size() < base) {
-          toInclude.add(c);
-        } else {
-          stepCnt = ++stepCnt % step;
-          if (stepCnt == 0) {
-            toInclude.add(c);
-          }
-        }
-      }
-    } catch (IOException err) {
-      log.error("Error trying to advertise history on " + projectName, err);
-    }
-    rw.reset();
-    return toInclude;
   }
 
   private static boolean skip(String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
index 25fbfb9..8c428a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -25,6 +25,7 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
@@ -47,11 +48,14 @@
 
   @Provides
   @Singleton
-  @EmailReviewCommentsExecutor
-  public WorkQueue.Executor createEmailReviewCommentsExecutor(
+  @SendEmailExecutor
+  public ExecutorService createSendEmailExecutor(
       @GerritServerConfig Config config, WorkQueue queues) {
     int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
-    return queues.createQueue(poolSize, "EmailReviewComments");
+    if (poolSize == 0) {
+      return MoreExecutors.newDirectExecutorService();
+    }
+    return queues.createQueue(poolSize, "SendEmail");
   }
 
   @Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
deleted file mode 100644
index 734512f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.util.OneOffRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.HashSet;
-
-@Singleton
-public class ReloadSubmitQueueOp extends DefaultQueueOp {
-  private static final Logger log =
-      LoggerFactory.getLogger(ReloadSubmitQueueOp.class);
-
-  private final OneOffRequestContext requestContext;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final MergeQueue mergeQueue;
-
-  @Inject
-  ReloadSubmitQueueOp(
-      OneOffRequestContext rc,
-      WorkQueue wq,
-      Provider<InternalChangeQuery> qp,
-      MergeQueue mq) {
-    super(wq);
-    requestContext = rc;
-    queryProvider = qp;
-    mergeQueue = mq;
-  }
-
-  @Override
-  public void run() {
-    try (AutoCloseable ctx = requestContext.open()) {
-      HashSet<Branch.NameKey> pending = new HashSet<>();
-      for (ChangeData cd : queryProvider.get().allSubmitted()) {
-        try {
-          pending.add(cd.change().getDest());
-        } catch (OrmException e) {
-          log.error("Error reading submitted change", e);
-        }
-      }
-
-      for (Branch.NameKey branch : pending) {
-        mergeQueue.schedule(branch);
-      }
-    } catch (Exception e) {
-      log.error("Cannot reload MergeQueue", e);
-    }
-  }
-
-  @Override
-  public String toString() {
-    return "Reload Submit Queue";
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
index a89a89b..06314db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -92,9 +92,7 @@
         }
       } catch (RepositoryNotFoundException noProject) {
         continue;
-      } catch (ConfigInvalidException err) {
-        log.error("Cannot rename group " + oldName + " in " + projectName, err);
-      } catch (IOException err) {
+      } catch (ConfigInvalidException | IOException err) {
         log.error("Cannot rename group " + oldName + " in " + projectName, err);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailReviewCommentsExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java
similarity index 80%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/EmailReviewCommentsExecutor.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java
index 9ad0dfc..68fa98a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailReviewCommentsExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java
@@ -16,16 +16,14 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
 
 /**
- * Marker on the global {@link WorkQueue.Executor} used by
- * {@link EmailReviewComments}.
+ * Marker on the global {@link WorkQueue.Executor} used to send email.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
-public @interface EmailReviewCommentsExecutor {
+public @interface SendEmailExecutor {
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 3cf9fed..6670450 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -14,29 +14,28 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -44,6 +43,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
@@ -61,126 +61,109 @@
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 public class SubmoduleOp {
-  public interface Factory {
-    SubmoduleOp create(Branch.NameKey destBranch, RevCommit mergeTip,
-        RevWalk rw, Repository db, Project destProject, List<Change> submitted,
-        Map<Change.Id, CodeReviewCommit> commits, Account account);
-  }
-
   private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
   private static final String GIT_MODULES = ".gitmodules";
 
-  private final Branch.NameKey destBranch;
-  private RevCommit mergeTip;
-  private RevWalk rw;
   private final Provider<String> urlProvider;
-  private ReviewDb schema;
-  private Repository db;
-  private Project destProject;
-  private List<Change> submitted;
-  private final Map<Change.Id, CodeReviewCommit> commits;
   private final PersonIdent myIdent;
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final Set<Branch.NameKey> updatedSubscribers;
   private final Account account;
   private final ChangeHooks changeHooks;
+  private final SubmoduleSectionParser.Factory subSecParserFactory;
+  private final boolean verboseSuperProject;
 
   @Inject
-  public SubmoduleOp(@Assisted final Branch.NameKey destBranch,
-      @Assisted RevCommit mergeTip, @Assisted RevWalk rw,
-      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
-      final SchemaFactory<ReviewDb> sf, @Assisted Repository db,
-      @Assisted Project destProject, @Assisted List<Change> submitted,
-      @Assisted final Map<Change.Id, CodeReviewCommit> commits,
-      @GerritPersonIdent final PersonIdent myIdent,
-      GitRepositoryManager repoManager, GitReferenceUpdated gitRefUpdated,
-      @Nullable @Assisted Account account, ChangeHooks changeHooks) {
-    this.destBranch = destBranch;
-    this.mergeTip = mergeTip;
-    this.rw = rw;
+  public SubmoduleOp(
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      @GerritPersonIdent PersonIdent myIdent,
+      @GerritServerConfig Config cfg,
+      GitRepositoryManager repoManager,
+      GitReferenceUpdated gitRefUpdated,
+      @Nullable Account account,
+      ChangeHooks changeHooks,
+      SubmoduleSectionParser.Factory subSecParserFactory) {
     this.urlProvider = urlProvider;
-    this.schemaFactory = sf;
-    this.db = db;
-    this.destProject = destProject;
-    this.submitted = submitted;
-    this.commits = commits;
     this.myIdent = myIdent;
     this.repoManager = repoManager;
     this.gitRefUpdated = gitRefUpdated;
     this.account = account;
     this.changeHooks = changeHooks;
+    this.subSecParserFactory = subSecParserFactory;
+    this.verboseSuperProject = cfg.getBoolean("submodule",
+        "verboseSuperprojectUpdate", true);
 
     updatedSubscribers = new HashSet<>();
   }
 
-  public void update() throws SubmoduleException {
-    try {
-      schema = schemaFactory.open();
-
-      updateSubmoduleSubscriptions();
-      updateSuperProjects(destBranch, rw, mergeTip.getId().toObjectId(), null);
-    } catch (OrmException e) {
-      throw new SubmoduleException("Cannot open database", e);
-    } finally {
-      if (schema != null) {
-        schema.close();
-        schema = null;
-      }
+  void updateSubmoduleSubscriptions(ReviewDb db, Set<Branch.NameKey> branches)
+      throws SubmoduleException {
+    for (Branch.NameKey branch : branches) {
+      updateSubmoduleSubscriptions(db, branch);
     }
   }
 
-  private void updateSubmoduleSubscriptions() throws SubmoduleException {
+  void updateSubmoduleSubscriptions(ReviewDb db, Branch.NameKey destBranch)
+      throws SubmoduleException {
     if (urlProvider.get() == null) {
-      logAndThrowSubmoduleException("Cannot establish canonical web url used to access gerrit."
-              + " It should be provided in gerrit.config file.");
+      logAndThrowSubmoduleException("Cannot establish canonical web url used "
+          + "to access gerrit. It should be provided in gerrit.config file.");
     }
+    try (Repository repo = repoManager.openRepository(
+            destBranch.getParentKey());
+        RevWalk rw = new RevWalk(repo)) {
 
-    try {
-      final TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
-      if (tw != null
-          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) || FileMode.EXECUTABLE_FILE
-              .equals(tw.getRawMode(0)))) {
-
-        BlobBasedConfig bbc =
-            new BlobBasedConfig(null, db, mergeTip, GIT_MODULES);
-
-        final String thisServer = new URI(urlProvider.get()).getHost();
-
-        final Branch.NameKey target =
-            new Branch.NameKey(new Project.NameKey(destProject.getName()),
-                destBranch.get());
-
-        final Set<SubmoduleSubscription> oldSubscriptions =
-            new HashSet<>(schema.submoduleSubscriptions()
-                .bySuperProject(destBranch).toList());
-        final List<SubmoduleSubscription> newSubscriptions =
-            new SubmoduleSectionParser(bbc, thisServer, target, repoManager)
-                .parseAllSections();
-
-        final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-        for (SubmoduleSubscription s : newSubscriptions) {
-          if (oldSubscriptions.contains(s)) {
-            alreadySubscribeds.add(s);
-          }
-        }
-
-        oldSubscriptions.removeAll(newSubscriptions);
-        newSubscriptions.removeAll(alreadySubscribeds);
-
-        if (!oldSubscriptions.isEmpty()) {
-          schema.submoduleSubscriptions().delete(oldSubscriptions);
-        }
-        schema.submoduleSubscriptions().insert(newSubscriptions);
+      ObjectId id = repo.resolve(destBranch.get());
+      if (id == null) {
+        logAndThrowSubmoduleException(
+            "Cannot resolve submodule destination branch " + destBranch);
       }
+      RevCommit commit = rw.parseCommit(id);
+
+      Set<SubmoduleSubscription> oldSubscriptions =
+          Sets.newHashSet(db.submoduleSubscriptions()
+              .bySuperProject(destBranch));
+
+      Set<SubmoduleSubscription> newSubscriptions;
+      TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
+      if (tw != null
+          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) ||
+              FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
+        BlobBasedConfig bbc =
+            new BlobBasedConfig(null, repo, commit, GIT_MODULES);
+
+        String thisServer = new URI(urlProvider.get()).getHost();
+
+        newSubscriptions = subSecParserFactory.create(bbc, thisServer,
+            destBranch).parseAllSections();
+      } else {
+        newSubscriptions = Collections.emptySet();
+      }
+
+      Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
+      for (SubmoduleSubscription s : newSubscriptions) {
+        if (oldSubscriptions.contains(s)) {
+          alreadySubscribeds.add(s);
+        }
+      }
+
+      oldSubscriptions.removeAll(newSubscriptions);
+      newSubscriptions.removeAll(alreadySubscribeds);
+
+      if (!oldSubscriptions.isEmpty()) {
+        db.submoduleSubscriptions().delete(oldSubscriptions);
+      }
+      if (!newSubscriptions.isEmpty()) {
+        db.submoduleSubscriptions().insert(newSubscriptions);
+      }
+
     } catch (OrmException e) {
       logAndThrowSubmoduleException(
           "Database problem at update of subscriptions table from "
@@ -200,71 +183,31 @@
     }
   }
 
-  private void updateSuperProjects(final Branch.NameKey updatedBranch, RevWalk myRw,
-      final ObjectId mergedCommit, final String msg) throws SubmoduleException {
+  protected void updateSuperProjects(ReviewDb db,
+      Collection<Branch.NameKey> updatedBranches) throws SubmoduleException {
     try {
-      final List<SubmoduleSubscription> subscribers =
-          schema.submoduleSubscriptions().bySubmodule(updatedBranch).toList();
+      // These (repo/branch) will be updated later with all the given
+      // individual submodule subscriptions
+      Multimap<Branch.NameKey, SubmoduleSubscription> targets =
+          HashMultimap.create();
 
-      if (!subscribers.isEmpty()) {
-        // Initialize the message buffer
-        StringBuilder sb = new StringBuilder();
-        if (msg != null) {
-          sb.append(msg);
-        } else {
-          // The first updatedBranch on a cascade event of automatic
-          // updates of repos is added to updatedSubscribers set so
-          // if we face a situation having
-          // submodule-a(master)-->super(master)-->submodule-a(master),
-          // it will be detected we have a circular subscription
-          // when updateSuperProjects is called having as updatedBranch
-          // the super(master) value.
-          updatedSubscribers.add(updatedBranch);
-
-          for (final Change chg : submitted) {
-            final CodeReviewCommit c = commits.get(chg.getId());
-            if (c != null
-                && (c.getStatusCode() == CommitMergeStatus.CLEAN_MERGE
-                    || c.getStatusCode() == CommitMergeStatus.CLEAN_PICK
-                    || c.getStatusCode() == CommitMergeStatus.CLEAN_REBASE)) {
-              sb.append("\n")
-                .append(c.getFullMessage());
-            }
-          }
+      for (Branch.NameKey updatedBranch : updatedBranches) {
+        for (SubmoduleSubscription sub : db.submoduleSubscriptions()
+            .bySubmodule(updatedBranch)) {
+          targets.put(sub.getSuperProject(), sub);
         }
-
-        // update subscribers of this module
-        List<SubmoduleSubscription> incorrectSubscriptions = Lists.newLinkedList();
-        for (final SubmoduleSubscription s : subscribers) {
-          try {
-            if (!updatedSubscribers.add(s.getSuperProject())) {
-              log.error("Possible circular subscription involving " + s);
-            } else {
-
-            Map<Branch.NameKey, ObjectId> modules = new HashMap<>(1);
-              modules.put(updatedBranch, mergedCommit);
-
-            Map<Branch.NameKey, String> paths = new HashMap<>(1);
-              paths.put(updatedBranch, s.getPath());
-              updateGitlinks(s.getSuperProject(), myRw, modules, paths, sb.toString());
-            }
-          } catch (SubmoduleException e) {
-              log.warn("Cannot update gitlinks for " + s + " due to " + e.getMessage());
-              incorrectSubscriptions.add(s);
-          } catch (Exception e) {
-              log.error("Cannot update gitlinks for " + s, e);
+      }
+      updatedSubscribers.addAll(updatedBranches);
+      // Update subscribers.
+      for (Branch.NameKey dest : targets.keySet()) {
+        try {
+          if (!updatedSubscribers.add(dest)) {
+            log.error("Possible circular subscription involving " + dest);
+          } else {
+            updateGitlinks(db, dest, targets.get(dest));
           }
-        }
-
-        if (!incorrectSubscriptions.isEmpty()) {
-          try {
-            schema.submoduleSubscriptions().delete(incorrectSubscriptions);
-            log.info("Deleted incorrect submodule subscription(s) "
-                + incorrectSubscriptions);
-          } catch (OrmException e) {
-            log.error("Cannot delete submodule subscription(s) "
-                + incorrectSubscriptions, e);
-          }
+        } catch (SubmoduleException e) {
+          log.warn("Cannot update gitlinks for " + dest, e);
         }
       }
     } catch (OrmException e) {
@@ -272,85 +215,109 @@
     }
   }
 
-  private void updateGitlinks(final Branch.NameKey subscriber, RevWalk myRw,
-      final Map<Branch.NameKey, ObjectId> modules,
-      final Map<Branch.NameKey, String> paths, final String msg)
-      throws SubmoduleException {
+  /**
+   * Update the submodules in one branch of one repository.
+   *
+   * @param subscriber the branch of the repository which should be changed.
+   * @param updates submodule updates which should be updated to.
+   * @throws SubmoduleException
+   */
+  private void updateGitlinks(ReviewDb db, Branch.NameKey subscriber,
+      Collection<SubmoduleSubscription> updates) throws SubmoduleException {
     PersonIdent author = null;
+    StringBuilder msgbuf = new StringBuilder("Updated git submodules\n\n");
+    boolean sameAuthorForAll = true;
 
-    final StringBuilder msgbuf = new StringBuilder();
-    msgbuf.append("Updated " + subscriber.getParentKey().get());
-    Repository pdb = null;
-    RevWalk recRw = null;
+    try (Repository pdb = repoManager.openRepository(subscriber.getParentKey())) {
+      if (pdb.exactRef(subscriber.get()) == null) {
+        throw new SubmoduleException(
+            "The branch was probably deleted from the subscriber repository");
+      }
 
-    try {
-      boolean sameAuthorForAll = true;
+      DirCache dc = readTree(pdb, pdb.exactRef(subscriber.get()));
+      DirCacheEditor ed = dc.editor();
 
-      for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
-        RevCommit c = myRw.parseCommit(me.getValue());
-        if (c == null) {
-          continue;
-        }
-
-        msgbuf.append("\nProject: ");
-        msgbuf.append(me.getKey().getParentKey().get());
-        msgbuf.append("  ").append(me.getValue().getName());
-        msgbuf.append("\n");
-        if (modules.size() == 1) {
-          if (!Strings.isNullOrEmpty(msg)) {
-            msgbuf.append(msg);
-          } else {
-            msgbuf.append("\n");
-            msgbuf.append(c.getFullMessage());
+      for (SubmoduleSubscription s : updates) {
+        try (Repository subrepo = repoManager.openRepository(
+            s.getSubmodule().getParentKey());
+            RevWalk rw = CodeReviewCommit.newRevWalk(subrepo)) {
+          Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
+          if (ref == null) {
+            ed.add(new DeletePath(s.getPath()));
+            continue;
           }
-        } else {
-          msgbuf.append(c.getShortMessage());
-        }
-        msgbuf.append("\n");
 
-        if (author == null) {
-          author = c.getAuthorIdent();
-        } else if (!author.equals(c.getAuthorIdent())) {
-          sameAuthorForAll = false;
+          final ObjectId updateTo = ref.getObjectId();
+          RevCommit newCommit = rw.parseCommit(updateTo);
+
+          if (author == null) {
+            author = newCommit.getAuthorIdent();
+          } else if (!author.equals(newCommit.getAuthorIdent())) {
+            sameAuthorForAll = false;
+          }
+
+          DirCacheEntry dce = dc.getEntry(s.getPath());
+          ObjectId oldId;
+          if (dce != null) {
+            if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+              log.error("Requested to update gitlink " + s.getPath() + " in "
+                  + s.getSubmodule().getParentKey().get() + " but entry "
+                  + "doesn't have gitlink file mode.");
+              continue;
+            }
+            oldId = dce.getObjectId();
+          } else {
+            // This submodule did not exist before. We do not want to add
+            // the full submodule history to the commit message, so omit it.
+            oldId = updateTo;
+          }
+
+          ed.add(new PathEdit(s.getPath()) {
+            @Override
+            public void apply(DirCacheEntry ent) {
+              ent.setFileMode(FileMode.GITLINK);
+              ent.setObjectId(updateTo);
+            }
+          });
+          if (verboseSuperProject) {
+            msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
+            msgbuf.append(" " + s.getSubmodule().getShortName());
+            msgbuf.append(" " + updateTo.getName());
+            msgbuf.append("\n\n");
+
+            try {
+              rw.markStart(newCommit);
+              rw.markUninteresting(rw.parseCommit(oldId));
+              for (RevCommit c : rw) {
+                msgbuf.append(c.getFullMessage() + "\n\n");
+              }
+            } catch (IOException e) {
+              logAndThrowSubmoduleException("Could not perform a revwalk to "
+                  + "create superproject commit message", e);
+            }
+          }
         }
       }
+      ed.finish();
 
       if (!sameAuthorForAll || author == null) {
         author = myIdent;
       }
 
-      pdb = repoManager.openRepository(subscriber.getParentKey());
-      if (pdb.getRef(subscriber.get()) == null) {
-        throw new SubmoduleException(
-            "The branch was probably deleted from the subscriber repository");
-      }
-
-      final ObjectId currentCommitId =
-          pdb.getRef(subscriber.get()).getObjectId();
-
-      DirCache dc = readTree(pdb, pdb.getRef(subscriber.get()));
-      DirCacheEditor ed = dc.editor();
-      for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
-        ed.add(new PathEdit(paths.get(me.getKey())) {
-          @Override
-          public void apply(DirCacheEntry ent) {
-            ent.setFileMode(FileMode.GITLINK);
-            ent.setObjectId(me.getValue().copy());
-          }
-        });
-      }
-      ed.finish();
-
       ObjectInserter oi = pdb.newObjectInserter();
       ObjectId tree = dc.writeTree(oi);
 
-      final CommitBuilder commit = new CommitBuilder();
+      ObjectId currentCommitId =
+          pdb.exactRef(subscriber.get()).getObjectId();
+
+      CommitBuilder commit = new CommitBuilder();
       commit.setTreeId(tree);
       commit.setParentIds(new ObjectId[] {currentCommitId});
       commit.setAuthor(author);
       commit.setCommitter(myIdent);
       commit.setMessage(msgbuf.toString());
       oi.insert(commit);
+      oi.flush();
 
       ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
 
@@ -372,21 +339,11 @@
         default:
           throw new IOException(rfu.getResult().name());
       }
-
-      recRw = new RevWalk(pdb);
-
       // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(subscriber, recRw, commitId, msgbuf.toString());
+      updateSuperProjects(db, Sets.newHashSet(subscriber));
     } catch (IOException e) {
-        throw new SubmoduleException("Cannot update gitlinks for "
-            + subscriber.get(), e);
-    } finally {
-      if (recRw != null) {
-        recRw.close();
-      }
-      if (pdb != null) {
-        pdb.close();
-      }
+      throw new SubmoduleException("Cannot update gitlinks for "
+          + subscriber.get(), e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
new file mode 100644
index 0000000..3cbac3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2015 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.server.git;
+
+import org.slf4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class TabFile {
+  public interface Parser {
+    public String parse(String str);
+  }
+
+  public static Parser TRIM = new Parser() {
+    @Override
+    public String parse(String str) {
+       return str.trim();
+    }
+  };
+
+  protected static class Row {
+    public String left;
+    public String right;
+
+    public Row(String left, String right) {
+      this.left = left;
+      this.right = right;
+    }
+  }
+
+  protected static List<Row> parse(String text, String filename, Parser left,
+      Parser right, ValidationError.Sink errors) throws IOException {
+    List<Row> rows = new ArrayList<>();
+    BufferedReader br = new BufferedReader(new StringReader(text));
+    String s;
+    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
+      if (s.isEmpty() || s.startsWith("#")) {
+        continue;
+      }
+
+      int tab = s.indexOf('\t');
+      if (tab < 0) {
+        errors.error(new ValidationError(filename, lineNumber,
+            "missing tab delimiter"));
+        continue;
+      }
+
+      Row row = new Row(s.substring(0, tab), s.substring(tab + 1));
+      rows.add(row);
+
+      if (left != null) {
+        row.left = left.parse(row.left);
+      }
+      if (right != null) {
+        row.right = right.parse(row.right);
+      }
+    }
+    return rows;
+  }
+
+  protected static Map<String, String> toMap(List<Row> rows) {
+    Map<String, String> map = new HashMap<>(rows.size());
+    for (Row row : rows) {
+      map.put(row.left, row.right);
+    }
+    return map;
+  }
+
+  protected static String asText(String left, String right,
+      Map<String, String> entries) {
+    if (entries.isEmpty()) {
+      return null;
+    }
+
+    List<Row> rows = new ArrayList<>(entries.size());
+    for (String key : sort(entries.keySet())) {
+      rows.add(new Row(key, entries.get(key)));
+    }
+    return asText(left, right, rows);
+  }
+
+  protected static String asText(String left, String right, List<Row> rows) {
+    if (rows.isEmpty()) {
+      return null;
+    }
+
+    left = "# " + left;
+    int leftLen = left.length();
+    for (Row row : rows) {
+      leftLen = Math.max(leftLen, row.left.length());
+    }
+
+    StringBuilder buf = new StringBuilder();
+    buf.append(pad(leftLen, left));
+    buf.append('\t');
+    buf.append(right);
+    buf.append('\n');
+
+    buf.append('#');
+    buf.append('\n');
+
+    for (Row row : rows) {
+      buf.append(pad(leftLen, row.left));
+      buf.append('\t');
+      buf.append(row.right);
+      buf.append('\n');
+    }
+    return buf.toString();
+  }
+
+  protected static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
+    ArrayList<T> r = new ArrayList<>(m);
+    Collections.sort(r);
+    return r;
+  }
+
+  protected static String pad(int len, String src) {
+    if (len <= src.length()) {
+      return src;
+    }
+
+    StringBuilder r = new StringBuilder(len);
+    r.append(src);
+    while (r.length() < len) {
+      r.append(' ');
+    }
+    return r.toString();
+  }
+
+  public static ValidationError.Sink createLoggerSink(String file, Logger log) {
+    return ValidationError.createLoggerSink("Error parsing file " + file + ": ",
+        log);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index a003235..3c7666e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -76,6 +76,7 @@
   }
 
   void prepare(TagMatcher m) {
+    @SuppressWarnings("resource")
     RevWalk rw = null;
     try {
       for (Ref currentRef : m.include) {
@@ -331,7 +332,7 @@
 
   static boolean skip(Ref ref) {
     return ref.isSymbolic() || ref.getObjectId() == null
-        || PatchSet.isRef(ref.getName());
+        || PatchSet.isChangeRef(ref.getName());
   }
 
   private static boolean isTag(Ref ref) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UpdateException.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/UpdateException.java
index d3ebb95..087af6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UpdateException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// Copyright (C) 2015 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,19 +14,19 @@
 
 package com.google.gerrit.server.git;
 
-/** Indicates the current branch's queue cannot be processed at this time. */
-public class MergeException extends Exception {
+/** Exception type thrown by {@link BatchUpdate} steps. */
+public class UpdateException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public MergeException(String msg) {
-    super(msg);
+  public UpdateException(String message) {
+    super(message);
   }
 
-  public MergeException(Throwable why) {
-    super(why);
+  public UpdateException(Throwable cause) {
+    super(cause);
   }
 
-  public MergeException(String msg, Throwable why) {
-    super(msg, why);
+  public UpdateException(String message, Throwable cause) {
+    super(message, cause);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
index 02bc8dc..a1c5b8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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,10 +14,17 @@
 
 package com.google.gerrit.server.git;
 
-/** Indicates that the commit cannot be merged without conflicts. */
-public class MergeConflictException extends Exception {
-  private static final long serialVersionUID = 1L;
-  public MergeConflictException(String msg) {
-    super(msg, null);
+public class UserConfigSections {
+
+  /** The my menu user preferences. */
+  public static final String MY = "my";
+
+  /** The edit user preferences. */
+  public static final String EDIT = "edit";
+
+  /** The diff user preferences. */
+  public static final String DIFF = "diff";
+
+  private UserConfigSections() {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
index ad84046..e6a8ae4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import org.slf4j.Logger;
+
 /** Indicates a problem with Git based data. */
 public class ValidationError {
   private final String message;
@@ -42,4 +44,13 @@
   public interface Sink {
     void error(ValidationError error);
   }
+
+  public static Sink createLoggerSink(final String message, final Logger log) {
+    return new ValidationError.Sink() {
+          @Override
+          public void error(ValidationError error) {
+            log.error(message + error.getMessage());
+          }
+        };
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index 37df726..e7b98d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -49,6 +50,7 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -59,6 +61,23 @@
  * later be written back to the repository.
  */
 public abstract class VersionedMetaData {
+  /**
+   * Path information that does not hold references to any repository
+   * data structures, allowing the application to retain this object
+   * for long periods of time.
+   */
+  public static class PathInfo {
+    public final FileMode fileMode;
+    public final String path;
+    public final ObjectId objectId;
+
+    protected PathInfo(TreeWalk tw) {
+      fileMode = tw.getFileMode(0);
+      path = tw.getPathString();
+      objectId = tw.getObjectId(0);
+    }
+  }
+
   private RevCommit revision;
   protected ObjectReader reader;
   protected ObjectInserter inserter;
@@ -96,7 +115,7 @@
    * @throws ConfigInvalidException
    */
   public void load(Repository db) throws IOException, ConfigInvalidException {
-    Ref ref = db.getRef(getRefName());
+    Ref ref = db.getRefDatabase().exactRef(getRefName());
     load(db, ref != null ? ref.getObjectId() : null);
   }
 
@@ -439,6 +458,17 @@
     return null;
   }
 
+  public List<PathInfo> getPathInfos(boolean recursive) throws IOException {
+    TreeWalk tw = new TreeWalk(reader);
+    tw.addTree(revision.getTree());
+    tw.setRecursive(recursive);
+    List<PathInfo> paths = Lists.newArrayList();
+    while (tw.next()) {
+      paths.add(new PathInfo(tw));
+    }
+    return paths;
+  }
+
   protected static void set(Config rc, String section, String subsection,
       String name, String value) {
     if (value != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index 9ccf153..46638f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -68,7 +68,7 @@
     this.showMetadata = showMetadata;
   }
 
-  public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeperately) {
+  public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeparately) {
     if (projectCtl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
       Map<String, Ref> r = Maps.newHashMap(refs);
       if (!projectCtl.controlForRef(RefNames.REFS_CONFIG).isVisible()) {
@@ -79,8 +79,8 @@
 
     Account.Id currAccountId;
     boolean canViewMetadata;
-    if (projectCtl.getCurrentUser().isIdentifiedUser()) {
-      IdentifiedUser user = ((IdentifiedUser) projectCtl.getCurrentUser());
+    if (projectCtl.getUser().isIdentifiedUser()) {
+      IdentifiedUser user = projectCtl.getUser().asIdentifiedUser();
       currAccountId = user.getAccountId();
       canViewMetadata = user.getCapabilities().canAccessDatabase();
     } else {
@@ -136,11 +136,11 @@
     // If we have tags that were deferred, we need to do a revision walk
     // to identify what tags we can actually reach, and what we cannot.
     //
-    if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeperately)) {
+    if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeparately)) {
       TagMatcher tags = tagCache.get(projectName).matcher(
           tagCache,
           db,
-          filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
+          filterTagsSeparately ? filter(db.getAllRefs()).values() : result.values());
       for (Ref tag : deferredTags) {
         if (tags.isReachable(tag)) {
           result.put(tag.getName(), tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index ca9b992..7d82d32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.ListenableFutureTask;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project;
@@ -110,8 +109,8 @@
     return defaultQueue;
   }
 
-  /** Create a new executor queue with one thread. */
-  public Executor createQueue(final int poolsize, final String prefix) {
+  /** Create a new executor queue. */
+  public Executor createQueue(int poolsize, String prefix) {
     final Executor r = new Executor(poolsize, prefix);
     r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
     r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
@@ -303,11 +302,9 @@
       final long delay = getDelay(TimeUnit.MILLISECONDS);
       if (delay <= 0) {
         return State.READY;
-      } else if (0 < delay) {
+      } else {
         return State.SLEEPING;
       }
-
-      return State.OTHER;
     }
 
     public Date getStartTime() {
@@ -390,24 +387,31 @@
     @Override
     public String toString() {
       //This is a workaround to be able to print a proper name when the task
-      //is wrapped into a ListenableFutureTask.
-      if (runnable instanceof ListenableFutureTask<?>) {
-        String errorMessage;
-        try {
-          for (Field field : ListenableFutureTask.class.getSuperclass()
-              .getDeclaredFields()) {
-            if (field.getType().isAssignableFrom(Callable.class)) {
+      //is wrapped into a TrustedListenableFutureTask.
+      try {
+        if (runnable.getClass().isAssignableFrom(Class.forName(
+            "com.google.common.util.concurrent.TrustedListenableFutureTask"))) {
+          Class<?> trustedFutureInterruptibleTask = Class.forName(
+              "com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask");
+          for (Field field : runnable.getClass().getDeclaredFields()) {
+            if (field.getType().isAssignableFrom(trustedFutureInterruptibleTask)) {
               field.setAccessible(true);
-              return ((Callable<?>) field.get(runnable)).toString();
+              Object innerObj = field.get(runnable);
+              if (innerObj != null) {
+                for (Field innerField : innerObj.getClass().getDeclaredFields()) {
+                  if (innerField.getType().isAssignableFrom(Callable.class)) {
+                    innerField.setAccessible(true);
+                    return ((Callable<?>) innerField.get(innerObj)).toString();
+                  }
+                }
+              }
             }
           }
-          errorMessage = "Cannot find wrapped Callable field";
-        } catch (SecurityException | IllegalArgumentException
-            | IllegalAccessException e) {
-          errorMessage = "Cannot call toString on Callable field";
         }
-        log.debug("Cannot get a proper name for ListenableFutureTask: {}",
-            errorMessage);
+      } catch (ClassNotFoundException | IllegalArgumentException
+          | IllegalAccessException e) {
+        log.debug("Cannot get a proper name for TrustedListenableFutureTask: {}",
+            e.getMessage());
       }
       return runnable.toString();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 60af3003..ecac2b35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -14,34 +14,37 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.GroupCollector;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeTip;
+import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -50,192 +53,199 @@
 
 public class CherryPick extends SubmitStrategy {
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final GitReferenceUpdated gitRefUpdated;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
 
   CherryPick(SubmitStrategy.Arguments args,
-      PatchSetInfoFactory patchSetInfoFactory,
-      GitReferenceUpdated gitRefUpdated) {
+      PatchSetInfoFactory patchSetInfoFactory) {
     super(args);
-
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
     this.newCommits = new HashMap<>();
   }
 
   @Override
   protected MergeTip _run(CodeReviewCommit branchTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException {
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
     List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
-    while (!sorted.isEmpty()) {
-      CodeReviewCommit n = sorted.remove(0);
-      try {
-        if (mergeTip.getCurrentTip() == null) {
-          cherryPickUnbornRoot(n, mergeTip);
+    boolean first = true;
+    try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
+      while (!sorted.isEmpty()) {
+        CodeReviewCommit n = sorted.remove(0);
+        Change.Id cid = n.change().getId();
+        if (first && branchTip == null) {
+          u.addOp(cid, new CherryPickUnbornRootOp(mergeTip, n));
         } else if (n.getParentCount() == 0) {
-          cherryPickRootOntoBranch(n);
+          u.addOp(cid, new CherryPickRootOp(n));
         } else if (n.getParentCount() == 1) {
-          cherryPickOne(n, mergeTip);
+          u.addOp(cid, new CherryPickOneOp(mergeTip, n));
         } else {
-          cherryPickMultipleParents(n, mergeTip);
+          u.addOp(cid, new CherryPickMultipleParentsOp(mergeTip, n));
         }
-      } catch (NoSuchChangeException | IOException | OrmException e) {
-        throw new MergeException("Cannot merge " + n.name(), e);
+        first = false;
       }
+      u.execute();
+    } catch (UpdateException | RestApiException e) {
+      throw new IntegrationException(
+          "Cannot cherry-pick onto " + args.destBranch);
     }
+    // TODO(dborowitz): When BatchUpdate is hoisted out of CherryPick,
+    // SubmitStrategy should probably no longer return MergeTip, instead just
+    // mutating a single shared MergeTip passed in from the caller.
     return mergeTip;
   }
 
-  private void cherryPickUnbornRoot(CodeReviewCommit n, MergeTip mergeTip) {
-    // The branch is unborn. Take fast-forward resolution to create the branch.
-    mergeTip.moveTipTo(n, n);
-    n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-  }
+  private static class CherryPickUnbornRootOp extends BatchUpdate.Op {
+    private final MergeTip mergeTip;
+    private final CodeReviewCommit toMerge;
 
-  private void cherryPickRootOntoBranch(CodeReviewCommit n) {
-    // Refuse to merge a root commit into an existing branch, we cannot obtain a
-    // delta for the cherry-pick to apply.
-    n.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
-  }
+    private CherryPickUnbornRootOp(MergeTip mergeTip,
+        CodeReviewCommit toMerge) {
+      this.mergeTip = mergeTip;
+      this.toMerge = toMerge;
+    }
 
-  private void cherryPickOne(CodeReviewCommit n, MergeTip mergeTip)
-      throws NoSuchChangeException, OrmException, IOException {
-    // If there is only one parent, a cherry-pick can be done by taking the
-    // delta relative to that one parent and redoing that on the current merge
-    // tip.
-    //
-    // Keep going in the case of a single merge failure; the goal is to
-    // cherry-pick as many commits as possible.
-    try {
-      CodeReviewCommit merge =
-          writeCherryPickCommit(mergeTip.getCurrentTip(), n);
-      mergeTip.moveTipTo(merge, merge);
-      newCommits.put(mergeTip.getCurrentTip().getPatchsetId()
-          .getParentKey(), mergeTip.getCurrentTip());
-    } catch (MergeConflictException mce) {
-      n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
-    } catch (MergeIdenticalTreeException mie) {
-      n.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
+    @Override
+    public void updateRepo(RepoContext ctx) {
+      // The branch is unborn. Take fast-forward resolution to create the
+      // branch.
+      mergeTip.moveTipTo(toMerge, toMerge);
+      toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
     }
   }
 
-  private void cherryPickMultipleParents(CodeReviewCommit n, MergeTip mergeTip)
-      throws IOException, MergeException {
-    // There are multiple parents, so this is a merge commit. We don't want
-    // to cherry-pick the merge as clients can't easily rebase their history
-    // with that merge present and replaced by an equivalent merge with a
-    // different first parent. So instead behave as though MERGE_IF_NECESSARY
-    // was configured.
-    if (!args.mergeUtil.hasMissingDependencies(args.mergeSorter, n)) {
-      if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
-        mergeTip.moveTipTo(n, n);
-      } else {
-        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(
-            args.serverIdent.get(), args.repo, args.rw, args.inserter,
-            args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(), n);
-        mergeTip.moveTipTo(result, n);
+  private static class CherryPickRootOp extends BatchUpdate.Op {
+    private final CodeReviewCommit toMerge;
+
+    private CherryPickRootOp(CodeReviewCommit toMerge) {
+      this.toMerge = toMerge;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) {
+      // Refuse to merge a root commit into an existing branch, we cannot obtain
+      // a delta for the cherry-pick to apply.
+      toMerge.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
+    }
+  }
+
+  private class CherryPickOneOp extends BatchUpdate.Op {
+    private final MergeTip mergeTip;
+    private final CodeReviewCommit toMerge;
+
+    private PatchSet.Id psId;
+    private CodeReviewCommit newCommit;
+    private PatchSetInfo patchSetInfo;
+
+    private CherryPickOneOp(MergeTip mergeTip, CodeReviewCommit n) {
+      this.mergeTip = mergeTip;
+      this.toMerge = n;
+    }
+
+    @Override
+    public void updateRepo(RepoContext ctx) throws IOException {
+      // If there is only one parent, a cherry-pick can be done by taking the
+      // delta relative to that one parent and redoing that on the current merge
+      // tip.
+      args.rw.parseBody(toMerge);
+      psId = ChangeUtil.nextPatchSetId(
+          args.repo, toMerge.change().currentPatchSetId());
+      String cherryPickCmtMsg =
+          args.mergeUtil.createCherryPickCommitMessage(toMerge);
+
+      PersonIdent committer = args.caller.newCommitterIdent(
+          ctx.getWhen(), args.serverIdent.get().getTimeZone());
+      try {
+        newCommit = args.mergeUtil.createCherryPickFromCommit(
+            args.repo, args.inserter, mergeTip.getCurrentTip(), toMerge,
+            committer, cherryPickCmtMsg, args.rw);
+        mergeTip.moveTipTo(newCommit, newCommit);
+        ctx.addRefUpdate(
+            new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName()));
+        patchSetInfo =
+            patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
+      } catch (MergeConflictException mce) {
+        // Keep going in the case of a single merge failure; the goal is to
+        // cherry-pick as many commits as possible.
+        toMerge.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
+      } catch (MergeIdenticalTreeException mie) {
+        toMerge.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
       }
-      PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(args.rw,
-          args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
-      setRefLogIdent(submitApproval);
-    } else {
-      // One or more dependencies were not met. The status was already marked on
-      // the commit so we have nothing further to perform at this time.
-    }
-  }
-
-  private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
-      CodeReviewCommit n) throws IOException, OrmException,
-      NoSuchChangeException, MergeConflictException,
-      MergeIdenticalTreeException {
-
-    args.rw.parseBody(n);
-
-    PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
-
-    IdentifiedUser cherryPickUser;
-    PersonIdent serverNow = args.serverIdent.get();
-    PersonIdent cherryPickCommitterIdent;
-    if (submitAudit != null) {
-      cherryPickUser =
-          args.identifiedUserFactory.create(submitAudit.getAccountId());
-      cherryPickCommitterIdent = cherryPickUser.newCommitterIdent(
-          serverNow.getWhen(), serverNow.getTimeZone());
-    } else {
-      cherryPickUser = args.identifiedUserFactory.create(n.change().getOwner());
-      cherryPickCommitterIdent = serverNow;
     }
 
-    String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
+    @Override
+    public void updateChange(ChangeContext ctx) throws OrmException,
+         NoSuchChangeException {
+      if (newCommit == null) {
+        // Merge conflict; don't update change.
+        return;
+      }
+      PatchSet ps = new PatchSet(psId);
+      ps.setCreatedOn(ctx.getWhen());
+      ps.setUploader(args.caller.getAccountId());
+      ps.setRevision(new RevId(newCommit.getId().getName()));
 
-    CodeReviewCommit newCommit =
-        (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
-            args.inserter, mergeTip, n, cherryPickCommitterIdent,
-            cherryPickCmtMsg, args.rw);
-
-    PatchSet.Id id =
-        ChangeUtil.nextPatchSetId(args.repo, n.change().currentPatchSetId());
-    PatchSet ps = new PatchSet(id);
-    ps.setCreatedOn(TimeUtil.nowTs());
-    ps.setUploader(cherryPickUser.getAccountId());
-    ps.setRevision(new RevId(newCommit.getId().getName()));
-
-    RefUpdate ru;
-
-    args.db.changes().beginTransaction(n.change().getId());
-    try {
-      insertAncestors(args.db, ps.getId(), newCommit);
+      Change c = toMerge.change();
+      ps.setGroups(GroupCollector.getCurrentGroups(args.db, c));
       args.db.patchSets().insert(Collections.singleton(ps));
-      n.change()
-          .setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
-      args.db.changes().update(Collections.singletonList(n.change()));
+      c.setCurrentPatchSet(patchSetInfo);
+      args.db.changes().update(Collections.singletonList(c));
 
       List<PatchSetApproval> approvals = Lists.newArrayList();
       for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
-          args.db, n.getControl(), n.getPatchsetId())) {
+          args.db, toMerge.getControl(), toMerge.getPatchsetId())) {
         approvals.add(new PatchSetApproval(ps.getId(), a));
       }
       args.db.patchSetApprovals().insert(approvals);
 
-      ru = args.repo.updateRef(ps.getRefName());
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(newCommit);
-      ru.disableRefLog();
-      if (ru.update(args.rw) != RefUpdate.Result.NEW) {
-        throw new IOException(String.format(
-            "Failed to create ref %s in %s: %s", ps.getRefName(), n.change()
-                .getDest().getParentKey().get(), ru.getResult()));
-      }
-
-      args.db.commit();
-    } finally {
-      args.db.rollback();
+      newCommit.copyFrom(toMerge);
+      newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
+      newCommit.setControl(
+          args.changeControlFactory.controlFor(toMerge.change(), args.caller));
+      newCommits.put(c.getId(), newCommit);
+      setRefLogIdent();
     }
-
-    gitRefUpdated.fire(n.change().getProject(), ru);
-
-    newCommit.copyFrom(n);
-    newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
-    newCommit.setControl(
-        args.changeControlFactory.controlFor(n.change(), cherryPickUser));
-    newCommits.put(newCommit.getPatchsetId().getParentKey(), newCommit);
-    setRefLogIdent(submitAudit);
-    return newCommit;
   }
 
-  private static void insertAncestors(ReviewDb db, PatchSet.Id id,
-      RevCommit src) throws OrmException {
-    int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
-    for (int p = 0; p < cnt; p++) {
-      PatchSetAncestor a;
+  private class CherryPickMultipleParentsOp extends BatchUpdate.Op {
+    private final MergeTip mergeTip;
+    private final CodeReviewCommit toMerge;
 
-      a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
-      a.setAncestorRevision(new RevId(src.getParent(p).getId().name()));
-      toInsert.add(a);
+    private CherryPickMultipleParentsOp(MergeTip mergeTip,
+        CodeReviewCommit toMerge) {
+      this.mergeTip = mergeTip;
+      this.toMerge = toMerge;
     }
-    db.patchSetAncestors().insert(toInsert);
+
+    @Override
+    public void updateRepo(RepoContext ctx)
+        throws IntegrationException, IOException {
+      if (args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)) {
+        // One or more dependencies were not met. The status was already marked
+        // on the commit so we have nothing further to perform at this time.
+        return;
+      }
+      // There are multiple parents, so this is a merge commit. We don't want
+      // to cherry-pick the merge as clients can't easily rebase their history
+      // with that merge present and replaced by an equivalent merge with a
+      // different first parent. So instead behave as though MERGE_IF_NECESSARY
+      // was configured.
+      if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
+        mergeTip.moveTipTo(toMerge, toMerge);
+      } else {
+        PersonIdent myIdent =
+            new PersonIdent(args.serverIdent.get(), ctx.getWhen());
+        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
+            myIdent, args.repo, args.rw, args.inserter,
+            args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
+            toMerge);
+        mergeTip.moveTipTo(result, toMerge);
+      }
+      RevCommit initialTip = mergeTip.getInitialTip();
+      args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+          mergeTip.getCurrentTip(), initialTip == null
+              ? ImmutableSet.<RevCommit> of() : ImmutableSet.of(initialTip));
+      setRefLogIdent();
+    }
   }
 
   @Override
@@ -245,7 +255,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
         mergeTip, args.rw, toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
index 7ff2107..69943c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.revwalk.RevCommit;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -30,7 +32,7 @@
 
   @Override
   protected MergeTip _run(final CodeReviewCommit branchTip,
-      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+      final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(
         args.mergeSorter, toMerge);
@@ -43,10 +45,11 @@
       n.setStatusCode(CommitMergeStatus.NOT_FAST_FORWARD);
     }
 
-    PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTipCommit,
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
+    RevCommit initialTip = mergeTip.getInitialTip();
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+        mergeTip.getCurrentTip(), initialTip == null
+            ? ImmutableSet.<RevCommit> of() : ImmutableSet.of(initialTip));
+    setRefLogIdent();
 
     return mergeTip;
   }
@@ -58,7 +61,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip,
-      CodeReviewCommit toMerge) throws MergeException {
+      CodeReviewCommit toMerge) throws IntegrationException {
     return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
         toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index 3c13af9..bd10970 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -29,7 +32,7 @@
 
   @Override
   protected MergeTip _run(CodeReviewCommit branchTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException {
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException {
   List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     MergeTip mergeTip;
     if (branchTip == null) {
@@ -42,24 +45,28 @@
     }
     while (!sorted.isEmpty()) {
       CodeReviewCommit mergedFrom = sorted.remove(0);
+      PersonIdent serverIdent = args.serverIdent.get();
+      PersonIdent caller = args.caller.newCommitterIdent(
+          serverIdent.getWhen(), serverIdent.getTimeZone());
       CodeReviewCommit newTip =
-          args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo, args.rw,
-              args.inserter, args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
-              mergedFrom);
+          args.mergeUtil.mergeOneCommit(caller, serverIdent,
+              args.repo, args.rw, args.inserter, args.canMergeFlag,
+              args.destBranch, mergeTip.getCurrentTip(), mergedFrom);
       mergeTip.moveTipTo(newTip, mergedFrom);
     }
 
-    final PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, mergeTip.getCurrentTip(),
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
+    RevCommit initialTip = mergeTip.getInitialTip();
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+        mergeTip.getCurrentTip(), initialTip == null
+            ? ImmutableSet.<RevCommit> of() : ImmutableSet.of(initialTip));
+    setRefLogIdent();
 
     return mergeTip;
   }
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
         toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index b49cb0a..46a62ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -29,7 +32,7 @@
 
   @Override
   protected MergeTip _run(CodeReviewCommit branchTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException {
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     List<CodeReviewCommit> sorted =
         args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     MergeTip mergeTip;
@@ -48,24 +51,26 @@
     // For every other commit do a pair-wise merge.
     while (!sorted.isEmpty()) {
       CodeReviewCommit mergedFrom = sorted.remove(0);
+      PersonIdent serverIdent = args.serverIdent.get();
+      PersonIdent caller = args.caller.newCommitterIdent(
+          serverIdent.getWhen(), serverIdent.getTimeZone());
       branchTip =
-          args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo,
-              args.rw, args.inserter, args.canMergeFlag, args.destBranch,
-              branchTip, mergedFrom);
+          args.mergeUtil.mergeOneCommit(caller, serverIdent,
+              args.repo, args.rw, args.inserter, args.canMergeFlag,
+              args.destBranch, branchTip, mergedFrom);
       mergeTip.moveTipTo(branchTip, mergedFrom);
     }
-
-    final PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
-
+    RevCommit initialTip = mergeTip.getInitialTip();
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
+        initialTip == null ? ImmutableSet.<RevCommit> of()
+            : ImmutableSet.of(initialTip));
+    setRefLogIdent();
     return mergeTip;
   }
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return args.mergeUtil.canFastForward(
           args.mergeSorter, mergeTip, args.rw, toMerge)
         || args.mergeUtil.canMerge(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index 94de8f7..0bb669b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -14,52 +14,74 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
-import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.RebaseSorter;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 public class RebaseIfNecessary extends SubmitStrategy {
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final RebaseChange rebaseChange;
+  private final RebaseChangeOp.Factory rebaseFactory;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
 
   RebaseIfNecessary(SubmitStrategy.Arguments args,
       PatchSetInfoFactory patchSetInfoFactory,
-      RebaseChange rebaseChange) {
+      RebaseChangeOp.Factory rebaseFactory) {
     super(args);
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.rebaseChange = rebaseChange;
+    this.rebaseFactory = rebaseFactory;
     this.newCommits = new HashMap<>();
   }
 
+  private PersonIdent getSubmitterIdent() {
+    PersonIdent serverIdent = args.serverIdent.get();
+    return args.caller.newCommitterIdent(
+        serverIdent.getWhen(), serverIdent.getTimeZone());
+  }
+
   @Override
   protected MergeTip _run(final CodeReviewCommit branchTip,
-      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+      final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
-    List<CodeReviewCommit> sorted = sort(toMerge);
+    List<CodeReviewCommit> sorted = sort(toMerge, branchTip);
+
+    for (CodeReviewCommit c : sorted) {
+      if (c.getParentCount() > 1) {
+        // Since there is a merge commit, sort and prune again using
+        // MERGE_IF_NECESSARY semantics to avoid creating duplicate
+        // commits.
+        //
+        sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
+        break;
+      }
+    }
+
     while (!sorted.isEmpty()) {
       CodeReviewCommit n = sorted.remove(0);
 
@@ -84,15 +106,7 @@
 
         } else {
           try {
-            IdentifiedUser uploader =
-                args.identifiedUserFactory.create(args.mergeUtil
-                    .getSubmitter(n).getAccountId());
-            PatchSet newPatchSet =
-                rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.getPatchsetId(), n.change(), uploader,
-                    mergeTip.getCurrentTip(), args.mergeUtil,
-                    args.serverIdent.get(), false, ValidatePolicy.NONE);
-
+            PatchSet newPatchSet = rebase(n, mergeTip);
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.approvalsUtil.byPatchSet(args.db,
                 n.getControl(), n.getPatchsetId())) {
@@ -101,26 +115,28 @@
             // rebaseChange.rebase() may already have copied some approvals,
             // use upsert, not insert, to avoid constraint violation on database
             args.db.patchSetApprovals().upsert(approvals);
-            CodeReviewCommit newTip = (CodeReviewCommit) args.rw.parseCommit(
+            CodeReviewCommit newTip = args.rw.parseCommit(
                 ObjectId.fromString(newPatchSet.getRevision().get()));
             mergeTip.moveTipTo(newTip, newTip);
             n.change().setCurrentPatchSet(
-                patchSetInfoFactory.get(mergeTip.getCurrentTip(),
+                patchSetInfoFactory.get(args.rw, mergeTip.getCurrentTip(),
                     newPatchSet.getId()));
             mergeTip.getCurrentTip().copyFrom(n);
             mergeTip.getCurrentTip().setControl(
-                args.changeControlFactory.controlFor(n.change(), uploader));
+                args.changeControlFactory.controlFor(n.change(), args.caller));
             mergeTip.getCurrentTip().setPatchsetId(newPatchSet.getId());
             mergeTip.getCurrentTip().setStatusCode(
                 CommitMergeStatus.CLEAN_REBASE);
             newCommits.put(newPatchSet.getId().getParentKey(),
                 mergeTip.getCurrentTip());
-            setRefLogIdent(args.mergeUtil.getSubmitter(n));
+            setRefLogIdent();
           } catch (MergeConflictException e) {
             n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
+            throw new IntegrationException(
+                "Cannot rebase " + n.name() + ": " + e.getMessage(), e);
           } catch (NoSuchChangeException | OrmException | IOException
-              | InvalidChangeOperationException e) {
-            throw new MergeException("Cannot rebase " + n.name(), e);
+              | RestApiException | UpdateException e) {
+            throw new IntegrationException("Cannot rebase " + n.name(), e);
           }
         }
 
@@ -135,17 +151,19 @@
           if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
             mergeTip.moveTipTo(n, n);
           } else {
+            PersonIdent myIdent = getSubmitterIdent();
             mergeTip.moveTipTo(
-                args.mergeUtil.mergeOneCommit(args.serverIdent.get(),
+                args.mergeUtil.mergeOneCommit(myIdent, myIdent,
                     args.repo, args.rw, args.inserter, args.canMergeFlag,
                     args.destBranch, mergeTip.getCurrentTip(), n), n);
           }
-          PatchSetApproval submitApproval =
-              args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
-                  mergeTip.getCurrentTip(), args.alreadyAccepted);
-          setRefLogIdent(submitApproval);
+          RevCommit initialTip = mergeTip.getInitialTip();
+          args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+              mergeTip.getCurrentTip(), initialTip == null ?
+                  ImmutableSet.<RevCommit>of() : ImmutableSet.of(initialTip));
+          setRefLogIdent();
         } catch (IOException e) {
-          throw new MergeException("Cannot merge " + n.name(), e);
+          throw new IntegrationException("Cannot merge " + n.name(), e);
         }
       }
 
@@ -155,18 +173,32 @@
     return mergeTip;
   }
 
-  private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
-      throws MergeException {
+  private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort,
+      RevCommit initialTip) throws IntegrationException {
     try {
-      List<CodeReviewCommit> result = new RebaseSorter(
-          args.rw, args.alreadyAccepted, args.canMergeFlag).sort(toSort);
-      Collections.sort(result, CodeReviewCommit.ORDER);
-      return result;
+      return new RebaseSorter(args.rw, initialTip, args.alreadyAccepted,
+          args.canMergeFlag).sort(toSort);
     } catch (IOException e) {
-      throw new MergeException("Commit sorting failed", e);
+      throw new IntegrationException("Commit sorting failed", e);
     }
   }
 
+  private PatchSet rebase(CodeReviewCommit n, MergeTip mergeTip)
+      throws RestApiException, UpdateException, OrmException {
+    RebaseChangeOp op = rebaseFactory.create(
+          n.getControl(),
+          args.db.patchSets().get(n.getPatchsetId()),
+          mergeTip.getCurrentTip().name())
+        .setCommitterIdent(getSubmitterIdent())
+        .setRunHooks(false)
+        .setValidatePolicy(CommitValidators.Policy.NONE);
+    try (BatchUpdate bu = args.newBatchUpdate(TimeUtil.nowTs())) {
+      bu.addOp(n.change().getId(), op);
+      bu.execute();
+    }
+    return op.getPatchSet();
+  }
+
   @Override
   public Map<Change.Id, CodeReviewCommit> getNewCommits() {
     return newCommits;
@@ -174,7 +206,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     // Test for merge instead of cherry pick to avoid false negatives
     // on commit chains.
     return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index b25b17e..5215f55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -14,15 +14,18 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeSorter;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.MergeUtil;
@@ -36,8 +39,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 
+import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -54,10 +57,11 @@
     protected final IdentifiedUser.GenericFactory identifiedUserFactory;
     protected final Provider<PersonIdent> serverIdent;
     protected final ReviewDb db;
+    protected final BatchUpdate.Factory batchUpdateFactory;
     protected final ChangeControl.GenericFactory changeControlFactory;
 
     protected final Repository repo;
-    protected final RevWalk rw;
+    protected final CodeReviewRevWalk rw;
     protected final ObjectInserter inserter;
     protected final RevFlag canMergeFlag;
     protected final Set<RevCommit> alreadyAccepted;
@@ -66,17 +70,20 @@
     protected final MergeUtil mergeUtil;
     protected final ChangeIndexer indexer;
     protected final MergeSorter mergeSorter;
+    protected final IdentifiedUser caller;
 
     Arguments(IdentifiedUser.GenericFactory identifiedUserFactory,
         Provider<PersonIdent> serverIdent, ReviewDb db,
+        BatchUpdate.Factory batchUpdateFactory,
         ChangeControl.GenericFactory changeControlFactory, Repository repo,
-        RevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
+        CodeReviewRevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
         Set<RevCommit> alreadyAccepted, Branch.NameKey destBranch,
         ApprovalsUtil approvalsUtil, MergeUtil mergeUtil,
-        ChangeIndexer indexer) {
+        ChangeIndexer indexer, IdentifiedUser caller) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.serverIdent = serverIdent;
       this.db = db;
+      this.batchUpdateFactory = batchUpdateFactory;
       this.changeControlFactory = changeControlFactory;
 
       this.repo = repo;
@@ -89,6 +96,13 @@
       this.mergeUtil = mergeUtil;
       this.indexer = indexer;
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
+      this.caller = caller;
+    }
+
+    BatchUpdate newBatchUpdate(Timestamp when) {
+      return batchUpdateFactory
+          .create(db, destBranch.getParentKey(), caller, when)
+          .setRepository(repo, rw, inserter);
     }
   }
 
@@ -110,21 +124,22 @@
    *        this submit strategy. Implementations are responsible for ordering
    *        of commits, and should not modify the input in place.
    * @return the new merge tip.
-   * @throws MergeException
+   * @throws IntegrationException
    */
   public final MergeTip run(final CodeReviewCommit currentTip,
-      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+      final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     refLogIdent = null;
+    checkState(args.caller != null);
     return _run(currentTip, toMerge);
   }
 
   /** @see #run(CodeReviewCommit, Collection) */
   protected abstract MergeTip _run(CodeReviewCommit currentTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException;
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException;
 
   /**
    * Checks whether the given commit can be merged.
-   *
+   * <p>
    * Implementations must ensure that invoking this method modifies neither the
    * git repository nor the Gerrit database.
    *
@@ -132,10 +147,10 @@
    * @param toMerge the commit that should be checked.
    * @return {@code true} if the given commit can be merged, otherwise
    *         {@code false}
-   * @throws MergeException
+   * @throws IntegrationException
    */
   public abstract boolean dryRun(CodeReviewCommit mergeTip,
-      CodeReviewCommit toMerge) throws MergeException;
+      CodeReviewCommit toMerge) throws IntegrationException;
 
   /**
    * Returns the identity that should be used for reflog entries when updating
@@ -156,7 +171,7 @@
    * <p>
    * By default this method returns an empty map, but subclasses may override
    * this method to provide any newly created commits.
-   *
+   * <p>
    * This method may only be called after {@link #run(CodeReviewCommit,
    * Collection)}.
    *
@@ -182,13 +197,11 @@
 
   /**
    * Set the ref log identity if it wasn't set yet.
-   *
-   * @param submitApproval the approval that submitted the patch set
    */
-  protected final void setRefLogIdent(PatchSetApproval submitApproval) {
-    if (refLogIdent == null && submitApproval != null) {
+  protected final void setRefLogIdent() {
+    if (refLogIdent == null) {
       refLogIdent = args.identifiedUserFactory.create(
-          submitApproval.getAccountId()) .newRefLogIdent();
+          args.caller.getAccountId()).newRefLogIdent();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index ac65f8d..0c8c2f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -20,9 +20,10 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.changedetail.RebaseChange;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -39,7 +40,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -53,10 +53,10 @@
 
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Provider<PersonIdent> myIdent;
+  private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final RebaseChange rebaseChange;
+  private final RebaseChangeOp.Factory rebaseFactory;
   private final ProjectCache projectCache;
   private final ApprovalsUtil approvalsUtil;
   private final MergeUtil.Factory mergeUtilFactory;
@@ -66,39 +66,40 @@
   SubmitStrategyFactory(
       final IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritPersonIdent Provider<PersonIdent> myIdent,
+      final BatchUpdate.Factory batchUpdateFactory,
       final ChangeControl.GenericFactory changeControlFactory,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
+      final RebaseChangeOp.Factory rebaseFactory,
       final ProjectCache projectCache,
       final ApprovalsUtil approvalsUtil,
       final MergeUtil.Factory mergeUtilFactory,
       final ChangeIndexer indexer) {
     this.identifiedUserFactory = identifiedUserFactory;
     this.myIdent = myIdent;
+    this.batchUpdateFactory = batchUpdateFactory;
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.gitRefUpdated = gitRefUpdated;
-    this.rebaseChange = rebaseChange;
+    this.rebaseFactory = rebaseFactory;
     this.projectCache = projectCache;
     this.approvalsUtil = approvalsUtil;
     this.mergeUtilFactory = mergeUtilFactory;
     this.indexer = indexer;
   }
 
-  public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
-      final Repository repo, final RevWalk rw, final ObjectInserter inserter,
-      final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
-      final Branch.NameKey destBranch)
-      throws MergeException, NoSuchProjectException {
+  public SubmitStrategy create(SubmitType submitType, ReviewDb db,
+      Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
+      RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
+      Branch.NameKey destBranch, IdentifiedUser caller)
+      throws IntegrationException, NoSuchProjectException {
     ProjectState project = getProject(destBranch);
-    final SubmitStrategy.Arguments args =
-        new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db,
-            changeControlFactory, repo, rw, inserter, canMergeFlag,
-            alreadyAccepted, destBranch,approvalsUtil,
-            mergeUtilFactory.create(project), indexer);
+    SubmitStrategy.Arguments args = new SubmitStrategy.Arguments(
+        identifiedUserFactory, myIdent, db, batchUpdateFactory,
+        changeControlFactory, repo, rw, inserter, canMergeFlag, alreadyAccepted,
+        destBranch,approvalsUtil, mergeUtilFactory.create(project), indexer,
+        caller);
     switch (submitType) {
       case CHERRY_PICK:
-        return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
+        return new CherryPick(args, patchSetInfoFactory);
       case FAST_FORWARD_ONLY:
         return new FastForwardOnly(args);
       case MERGE_ALWAYS:
@@ -106,11 +107,11 @@
       case MERGE_IF_NECESSARY:
         return new MergeIfNecessary(args);
       case REBASE_IF_NECESSARY:
-        return new RebaseIfNecessary(args, patchSetInfoFactory, rebaseChange);
+        return new RebaseIfNecessary(args, patchSetInfoFactory, rebaseFactory);
       default:
         final String errorMsg = "No submit strategy for: " + submitType;
         log.error(errorMsg);
-        throw new MergeException(errorMsg);
+        throw new IntegrationException(errorMsg);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index fc8658f..6ac5707 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -66,6 +66,17 @@
   private static final Logger log = LoggerFactory
       .getLogger(CommitValidators.class);
 
+  public static enum Policy {
+    /** Use {@link #validateForGerritCommits}. */
+    GERRIT,
+
+    /** Use {@link #validateForReceiveCommits}. */
+    RECEIVE_COMMITS,
+
+    /** Do not validate commits. */
+    NONE
+  }
+
   public interface Factory {
     CommitValidators create(RefControl refControl, SshInfo sshInfo,
         Repository repo);
@@ -182,7 +193,7 @@
       this.canonicalWebUrl = canonicalWebUrl;
       this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
-      this.user = (IdentifiedUser) projectControl.getCurrentUser();
+      this.user = projectControl.getUser().asIdentifiedUser();
     }
 
     @Override
@@ -232,7 +243,7 @@
       sb.append("ERROR: ").append(errMsg);
 
       if (c.getFullMessage().indexOf(changeId) >= 0) {
-        String lines[] = c.getFullMessage().trim().split("\n");
+        String[] lines = c.getFullMessage().trim().split("\n");
         String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
 
         if (lastLine.indexOf(changeId) == -1) {
@@ -305,7 +316,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
 
       if (REFS_CONFIG.equals(refControl.getRefName())) {
         List<CommitValidationMessage> messages = new LinkedList<>();
@@ -391,13 +402,15 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
       final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
       final PersonIdent author = receiveEvent.commit.getAuthorIdent();
       final ProjectControl projectControl = refControl.getProjectControl();
 
       if (projectControl.getProjectState().isUseSignedOffBy()) {
-        boolean sboAuthor = false, sboCommitter = false, sboMe = false;
+        boolean sboAuthor = false;
+        boolean sboCommitter = false;
+        boolean sboMe = false;
         for (final FooterLine footer : receiveEvent.commit.getFooterLines()) {
           if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
             final String e = footer.getEmailAddress();
@@ -432,7 +445,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
       final PersonIdent author = receiveEvent.commit.getAuthorIdent();
 
       if (!currentUser.hasEmailAddress(author.getEmailAddress())
@@ -462,7 +475,7 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser();
       final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
       if (!currentUser.hasEmailAddress(committer.getEmailAddress())
           && !refControl.canForgeCommitter()) {
@@ -547,8 +560,8 @@
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
 
-      if (refControl.getCurrentUser().isIdentifiedUser()) {
-        IdentifiedUser user = (IdentifiedUser) refControl.getCurrentUser();
+      if (refControl.getUser().isIdentifiedUser()) {
+        IdentifiedUser user = refControl.getUser().asIdentifiedUser();
 
         String refname = receiveEvent.refName;
         ObjectId old = ObjectId.zeroId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
index 0a8d245..5951e04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.project.ProjectState;
 
@@ -37,12 +38,14 @@
    * @param destProject the destination project
    * @param destBranch the destination branch
    * @param patchSetId the patch set ID
+   * @param caller the user who initiated the merge request
    * @throws MergeValidationException if the commit fails to validate
    */
   public void onPreMerge(Repository repo,
       CodeReviewCommit commit,
       ProjectState destProject,
       Branch.NameKey destBranch,
-      PatchSet.Id patchSetId)
+      PatchSet.Id patchSetId,
+      IdentifiedUser caller)
       throws MergeValidationException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 6f70d46..d08095c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -20,11 +20,8 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfig;
@@ -61,7 +58,8 @@
       CodeReviewCommit commit,
       ProjectState destProject,
       Branch.NameKey destBranch,
-      PatchSet.Id patchSetId)
+      PatchSet.Id patchSetId,
+      IdentifiedUser caller)
       throws MergeValidationException {
     List<MergeValidationListener> validators = Lists.newLinkedList();
 
@@ -69,17 +67,15 @@
     validators.add(projectConfigValidatorFactory.create());
 
     for (MergeValidationListener validator : validators) {
-      validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId);
+      validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId,
+          caller);
     }
   }
 
   public static class ProjectConfigValidator implements
       MergeValidationListener {
     private final AllProjectsName allProjectsName;
-    private final ReviewDb db;
     private final ProjectCache projectCache;
-    private final IdentifiedUser.GenericFactory identifiedUserFactory;
-    private final ApprovalsUtil approvalsUtil;
     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
 
     public interface Factory {
@@ -88,15 +84,10 @@
 
     @Inject
     public ProjectConfigValidator(AllProjectsName allProjectsName,
-        ReviewDb db, ProjectCache projectCache,
-        IdentifiedUser.GenericFactory iuf,
-        ApprovalsUtil approvalsUtil,
+        ProjectCache projectCache,
         DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
       this.allProjectsName = allProjectsName;
-      this.db = db;
       this.projectCache = projectCache;
-      this.identifiedUserFactory = iuf;
-      this.approvalsUtil = approvalsUtil;
       this.pluginConfigEntries = pluginConfigEntries;
     }
 
@@ -105,7 +96,8 @@
         final CodeReviewCommit commit,
         final ProjectState destProject,
         final Branch.NameKey destBranch,
-        final PatchSet.Id patchSetId)
+        final PatchSet.Id patchSetId,
+        IdentifiedUser caller)
         throws MergeValidationException {
       if (RefNames.REFS_CONFIG.equals(destBranch.get())) {
         final Project.NameKey newParent;
@@ -124,15 +116,7 @@
             }
           } else {
             if (!oldParent.equals(newParent)) {
-              PatchSetApproval psa =
-                  approvalsUtil.getSubmitter(db, commit.notes(), patchSetId);
-              if (psa == null) {
-                throw new MergeValidationException(CommitMergeStatus.
-                    SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
-              }
-              final IdentifiedUser submitter =
-                  identifiedUserFactory.create(psa.getAccountId());
-              if (!submitter.getCapabilities().canAdministrateServer()) {
+              if (!caller.getCapabilities().canAdministrateServer()) {
                 throw new MergeValidationException(CommitMergeStatus.
                     SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
               }
@@ -188,10 +172,12 @@
         CodeReviewCommit commit,
         ProjectState destProject,
         Branch.NameKey destBranch,
-        PatchSet.Id patchSetId)
+        PatchSet.Id patchSetId,
+        IdentifiedUser caller)
         throws MergeValidationException {
       for (MergeValidationListener validator : mergeValidationListeners) {
-        validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId);
+        validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId,
+            caller);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index 9a3f02f..c274a37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -20,20 +20,20 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -100,7 +100,7 @@
     GroupControl control = resource.getControl();
     Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
     List<GroupInfo> result = Lists.newLinkedList();
-    Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
+    Account.Id me = control.getUser().getAccountId();
 
     for (String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
@@ -149,14 +149,18 @@
     @Override
     public GroupInfo apply(GroupResource resource, Input input)
         throws AuthException, MethodNotAllowedException,
-        UnprocessableEntityException, OrmException {
+        ResourceNotFoundException, OrmException {
       AddIncludedGroups.Input in = new AddIncludedGroups.Input();
       in.groups = ImmutableList.of(id);
-      List<GroupInfo> list = put.apply(resource, in);
-      if (list.size() == 1) {
-        return list.get(0);
+      try {
+        List<GroupInfo> list = put.apply(resource, in);
+        if (list.size() == 1) {
+          return list.get(0);
+        }
+        throw new IllegalStateException();
+      } catch (UnprocessableEntityException e) {
+        throw new ResourceNotFoundException(id);
       }
-      throw new IllegalStateException();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 5c7fcbc..18f5ee2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -45,8 +46,11 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @Singleton
 public class AddMembers implements RestModifyView<GroupResource, Input> {
@@ -75,6 +79,7 @@
     }
   }
 
+  private final Provider<IdentifiedUser> self;
   private final AccountManager accountManager;
   private final AuthType authType;
   private final AccountsCollection accounts;
@@ -85,7 +90,8 @@
   private final AuditService auditService;
 
   @Inject
-  AddMembers(AccountManager accountManager,
+  AddMembers(Provider<IdentifiedUser> self,
+      AccountManager accountManager,
       AuthConfig authConfig,
       AccountsCollection accounts,
       AccountResolver accountResolver,
@@ -93,6 +99,7 @@
       AccountLoader.Factory infoFactory,
       Provider<ReviewDb> db,
       AuditService auditService) {
+    this.self = self;
     this.accountManager = accountManager;
     this.auditService = auditService;
     this.authType = authConfig.getAuthType();
@@ -114,11 +121,8 @@
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
-    Map<Account.Id, AccountGroupMember> newAccountGroupMembers = Maps.newHashMap();
-    List<AccountInfo> result = Lists.newLinkedList();
-    Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
-    AccountLoader loader = infoFactory.create(true);
 
+    Set<Account.Id> newMemberIds = new HashSet<>();
     for (String nameOrEmail : input.members) {
       Account a = findAccount(nameOrEmail);
       if (!a.isActive()) {
@@ -129,27 +133,11 @@
       if (!control.canAddMember()) {
         throw new AuthException("Cannot add member: " + a.getFullName());
       }
-
-      if (!newAccountGroupMembers.containsKey(a.getId())) {
-        AccountGroupMember.Key key =
-            new AccountGroupMember.Key(a.getId(), internalGroup.getId());
-        AccountGroupMember m = db.get().accountGroupMembers().get(key);
-        if (m == null) {
-          m = new AccountGroupMember(key);
-          newAccountGroupMembers.put(m.getAccountId(), m);
-        }
-      }
-      result.add(loader.get(a.getId()));
+      newMemberIds.add(a.getId());
     }
 
-    auditService.dispatchAddAccountsToGroup(me, newAccountGroupMembers.values());
-    db.get().accountGroupMembers().insert(newAccountGroupMembers.values());
-    for (AccountGroupMember m : newAccountGroupMembers.values()) {
-      accountCache.evict(m.getAccountId());
-    }
-
-    loader.fill();
-    return result;
+    addMembers(internalGroup.getId(), newMemberIds);
+    return toAccountInfoList(newMemberIds);
   }
 
   private Account findAccount(String nameOrEmail) throws AuthException,
@@ -177,6 +165,29 @@
     }
   }
 
+  public void addMembers(AccountGroup.Id groupId,
+      Collection<? extends Account.Id> newMemberIds) throws OrmException {
+    Map<Account.Id, AccountGroupMember> newAccountGroupMembers = Maps.newHashMap();
+    for (Account.Id accId : newMemberIds) {
+      if (!newAccountGroupMembers.containsKey(accId)) {
+        AccountGroupMember.Key key =
+            new AccountGroupMember.Key(accId, groupId);
+        AccountGroupMember m = db.get().accountGroupMembers().get(key);
+        if (m == null) {
+          m = new AccountGroupMember(key);
+          newAccountGroupMembers.put(m.getAccountId(), m);
+        }
+      }
+    }
+
+    auditService.dispatchAddAccountsToGroup(self.get().getAccountId(),
+        newAccountGroupMembers.values());
+    db.get().accountGroupMembers().insert(newAccountGroupMembers.values());
+    for (AccountGroupMember m : newAccountGroupMembers.values()) {
+      accountCache.evict(m.getAccountId());
+    }
+  }
+
   private Account createAccountByLdap(String user) {
     if (!user.matches(Account.USER_NAME_PATTERN)) {
       return null;
@@ -192,6 +203,17 @@
     }
   }
 
+  private List<AccountInfo> toAccountInfoList(Set<Account.Id> accountIds)
+      throws OrmException {
+    List<AccountInfo> result = Lists.newLinkedList();
+    AccountLoader loader = infoFactory.create(true);
+    for (Account.Id accId : accountIds) {
+      result.add(loader.get(accId));
+    }
+    loader.fill();
+    return result;
+  }
+
   static class PutMember implements RestModifyView<GroupResource, PutMember.Input> {
     static class Input {
     }
@@ -207,14 +229,18 @@
     @Override
     public AccountInfo apply(GroupResource resource, PutMember.Input input)
         throws AuthException, MethodNotAllowedException,
-        UnprocessableEntityException, OrmException {
+        ResourceNotFoundException, OrmException {
       AddMembers.Input in = new AddMembers.Input();
       in._oneMember = id;
-      List<AccountInfo> list = put.apply(resource, in);
-      if (list.size() == 1) {
-        return list.get(0);
+      try {
+        List<AccountInfo> list = put.apply(resource, in);
+        if (list.size() == 1) {
+          return list.get(0);
+        }
+        throw new IllegalStateException();
+      } catch (UnprocessableEntityException e) {
+        throw new ResourceNotFoundException(id);
       }
-      throw new IllegalStateException();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index cb80702..4e67c02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -19,11 +19,10 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -32,100 +31,102 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateGroupArgs;
-import com.google.gerrit.server.account.PerformCreateGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.group.CreateGroup.Input;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.util.Collections;
 
 @RequiresCapability(GlobalCapability.CREATE_GROUP)
-public class CreateGroup implements RestModifyView<TopLevelResource, Input> {
-  public static class Input {
-    public String name;
-    public String description;
-    public Boolean visibleToAll;
-    public String ownerId;
-  }
-
+public class CreateGroup implements RestModifyView<TopLevelResource, GroupInput> {
   public static interface Factory {
     CreateGroup create(@Assisted String name);
   }
 
   private final Provider<IdentifiedUser> self;
+  private final PersonIdent serverIdent;
+  private final ReviewDb db;
+  private final GroupCache groupCache;
   private final GroupsCollection groups;
-  private final PerformCreateGroup.Factory op;
   private final GroupJson json;
   private final DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners;
+  private final AddMembers addMembers;
   private final boolean defaultVisibleToAll;
   private final String name;
 
   @Inject
-  CreateGroup(Provider<IdentifiedUser> self, GroupsCollection groups,
-      PerformCreateGroup.Factory performCreateGroupFactory, GroupJson json,
+  CreateGroup(
+      Provider<IdentifiedUser> self,
+      @GerritPersonIdent PersonIdent serverIdent,
+      ReviewDb db,
+      GroupCache groupCache,
+      GroupsCollection groups,
+      GroupJson json,
       DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners,
-      @GerritServerConfig Config cfg, @Assisted String name) {
+      AddMembers addMembers,
+      @GerritServerConfig Config cfg,
+      @Assisted String name) {
     this.self = self;
+    this.serverIdent = serverIdent;
+    this.db = db;
+    this.groupCache = groupCache;
     this.groups = groups;
-    this.op = performCreateGroupFactory;
     this.json = json;
     this.groupCreationValidationListeners = groupCreationValidationListeners;
+    this.addMembers = addMembers;
     this.defaultVisibleToAll = cfg.getBoolean("groups", "newGroupsVisibleToAll", false);
     this.name = name;
   }
 
   @Override
-  public GroupInfo apply(TopLevelResource resource, Input input)
-      throws AuthException, BadRequestException, UnprocessableEntityException,
+  public GroupInfo apply(TopLevelResource resource, GroupInput input)
+      throws BadRequestException, UnprocessableEntityException,
       ResourceConflictException, OrmException {
     if (input == null) {
-      input = new Input();
+      input = new GroupInput();
     }
     if (input.name != null && !name.equals(input.name)) {
       throw new BadRequestException("name must match URL");
     }
 
     AccountGroup.Id ownerId = owner(input);
-    AccountGroup group;
-    try {
-      CreateGroupArgs args = new CreateGroupArgs();
-      args.setGroupName(name);
-      args.groupDescription = Strings.emptyToNull(input.description);
-      args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll,
-          defaultVisibleToAll);
-      args.ownerGroupId = ownerId;
-      args.initialMembers = ownerId == null
-          ? Collections.singleton(self.get().getAccountId())
-          : Collections.<Account.Id> emptySet();
+    CreateGroupArgs args = new CreateGroupArgs();
+    args.setGroupName(name);
+    args.groupDescription = Strings.emptyToNull(input.description);
+    args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll,
+        defaultVisibleToAll);
+    args.ownerGroupId = ownerId;
+    args.initialMembers = ownerId == null
+        ? Collections.singleton(self.get().getAccountId())
+        : Collections.<Account.Id> emptySet();
 
-      for (GroupCreationValidationListener l : groupCreationValidationListeners) {
-        try {
-          l.validateNewGroup(args);
-        } catch (ValidationException e) {
-          throw new ResourceConflictException(e.getMessage(), e);
-        }
+    for (GroupCreationValidationListener l : groupCreationValidationListeners) {
+      try {
+        l.validateNewGroup(args);
+      } catch (ValidationException e) {
+        throw new ResourceConflictException(e.getMessage(), e);
       }
-
-      group = op.create(args).createGroup();
-    } catch (PermissionDeniedException e) {
-      throw new AuthException(e.getMessage());
-    } catch (NameAlreadyUsedException e) {
-      throw new ResourceConflictException(e.getMessage());
     }
-    return json.format(GroupDescriptions.forAccountGroup(group));
+
+    return json.format(GroupDescriptions.forAccountGroup(createGroup(args)));
   }
 
-  private AccountGroup.Id owner(Input input)
+  private AccountGroup.Id owner(GroupInput input)
       throws UnprocessableEntityException {
     if (input.ownerId != null) {
       GroupDescription.Basic d = groups.parseInternal(Url.decode(input.ownerId));
@@ -133,4 +134,42 @@
     }
     return null;
   }
+
+  private AccountGroup createGroup(CreateGroupArgs createGroupArgs)
+      throws OrmException, ResourceConflictException {
+    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
+    AccountGroup.UUID uuid =
+        GroupUUID.make(
+            createGroupArgs.getGroupName(),
+            self.get().newCommitterIdent(serverIdent.getWhen(),
+                serverIdent.getTimeZone()));
+    AccountGroup group =
+        new AccountGroup(createGroupArgs.getGroup(), groupId, uuid);
+    group.setVisibleToAll(createGroupArgs.visibleToAll);
+    if (createGroupArgs.ownerGroupId != null) {
+      AccountGroup ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);
+      if (ownerGroup != null) {
+        group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
+      }
+    }
+    if (createGroupArgs.groupDescription != null) {
+      group.setDescription(createGroupArgs.groupDescription);
+    }
+    AccountGroupName gn = new AccountGroupName(group);
+    // first insert the group name to validate that the group name hasn't
+    // already been used to create another group
+    try {
+      db.accountGroupNames().insert(Collections.singleton(gn));
+    } catch (OrmDuplicateKeyException e) {
+      throw new ResourceConflictException("group '"
+          + createGroupArgs.getGroupName() + "' already exists");
+    }
+    db.accountGroups().insert(Collections.singleton(group));
+
+    addMembers.addMembers(groupId, createGroupArgs.initialMembers);
+
+    groupCache.onCreateGroup(createGroupArgs.getGroup());
+
+    return group;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
index 9e261f9..bc8bff7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -67,13 +67,8 @@
           new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
       auditInserts.add(audit);
     }
-    try {
-      ReviewDb db = schema.open();
-      try {
-        db.accountGroupMembersAudit().insert(auditInserts);
-      } finally {
-        db.close();
-      }
+    try (ReviewDb db = schema.open()) {
+      db.accountGroupMembersAudit().insert(auditInserts);
     } catch (OrmException e) {
       logOrmExceptionForAccounts(
           "Cannot log add accounts to group event performed by user", me,
@@ -86,33 +81,28 @@
       Collection<AccountGroupMember> removed) {
     List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
     List<AccountGroupMemberAudit> auditUpdates = Lists.newLinkedList();
-    try {
-      ReviewDb db = schema.open();
-      try {
-        for (AccountGroupMember m : removed) {
-          AccountGroupMemberAudit audit = null;
-          for (AccountGroupMemberAudit a : db.accountGroupMembersAudit()
-              .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
-            if (a.isActive()) {
-              audit = a;
-              break;
-            }
-          }
-
-          if (audit != null) {
-            audit.removed(me, TimeUtil.nowTs());
-            auditUpdates.add(audit);
-          } else {
-            audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
-            audit.removedLegacy();
-            auditInserts.add(audit);
+    try (ReviewDb db = schema.open()) {
+      for (AccountGroupMember m : removed) {
+        AccountGroupMemberAudit audit = null;
+        for (AccountGroupMemberAudit a : db.accountGroupMembersAudit()
+            .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
+          if (a.isActive()) {
+            audit = a;
+            break;
           }
         }
-        db.accountGroupMembersAudit().update(auditUpdates);
-        db.accountGroupMembersAudit().insert(auditInserts);
-      } finally {
-        db.close();
+
+        if (audit != null) {
+          audit.removed(me, TimeUtil.nowTs());
+          auditUpdates.add(audit);
+        } else {
+          audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
+          audit.removedLegacy();
+          auditInserts.add(audit);
+        }
       }
+      db.accountGroupMembersAudit().update(auditUpdates);
+      db.accountGroupMembersAudit().insert(auditInserts);
     } catch (OrmException e) {
       logOrmExceptionForAccounts(
           "Cannot log delete accounts from group event performed by user", me,
@@ -129,13 +119,8 @@
           new AccountGroupByIdAud(groupInclude, me, TimeUtil.nowTs());
       includesAudit.add(audit);
     }
-    try {
-      ReviewDb db = schema.open();
-      try {
-        db.accountGroupByIdAud().insert(includesAudit);
-      } finally {
-        db.close();
-      }
+    try (ReviewDb db = schema.open()) {
+      db.accountGroupByIdAud().insert(includesAudit);
     } catch (OrmException e) {
       logOrmExceptionForGroups(
           "Cannot log add groups to group event performed by user", me, added,
@@ -147,28 +132,23 @@
   public void onDeleteGroupsFromGroup(Account.Id me,
       Collection<AccountGroupById> removed) {
     final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
-    try {
-      ReviewDb db = schema.open();
-      try {
-        for (final AccountGroupById g : removed) {
-          AccountGroupByIdAud audit = null;
-          for (AccountGroupByIdAud a : db.accountGroupByIdAud()
-              .byGroupInclude(g.getGroupId(), g.getIncludeUUID())) {
-            if (a.isActive()) {
-              audit = a;
-              break;
-            }
-          }
-
-          if (audit != null) {
-            audit.removed(me, TimeUtil.nowTs());
-            auditUpdates.add(audit);
+    try (ReviewDb db = schema.open()) {
+      for (final AccountGroupById g : removed) {
+        AccountGroupByIdAud audit = null;
+        for (AccountGroupByIdAud a : db.accountGroupByIdAud()
+            .byGroupInclude(g.getGroupId(), g.getIncludeUUID())) {
+          if (a.isActive()) {
+            audit = a;
+            break;
           }
         }
-        db.accountGroupByIdAud().update(auditUpdates);
-      } finally {
-        db.close();
+
+        if (audit != null) {
+          audit.removed(me, TimeUtil.nowTs());
+          auditUpdates.add(audit);
+        }
       }
+      db.accountGroupByIdAud().update(auditUpdates);
     } catch (OrmException e) {
       logOrmExceptionForGroups(
           "Cannot log delete groups from group event performed by user", me,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 3d99565..bde8fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
@@ -110,7 +109,7 @@
   }
 
   private void writeAudits(final List<AccountGroupById> toRemoved) {
-    final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
+    final Account.Id me = self.get().getAccountId();
     auditService.dispatchDeleteGroupsFromGroup(me, toRemoved);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index 3047994..b14974b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupControl;
@@ -97,7 +96,7 @@
   }
 
   private void writeAudits(final List<AccountGroupMember> toRemove) {
-    final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
+    final Account.Id me = self.get().getAccountId();
     auditService.dispatchDeleteAccountsFromGroup(me, toRemove);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
new file mode 100644
index 0000000..34c2b76
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2015 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.server.group;
+
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+@Singleton
+public class GetAuditLog implements RestReadView<GroupResource> {
+  private final Provider<ReviewDb> db;
+  private final AccountLoader.Factory accountLoaderFactory;
+  private final GroupCache groupCache;
+  private final GroupJson groupJson;
+
+  @Inject
+  public GetAuditLog(Provider<ReviewDb> db,
+      AccountLoader.Factory accountLoaderFactory,
+      GroupCache groupCache,
+      GroupJson groupJson) {
+    this.db = db;
+    this.accountLoaderFactory = accountLoaderFactory;
+    this.groupCache = groupCache;
+    this.groupJson = groupJson;
+  }
+
+  @Override
+  public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
+      throws AuthException, ResourceNotFoundException,
+      MethodNotAllowedException, OrmException {
+    if (rsrc.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    } else if (!rsrc.getControl().isOwner()) {
+      throw new AuthException("Not group owner");
+    }
+
+    AccountGroup group = db.get().accountGroups().get(
+        rsrc.toAccountGroup().getId());
+    if (group == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    AccountLoader accountLoader = accountLoaderFactory.create(true);
+
+    List<GroupAuditEventInfo> auditEvents = new ArrayList<>();
+
+    for (AccountGroupMemberAudit auditEvent :
+      db.get().accountGroupMembersAudit().byGroup(group.getId()).toList()) {
+      AccountInfo member = accountLoader.get(auditEvent.getKey().getParentKey());
+
+      auditEvents.add(GroupAuditEventInfo.createAddUserEvent(
+          accountLoader.get(auditEvent.getAddedBy()),
+          auditEvent.getKey().getAddedOn(), member));
+
+      if (!auditEvent.isActive()) {
+        auditEvents.add(GroupAuditEventInfo.createRemoveUserEvent(
+            accountLoader.get(auditEvent.getRemovedBy()),
+            auditEvent.getRemovedOn(), member));
+      }
+    }
+
+    for (AccountGroupByIdAud auditEvent :
+        db.get().accountGroupByIdAud().byGroup(group.getId()).toList()) {
+      AccountGroup.UUID includedGroupUUID = auditEvent.getKey().getIncludeUUID();
+      AccountGroup includedGroup = groupCache.get(includedGroupUUID);
+      GroupInfo member;
+      if (includedGroup != null) {
+        member = groupJson.format(GroupDescriptions.forAccountGroup(includedGroup));
+      } else {
+        member = new GroupInfo();
+        member.id = Url.encode(includedGroupUUID.get());
+      }
+
+      auditEvents.add(GroupAuditEventInfo.createAddGroupEvent(
+          accountLoader.get(auditEvent.getAddedBy()),
+          auditEvent.getKey().getAddedOn(), member));
+
+      if (!auditEvent.isActive()) {
+        auditEvents.add(GroupAuditEventInfo.createRemoveGroupEvent(
+            accountLoader.get(auditEvent.getRemovedBy()),
+            auditEvent.getRemovedOn(), member));
+      }
+    }
+
+    accountLoader.fill();
+
+    // sort by date in reverse order so that the newest audit event comes first
+    Collections.sort(auditEvents, new Comparator<GroupAuditEventInfo>() {
+      @Override
+      public int compare(GroupAuditEventInfo e1, GroupAuditEventInfo e2) {
+        return e2.date.compareTo(e1.date);
+      }
+    });
+
+    return auditEvents;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
index 8c804dd..6900b83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
@@ -21,7 +21,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class GetDescription implements RestReadView<GroupResource> {
+public class GetDescription implements RestReadView<GroupResource> {
   @Override
   public String apply(GroupResource resource) throws MethodNotAllowedException {
     AccountGroup group = resource.toAccountGroup();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
index 936798d..615c862 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
index 95042a2..03c6d6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-class GetGroup implements RestReadView<GroupResource> {
+public class GetGroup implements RestReadView<GroupResource> {
   private final GroupJson json;
 
   @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
index 5d3853e..dbc2e0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
index 5d1ede0..7b55666 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
 
@@ -22,6 +23,6 @@
 
   @Override
   public GroupOptionsInfo apply(GroupResource resource) {
-    return new GroupOptionsInfo(resource.getGroup());
+    return GroupJson.createOptions(resource.getGroup());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
index 5fc62c6..464be18 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index 96b4234..8f339de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -14,14 +14,15 @@
 
 package com.google.gerrit.server.group;
 
-import static com.google.gerrit.common.groups.ListGroupsOption.INCLUDES;
-import static com.google.gerrit.common.groups.ListGroupsOption.MEMBERS;
+import static com.google.gerrit.extensions.client.ListGroupsOption.INCLUDES;
+import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
-import com.google.gerrit.common.groups.ListGroupsOption;
-import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -33,9 +34,17 @@
 
 import java.util.Collection;
 import java.util.EnumSet;
-import java.util.List;
 
 public class GroupJson {
+  public static GroupOptionsInfo createOptions(GroupDescription.Basic group) {
+    GroupOptionsInfo options = new GroupOptionsInfo();
+    AccountGroup ag = GroupDescriptions.toAccountGroup(group);
+    if (ag != null && ag.isVisibleToAll()) {
+      options.visibleToAll = true;
+    }
+    return options;
+  }
+
   private final GroupBackend groupBackend;
   private final GroupControl.Factory groupControlFactory;
   private final Provider<ListMembers> listMembers;
@@ -86,7 +95,7 @@
     info.id = Url.encode(group.getGroupUUID().get());
     info.name = Strings.emptyToNull(group.getName());
     info.url = Strings.emptyToNull(group.getUrl());
-    info.options = new GroupOptionsInfo(group);
+    info.options = createOptions(group);
 
     AccountGroup g = GroupDescriptions.toAccountGroup(group);
     if (g != null) {
@@ -125,24 +134,4 @@
       throw new IllegalStateException(e);
     }
   }
-
-  public static class GroupInfo extends GroupBaseInfo {
-    public String url;
-    public GroupOptionsInfo options;
-
-    // These fields are only supplied for internal groups.
-    public String description;
-    public Integer groupId;
-    public String owner;
-    public String ownerId;
-
-    // These fields are only supplied for internal groups, but only if requested
-    public List<AccountInfo> members;
-    public List<GroupInfo> includes;
-  }
-
-  public static class GroupBaseInfo {
-    public String id;
-    public String name;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupModule.java
index 7fb5f58..5939fd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupModule.java
@@ -16,13 +16,13 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.IncludingGroupMembership;
 import com.google.gerrit.server.account.InternalGroupBackend;
 import com.google.gerrit.server.account.UniversalGroupBackend;
-import com.google.gerrit.server.config.FactoryModule;
 
 public class GroupModule extends FactoryModule {
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupOptionsInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupOptionsInfo.java
deleted file mode 100644
index 6be92d1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupOptionsInfo.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2013 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.server.group;
-
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-public class GroupOptionsInfo {
-  public Boolean visibleToAll;
-
-  public GroupOptionsInfo(GroupDescription.Basic group) {
-    AccountGroup ag = GroupDescriptions.toAccountGroup(group);
-    visibleToAll = ag != null && ag.isVisibleToAll() ? true : null;
-  }
-
-  public GroupOptionsInfo(AccountGroup group) {
-    visibleToAll = group.isVisibleToAll() ? true : null;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
index 76cd137..4636c2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -159,8 +159,8 @@
       try {
         AccountGroup.Id legacyId = AccountGroup.Id.parse(id);
         return groupControlFactory.controlFor(legacyId).getGroup();
-      } catch (IllegalArgumentException invalidId) {
-      } catch (NoSuchGroupException e) {
+      } catch (IllegalArgumentException | NoSuchGroupException e) {
+        // Ignored
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index 40d0420..44e8a4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -16,28 +16,30 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupComparator;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -51,53 +53,87 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
 
 /** List groups visible to the calling user. */
 public class ListGroups implements RestReadView<TopLevelResource> {
 
   protected final GroupCache groupCache;
 
+  private final List<ProjectControl> projects = new ArrayList<>();
+  private final Set<AccountGroup.UUID> groupsToInspect = Sets.newHashSet();
   private final GroupControl.Factory groupControlFactory;
   private final GroupControl.GenericFactory genericGroupControlFactory;
   private final Provider<IdentifiedUser> identifiedUser;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<GetGroups> accountGetGroups;
   private final GroupJson json;
-  private EnumSet<ListGroupsOption> options;
+  private final GroupBackend groupBackend;
+
+  private EnumSet<ListGroupsOption> options =
+      EnumSet.noneOf(ListGroupsOption.class);
+  private boolean visibleToAll;
+  private Account.Id user;
+  private boolean owned;
+  private int limit;
+  private int start;
+  private String matchSubstring;
+  private String suggest;
 
   @Option(name = "--project", aliases = {"-p"},
       usage = "projects for which the groups should be listed")
-  private final List<ProjectControl> projects = new ArrayList<>();
+  public void addProject(ProjectControl project) {
+    projects.add(project);
+  }
 
-  @Option(name = "--visible-to-all", usage = "to list only groups that are visible to all registered users")
-  private boolean visibleToAll;
+  @Option(name = "--visible-to-all",
+      usage = "to list only groups that are visible to all registered users")
+  public void setVisibleToAll(boolean visibleToAll) {
+    this.visibleToAll = visibleToAll;
+  }
 
   @Option(name = "--user", aliases = {"-u"},
       usage = "user for which the groups should be listed")
-  private Account.Id user;
+  public void setUser(Account.Id user) {
+    this.user = user;
+  }
 
-  @Option(name = "--owned", usage = "to list only groups that are owned by the specified user"
-      + " or by the calling user if no user was specifed")
-  private boolean owned;
-
-  private Set<AccountGroup.UUID> groupsToInspect = Sets.newHashSet();
+  @Option(name = "--owned", usage = "to list only groups that are owned by the"
+      + " specified user or by the calling user if no user was specifed")
+  public void setOwned(boolean owned) {
+    this.owned = owned;
+  }
 
   @Option(name = "-q", usage = "group to inspect")
-  void addGroup(final AccountGroup.UUID id) {
+  public void addGroup(AccountGroup.UUID id) {
     groupsToInspect.add(id);
   }
 
-  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of groups to list")
-  private int limit;
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
+      usage = "maximum number of groups to list")
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
 
-  @Option(name = "--start", aliases = {"-S"}, metaVar = "CNT", usage = "number of groups to skip")
-  private int start;
+  @Option(name = "--start", aliases = {"-S"}, metaVar = "CNT",
+      usage = "number of groups to skip")
+  public void setStart(int start) {
+    this.start = start;
+  }
 
-  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match group substring")
-  private String matchSubstring;
+  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH",
+      usage = "match group substring")
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
+
+  @Option(name = "--suggest", usage = "to get a suggestion of groups")
+  public void setSuggest(String suggest) {
+    this.suggest = suggest;
+  }
 
   @Option(name = "-o", usage = "Output options per group")
-  public void addOption(ListGroupsOption o) {
+  void addOption(ListGroupsOption o) {
     options.add(o);
   }
 
@@ -112,7 +148,8 @@
       final GroupControl.GenericFactory genericGroupControlFactory,
       final Provider<IdentifiedUser> identifiedUser,
       final IdentifiedUser.GenericFactory userFactory,
-      final Provider<GetGroups> accountGetGroups, GroupJson json) {
+      final Provider<GetGroups> accountGetGroups, GroupJson json,
+      GroupBackend groupBackend) {
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.genericGroupControlFactory = genericGroupControlFactory;
@@ -120,7 +157,11 @@
     this.userFactory = userFactory;
     this.accountGetGroups = accountGetGroups;
     this.json = json;
-    this.options = EnumSet.noneOf(ListGroupsOption.class);
+    this.groupBackend = groupBackend;
+  }
+
+  public void setOptions(EnumSet<ListGroupsOption> options) {
+    this.options = options;
   }
 
   public Account.Id getUser() {
@@ -132,65 +173,119 @@
   }
 
   @Override
-  public Object apply(TopLevelResource resource) throws OrmException {
-    final Map<String, GroupInfo> output = Maps.newTreeMap();
+  public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
+      throws OrmException, BadRequestException {
+    SortedMap<String, GroupInfo> output = Maps.newTreeMap();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(
           info.name,
           "Group " + Url.decode(info.id)), info);
       info.name = null;
     }
-    return OutputFormat.JSON.newGson().toJsonTree(output,
-        new TypeToken<Map<String, GroupInfo>>() {}.getType());
+    return output;
   }
 
-  public List<GroupInfo> get() throws OrmException {
-    List<GroupInfo> groupInfos;
+  public List<GroupInfo> get() throws OrmException, BadRequestException {
+    if (!Strings.isNullOrEmpty(suggest)) {
+      return suggestGroups();
+    }
+
+    if (owned) {
+      return getGroupsOwnedBy(
+          user != null ? userFactory.create(user) : identifiedUser.get());
+    }
+
     if (user != null) {
-      if (owned) {
-        groupInfos = getGroupsOwnedBy(userFactory.create(user));
-      } else {
-        groupInfos = accountGetGroups.get().apply(
-            new AccountResource(userFactory.create(user)));
+      return accountGetGroups.get().apply(
+          new AccountResource(userFactory.create(user)));
+    }
+
+    return getAllGroups();
+  }
+
+  private List<GroupInfo> getAllGroups() throws OrmException {
+    List<GroupInfo> groupInfos;
+    List<AccountGroup> groupList;
+    if (!projects.isEmpty()) {
+      Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+      for (final ProjectControl projectControl : projects) {
+        final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
+        for (final GroupReference groupRef : groupsRefs) {
+          final AccountGroup group = groupCache.get(groupRef.getUUID());
+          if (group != null) {
+            groups.put(group.getGroupUUID(), group);
+          }
+        }
       }
+      groupList = filterGroups(groups.values());
     } else {
-      if (owned) {
-        groupInfos = getGroupsOwnedBy(identifiedUser.get());
-      } else {
-        List<AccountGroup> groupList;
-        if (!projects.isEmpty()) {
-          Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
-          for (final ProjectControl projectControl : projects) {
-            final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
-            for (final GroupReference groupRef : groupsRefs) {
-              final AccountGroup group = groupCache.get(groupRef.getUUID());
-              if (group != null) {
-                groups.put(group.getGroupUUID(), group);
-              }
-            }
-          }
-          groupList = filterGroups(groups.values());
-        } else {
-          groupList = filterGroups(groupCache.all());
-        }
-        groupInfos = Lists.newArrayListWithCapacity(groupList.size());
-        int found = 0;
-        int foundIndex = 0;
-        for (AccountGroup group : groupList) {
-          if (foundIndex++ < start) {
-            continue;
-          }
-          if (limit > 0 && ++found > limit) {
-            break;
-          }
-          groupInfos.add(json.addOptions(options).format(
-              GroupDescriptions.forAccountGroup(group)));
-        }
+      groupList = filterGroups(groupCache.all());
+    }
+    groupInfos = Lists.newArrayListWithCapacity(groupList.size());
+    int found = 0;
+    int foundIndex = 0;
+    for (AccountGroup group : groupList) {
+      if (foundIndex++ < start) {
+        continue;
+      }
+      if (limit > 0 && ++found > limit) {
+        break;
+      }
+      groupInfos.add(json.addOptions(options).format(
+          GroupDescriptions.forAccountGroup(group)));
+    }
+    return groupInfos;
+  }
+
+  private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
+    if (conflictingSuggestParameters()) {
+      throw new BadRequestException(
+          "You should only have no more than one --project and -n with --suggest");
+    }
+
+    List<GroupReference> groupRefs = Lists.newArrayList(Iterables.limit(
+        groupBackend.suggest(
+            suggest, Iterables.getFirst(projects, null)),
+        limit <= 0 ? 10 : Math.min(limit, 10)));
+
+    List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
+    for (final GroupReference ref : groupRefs) {
+      GroupDescription.Basic desc = groupBackend.get(ref.getUUID());
+      if (desc != null) {
+        groupInfos.add(json.addOptions(options).format(desc));
       }
     }
     return groupInfos;
   }
 
+  private boolean conflictingSuggestParameters() {
+    if (Strings.isNullOrEmpty(suggest)) {
+      return false;
+    }
+    if (projects.size() > 1) {
+      return true;
+    }
+    if (visibleToAll) {
+      return true;
+    }
+    if (user != null) {
+      return true;
+    }
+    if (owned) {
+      return true;
+    }
+    if (start != 0) {
+      return true;
+    }
+    if (!groupsToInspect.isEmpty()) {
+      return true;
+    }
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      return true;
+    }
+    return false;
+  }
+
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
       throws OrmException {
     List<GroupInfo> groups = Lists.newArrayList();
@@ -227,12 +322,6 @@
           continue;
         }
       }
-      if (!isAdmin) {
-        final GroupControl c = groupControlFactory.controlFor(group);
-        if (!c.isVisible()) {
-          continue;
-        }
-      }
       if (visibleToAll && !group.isVisibleToAll()) {
         continue;
       }
@@ -240,6 +329,12 @@
           && !groupsToInspect.contains(group.getGroupUUID())) {
         continue;
       }
+      if (!isAdmin) {
+        final GroupControl c = groupControlFactory.controlFor(group);
+        if (!c.isVisible()) {
+          continue;
+        }
+      }
       filteredGroups.add(group);
     }
     Collections.sort(filteredGroups, new GroupComparator());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
index 671486c..8e22ef9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -18,12 +18,12 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
index 9b5d9ab..58b3ffb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.group.AddMembers.UpdateMember;
 import com.google.gerrit.server.group.DeleteIncludedGroups.DeleteIncludedGroup;
 import com.google.gerrit.server.group.DeleteMembers.DeleteMember;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
   @Override
@@ -55,6 +54,7 @@
     put(GROUP_KIND, "owner").to(PutOwner.class);
     get(GROUP_KIND, "options").to(GetOptions.class);
     put(GROUP_KIND, "options").to(PutOptions.class);
+    get(GROUP_KIND, "log.audit").to(GetAuditLog.class);
 
     child(GROUP_KIND, "members").to(MembersCollection.class);
     get(MEMBER_KIND).to(GetMember.class);
@@ -66,7 +66,7 @@
     put(INCLUDED_GROUP_KIND).to(UpdateIncludedGroup.class);
     delete(INCLUDED_GROUP_KIND).to(DeleteIncludedGroup.class);
 
-    install(new FactoryModuleBuilder().build(CreateGroup.Factory.class));
+    factory(CreateGroup.Factory.class);
 
     DynamicSet.bind(binder(), GroupMemberAuditListener.class).to(
         DbGroupMemberAuditListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
index 9768270..abaa317 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.group.CreateGroup.Input;
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutGroup implements RestModifyView<GroupResource, Input> {
+public class PutGroup implements RestModifyView<GroupResource, GroupInput> {
   @Override
-  public Response<?> apply(GroupResource resource, Input input)
+  public Response<?> apply(GroupResource resource, GroupInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("Group already exists");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
index 6d980ae..8266162 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -15,22 +15,32 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 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.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupDetailFactory;
+import com.google.gerrit.server.git.RenameGroupOp;
 import com.google.gerrit.server.group.PutName.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.Collections;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
 @Singleton
 public class PutName implements RestModifyView<GroupResource, Input> {
   public static class Input {
@@ -38,39 +48,91 @@
     public String name;
   }
 
-  private final PerformRenameGroup.Factory performRenameGroupFactory;
+  private final Provider<ReviewDb> db;
+  private final GroupCache groupCache;
+  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final RenameGroupOp.Factory renameGroupOpFactory;
+  private final Provider<IdentifiedUser> currentUser;
 
   @Inject
-  PutName(PerformRenameGroup.Factory performRenameGroupFactory) {
-    this.performRenameGroupFactory = performRenameGroupFactory;
+  PutName(Provider<ReviewDb> db,
+      GroupCache groupCache,
+      GroupDetailFactory.Factory groupDetailFactory,
+      RenameGroupOp.Factory renameGroupOpFactory,
+      Provider<IdentifiedUser> currentUser) {
+    this.db = db;
+    this.groupCache = groupCache;
+    this.groupDetailFactory = groupDetailFactory;
+    this.renameGroupOpFactory = renameGroupOpFactory;
+    this.currentUser = currentUser;
   }
 
   @Override
-  public String apply(GroupResource resource, Input input)
+  public String apply(GroupResource rsrc, Input input)
       throws MethodNotAllowedException, AuthException, BadRequestException,
-      ResourceNotFoundException, ResourceConflictException, OrmException {
-    if (resource.toAccountGroup() == null) {
+      ResourceConflictException, OrmException, NoSuchGroupException {
+    if (rsrc.toAccountGroup() == null) {
       throw new MethodNotAllowedException();
-    } else if (!resource.getControl().isOwner()) {
+    } else if (!rsrc.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     } else if (input == null || Strings.isNullOrEmpty(input.name)) {
       throw new BadRequestException("name is required");
     }
+    String newName = input.name.trim();
+    if (newName.isEmpty()) {
+      throw new BadRequestException("name is required");
+    }
 
-    final String newName = input.name.trim();
-    if (resource.toAccountGroup().getName().equals(newName)) {
+    if (rsrc.toAccountGroup().getName().equals(newName)) {
       return newName;
     }
 
+    return renameGroup(rsrc.toAccountGroup(), newName).group.getName();
+  }
+
+  private GroupDetail renameGroup(AccountGroup group, String newName)
+      throws ResourceConflictException, OrmException,
+      NoSuchGroupException {
+    AccountGroup.Id groupId = group.getId();
+    AccountGroup.NameKey old = group.getNameKey();
+    AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
+
     try {
-      return performRenameGroupFactory.create().renameGroup(
-          resource.toAccountGroup().getId(), newName).group.getName();
-    } catch (NoSuchGroupException e) {
-      throw new ResourceNotFoundException();
-    } catch (InvalidNameException e) {
-      throw new BadRequestException(e.getMessage());
-    } catch (NameAlreadyUsedException e) {
-      throw new ResourceConflictException(e.getMessage());
+      AccountGroupName id = new AccountGroupName(key, groupId);
+      db.get().accountGroupNames().insert(Collections.singleton(id));
+    } catch (OrmException e) {
+      AccountGroupName other = db.get().accountGroupNames().get(key);
+      if (other != null) {
+        // If we are using this identity, don't report the exception.
+        //
+        if (other.getId().equals(groupId)) {
+          return groupDetailFactory.create(groupId).call();
+        }
+
+        // Otherwise, someone else has this identity.
+        //
+        throw new ResourceConflictException("group with name " + newName
+            + "already exists");
+      } else {
+        throw e;
+      }
     }
+
+    group.setNameKey(key);
+    db.get().accountGroups().update(Collections.singleton(group));
+
+    AccountGroupName priorName = db.get().accountGroupNames().get(old);
+    if (priorName != null) {
+      db.get().accountGroupNames().delete(Collections.singleton(priorName));
+    }
+
+    groupCache.evict(group);
+    groupCache.evictAfterRename(old, key);
+    renameGroupOpFactory.create(
+        currentUser.get().newCommitterIdent(new Date(), TimeZone.getDefault()),
+        group.getGroupUUID(),
+        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
+
+    return groupDetailFactory.create(groupId).call();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
index 6ed6703..5788503 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -22,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.group.PutOptions.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -31,11 +31,8 @@
 import java.util.Collections;
 
 @Singleton
-public class PutOptions implements RestModifyView<GroupResource, Input> {
-  public static class Input {
-    public Boolean visibleToAll;
-  }
-
+public class PutOptions
+    implements RestModifyView<GroupResource, GroupOptionsInfo> {
   private final GroupCache groupCache;
   private final Provider<ReviewDb> db;
 
@@ -46,7 +43,7 @@
   }
 
   @Override
-  public GroupOptionsInfo apply(GroupResource resource, Input input)
+  public GroupOptionsInfo apply(GroupResource resource, GroupOptionsInfo input)
       throws MethodNotAllowedException, AuthException, BadRequestException,
       ResourceNotFoundException, OrmException {
     if (resource.toAccountGroup() == null) {
@@ -72,6 +69,10 @@
     db.get().accountGroups().update(Collections.singleton(group));
     groupCache.evict(group);
 
-    return new GroupOptionsInfo(group);
+    GroupOptionsInfo options = new GroupOptionsInfo();
+    if (group.isVisibleToAll()) {
+      options.visibleToAll = true;
+    }
+    return options;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
index 11d34ab..b88ead5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -26,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gerrit.server.group.PutOwner.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 59ca272..32a051b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -101,30 +101,27 @@
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     final GroupReference ref = getGroup(uuid);
-    if (ref != null) {
-      return new GroupDescription.Basic() {
-        @Override
-        public String getName() {
-          return ref.getName();
-        }
+    return new GroupDescription.Basic() {
+      @Override
+      public String getName() {
+        return ref.getName();
+      }
 
-        @Override
-        public AccountGroup.UUID getGroupUUID() {
-          return ref.getUUID();
-        }
+      @Override
+      public AccountGroup.UUID getGroupUUID() {
+        return ref.getUUID();
+      }
 
-        @Override
-        public String getUrl() {
-          return null;
-        }
+      @Override
+      public String getUrl() {
+        return null;
+      }
 
-        @Override
-        public String getEmailAddress() {
-          return null;
-        }
-      };
-    }
-    return null;
+      @Override
+      public String getEmailAddress() {
+        return null;
+      }
+    };
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 2993739..4fa5cd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -38,12 +39,16 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.protobuf.CodedOutputStream;
 
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.FooterLine;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -54,8 +59,13 @@
  * {@link ChangeQueryBuilder} for querying that field, and a method on
  * {@link ChangeData} used for populating the corresponding document fields in
  * the secondary index.
+ * <p>
+ * Field names are all lowercase alphanumeric plus underscore; index
+ * implementations may create unambiguous derived field names containing other
+ * characters.
  */
 public class ChangeField {
+  @Deprecated
   /** Legacy change ID. */
   public static final FieldDef<ChangeData, Integer> LEGACY_ID =
       new FieldDef.Single<ChangeData, Integer>("_id",
@@ -66,6 +76,16 @@
         }
       };
 
+  /** Legacy change ID without underscore prefix. */
+  public static final FieldDef<ChangeData, Integer> LEGACY_ID2 =
+      new FieldDef.Single<ChangeData, Integer>("legacy_id",
+          FieldType.INTEGER, true) {
+        @Override
+        public Integer get(ChangeData input, FillArgs args) {
+          return input.getId().get();
+        }
+      };
+
   /** Newer style Change-Id key. */
   public static final FieldDef<ChangeData, String> ID =
       new FieldDef.Single<ChangeData, String>("change_id",
@@ -141,18 +161,64 @@
         }
       };
 
+  @Deprecated
   /** Topic, a short annotation on the branch. */
-  public static final FieldDef<ChangeData, String> TOPIC =
+  public static final FieldDef<ChangeData, String> LEGACY_TOPIC2 =
       new FieldDef.Single<ChangeData, String>(
           "topic2", FieldType.EXACT, false) {
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
+          return getTopic(input);
+        }
+      };
+
+  @Deprecated
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> LEGACY_TOPIC3 =
+      new FieldDef.Single<ChangeData, String>(
+          "topic3", FieldType.PREFIX, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getTopic(input);
+        }
+      };
+
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> EXACT_TOPIC =
+      new FieldDef.Single<ChangeData, String>(
+          "topic4", FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getTopic(input);
+        }
+      };
+
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> FUZZY_TOPIC =
+      new FieldDef.Single<ChangeData, String>(
+          "topic5", FieldType.FULL_TEXT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getTopic(input);
+        }
+      };
+
+  /** Submission id assigned by MergeOp. */
+  public static final FieldDef<ChangeData, String> SUBMISSIONID =
+      new FieldDef.Single<ChangeData, String>(
+          "submissionid", FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
           Change c = input.change();
           if (c == null) {
             return null;
           }
-          return firstNonNull(c.getTopic(), "");
+          return c.getSubmissionId();
         }
       };
 
@@ -253,7 +319,7 @@
             throws OrmException {
           Change c = input.change();
           if (c == null) {
-            return null;
+            return ImmutableSet.of();
           }
           Set<Integer> r = Sets.newHashSet();
           if (!args.allowsDrafts && c.getStatus() == Change.Status.DRAFT) {
@@ -266,23 +332,38 @@
         }
       };
 
-  /** Commit id of any PatchSet on the change */
+  /** Commit ID of any patch set on the change, using prefix match. */
   public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
       new FieldDef.Repeatable<ChangeData, String>(
           ChangeQueryBuilder.FIELD_COMMIT, FieldType.PREFIX, false) {
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
-          Set<String> revisions = Sets.newHashSet();
-          for (PatchSet ps : input.patches()) {
-            if (ps.getRevision() != null) {
-              revisions.add(ps.getRevision().get());
-            }
-          }
-          return revisions;
+          return getRevisions(input);
         }
       };
 
+  /** Commit ID of any patch set on the change, using exact match. */
+  public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
+      new FieldDef.Repeatable<ChangeData, String>(
+          "exactcommit", FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getRevisions(input);
+        }
+      };
+
+  private static Set<String> getRevisions(ChangeData cd) throws OrmException {
+    Set<String> revisions = Sets.newHashSet();
+    for (PatchSet ps : cd.patchSets()) {
+      if (ps.getRevision() != null) {
+        revisions.add(ps.getRevision().get());
+      }
+    }
+    return revisions;
+  }
+
   /** Tracking id extracted from a footer. */
   public static final FieldDef<ChangeData, Iterable<String>> TR =
       new FieldDef.Repeatable<ChangeData, String>(
@@ -293,7 +374,7 @@
           try {
             List<FooterLine> footers = input.commitFooters();
             if (footers == null) {
-              return null;
+              return ImmutableSet.of();
             }
             return Sets.newHashSet(
                 args.trackingFooters.extract(footers).values());
@@ -325,7 +406,8 @@
       };
 
   /** Set true if the change has a non-zero label score. */
-  public static final FieldDef<ChangeData, String> REVIEWED =
+  @Deprecated
+  public static final FieldDef<ChangeData, String> LEGACY_REVIEWED =
       new FieldDef.Single<ChangeData, String>(
           "reviewed", FieldType.EXACT, false) {
         @Override
@@ -340,6 +422,64 @@
         }
       };
 
+  private static Set<String> getPersonParts(PersonIdent person) {
+    if (person == null) {
+      return ImmutableSet.of();
+    }
+    HashSet<String> parts = Sets.newHashSet();
+    String email = person.getEmailAddress().toLowerCase();
+    parts.add(email);
+    parts.addAll(Arrays.asList(email.split("@")));
+    Splitter s = Splitter.on(CharMatcher.anyOf("@.- ")).omitEmptyStrings();
+    Iterables.addAll(parts, s.split(email));
+    Iterables.addAll(parts, s.split(person.getName().toLowerCase()));
+    return parts;
+  }
+
+  public static Set<String> getAuthorParts(ChangeData cd) throws OrmException {
+    try {
+      return getPersonParts(cd.getAuthor());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  public static Set<String> getCommitterParts(ChangeData cd) throws OrmException {
+    try {
+      return getPersonParts(cd.getCommitter());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  /**
+   * The exact email address, or any part of the author name or email address,
+   * in the current patch set.
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_AUTHOR, FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getAuthorParts(input);
+        }
+      };
+
+  /**
+   * The exact email address, or any part of the committer name or email address,
+   * in the current patch set.
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_COMMITTER, FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getCommitterParts(input);
+        }
+      };
+
   public static class ChangeProtoField extends FieldDef.Single<ChangeData, byte[]> {
     public static final ProtobufCodec<Change> CODEC =
         CodecFactory.encoder(Change.class);
@@ -427,22 +567,6 @@
       };
 
   /** Whether the change is mergeable. */
-  @Deprecated
-  public static final FieldDef<ChangeData, String> LEGACY_MERGEABLE =
-      new FieldDef.Single<ChangeData, String>(
-          ChangeQueryBuilder.FIELD_MERGEABLE, FieldType.EXACT, false) {
-        @Override
-        public String get(ChangeData input, FillArgs args)
-            throws OrmException {
-          Boolean m = input.isMergeable();
-          if (m == null) {
-            return null;
-          }
-          return m ? "1" : null;
-        }
-      };
-
-  /** Whether the change is mergeable. */
   public static final FieldDef<ChangeData, String> MERGEABLE =
       new FieldDef.Single<ChangeData, String>(
           "mergeable2", FieldType.EXACT, true) {
@@ -498,6 +622,118 @@
         }
       };
 
+  /** Users who have commented on this change. */
+  public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_COMMENTBY, FieldType.INTEGER, false) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<Integer> r = new HashSet<>();
+          for (ChangeMessage m : input.messages()) {
+            if (m.getAuthor() != null) {
+              r.add(m.getAuthor().get());
+            }
+          }
+          for (PatchLineComment c : input.publishedComments()) {
+            r.add(c.getAuthor().get());
+          }
+          return r;
+        }
+      };
+
+  /** Opaque group identifiers for this change's patch sets. */
+  public static final FieldDef<ChangeData, Iterable<String>> GROUP =
+      new FieldDef.Repeatable<ChangeData, String>(
+          "group", FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<String> r = Sets.newHashSetWithExpectedSize(1);
+          for (PatchSet ps : input.patchSets()) {
+            List<String> groups = ps.getGroups();
+            if (groups != null) {
+              r.addAll(groups);
+            }
+          }
+          return r;
+        }
+      };
+
+  public static class PatchSetProtoField
+      extends FieldDef.Repeatable<ChangeData, byte[]> {
+    public static final ProtobufCodec<PatchSet> CODEC =
+        CodecFactory.encoder(PatchSet.class);
+
+    private PatchSetProtoField() {
+      super("_patch_set", FieldType.STORED_ONLY, true);
+    }
+
+    @Override
+    public Iterable<byte[]> get(ChangeData input, FieldDef.FillArgs args)
+        throws OrmException {
+      return toProtos(CODEC, input.patchSets());
+    }
+  }
+
+  /** Serialized patch set object, used for pre-populating results. */
+  public static final PatchSetProtoField PATCH_SET = new PatchSetProtoField();
+
+  /** Users who have edits on this change. */
+  public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_EDITBY, FieldType.INTEGER, false) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return ImmutableSet.copyOf(Iterables.transform(input.editsByUser(),
+              new Function<Account.Id, Integer>() {
+            @Override
+            public Integer apply(Account.Id account) {
+              return account.get();
+            }
+          }));
+        }
+      };
+
+  /**
+   * Users the change was reviewed by since the last author update.
+   * <p>
+   * A change is considered reviewed by a user if the latest update by that user
+   * is newer than the latest update by the change author. Both top-level change
+   * messages and new patch sets are considered to be updates.
+   * <p>
+   * If the latest update is by the change owner, then the special value {@link
+   * #NOT_REVIEWED} is emitted.
+   */
+  public static final FieldDef<ChangeData, Iterable<Integer>> REVIEWEDBY =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_REVIEWEDBY, FieldType.INTEGER, true) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<Account.Id> reviewedBy = input.reviewedBy();
+          if (reviewedBy.isEmpty()) {
+            return ImmutableSet.of(NOT_REVIEWED);
+          }
+          List<Integer> result = new ArrayList<>(reviewedBy.size());
+          for (Account.Id id : reviewedBy) {
+            result.add(id.get());
+          }
+          return result;
+        }
+      };
+
+  public static final Integer NOT_REVIEWED = -1;
+
+  private static String getTopic(ChangeData input) throws OrmException {
+    Change c = input.change();
+    if (c == null) {
+      return null;
+    }
+    return firstNonNull(c.getTopic(), "");
+  }
+
   private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
       throws OrmException {
     List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index 3b04f05..02e737a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 
 import java.io.IOException;
 
@@ -81,16 +82,16 @@
    * @param p the predicate to match. Must be a tree containing only AND, OR,
    *     or NOT predicates as internal nodes, and {@link IndexPredicate}s as
    *     leaves.
-   * @param start offset in results list at which to start returning results.
-   * @param limit maximum number of results to return.
+   * @param opts query options not implied by the predicate, such as start and
+   *     limit.
    * @return a source of documents matching the predicate. Documents must be
    *     returned in descending updated timestamp order.
    *
    * @throws QueryParseException if the predicate could not be converted to an
    *     indexed data source.
    */
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException;
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException;
 
   /**
    * Mark whether this index is up-to-date and ready to serve reads.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index 5cb5c65..b7ea69e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -63,6 +63,15 @@
         IndexCollection indexes);
   }
 
+  public static CheckedFuture<?, IOException> allAsList(
+      List<? extends ListenableFuture<?>> futures) {
+    // allAsList propagates the first seen exception, wrapped in
+    // ExecutionException, so we can reuse the same mapper as for a single
+    // future. Assume the actual contents of the exception are not useful to
+    // callers. All exceptions are already logged by IndexTask.
+    return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+  }
+
   private static final Function<Exception, IOException> MAPPER =
       new Function<Exception, IOException>() {
     @Override
@@ -136,11 +145,7 @@
     for (Change.Id id : ids) {
       futures.add(indexAsync(id));
     }
-    // allAsList propagates the first seen exception, wrapped in
-    // ExecutionException, so we can reuse the same mapper as for a single
-    // future. Assume the actual contents of the exception are not useful to
-    // callers. All exceptions are already logged by IndexTask.
-    return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+    return allAsList(futures);
   }
 
   /**
@@ -192,7 +197,8 @@
   }
 
   private CheckedFuture<?, IOException> submit(Callable<?> task) {
-    return Futures.makeChecked(executor.submit(task), MAPPER);
+    return Futures.makeChecked(
+        Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
   }
 
   private class IndexTask implements Callable<Void> {
@@ -226,7 +232,7 @@
           }
 
           @Override
-          public CurrentUser getCurrentUser() {
+          public CurrentUser getUser() {
             throw new OutOfScopeException("No user during ChangeIndexer");
           }
         };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 05bf9bd..4789a14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -31,63 +31,6 @@
 /** Secondary index schemas for changes. */
 public class ChangeSchemas {
   @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V11 = schema(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.PROJECTS,
-        ChangeField.REF,
-        ChangeField.TOPIC,
-        ChangeField.UPDATED,
-        ChangeField.FILE_PART,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL,
-        ChangeField.LEGACY_MERGEABLE,
-        ChangeField.ADDED,
-        ChangeField.DELETED,
-        ChangeField.DELTA);
-
-  // For upgrade to Lucene 4.10.0 index format only.
-  static final Schema<ChangeData> V12 = schema(V11.getFields().values());
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V13 = schema(
-      ChangeField.LEGACY_ID,
-      ChangeField.ID,
-      ChangeField.STATUS,
-      ChangeField.PROJECT,
-      ChangeField.PROJECTS,
-      ChangeField.REF,
-      ChangeField.TOPIC,
-      ChangeField.UPDATED,
-      ChangeField.FILE_PART,
-      ChangeField.PATH,
-      ChangeField.OWNER,
-      ChangeField.REVIEWER,
-      ChangeField.COMMIT,
-      ChangeField.TR,
-      ChangeField.LABEL,
-      ChangeField.REVIEWED,
-      ChangeField.COMMIT_MESSAGE,
-      ChangeField.COMMENT,
-      ChangeField.CHANGE,
-      ChangeField.APPROVAL,
-      ChangeField.LEGACY_MERGEABLE,
-      ChangeField.ADDED,
-      ChangeField.DELETED,
-      ChangeField.DELTA,
-      ChangeField.HASHTAG);
-
   static final Schema<ChangeData> V14 = schema(
       ChangeField.LEGACY_ID,
       ChangeField.ID,
@@ -95,7 +38,7 @@
       ChangeField.PROJECT,
       ChangeField.PROJECTS,
       ChangeField.REF,
-      ChangeField.TOPIC,
+      ChangeField.LEGACY_TOPIC2,
       ChangeField.UPDATED,
       ChangeField.FILE_PART,
       ChangeField.PATH,
@@ -104,7 +47,7 @@
       ChangeField.COMMIT,
       ChangeField.TR,
       ChangeField.LABEL,
-      ChangeField.REVIEWED,
+      ChangeField.LEGACY_REVIEWED,
       ChangeField.COMMIT_MESSAGE,
       ChangeField.COMMENT,
       ChangeField.CHANGE,
@@ -115,6 +58,361 @@
       ChangeField.DELTA,
       ChangeField.HASHTAG);
 
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V15 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.LEGACY_TOPIC2,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.LEGACY_REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V16 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.LEGACY_TOPIC3,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.LEGACY_REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V17 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.LEGACY_TOPIC3,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.LEGACY_REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V18 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.LEGACY_TOPIC3,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.LEGACY_REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V19 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.LEGACY_TOPIC3,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.LEGACY_REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V20 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.LEGACY_TOPIC3,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY);
+
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V21 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY);
+
+  @Deprecated
+  static final Schema<ChangeData> V22 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT);
+
+  @Deprecated
+  static final Schema<ChangeData> V23 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT);
+
+  static final Schema<ChangeData> V24 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT,
+      ChangeField.AUTHOR,
+      ChangeField.COMMITTER);
+
+  static final Schema<ChangeData> V25 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.SUBMISSIONID,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT,
+      ChangeField.AUTHOR,
+      ChangeField.COMMITTER);
+
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
   }
@@ -150,9 +448,7 @@
             checkArgument(f.getName().startsWith("V"));
             schema.setVersion(Integer.parseInt(f.getName().substring(1)));
             all.put(schema.getVersion(), schema);
-          } catch (IllegalArgumentException e) {
-            throw new ExceptionInInitializerError(e);
-          } catch (IllegalAccessException e) {
+          } catch (IllegalArgumentException | IllegalAccessException e) {
             throw new ExceptionInInitializerError(e);
           }
         } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
index 09226bd..d204905 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 
 import java.io.IOException;
 
@@ -45,8 +45,7 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException {
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts) {
     throw new UnsupportedOperationException();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
index 557faeb..cf3fd09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Preconditions;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -78,11 +81,17 @@
   private final boolean stored;
 
   private FieldDef(String name, FieldType<?> type, boolean stored) {
-    this.name = name;
+    this.name = checkName(name);
     this.type = type;
     this.stored = stored;
   }
 
+  private static String checkName(String name) {
+    CharMatcher m = CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyz0123456789_");
+    checkArgument(m.matchesAllOf(name), "illegal field name: %s", name);
+    return name;
+  }
+
   /** @return name of the field. */
   public final String getName() {
     return name;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
index dce8a20..89dc808 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
@@ -65,4 +65,8 @@
   public String toString() {
     return name;
   }
+
+  public static IllegalArgumentException badFieldType(FieldType<?> t) {
+    return new IllegalArgumentException("unknown index field type " + t);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
index 1857e55..125d32d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
@@ -18,6 +18,8 @@
 
 import com.google.auto.value.AutoValue;
 
+import org.eclipse.jgit.lib.Config;
+
 /**
  * Implementation-specific configuration for secondary indexes.
  * <p>
@@ -27,14 +29,61 @@
  */
 @AutoValue
 public abstract class IndexConfig {
+  private static final int DEFAULT_MAX_TERMS = 1024;
+  private static final int DEFAULT_MAX_PREFIX_TERMS = 100;
+
   public static IndexConfig createDefault() {
-    return create(Integer.MAX_VALUE);
+    return create(0, 0, DEFAULT_MAX_TERMS, DEFAULT_MAX_PREFIX_TERMS);
   }
 
-  public static IndexConfig create(int maxLimit) {
-    checkArgument(maxLimit > 0, "maxLimit must be positive: %s", maxLimit);
-    return new AutoValue_IndexConfig(maxLimit);
+  public static IndexConfig fromConfig(Config cfg) {
+    return create(
+        cfg.getInt("index", null, "maxLimit", 0),
+        cfg.getInt("index", null, "maxPages", 0),
+        cfg.getInt("index", null, "maxTerms", 0),
+        cfg.getInt("index", null, "maxPrefixTerms", DEFAULT_MAX_PREFIX_TERMS));
   }
 
+  public static IndexConfig create(int maxLimit, int maxPages,
+      int maxTerms, int maxPrefixTerms) {
+    return new AutoValue_IndexConfig(
+        checkLimit(maxLimit, "maxLimit", Integer.MAX_VALUE),
+        checkLimit(maxPages, "maxPages", Integer.MAX_VALUE),
+        checkLimit(maxTerms, "maxTerms", DEFAULT_MAX_TERMS),
+        checkLimit(maxPrefixTerms, "maxPrefixTerms", DEFAULT_MAX_PREFIX_TERMS));
+  }
+
+  private static int checkLimit(int limit, String name, int defaultValue) {
+    if (limit == 0) {
+      return defaultValue;
+    }
+    checkArgument(limit > 0, "%s must be positive: %s", name, limit);
+    return limit;
+  }
+
+  /**
+   * @return maximum limit supported by the underlying index, or limited for
+   * performance reasons.
+   */
   public abstract int maxLimit();
+
+  /**
+   * @return maximum number of pages (limit / start) supported by the
+   *     underlying index, or limited for performance reasons.
+   */
+  public abstract int maxPages();
+
+  /**
+   * @return maximum number of total index query terms supported by the
+   *     underlying index, or limited for performance reasons.
+   */
+  public abstract int maxTerms();
+
+  /**
+   * @return maximum number of prefix terms per query supported by the
+   *     underlying index, or limited for performance reasons. Not enforced for
+   *     general queries; only for specific cases where the query system can
+   *     split into equivalent subqueries.
+   */
+  public abstract int maxPrefixTerms();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 0cfc659..2799144 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,8 +22,6 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.query.change.BasicChangeRewrites;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Provides;
@@ -39,7 +37,7 @@
  */
 public class IndexModule extends LifecycleModule {
   public enum IndexType {
-    LUCENE, SOLR
+    LUCENE
   }
 
   /** Type of secondary index. */
@@ -68,8 +66,7 @@
 
   @Override
   protected void configure() {
-    bind(ChangeQueryRewriter.class).to(IndexRewriteImpl.class);
-    bind(BasicChangeRewrites.class);
+    bind(IndexRewriter.class);
     bind(IndexCollection.class);
     listener().to(IndexCollection.class);
     factory(ChangeIndexer.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriter.java
similarity index 85%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriter.java
index 7fbddfb..d56348b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriter.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.index;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
@@ -26,13 +24,15 @@
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.AndSource;
-import com.google.gerrit.server.query.change.BasicChangeRewrites;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gerrit.server.query.change.LimitPredicate;
 import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.util.MutableInteger;
 
 import java.util.BitSet;
 import java.util.EnumSet;
@@ -40,7 +40,8 @@
 import java.util.Set;
 
 /** Rewriter that pushes boolean logic into the secondary index. */
-public class IndexRewriteImpl implements ChangeQueryRewriter {
+@Singleton
+public class IndexRewriter {
   /** Set of all open change statuses. */
   public static final Set<Change.Status> OPEN_STATUSES;
 
@@ -118,28 +119,23 @@
   }
 
   private final IndexCollection indexes;
-  private final BasicChangeRewrites basicRewrites;
+  private final IndexConfig config;
 
   @Inject
-  IndexRewriteImpl(IndexCollection indexes,
-      BasicChangeRewrites basicRewrites) {
+  IndexRewriter(IndexCollection indexes,
+      IndexConfig config) {
     this.indexes = indexes;
-    this.basicRewrites = basicRewrites;
+    this.config = config;
   }
 
-  @Override
-  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start,
-      int limit) throws QueryParseException {
-    checkArgument(limit > 0, "limit must be positive: %s", limit);
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in,
+      QueryOptions opts) throws QueryParseException {
     ChangeIndex index = indexes.getSearchIndex();
-    in = basicRewrites.rewrite(in);
-    // Increase the limit rather than skipping, since we don't know how many
-    // skipped results would have been filtered out by the enclosing AndSource.
-    limit += start;
 
-    Predicate<ChangeData> out = rewriteImpl(in, index, limit);
+    MutableInteger leafTerms = new MutableInteger();
+    Predicate<ChangeData> out = rewriteImpl(in, index, opts, leafTerms);
     if (in == out || out instanceof IndexPredicate) {
-      return new IndexedChangeQuery(index, out, limit);
+      return new IndexedChangeQuery(index, out, opts);
     } else if (out == null /* cannot rewrite */) {
       return in;
     } else {
@@ -152,7 +148,8 @@
    *
    * @param in predicate to rewrite.
    * @param index index whose schema determines which fields are indexed.
-   * @param limit maximum number of results to return.
+   * @param opts other query options.
+   * @param leafTerms number of leaf index query terms encountered so far.
    * @return {@code null} if no part of this subtree can be queried in the
    *     index directly. {@code in} if this subtree and all its children can be
    *     queried directly in the index. Otherwise, a predicate that is
@@ -162,12 +159,18 @@
    *     support this predicate.
    */
   private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in,
-      ChangeIndex index, int limit) throws QueryParseException {
+      ChangeIndex index, QueryOptions opts, MutableInteger leafTerms)
+      throws QueryParseException {
     if (isIndexPredicate(in, index)) {
+      if (++leafTerms.value > config.maxTerms()) {
+        throw new QueryParseException("too many terms in query");
+      }
       return in;
     } else if (in instanceof LimitPredicate) {
-      // Replace any limits with the limit provided by the caller.
-      return new LimitPredicate(limit);
+      // Replace any limits with the limit provided by the caller. The caller
+      // should have already searched the predicate tree for limit predicates
+      // and included that in their limit computation.
+      return new LimitPredicate(opts.limit());
     } else if (!isRewritePossible(in)) {
       return null; // magic to indicate "in" cannot be rewritten
     }
@@ -179,7 +182,7 @@
     List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
     for (int i = 0; i < n; i++) {
       Predicate<ChangeData> c = in.getChild(i);
-      Predicate<ChangeData> nc = rewriteImpl(c, index, limit);
+      Predicate<ChangeData> nc = rewriteImpl(c, index, opts, leafTerms);
       if (nc == c) {
         isIndexed.set(i);
         newChildren.add(c);
@@ -199,7 +202,7 @@
     } else if (rewritten.cardinality() == n) {
       return in.copy(newChildren); // All children were rewritten.
     }
-    return partitionChildren(in, newChildren, isIndexed, index, limit);
+    return partitionChildren(in, newChildren, isIndexed, index, opts);
   }
 
   private boolean isIndexPredicate(Predicate<ChangeData> in, ChangeIndex index) {
@@ -207,7 +210,7 @@
       return false;
     }
     IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in;
-    return index.getSchema().getFields().containsKey(p.getField().getName());
+    return index.getSchema().hasField(p.getField());
   }
 
   private Predicate<ChangeData> partitionChildren(
@@ -215,11 +218,11 @@
       List<Predicate<ChangeData>> newChildren,
       BitSet isIndexed,
       ChangeIndex index,
-      int limit) throws QueryParseException {
+      QueryOptions opts) throws QueryParseException {
     if (isIndexed.cardinality() == 1) {
       int i = isIndexed.nextSetBit(0);
       newChildren.add(
-          0, new IndexedChangeQuery(index, newChildren.remove(i), limit));
+          0, new IndexedChangeQuery(index, newChildren.remove(i), opts));
       return copy(in, newChildren);
     }
 
@@ -239,7 +242,7 @@
         all.add(c);
       }
     }
-    all.add(0, new IndexedChangeQuery(index, in.copy(indexed), limit));
+    all.add(0, new IndexedChangeQuery(index, in.copy(indexed), opts));
     return copy(in, all);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
index 34d37d8..683f8cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -14,15 +14,18 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gerrit.server.query.change.Paginated;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
@@ -40,19 +43,28 @@
  */
 public class IndexedChangeQuery extends Predicate<ChangeData>
     implements ChangeDataSource, Paginated {
+  @VisibleForTesting
+  static QueryOptions convertOptions(QueryOptions opts) {
+    // Increase the limit rather than skipping, since we don't know how many
+    // skipped results would have been filtered out by the enclosing AndSource.
+    int backendLimit = opts.config().maxLimit();
+    int limit = Ints.saturatedCast((long) opts.limit() + opts.start());
+    limit = Math.min(limit, backendLimit);
+    return QueryOptions.create(opts.config(), 0, limit);
+  }
 
   private final ChangeIndex index;
-  private final int limit;
 
+  private QueryOptions opts;
   private Predicate<ChangeData> pred;
   private ChangeDataSource source;
 
   public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred,
-      int limit) throws QueryParseException {
+      QueryOptions opts) throws QueryParseException {
     this.index = index;
-    this.limit = limit;
+    this.opts = convertOptions(opts);
     this.pred = pred;
-    this.source = index.getSource(pred, 0, limit);
+    this.source = index.getSource(pred, this.opts);
   }
 
   @Override
@@ -74,19 +86,18 @@
   }
 
   @Override
-  public int limit() {
-    return limit;
+  public QueryOptions getOptions() {
+    return opts;
   }
 
   @Override
   public int getCardinality() {
-    return source != null ? source.getCardinality() : limit();
+    return source != null ? source.getCardinality() : opts.limit();
   }
 
   @Override
   public boolean hasChange() {
-    return index.getSchema().getFields()
-        .containsKey(ChangeField.CHANGE.getName());
+    return index.getSchema().hasField(ChangeField.CHANGE);
   }
 
   @Override
@@ -127,14 +138,17 @@
 
   @Override
   public ResultSet<ChangeData> restart(int start) throws OrmException {
+    opts = opts.withStart(start);
     try {
-      source = index.getSource(pred, start, limit);
+      source = index.getSource(pred, opts);
     } catch (QueryParseException e) {
       // Don't need to show this exception to the user; the only thing that
       // changed about pred was its start, and any other QPEs that might happen
       // should have already thrown from the constructor.
       throw new OrmException(e);
     }
+    // Don't convert start to a limit, since the caller of this method (see
+    // AndSource) has calculated the actual number to skip.
     return read();
   }
 
@@ -169,14 +183,14 @@
     }
     IndexedChangeQuery o = (IndexedChangeQuery) other;
     return pred.equals(o.pred)
-        && limit == o.limit;
+        && opts.equals(o.opts);
   }
 
   @Override
   public String toString() {
     return MoreObjects.toStringHelper("index")
         .add("p", pred)
-        .add("limit", limit)
+        .add("opts", opts)
         .toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
index 8ec82c3..db7f31a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -69,7 +69,7 @@
 
   @Override
   public void onGitReferenceUpdated(final Event event) {
-    Futures.transform(
+    Futures.transformAsync(
         executor.submit(new GetChanges(event)),
         new AsyncFunction<List<Change>, List<Void>>() {
           @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
index c0eb276..df70292 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
@@ -50,6 +53,13 @@
     }
   }
 
+  private static <T> FieldDef<T, ?> checkSame(FieldDef<T, ?> f1,
+      FieldDef<T, ?> f2) {
+    checkState(f1 == f2, "Mismatched %s fields: %s != %s",
+        f1.getName(), f1, f2);
+    return f1;
+  }
+
   private final ImmutableMap<String, FieldDef<T, ?>> fields;
   private int version;
 
@@ -71,12 +81,56 @@
     return version;
   }
 
+  /**
+   * Get all fields in this schema.
+   * <p>
+   * This is primarily useful for iteration. Most callers should prefer one
+   * of the helper methods {@link #getField(FieldDef, FieldDef...)} or {@link
+   * #hasField(FieldDef)} to looking up fields by name
+   *
+   * @return all fields in this schema indexed by name.
+   */
   public final ImmutableMap<String, FieldDef<T, ?>> getFields() {
     return fields;
   }
 
+  /**
+   * Look up fields in this schema.
+   *
+   * @param first the preferred field to look up.
+   * @param rest additional fields to look up.
+   * @return the first field in the schema matching {@code first} or {@code
+   *     rest}, in order, or absent if no field matches.
+   */
+  @SafeVarargs
+  public final Optional<FieldDef<T, ?>> getField(FieldDef<T, ?> first,
+      FieldDef<T, ?>... rest) {
+    FieldDef<T, ?> field = fields.get(first.getName());
+    if (field != null) {
+      return Optional.<FieldDef<T, ?>> of(checkSame(field, first));
+    }
+    for (FieldDef<T, ?> f : rest) {
+      field = fields.get(f.getName());
+      if (field != null) {
+        return Optional.<FieldDef<T, ?>> of(checkSame(field, f));
+      }
+    }
+    return Optional.absent();
+  }
+
+  /**
+   * Check whether a field is present in this schema.
+   *
+   * @param field field to look up.
+   * @return whether the field is present.
+   */
   public final boolean hasField(FieldDef<T, ?> field) {
-    return fields.get(field.getName()) == field;
+    FieldDef<T, ?> f = fields.get(field.getName());
+    if (f == null) {
+      return false;
+    }
+    checkSame(f, field);
+    return true;
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index 6dfc847..53af8ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -210,7 +210,7 @@
     }
 
     try {
-      mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
+      mpm.waitFor(Futures.transformAsync(Futures.successfulAsList(futures),
           new AsyncFunction<List<?>, Void>() {
             @Override
             public ListenableFuture<Void> apply(List<?> input) {
@@ -232,12 +232,12 @@
       @Override
       public Void call() throws Exception {
         Multimap<ObjectId, ChangeData> byId = ArrayListMultimap.create();
-        Repository repo = null;
-        ReviewDb db = null;
-        try {
-          repo = repoManager.openRepository(project);
+        // TODO(dborowitz): Opening all repositories in a live server may be
+        // wasteful; see if we can determine which ones it is safe to close
+        // with RepositoryCache.close(repo).
+        try (Repository repo = repoManager.openRepository(project);
+            ReviewDb db = schemaFactory.open()) {
           Map<String, Ref> refs = repo.getRefDatabase().getRefs(ALL);
-          db = schemaFactory.open();
           for (Change c : ScanningChangeCacheImpl.scan(repo, db)) {
             Ref r = refs.get(c.currentPatchSetId().toRefName());
             if (r != null) {
@@ -253,16 +253,6 @@
               verboseWriter).call();
         } catch (RepositoryNotFoundException rnfe) {
           log.error(rnfe.getMessage());
-        } finally {
-          if (db != null) {
-            db.close();
-          }
-          if (repo != null) {
-            repo.close();
-          }
-          // TODO(dborowitz): Opening all repositories in a live server may be
-          // wasteful; see if we can determine which ones it is safe to close
-          // with RepositoryCache.close(repo).
         }
         return null;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
index 4210363..8ea6653 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -29,6 +29,8 @@
 
 package com.google.gerrit.server.ioutil;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.reviewdb.client.CodedEnum;
 
 import org.eclipse.jgit.util.IO;
@@ -139,7 +141,7 @@
     if (bin.length == 0) {
       return null;
     }
-    return new String(bin, 0, bin.length, "UTF-8");
+    return new String(bin, 0, bin.length, UTF_8);
   }
 
   /** Write a UTF-8 string, prefixed by its byte length in a varint. */
@@ -148,7 +150,7 @@
     if (s == null) {
       writeVarInt32(output, 0);
     } else {
-      writeBytes(output, s.getBytes("UTF-8"));
+      writeBytes(output, s.getBytes(UTF_8));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index adcf242..409c155 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,12 +26,13 @@
   public static interface Factory extends
       ReplyToChangeSender.Factory<AbandonedSender> {
     @Override
-    AbandonedSender create(Change change);
+    AbandonedSender create(Change.Id change);
   }
 
   @Inject
-  public AbandonedSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "abandon");
+  public AbandonedSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "abandon", newChangeData(ea, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
new file mode 100644
index 0000000..0f1e86e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2015 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.server.mail;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import java.util.List;
+
+public class AddKeySender extends OutgoingEmail {
+  public interface Factory {
+    public AddKeySender create(IdentifiedUser user, AccountSshKey sshKey);
+
+    public AddKeySender create(IdentifiedUser user, List<String> gpgKey);
+  }
+
+  private final IdentifiedUser callingUser;
+  private final IdentifiedUser user;
+  private final AccountSshKey sshKey;
+  private final List<String> gpgKeys;
+
+  @AssistedInject
+  public AddKeySender(EmailArguments ea,
+      IdentifiedUser callingUser,
+      @Assisted IdentifiedUser user,
+      @Assisted AccountSshKey sshKey) {
+    super(ea, "addkey");
+    this.callingUser = callingUser;
+    this.user = user;
+    this.sshKey = sshKey;
+    this.gpgKeys = null;
+  }
+
+  @AssistedInject
+  public AddKeySender(EmailArguments ea,
+      IdentifiedUser callingUser,
+      @Assisted IdentifiedUser user,
+      @Assisted List<String> gpgKeys) {
+    super(ea, "addkey");
+    this.callingUser = callingUser;
+    this.user = user;
+    this.sshKey = null;
+    this.gpgKeys = gpgKeys;
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setHeader("Subject",
+        String.format("[Gerrit Code Review] New %s Keys Added", getKeyType()));
+    add(RecipientType.TO, new Address(getEmail()));
+  }
+
+  @Override
+  protected boolean shouldSendMessage() {
+    /*
+     * Don't send an email if no keys are added, or an admin is adding a key to
+     * a user.
+     */
+    return (sshKey != null || gpgKeys.size() > 0) &&
+        (user.equals(callingUser) ||
+        !callingUser.getCapabilities().canAdministrateServer());
+  }
+
+  @Override
+  protected void format() throws EmailException {
+    appendText(velocifyFile("AddKey.vm"));
+  }
+
+  public String getEmail() {
+    return user.getAccount().getPreferredEmail();
+  }
+
+  public String getUserNameEmail() {
+    return getUserNameEmailFor(user.getAccountId());
+  }
+
+  public String getKeyType() {
+    if (sshKey != null) {
+      return "SSH";
+    } else if (gpgKeys != null) {
+      return "GPG";
+    }
+    return "Unknown";
+  }
+
+  public String getSshKey() {
+    return (sshKey != null) ? sshKey.getSshPublicKey() + "\n" : null;
+  }
+
+  public String getGpgKeys() {
+    if (gpgKeys != null) {
+      return Joiner.on("\n").join(gpgKeys);
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index c181a9c..7a6d204 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -16,18 +16,20 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 /** Asks a user to review a change. */
 public class AddReviewerSender extends NewChangeSender {
   public static interface Factory {
-    AddReviewerSender create(Change change);
+    AddReviewerSender create(Change.Id id);
   }
 
   @Inject
-  public AddReviewerSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c);
+  public AddReviewerSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, newChangeData(ea, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 4e9ed2b..863cb82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.mail;
 
-import java.io.UnsupportedEncodingException;
-
 public class Address {
   public static Address parse(final String in) {
     final int lt = in.indexOf('<');
@@ -69,14 +67,10 @@
 
   @Override
   public String toString() {
-    try {
-      return toHeaderString();
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("Cannot encode address", e);
-    }
+    return toHeaderString();
   }
 
-  public String toHeaderString() throws UnsupportedEncodingException {
+  public String toHeaderString() {
     if (name != null) {
       return quotedPhrase(name) + " <" + email + ">";
     } else if (isSimple()) {
@@ -98,8 +92,7 @@
     return true;
   }
 
-  private static String quotedPhrase(final String name)
-      throws UnsupportedEncodingException {
+  private static String quotedPhrase(final String name) {
     if (EmailHeader.needsQuotedPrintable(name)) {
       return EmailHeader.quotedPrintable(name);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index ac23455..8948ce3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -55,6 +55,10 @@
 public abstract class ChangeEmail extends NotificationEmail {
   private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
 
+  protected static ChangeData newChangeData(EmailArguments ea, Change.Id id) {
+    return ea.changeDataFactory.create(ea.db.get(), id);
+  }
+
   protected final Change change;
   protected final ChangeData changeData;
   protected PatchSet patchSet;
@@ -65,10 +69,11 @@
   protected Set<Account.Id> authors;
   protected boolean emailOnlyAuthors;
 
-  protected ChangeEmail(EmailArguments ea, Change c, String mc) {
-    super(ea, mc, c.getProject(), c.getDest());
-    change = c;
-    changeData = ea.changeDataFactory.create(ea.db.get(), c);
+  protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd)
+      throws OrmException {
+    super(ea, mc, cd.change().getDest());
+    changeData = cd;
+    change = cd.change();
     emailOnlyAuthors = false;
   }
 
@@ -104,11 +109,11 @@
       for (Account.Id who : changeData.reviewers().values()) {
         names.add(getNameEmailFor(who));
       }
-
       for (String name : names) {
         appendText("Gerrit-Reviewer: " + name + "\n");
       }
     } catch (OrmException e) {
+      log.warn("Cannot get change reviewers", e);
     }
     formatFooter();
   }
@@ -305,7 +310,8 @@
 
   @Override
   protected final Watchers getWatchers(NotifyType type) throws OrmException {
-    ProjectWatch watch = new ProjectWatch(args, project, projectState, changeData);
+    ProjectWatch watch = new ProjectWatch(
+        args, branch.getParentKey(), projectState, changeData);
     return watch.getWatchers(type);
   }
 
@@ -379,6 +385,8 @@
     return args.settings.includeDiff;
   }
 
+  private static int HEAP_EST_SIZE = 32 * 1024;
+
   /** Show patch set as unified difference. */
   public String getUnifiedDiff() {
     PatchList patchList;
@@ -394,30 +402,27 @@
       return "";
     }
 
+    int maxSize = args.settings.maximumDiffSize;
     TemporaryBuffer.Heap buf =
-        new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
+        new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxSize), maxSize);
     try (DiffFormatter fmt = new DiffFormatter(buf)) {
-      Repository git;
-      try {
-        git = args.server.openRepository(change.getProject());
+      try (Repository git = args.server.openRepository(change.getProject())) {
+        try {
+          fmt.setRepository(git);
+          fmt.setDetectRenames(true);
+          fmt.format(patchList.getOldId(), patchList.getNewId());
+          return RawParseUtils.decode(buf.toByteArray());
+        } catch (IOException e) {
+          if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+            return "";
+          }
+          log.error("Cannot format patch", e);
+          return "";
+        }
       } catch (IOException e) {
         log.error("Cannot open repository to format patch", e);
         return "";
       }
-      try {
-        fmt.setRepository(git);
-        fmt.setDetectRenames(true);
-        fmt.format(patchList.getOldId(), patchList.getNewId());
-        return RawParseUtils.decode(buf.toByteArray());
-      } catch (IOException e) {
-        if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
-          return "";
-        }
-        log.error("Cannot format patch", e);
-        return "";
-      } finally {
-        git.close();
-      }
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index b587791..3d0041c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.gerrit.server.PatchLineCommentsUtil.getCommentPsId;
+
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
@@ -50,7 +52,7 @@
       .getLogger(CommentSender.class);
 
   public static interface Factory {
-    public CommentSender create(NotifyHandling notify, Change change);
+    public CommentSender create(NotifyHandling notify, Change.Id id);
   }
 
   private final NotifyHandling notify;
@@ -59,10 +61,10 @@
 
   @Inject
   public CommentSender(EmailArguments ea,
+      PatchLineCommentsUtil plcUtil,
       @Assisted NotifyHandling notify,
-      @Assisted Change c,
-      PatchLineCommentsUtil plcUtil) {
-    super(ea, c, "comment");
+      @Assisted Change.Id id) throws OrmException {
+    super(ea, "comment", newChangeData(ea, id));
     this.notify = notify;
     this.plcUtil = plcUtil;
   }
@@ -114,8 +116,7 @@
 
   public String getInlineComments(int lines) {
     StringBuilder cmts = new StringBuilder();
-    final Repository repo = getRepository();
-    try {
+    try (Repository repo = getRepository()) {
       PatchList patchList = null;
       if (repo != null) {
         try {
@@ -162,10 +163,6 @@
         }
         cmts.append("\n\n");
       }
-    } finally {
-      if (repo != null) {
-        repo.close();
-      }
     }
     return cmts.toString();
   }
@@ -175,7 +172,8 @@
     short side = comment.getSide();
     CommentRange range = comment.getRange();
     if (range != null) {
-      String prefix = String.format("Line %d: ", range.getStartLine());
+      String prefix = "PS" + getCommentPsId(comment).get()
+        + ", Line " + range.getStartLine() + ": ";
       for (int n = range.getStartLine(); n <= range.getEndLine(); n++) {
         out.append(n == range.getStartLine()
             ? prefix
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index f570ac8..29895d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -33,12 +33,13 @@
       LoggerFactory.getLogger(CreateChangeSender.class);
 
   public static interface Factory {
-    public CreateChangeSender create(Change change);
+    public CreateChangeSender create(Change.Id id);
   }
 
   @Inject
-  public CreateChangeSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c);
+  public CreateChangeSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, newChangeData(ea, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index c1ab58a..8e5fa6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
@@ -41,6 +42,7 @@
 import com.google.inject.Provider;
 
 import org.apache.velocity.runtime.RuntimeInstance;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.util.List;
 
@@ -60,6 +62,7 @@
   final ChangeNotes.Factory changeNotesFactory;
   final AnonymousUser anonymousUser;
   final String anonymousCowardName;
+  final PersonIdent gerritPersonIdent;
   final Provider<String> urlProvider;
   final AllProjectsName allProjectsName;
   final List<String> sshAddresses;
@@ -84,6 +87,7 @@
       ChangeNotes.Factory changeNotesFactory,
       AnonymousUser anonymousUser,
       @AnonymousCowardName String anonymousCowardName,
+      GerritPersonIdentProvider gerritPersonIdentProvider,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder queryBuilder,
@@ -108,6 +112,7 @@
     this.changeNotesFactory = changeNotesFactory;
     this.anonymousUser = anonymousUser;
     this.anonymousCowardName = anonymousCowardName;
+    this.gerritPersonIdent = gerritPersonIdentProvider.get();
     this.urlProvider = urlProvider;
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index 46e151b..30026bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -23,6 +26,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 public abstract class EmailHeader {
   public abstract boolean isEmpty();
@@ -30,7 +34,7 @@
   public abstract void write(Writer w) throws IOException;
 
   public static class String extends EmailHeader {
-    private java.lang.String value;
+    private final java.lang.String value;
 
     public String(java.lang.String v) {
       value = v;
@@ -53,6 +57,22 @@
         w.write(value);
       }
     }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return (o instanceof String)
+          && Objects.equals(value, ((String) o).value);
+    }
+
+    @Override
+    public java.lang.String toString() {
+      return MoreObjects.toStringHelper(this).addValue(value).toString();
+    }
   }
 
   static boolean needsQuotedPrintable(java.lang.String value) {
@@ -85,8 +105,7 @@
     }
   }
 
-  static java.lang.String quotedPrintable(java.lang.String value)
-      throws UnsupportedEncodingException {
+  static java.lang.String quotedPrintable(java.lang.String value) {
     final StringBuilder r = new StringBuilder();
 
     r.append("=?UTF-8?Q?");
@@ -96,7 +115,7 @@
         r.append('_');
 
       } else if (needsQuotedPrintableWithinPhrase(cp)) {
-        byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes("UTF-8");
+        byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes(UTF_8);
         for (byte b: buf) {
           r.append('=');
           r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase());
@@ -113,7 +132,7 @@
   }
 
   public static class Date extends EmailHeader {
-    private java.util.Date value;
+    private final java.util.Date value;
 
     public Date(java.util.Date v) {
       value = v;
@@ -132,9 +151,25 @@
     public void write(Writer w) throws IOException {
       final SimpleDateFormat fmt;
       // Mon, 1 Jun 2009 10:49:44 -0700
-      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
+      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US);
       w.write(fmt.format(value));
     }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return (o instanceof Date)
+          && Objects.equals(value, ((Date) o).value);
+    }
+
+    @Override
+    public java.lang.String toString() {
+      return MoreObjects.toStringHelper(this).addValue(value).toString();
+    }
   }
 
   public static class AddressList extends EmailHeader {
@@ -191,5 +226,21 @@
         needComma = true;
       }
     }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(list);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return (o instanceof AddressList)
+          && Objects.equals(list, ((AddressList) o).list);
+    }
+
+    @Override
+    public java.lang.String toString() {
+      return MoreObjects.toStringHelper(this).addValue(list).toString();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
index f50f538..a125517 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
 public class EmailModule extends FactoryModule {
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
index 7e44877..3c14f2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -21,9 +21,9 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-class EmailSettings {
-  final boolean includeDiff;
-  final int maximumDiffSize;
+public class EmailSettings {
+  public final boolean includeDiff;
+  public final int maximumDiffSize;
 
   @Inject
   EmailSettings(@GerritServerConfig Config cfg) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 8501426..58bdac1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -26,13 +26,13 @@
    * @param emailAddress the address to add.
    * @return an unforgeable string to email to {@code emailAddress}. Presenting
    *         the string provides proof the user has the ability to read messages
-   *         sent to that address.
+   *         sent to that address. Must not be null.
    */
   public String encode(Account.Id accountId, String emailAddress);
 
   /**
    * Decode a token previously created.
-   * @param tokenString the string created by encode.
+   * @param tokenString the string created by encode. Never null.
    * @return a pair of account id and email address.
    * @throws InvalidTokenException the token is invalid, expired, malformed, etc.
    */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index 058bbc8..51f7ad1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -41,7 +41,7 @@
 
   @Inject
   FromAddressGeneratorProvider(@GerritServerConfig final Config cfg,
-      final @AnonymousCowardName String anonymousCowardName,
+      @AnonymousCowardName final String anonymousCowardName,
       @GerritPersonIdent final PersonIdent myIdent,
       final AccountCache accountCache) {
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
index 3a5f7eb..ba75723 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
@@ -16,18 +16,20 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 /** Send notice about a change failing to merged. */
 public class MergeFailSender extends ReplyToChangeSender {
   public static interface Factory {
-    public MergeFailSender create(Change change);
+    public MergeFailSender create(Change.Id id);
   }
 
   @Inject
-  public MergeFailSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "merge-failed");
+  public MergeFailSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "merge-failed", newChangeData(ea, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 5cb1ba1..7fbcf8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -31,15 +31,16 @@
 /** Send notice about a change successfully merged. */
 public class MergedSender extends ReplyToChangeSender {
   public static interface Factory {
-    public MergedSender create(ChangeControl change);
+    public MergedSender create(Change.Id id);
   }
 
   private final LabelTypes labelTypes;
 
   @Inject
-  public MergedSender(EmailArguments ea, @Assisted ChangeControl c) {
-    super(ea, c.getChange(), "merged");
-    labelTypes = c.getLabelTypes();
+  public MergedSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "merged", newChangeData(ea, id));
+    labelTypes = changeData.changeControl().getLabelTypes();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 0dbcbe0..e18c7e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -16,7 +16,8 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -29,8 +30,9 @@
   private final Set<Account.Id> reviewers = new HashSet<>();
   private final Set<Account.Id> extraCC = new HashSet<>();
 
-  protected NewChangeSender(EmailArguments ea, Change c) {
-    super(ea, c, "newchange");
+  protected NewChangeSender(EmailArguments ea, ChangeData cd)
+      throws OrmException {
+    super(ea, "newchange", cd);
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
index f0c43d3..de338ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.mail.ProjectWatch.Watchers;
 import com.google.gwtorm.server.OrmException;
 
@@ -33,14 +32,11 @@
   private static final Logger log =
       LoggerFactory.getLogger(NotificationEmail.class);
 
-  protected Project.NameKey project;
   protected Branch.NameKey branch;
 
   protected NotificationEmail(EmailArguments ea,
-      String mc, Project.NameKey project, Branch.NameKey branch) {
+      String mc, Branch.NameKey branch) {
     super(ea, mc);
-
-    this.project = project;
     this.branch = branch;
   }
 
@@ -104,7 +100,7 @@
   @Override
   protected void setupVelocityContext() {
     super.setupVelocityContext();
-    velocityContext.put("projectName", project.get());
+    velocityContext.put("projectName", branch.getParentKey().get());
     velocityContext.put("branch", branch);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index af8a381..8a288e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -25,7 +27,6 @@
 import com.google.gwtorm.server.OrmException;
 
 import org.apache.commons.lang.StringUtils;
-import org.apache.commons.validator.routines.EmailValidator;
 import org.apache.velocity.Template;
 import org.apache.velocity.VelocityContext;
 import org.apache.velocity.context.InternalContextAdapterImpl;
@@ -255,7 +256,7 @@
   /** Lookup a human readable name for an account, usually the "full name". */
   protected String getNameFor(final Account.Id accountId) {
     if (accountId == null) {
-      return args.anonymousCowardName;
+      return args.gerritPersonIdent.getName();
     }
 
     final Account userAccount = args.accountCache.get(accountId).getAccount();
@@ -269,6 +270,13 @@
     return name;
   }
 
+  /**
+   * Gets the human readable name and email for an account;
+   * if neither are available, returns the Anonymous Coward name.
+   *
+   * @param accountId user to fetch.
+   * @return name/email of account, or Anonymous Coward if unset.
+   */
   public String getNameEmailFor(Account.Id accountId) {
     AccountState who = args.accountCache.get(accountId);
     String name = who.getAccount().getFullName();
@@ -287,6 +295,33 @@
     }
   }
 
+  /**
+   * Gets the human readable name and email for an account;
+   * if both are unavailable, returns the username.  If no
+   * username is set, this function returns null.
+   *
+   * @param accountId user to fetch.
+   * @return name/email of account, username, or null if unset.
+   */
+  public String getUserNameEmailFor(Account.Id accountId) {
+    AccountState who = args.accountCache.get(accountId);
+    String name = who.getAccount().getFullName();
+    String email = who.getAccount().getPreferredEmail();
+
+    if (name != null && email != null) {
+      return name + " <" + email + ">";
+    } else if (email != null) {
+      return email;
+    } else if (name != null) {
+      return name;
+    }
+    String username = who.getUserName();
+    if (username != null) {
+      return username;
+    }
+    return null;
+  }
+
   protected boolean shouldSendMessage() {
     if (body.length() == 0) {
       // If we have no message body, don't send.
@@ -346,7 +381,7 @@
   /** Schedule delivery of this message to the given account. */
   protected void add(final RecipientType rt, final Address addr) {
     if (addr != null && addr.email != null && addr.email.length() > 0) {
-      if (!EmailValidator.getInstance().isValid(addr.email)) {
+      if (!OutgoingEmailValidator.isValid(addr.email)) {
         log.warn("Not emailing " + addr.email + " (invalid email address)");
       } else if (!args.emailSender.canEmail(addr.email)) {
         log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
@@ -408,7 +443,7 @@
       if (runtime.getLoaderNameForResource(name) == null) {
         name = "com/google/gerrit/server/mail/" + name;
       }
-      Template template = runtime.getTemplate(name, "UTF-8");
+      Template template = runtime.getTemplate(name, UTF_8.name());
       StringWriter w = new StringWriter();
       template.merge(velocityContext, w);
       return w.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java
new file mode 100644
index 0000000..5ab5f4e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2016 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.server.mail;
+
+import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS;
+
+import org.apache.commons.validator.routines.DomainValidator;
+import org.apache.commons.validator.routines.EmailValidator;
+
+public class OutgoingEmailValidator {
+  static {
+    DomainValidator.updateTLDOverride(GENERIC_PLUS, new String[]{"local"});
+  }
+
+  public static boolean isValid(String addr) {
+    return EmailValidator.getInstance(true, true).isValid(addr);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
deleted file mode 100644
index 53efa1e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright (C) 2013 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.server.mail;
-
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.MailUtil.MailRecipients;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.FooterLine;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-
-public class PatchSetNotificationSender {
-  private static final Logger log =
-      LoggerFactory.getLogger(PatchSetNotificationSender.class);
-
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager repoManager;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ApprovalsUtil approvalsUtil;
-  private final AccountResolver accountResolver;
-  private final CreateChangeSender.Factory createChangeSenderFactory;
-  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
-
-  @Inject
-  public PatchSetNotificationSender(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager,
-      PatchSetInfoFactory patchSetInfoFactory,
-      ApprovalsUtil approvalsUtil,
-      AccountResolver accountResolver,
-      CreateChangeSender.Factory createChangeSenderFactory,
-      ReplacePatchSetSender.Factory replacePatchSetFactory) {
-    this.db = db;
-    this.repoManager = repoManager;
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.approvalsUtil = approvalsUtil;
-    this.accountResolver = accountResolver;
-    this.createChangeSenderFactory = createChangeSenderFactory;
-    this.replacePatchSetFactory = replacePatchSetFactory;
-  }
-
-  public void send(final ChangeNotes notes, final ChangeUpdate update,
-      final boolean newChange, final IdentifiedUser currentUser,
-      final Change updatedChange, final PatchSet updatedPatchSet,
-      final LabelTypes labelTypes)
-      throws OrmException, IOException {
-    try (Repository git = repoManager.openRepository(updatedChange.getProject())) {
-      final RevCommit commit;
-      try (RevWalk revWalk = new RevWalk(git)) {
-        commit = revWalk.parseCommit(ObjectId.fromString(
-            updatedPatchSet.getRevision().get()));
-      }
-      final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
-      final List<FooterLine> footerLines = commit.getFooterLines();
-      final Account.Id me = currentUser.getAccountId();
-      final MailRecipients recipients =
-          getRecipientsFromFooters(accountResolver, updatedPatchSet, footerLines);
-      recipients.remove(me);
-
-      if (newChange) {
-        approvalsUtil.addReviewers(db.get(), update, labelTypes, updatedChange,
-            updatedPatchSet, info, recipients.getReviewers(),
-            Collections.<Account.Id> emptySet());
-        try {
-          CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
-          cm.setFrom(me);
-          cm.setPatchSet(updatedPatchSet, info);
-          cm.addReviewers(recipients.getReviewers());
-          cm.addExtraCC(recipients.getCcOnly());
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new change " + updatedChange.getId(), e);
-        }
-      } else {
-        approvalsUtil.addReviewers(db.get(), update, labelTypes, updatedChange,
-            updatedPatchSet, info, recipients.getReviewers(),
-            approvalsUtil.getReviewers(db.get(), notes).values());
-        final ChangeMessage msg =
-            new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
-                ChangeUtil.messageUUID(db.get())), me,
-                updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
-        msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
-        try {
-          ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
-          cm.setFrom(me);
-          cm.setPatchSet(updatedPatchSet, info);
-          cm.setChangeMessage(msg);
-          cm.addReviewers(recipients.getReviewers());
-          cm.addExtraCC(recipients.getCcOnly());
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 63709c6..95b0219 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -87,10 +87,9 @@
           try {
             add(matching, nc);
           } catch (QueryParseException e) {
-            log.warn(String.format(
-                "Project %s has invalid notify %s filter \"%s\"",
+            log.warn("Project {} has invalid notify {} filter \"{}\": {}",
                 state.getProject().getName(), nc.getName(),
-                nc.getFilter()), e);
+                nc.getFilter(), e.getMessage());
           }
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index 1d87972..3e6bd8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
@@ -53,27 +55,13 @@
   }
 
   public String getUserNameEmail() {
-    String name = user.getAccount().getFullName();
-    String email = user.getAccount().getPreferredEmail();
-
-    if (name != null && email != null) {
-      return name + " <" + email + ">";
-    } else if (email != null) {
-      return email;
-    } else if (name != null) {
-      return name;
-    } else {
-      String username = user.getUserName();
-      if (username != null) {
-        return username;
-      }
-    }
-    return null;
+    return getUserNameEmailFor(user.getAccountId());
   }
 
   public String getEmailRegistrationToken() {
     if (emailToken == null) {
-      emailToken = tokenVerifier.encode(user.getAccountId(), addr);
+      emailToken = checkNotNull(
+          tokenVerifier.encode(user.getAccountId(), addr), "token");
     }
     return emailToken;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index 8412d22..05c7933 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -30,15 +31,16 @@
 /** Send notice of new patch sets for reviewers. */
 public class ReplacePatchSetSender extends ReplyToChangeSender {
   public static interface Factory {
-    public ReplacePatchSetSender create(Change change);
+    public ReplacePatchSetSender create(Change.Id id);
   }
 
   private final Set<Account.Id> reviewers = new HashSet<>();
   private final Set<Account.Id> extraCC = new HashSet<>();
 
   @Inject
-  public ReplacePatchSetSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "newpatchset");
+  public ReplacePatchSetSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "newpatchset", newChangeData(ea, id));
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 42ac917..62a6c72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -16,15 +16,18 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends ChangeEmail {
   public static interface Factory<T extends ReplyToChangeSender> {
-    public T create(Change change);
+    public T create(Change.Id id);
   }
 
-  protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
-    super(ea, c, mc);
+  protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd)
+      throws OrmException {
+    super(ea, mc, cd);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index 4f65ab4..a43c7b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,12 +26,13 @@
   public static interface Factory extends
       ReplyToChangeSender.Factory<RestoredSender> {
     @Override
-    RestoredSender create(Change change);
+    RestoredSender create(Change.Id id);
   }
 
   @Inject
-  public RestoredSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "restore");
+  public RestoredSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "restore", newChangeData(ea, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index d1389fb..dda68ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -17,18 +17,20 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 /** Send notice about a change being reverted. */
 public class RevertedSender extends ReplyToChangeSender {
   public static interface Factory {
-    RevertedSender create(Change change);
+    RevertedSender create(Change.Id id);
   }
 
   @Inject
-  public RevertedSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "revert");
+  public RevertedSender(EmailArguments ea, @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "revert", newChangeData(ea, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index d74eaeb..f12859f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
@@ -25,7 +27,6 @@
 
 import org.eclipse.jgit.util.Base64;
 
-import java.io.UnsupportedEncodingException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -50,13 +51,11 @@
   public String encode(Account.Id accountId, String emailAddress) {
     try {
       String payload = String.format("%s:%s", accountId, emailAddress);
-      byte[] utf8 = payload.getBytes("UTF-8");
+      byte[] utf8 = payload.getBytes(UTF_8);
       String base64 = Base64.encodeBytes(utf8);
       return emailRegistrationToken.newToken(base64);
     } catch (XsrfException e) {
       throw new IllegalArgumentException(e);
-    } catch (UnsupportedEncodingException e) {
-      throw new IllegalArgumentException(e);
     }
   }
 
@@ -72,13 +71,7 @@
       throw new InvalidTokenException();
     }
 
-    String payload;
-    try {
-      payload = new String(Base64.decode(token.getData()), "UTF-8");
-    } catch (UnsupportedEncodingException err) {
-      throw new InvalidTokenException(err);
-    }
-
+    String payload = new String(Base64.decode(token.getData()), UTF_8);
     Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload);
     if (!matcher.matches()) {
       throw new InvalidTokenException();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index ee21316..aa57247 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
@@ -192,8 +194,8 @@
           }
         }
 
-        Writer w = client.sendMessageData();
-        if (w == null) {
+        Writer messageDataWriter = client.sendMessageData();
+        if (messageDataWriter == null) {
           /* Include rejected recipient error messages here to not lose that
            * information. That piece of the puzzle is vital if zero recipients
            * are accepted and the server consequently rejects the DATA command.
@@ -201,21 +203,20 @@
           throw new EmailException(rejected + "Server " + smtpHost
               + " rejected DATA command: " + client.getReplyString());
         }
-        w = new BufferedWriter(w);
-
-        for (Map.Entry<String, EmailHeader> h : hdrs.entrySet()) {
-          if (!h.getValue().isEmpty()) {
-            w.write(h.getKey());
-            w.write(": ");
-            h.getValue().write(w);
-            w.write("\r\n");
+        try (Writer w = new BufferedWriter(messageDataWriter)) {
+          for (Map.Entry<String, EmailHeader> h : hdrs.entrySet()) {
+            if (!h.getValue().isEmpty()) {
+              w.write(h.getKey());
+              w.write(": ");
+              h.getValue().write(w);
+              w.write("\r\n");
+            }
           }
-        }
 
-        w.write("\r\n");
-        w.write(body);
-        w.flush();
-        w.close();
+          w.write("\r\n");
+          w.write(body);
+          w.flush();
+        }
 
         if (!client.completePendingCommand()) {
           throw new EmailException("Server " + smtpHost
@@ -242,7 +243,7 @@
   }
 
   private SMTPClient open() throws EmailException {
-    final AuthSMTPClient client = new AuthSMTPClient("UTF-8");
+    final AuthSMTPClient client = new AuthSMTPClient(UTF_8.name());
 
     if (smtpEncryption == Encryption.SSL) {
       client.enableSSL(sslVerify);
@@ -281,6 +282,7 @@
         try {
           client.disconnect();
         } catch (IOException e2) {
+          //Ignored
         }
       }
       if (e instanceof EmailException) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
index ace1f5b..3fdc550 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
@@ -18,6 +18,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
 
 import org.apache.velocity.runtime.RuntimeConstants;
 import org.apache.velocity.runtime.RuntimeInstance;
@@ -26,9 +27,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.file.Files;
 import java.util.Properties;
 
 /** Configures Velocity template engine for sending email. */
+@Singleton
 public class VelocityRuntimeProvider implements Provider<RuntimeInstance> {
   private final SitePaths site;
 
@@ -49,10 +52,11 @@
     p.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true");
     p.setProperty("runtime.log.logsystem.log4j.category", "velocity");
 
-    if (site.mail_dir.isDirectory()) {
+    if (Files.isDirectory(site.mail_dir)) {
       p.setProperty(rl, "file, class");
       p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader");
-      p.setProperty("file." + rl + ".path", site.mail_dir.getAbsolutePath());
+      p.setProperty("file." + rl + ".path",
+          site.mail_dir.toAbsolutePath().toString());
       p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader");
     } else {
       p.setProperty(rl, "class");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 80d4504..b1ca9e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -54,19 +54,11 @@
       loadDefaults();
       return self();
     }
-    Repository repo;
-    try {
-      repo = repoManager.openMetadataRepository(getProjectName());
-    } catch (IOException e) {
-      throw new OrmException(e);
-    }
-    try {
+    try (Repository repo = repoManager.openMetadataRepository(getProjectName())) {
       load(repo);
       loaded = true;
     } catch (ConfigInvalidException | IOException e) {
       throw new OrmException(e);
-    } finally {
-      repo.close();
     }
     return self();
   }
@@ -77,15 +69,9 @@
     } else if (!migration.enabled()) {
       return null;
     }
-    Repository repo;
-    try {
-      repo = repoManager.openMetadataRepository(getProjectName());
-      try {
-        Ref ref = repo.getRef(getRefName());
-        return ref != null ? ref.getObjectId() : null;
-      } finally {
-        repo.close();
-      }
+    try (Repository repo = repoManager.openMetadataRepository(getProjectName())) {
+      Ref ref = repo.getRefDatabase().exactRef(getRefName());
+      return ref != null ? ref.getObjectId() : null;
     } catch (IOException e) {
       throw new OrmException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index fbca668..fb41027 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -78,7 +78,7 @@
   }
 
   public IdentifiedUser getUser() {
-    return (IdentifiedUser) ctl.getCurrentUser();
+    return ctl.getUser().asIdentifiedUser();
   }
 
   public PatchSet.Id getPatchSetId() {
@@ -93,13 +93,10 @@
 
   private void load() throws IOException {
     if (migration.writeChanges() && getRevision() == null) {
-      Repository repo = repoManager.openMetadataRepository(getProjectName());
-      try {
+      try (Repository repo = repoManager.openMetadataRepository(getProjectName())) {
         load(repo);
       } catch (ConfigInvalidException e) {
         throw new IOException(e);
-      } finally {
-        repo.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 3b51bb4..bd8f797 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -16,14 +16,14 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.addCommentToMap;
 
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Table;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -46,8 +46,12 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -89,9 +93,9 @@
         anonymousCowardName, when);
     this.draftsProject = allUsers;
     this.commentsUtil = commentsUtil;
-    checkState(ctl.getCurrentUser().isIdentifiedUser(),
+    checkState(ctl.getUser().isIdentifiedUser(),
         "Current user must be identified");
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     this.accountId = user.getAccountId();
     this.changeNotes = getChangeNotes().load();
     this.draftNotes = draftNotesFactory.create(ctl.getChange().getId(),
@@ -165,11 +169,6 @@
   }
 
   private void verifyComment(PatchLineComment comment) {
-    checkState(psId != null,
-        "setPatchSetId must be called first");
-    checkArgument(getCommentPsId(comment).equals(psId),
-        "Comment on %s does not match configured patch set %s",
-        getCommentPsId(comment), psId);
     if (migration.writeChanges()) {
       checkArgument(comment.getRevId() != null);
     }
@@ -190,76 +189,55 @@
       noteMap = NoteMap.newEmptyMap();
     }
 
-    Table<PatchSet.Id, String, PatchLineComment> baseDrafts =
-        draftNotes.getDraftBaseComments();
-    Table<PatchSet.Id, String, PatchLineComment> psDrafts =
-        draftNotes.getDraftPsComments();
+    Map<RevId, List<PatchLineComment>> allComments = new HashMap<>();
 
-    boolean draftsEmpty = baseDrafts.isEmpty() && psDrafts.isEmpty();
-
-    // There is no need to rewrite the note for one of the sides of the patch
-    // set if all of the modifications were made to the comments of one side,
-    // so we set these flags to potentially save that extra work.
-    boolean baseSideChanged = false;
-    boolean revisionSideChanged = false;
-
-    // We must define these RevIds so that if this update deletes all
-    // remaining comments on a given side, then we can remove that note.
-    // However, if this update doesn't delete any comments, it is okay for these
-    // to be null because they won't be used.
-    RevId baseRevId = null;
-    RevId psRevId = null;
-
+    boolean hasComments = false;
+    int n = deleteComments.size() + upsertComments.size();
+    Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(n);
+    Set<PatchLineComment.Key> updatedKeys = Sets.newHashSetWithExpectedSize(n);
     for (PatchLineComment c : deleteComments) {
-      if (c.getSide() == (short) 0) {
-        baseSideChanged = true;
-        baseRevId = c.getRevId();
-        baseDrafts.remove(psId, c.getKey().get());
-      } else {
-        revisionSideChanged = true;
-        psRevId = c.getRevId();
-        psDrafts.remove(psId, c.getKey().get());
-      }
+      allComments.put(c.getRevId(), new ArrayList<PatchLineComment>());
+      updatedRevs.add(c.getRevId());
+      updatedKeys.add(c.getKey());
     }
 
     for (PatchLineComment c : upsertComments) {
-      if (c.getSide() == (short) 0) {
-        baseSideChanged = true;
-        baseDrafts.put(psId, c.getKey().get(), c);
-      } else {
-        revisionSideChanged = true;
-        psDrafts.put(psId, c.getKey().get(), c);
+      hasComments = true;
+      addCommentToMap(allComments, c);
+      updatedRevs.add(c.getRevId());
+      updatedKeys.add(c.getKey());
+    }
+
+    // Re-add old comments for updated revisions so the new note contents
+    // includes both old and new comments merged in the right order.
+    //
+    // writeCommentsToNoteMap doesn't touch notes for SHA-1s that are not
+    // mentioned in the input map, so by omitting comments for those revisions,
+    // we avoid the work of having to re-serialize identical comment data for
+    // those revisions.
+    ListMultimap<RevId, PatchLineComment> existing =
+        draftNotes.getComments();
+    for (Map.Entry<RevId, PatchLineComment> e : existing.entries()) {
+      PatchLineComment c = e.getValue();
+      if (updatedRevs.contains(c.getRevId())
+          && !updatedKeys.contains(c.getKey())) {
+        hasComments = true;
+        addCommentToMap(allComments, e.getValue());
       }
     }
 
-    List<PatchLineComment> newBaseDrafts =
-        Lists.newArrayList(baseDrafts.row(psId).values());
-    List<PatchLineComment> newPsDrafts =
-        Lists.newArrayList(psDrafts.row(psId).values());
+    // If we touched every revision and there are no comments left, set the flag
+    // for the caller to delete the entire ref.
+    boolean touchedAllRevs = updatedRevs.equals(existing.keySet());
+    if (touchedAllRevs && !hasComments) {
+      removedAllComments.set(touchedAllRevs && !hasComments);
+      return null;
+    }
 
-    updateNoteMap(baseSideChanged, noteMap, newBaseDrafts,
-        baseRevId);
-    updateNoteMap(revisionSideChanged, noteMap, newPsDrafts,
-        psRevId);
-
-    removedAllComments.set(
-        baseDrafts.isEmpty() && psDrafts.isEmpty() && !draftsEmpty);
-
+    commentsUtil.writeCommentsToNoteMap(noteMap, allComments, inserter);
     return noteMap.writeTree(inserter);
   }
 
-  private void updateNoteMap(boolean changed, NoteMap noteMap,
-      List<PatchLineComment> comments, RevId commitId)
-      throws IOException {
-    if (changed) {
-      if (comments.isEmpty()) {
-        commentsUtil.removeNote(noteMap, commitId);
-      } else {
-        commentsUtil.writeCommentsToNoteMap(noteMap, comments, inserter);
-      }
-    }
-  }
-
   public RevCommit commit() throws IOException {
     BatchMetaDataUpdate batch = openUpdate();
     try {
@@ -279,13 +257,11 @@
     if (migration.writeChanges()) {
       AtomicBoolean removedAllComments = new AtomicBoolean();
       ObjectId treeId = storeCommentsInNotes(removedAllComments);
-      if (treeId != null) {
-        if (removedAllComments.get()) {
-          batch.removeRef(getRefName());
-        } else {
-          builder.setTreeId(treeId);
-          batch.write(builder);
-        }
+      if (removedAllComments.get()) {
+        batch.removeRef(getRefName());
+      } else if (treeId != null) {
+        builder.setTreeId(treeId);
+        batch.write(builder);
       }
     }
   }
@@ -308,7 +284,7 @@
     }
     commit.setAuthor(newIdent(getUser().getAccount(), when));
     commit.setCommitter(new PersonIdent(serverIdent, when));
-    commit.setMessage(String.format("Comment on patch set %d", psId.get()));
+    commit.setMessage("Update draft comments");
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 21981ab..063ff5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -15,18 +15,15 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Ordering;
-import com.google.common.collect.Table;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -36,6 +33,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -52,8 +50,6 @@
 
 import java.io.IOException;
 import java.sql.Timestamp;
-import java.util.Comparator;
-import java.util.List;
 import java.util.Map;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
@@ -76,20 +72,6 @@
           }
         });
 
-  public static final Comparator<PatchLineComment> PLC_ORDER =
-      new Comparator<PatchLineComment>() {
-    @Override
-    public int compare(PatchLineComment c1, PatchLineComment c2) {
-      String filename1 = c1.getKey().getParentKey().get();
-      String filename2 = c2.getKey().getParentKey().get();
-      return ComparisonChain.start()
-          .compare(filename1, filename2)
-          .compare(c1.getLine(), c2.getLine())
-          .compare(c1.getWrittenOn(), c2.getWrittenOn())
-          .result();
-    }
-  };
-
   public static ConfigInvalidException parseException(Change.Id changeId,
       String fmt, Object... args) {
     return new ConfigInvalidException("Change " + changeId + ": "
@@ -138,8 +120,7 @@
   private ImmutableList<Account.Id> allPastReviewers;
   private ImmutableList<SubmitRecord> submitRecords;
   private ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessages;
-  private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForBase;
-  private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForPS;
+  private ImmutableListMultimap<RevId, PatchLineComment> comments;
   private ImmutableSet<String> hashtags;
   NoteMap noteMap;
 
@@ -194,28 +175,15 @@
     return changeMessages;
   }
 
-  /** @return inline comments on each patchset's base (side == 0). */
-  public ImmutableListMultimap<PatchSet.Id, PatchLineComment>
-      getBaseComments() {
-    return commentsForBase;
+  /** @return inline comments on each revision. */
+  public ImmutableListMultimap<RevId, PatchLineComment> getComments() {
+    return comments;
   }
 
-  /** @return inline comments on each patchset (side == 1). */
-  public ImmutableListMultimap<PatchSet.Id, PatchLineComment>
-      getPatchSetComments() {
-    return commentsForPS;
-  }
-
-  public Table<PatchSet.Id, String, PatchLineComment> getDraftBaseComments(
+  public ImmutableListMultimap<RevId, PatchLineComment> getDraftComments(
       Account.Id author) throws OrmException {
     loadDraftComments(author);
-    return draftCommentNotes.getDraftBaseComments();
-  }
-
-  public Table<PatchSet.Id, String, PatchLineComment> getDraftPsComments(
-      Account.Id author) throws OrmException {
-    loadDraftComments(author);
-    return draftCommentNotes.getDraftPsComments();
+    return draftCommentNotes.getComments();
   }
 
   /**
@@ -234,6 +202,11 @@
     }
   }
 
+  @VisibleForTesting
+  DraftCommentNotes getDraftCommentNotes() {
+    return draftCommentNotes;
+  }
+
   public boolean containsComment(PatchLineComment c) throws OrmException {
     if (containsCommentPublished(c)) {
       return true;
@@ -243,11 +216,7 @@
   }
 
   public boolean containsCommentPublished(PatchLineComment c) {
-    PatchSet.Id psId = getCommentPsId(c);
-    List<PatchLineComment> list = (c.getSide() == (short) 0)
-        ? getBaseComments().get(psId)
-        : getPatchSetComments().get(psId);
-    for (PatchLineComment l : list) {
+    for (PatchLineComment l : getComments().values()) {
       if (c.getKey().equals(l.getKey())) {
         return true;
       }
@@ -282,8 +251,7 @@
       }
       approvals = parser.buildApprovals();
       changeMessages = parser.buildMessages();
-      commentsForBase = ImmutableListMultimap.copyOf(parser.commentsForBase);
-      commentsForPS = ImmutableListMultimap.copyOf(parser.commentsForPs);
+      comments = ImmutableListMultimap.copyOf(parser.comments);
       noteMap = parser.commentNoteMap;
 
       if (parser.hashtags != null) {
@@ -310,8 +278,7 @@
     reviewers = ImmutableSetMultimap.of();
     submitRecords = ImmutableList.of();
     changeMessages = ImmutableListMultimap.of();
-    commentsForBase = ImmutableListMultimap.of();
-    commentsForPS = ImmutableListMultimap.of();
+    comments = ImmutableListMultimap.of();
     hashtags = ImmutableSet.of();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index b5b3c74..6f8cb2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.util.LabelVote;
 
@@ -72,8 +73,7 @@
   final Map<Account.Id, ReviewerState> reviewers;
   final List<Account.Id> allPastReviewers;
   final List<SubmitRecord> submitRecords;
-  final Multimap<PatchSet.Id, PatchLineComment> commentsForPs;
-  final Multimap<PatchSet.Id, PatchLineComment> commentsForBase;
+  final Multimap<RevId, PatchLineComment> comments;
   NoteMap commentNoteMap;
   Change.Status status;
   Set<String> hashtags;
@@ -99,8 +99,7 @@
     allPastReviewers = Lists.newArrayList();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
     changeMessages = LinkedListMultimap.create();
-    commentsForPs = ArrayListMultimap.create();
-    commentsForBase = ArrayListMultimap.create();
+    comments = ArrayListMultimap.create();
   }
 
   @Override
@@ -275,7 +274,7 @@
       throws IOException, ConfigInvalidException {
     commentNoteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
         ChangeNoteUtil.changeRefName(changeId), walk, changeId,
-        commentsForBase, commentsForPs, PatchLineComment.Status.PUBLISHED);
+        comments, PatchLineComment.Status.PUBLISHED);
   }
 
   private void parseApproval(PatchSet.Id psId, Account.Id accountId,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
index 76dfdc8..d715947 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ArrayListMultimap;
@@ -33,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
@@ -306,7 +306,7 @@
 
     PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
         PatchListCache cache) {
-      super(getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
+      super(PatchLineCommentsUtil.getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
       this.c = c;
       this.change = change;
       this.ps = ps;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 7302425..cb35619 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -20,7 +20,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.addCommentToMap;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
@@ -28,14 +28,13 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
@@ -57,6 +56,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
@@ -91,8 +91,7 @@
   private String subject;
   private List<SubmitRecord> submitRecords;
   private final CommentsInNotesUtil commentsUtil;
-  private List<PatchLineComment> commentsForBase;
-  private List<PatchLineComment> commentsForPs;
+  private List<PatchLineComment> comments;
   private Set<String> hashtags;
   private String changeMessage;
   private ChangeNotes notes;
@@ -161,12 +160,11 @@
     this.commentsUtil = commentsUtil;
     this.approvals = Maps.newTreeMap(labelNameComparator);
     this.reviewers = Maps.newLinkedHashMap();
-    this.commentsForPs = Lists.newArrayList();
-    this.commentsForBase = Lists.newArrayList();
+    this.comments = Lists.newArrayList();
   }
 
   public void setStatus(Change.Status status) {
-    checkArgument(status != Change.Status.SUBMITTED,
+    checkArgument(status != Change.Status.MERGED,
         "use submit(Iterable<PatchSetApproval>)");
     this.status = status;
   }
@@ -179,8 +177,8 @@
     approvals.put(label, Optional.<Short> absent());
   }
 
-  public void submit(Iterable<SubmitRecord> submitRecords) {
-    status = Change.Status.SUBMITTED;
+  public void merge(Iterable<SubmitRecord> submitRecords) {
+    this.status = Change.Status.MERGED;
     this.submitRecords = ImmutableList.copyOf(submitRecords);
     checkArgument(!this.submitRecords.isEmpty(),
         "no submit records specified at submit time");
@@ -238,15 +236,11 @@
           "A comment already exists with the same key as the following comment,"
           + " so we cannot insert this comment: %s", c);
     }
-    if (c.getSide() == 0) {
-      commentsForBase.add(c);
-    } else {
-      commentsForPs.add(c);
-    }
+    comments.add(c);
   }
 
   private void insertDraftComment(PatchLineComment c) throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.insertComment(c);
   }
 
@@ -263,15 +257,11 @@
       checkArgument(!notes.containsCommentPublished(c),
           "Cannot update a comment that has already been published and saved");
     }
-    if (c.getSide() == 0) {
-      commentsForBase.add(c);
-    } else {
-      commentsForPs.add(c);
-    }
+    comments.add(c);
   }
 
   private void upsertDraftComment(PatchLineComment c) {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.upsertComment(c);
   }
 
@@ -286,46 +276,32 @@
       checkArgument(!notes.containsCommentPublished(c),
           "Cannot update a comment that has already been published and saved");
     }
-    if (c.getSide() == 0) {
-      commentsForBase.add(c);
-    } else {
-      commentsForPs.add(c);
-    }
+    comments.add(c);
   }
 
   private void updateDraftComment(PatchLineComment c) throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.updateComment(c);
   }
 
   private void deleteDraftComment(PatchLineComment c) throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.deleteComment(c);
   }
 
   private void deleteDraftCommentIfPresent(PatchLineComment c)
       throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.deleteCommentIfPresent(c);
   }
 
-  private void createDraftUpdateIfNull(PatchLineComment c) {
+  private void createDraftUpdateIfNull() {
     if (draftUpdate == null) {
       draftUpdate = draftUpdateFactory.create(ctl, when);
-      if (psId != null) {
-        draftUpdate.setPatchSetId(psId);
-      } else {
-        draftUpdate.setPatchSetId(getCommentPsId(c));
-      }
     }
   }
 
   private void verifyComment(PatchLineComment c) {
-    checkArgument(psId != null,
-        "setPatchSetId must be called first");
-    checkArgument(getCommentPsId(c).equals(psId),
-        "Comment on %s doesn't match previous patch set %s",
-        getCommentPsId(c), psId);
     checkArgument(c.getRevId() != null);
     checkArgument(c.getStatus() == Status.PUBLISHED,
         "Cannot add a draft comment to a ChangeUpdate. Use a ChangeDraftUpdate"
@@ -356,31 +332,23 @@
     if (noteMap == null) {
       noteMap = NoteMap.newEmptyMap();
     }
-    if (commentsForPs.isEmpty() && commentsForBase.isEmpty()) {
+    if (comments.isEmpty()) {
       return null;
     }
 
-    Multimap<PatchSet.Id, PatchLineComment> allCommentsOnBases =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> allCommentsOnPs =
-        notes.getPatchSetComments();
-
-    // This writes all comments for the base of this PS to the note map.
-    if (!commentsForBase.isEmpty()) {
-      List<PatchLineComment> baseCommentsForThisPs =
-          new ArrayList<>(allCommentsOnBases.get(psId));
-      baseCommentsForThisPs.addAll(commentsForBase);
-      commentsUtil.writeCommentsToNoteMap(noteMap, baseCommentsForThisPs,
-          inserter);
+    Map<RevId, List<PatchLineComment>> allComments = Maps.newHashMap();
+    for (Map.Entry<RevId, Collection<PatchLineComment>> e
+        : notes.getComments().asMap().entrySet()) {
+      List<PatchLineComment> comments = new ArrayList<>();
+      for (PatchLineComment c : e.getValue()) {
+        comments.add(c);
+      }
+      allComments.put(e.getKey(), comments);
     }
-
-    // This write all comments for this PS to the note map.
-    if (!commentsForPs.isEmpty()) {
-      List<PatchLineComment> commentsForThisPs =
-          new ArrayList<>(allCommentsOnPs.get(psId));
-      commentsForThisPs.addAll(commentsForPs);
-      commentsUtil.writeCommentsToNoteMap(noteMap, commentsForThisPs, inserter);
+    for (PatchLineComment c : comments) {
+      addCommentToMap(allComments, c);
     }
+    commentsUtil.writeCommentsToNoteMap(noteMap, allComments, inserter);
     return noteMap.writeTree(inserter);
   }
 
@@ -504,8 +472,7 @@
   private boolean isEmpty() {
     return approvals.isEmpty()
         && changeMessage == null
-        && commentsForBase.isEmpty()
-        && commentsForPs.isEmpty()
+        && comments.isEmpty()
         && reviewers.isEmpty()
         && status == null
         && subject == null
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
index f3e03c0..e65f8e82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
 import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -33,6 +34,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
@@ -63,9 +65,12 @@
 import java.nio.charset.Charset;
 import java.sql.Timestamp;
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 
 /**
  * Utility functions to parse PatchLineComments out of a note byte array and
@@ -86,11 +91,10 @@
 
   public static NoteMap parseCommentsFromNotes(Repository repo, String refName,
       RevWalk walk, Change.Id changeId,
-      Multimap<PatchSet.Id, PatchLineComment> commentsForBase,
-      Multimap<PatchSet.Id, PatchLineComment> commentsForPs,
+      Multimap<RevId, PatchLineComment> comments,
       Status status)
       throws IOException, ConfigInvalidException {
-    Ref ref = repo.getRef(refName);
+    Ref ref = repo.getRefDatabase().exactRef(refName);
     if (ref == null) {
       return null;
     }
@@ -99,20 +103,14 @@
     RevCommit commit = walk.parseCommit(ref.getObjectId());
     NoteMap noteMap = NoteMap.read(reader, commit);
 
-    for (Note note: noteMap) {
+    for (Note note : noteMap) {
       byte[] bytes =
           reader.open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
       List<PatchLineComment> result = parseNote(bytes, changeId, status);
       if (result == null || result.isEmpty()) {
         continue;
       }
-      PatchSet.Id psId = result.get(0).getKey().getParentKey().getParentKey();
-      short side = result.get(0).getSide();
-      if (side == 0) {
-        commentsForBase.putAll(psId, result);
-      } else {
-        commentsForPs.putAll(psId, result);
-      }
+      comments.putAll(new RevId(note.name()), result);
     }
     return noteMap;
   }
@@ -152,10 +150,6 @@
     return dateFormatter.formatDate(newIdent);
   }
 
-  public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
-    return plc.getKey().getParentKey().getParentKey();
-  }
-
   private static PatchLineComment parseComment(byte[] note, MutableInteger curr,
       String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
       Charset enc, Status status)
@@ -321,8 +315,8 @@
     String dateString =
         RawParseUtils.decode(enc, note, curr.value, endOfLine - 1);
     try {
-      commentTime =
-          new Timestamp(GitDateParser.parse(dateString, null).getTime());
+      commentTime = new Timestamp(
+          GitDateParser.parse(dateString, null, Locale.US).getTime());
     } catch (ParseException e) {
       throw new ConfigInvalidException("could not parse comment timestamp", e);
     }
@@ -445,98 +439,126 @@
   public byte[] buildNote(List<PatchLineComment> comments) {
     ByteArrayOutputStream buf = new ByteArrayOutputStream();
     OutputStreamWriter streamWriter = new OutputStreamWriter(buf, UTF_8);
-    PrintWriter writer = new PrintWriter(streamWriter);
-    PatchLineComment first = comments.get(0);
+    try (PrintWriter writer = new PrintWriter(streamWriter)) {
+      PatchLineComment first = comments.get(0);
 
-    short side = first.getSide();
-    PatchSet.Id psId = getCommentPsId(first);
-    appendHeaderField(writer, side == 0
-        ? BASE_PATCH_SET
-        : PATCH_SET,
-        Integer.toString(psId.get()));
-    appendHeaderField(writer, REVISION, first.getRevId().get());
+      short side = first.getSide();
+      PatchSet.Id psId = PatchLineCommentsUtil.getCommentPsId(first);
+      appendHeaderField(writer, side == 0
+          ? BASE_PATCH_SET
+          : PATCH_SET,
+          Integer.toString(psId.get()));
+      appendHeaderField(writer, REVISION, first.getRevId().get());
 
-    String currentFilename = null;
+      String currentFilename = null;
 
-    for (PatchLineComment c : comments) {
-      PatchSet.Id currentPsId = getCommentPsId(c);
-      checkArgument(psId.equals(currentPsId),
-          "All comments being added must all have the same PatchSet.Id. The"
-          + "comment below does not have the same PatchSet.Id as the others "
-          + "(%s).\n%s", psId.toString(), c.toString());
-      checkArgument(side == c.getSide(),
-          "All comments being added must all have the same side. The"
-          + "comment below does not have the same side as the others "
-          + "(%s).\n%s", side, c.toString());
-      String commentFilename =
-          QuotedString.GIT_PATH.quote(c.getKey().getParentKey().getFileName());
+      for (PatchLineComment c : comments) {
+        PatchSet.Id currentPsId = PatchLineCommentsUtil.getCommentPsId(c);
+        checkArgument(psId.equals(currentPsId),
+            "All comments being added must all have the same PatchSet.Id. The"
+            + "comment below does not have the same PatchSet.Id as the others "
+            + "(%s).\n%s", psId.toString(), c.toString());
+        checkArgument(side == c.getSide(),
+            "All comments being added must all have the same side. The"
+            + "comment below does not have the same side as the others "
+            + "(%s).\n%s", side, c.toString());
+        String commentFilename =
+            QuotedString.GIT_PATH.quote(c.getKey().getParentKey().getFileName());
 
-      if (!commentFilename.equals(currentFilename)) {
-        currentFilename = commentFilename;
-        writer.print("File: ");
-        writer.print(commentFilename);
+        if (!commentFilename.equals(currentFilename)) {
+          currentFilename = commentFilename;
+          writer.print("File: ");
+          writer.print(commentFilename);
+          writer.print("\n\n");
+        }
+
+        // The CommentRange field for a comment is allowed to be null.
+        // If it is indeed null, then in the first line, we simply use the line
+        // number field for a comment instead. If it isn't null, we write the
+        // comment range itself.
+        CommentRange range = c.getRange();
+        if (range != null) {
+          writer.print(range.getStartLine());
+          writer.print(':');
+          writer.print(range.getStartCharacter());
+          writer.print('-');
+          writer.print(range.getEndLine());
+          writer.print(':');
+          writer.print(range.getEndCharacter());
+        } else {
+          writer.print(c.getLine());
+        }
+        writer.print("\n");
+
+        writer.print(formatTime(serverIdent, c.getWrittenOn()));
+        writer.print("\n");
+
+        PersonIdent ident =
+            newIdent(accountCache.get(c.getAuthor()).getAccount(),
+                c.getWrittenOn());
+        String nameString = ident.getName() + " <" + ident.getEmailAddress()
+            + ">";
+        appendHeaderField(writer, AUTHOR, nameString);
+
+        String parent = c.getParentUuid();
+        if (parent != null) {
+          appendHeaderField(writer, PARENT, parent);
+        }
+
+        appendHeaderField(writer, UUID, c.getKey().get());
+
+        byte[] messageBytes = c.getMessage().getBytes(UTF_8);
+        appendHeaderField(writer, LENGTH,
+            Integer.toString(messageBytes.length));
+
+        writer.print(c.getMessage());
         writer.print("\n\n");
       }
-
-      // The CommentRange field for a comment is allowed to be null.
-      // If it is indeed null, then in the first line, we simply use the line
-      // number field for a comment instead. If it isn't null, we write the
-      // comment range itself.
-      CommentRange range = c.getRange();
-      if (range != null) {
-        writer.print(range.getStartLine());
-        writer.print(':');
-        writer.print(range.getStartCharacter());
-        writer.print('-');
-        writer.print(range.getEndLine());
-        writer.print(':');
-        writer.print(range.getEndCharacter());
-      } else {
-        writer.print(c.getLine());
-      }
-      writer.print("\n");
-
-      writer.print(formatTime(serverIdent, c.getWrittenOn()));
-      writer.print("\n");
-
-      PersonIdent ident =
-          newIdent(accountCache.get(c.getAuthor()).getAccount(),
-              c.getWrittenOn());
-      String nameString = ident.getName() + " <" + ident.getEmailAddress()
-          + ">";
-      appendHeaderField(writer, AUTHOR, nameString);
-
-      String parent = c.getParentUuid();
-      if (parent != null) {
-        appendHeaderField(writer, PARENT, parent);
-      }
-
-      appendHeaderField(writer, UUID, c.getKey().get());
-
-      byte[] messageBytes = c.getMessage().getBytes(UTF_8);
-      appendHeaderField(writer, LENGTH,
-          Integer.toString(messageBytes.length));
-
-      writer.print(c.getMessage());
-      writer.print("\n\n");
     }
-    writer.close();
     return buf.toByteArray();
   }
 
+  /**
+   * Write comments for multiple revisions to a note map.
+   * <p>
+   * Mutates the map in-place. only notes for SHA-1s found as keys in the map
+   * are modified; all other notes are left untouched.
+   *
+   * @param noteMap note map to modify.
+   * @param allComments map of revision to all comments for that revision;
+   *     callers are responsible for reading the original comments and applying
+   *     any changes. Differs from a multimap in that present-but-empty values
+   *     are significant, and indicate the note for that SHA-1 should be
+   *     deleted.
+   * @param inserter object inserter for writing notes.
+   * @throws IOException if an error occurred.
+   */
   public void writeCommentsToNoteMap(NoteMap noteMap,
-      List<PatchLineComment> allComments, ObjectInserter inserter)
-        throws IOException {
-    checkArgument(!allComments.isEmpty(),
-        "No comments to write; to delete, use removeNoteFromNoteMap().");
-    ObjectId commit =
-        ObjectId.fromString(allComments.get(0).getRevId().get());
-    Collections.sort(allComments, ChangeNotes.PLC_ORDER);
-    noteMap.set(commit, inserter.insert(OBJ_BLOB, buildNote(allComments)));
+      Map<RevId, List<PatchLineComment>> allComments, ObjectInserter inserter)
+      throws IOException {
+    for (Map.Entry<RevId, List<PatchLineComment>> e : allComments.entrySet()) {
+      List<PatchLineComment> comments = e.getValue();
+      ObjectId commit = ObjectId.fromString(e.getKey().get());
+      if (comments.isEmpty()) {
+        noteMap.remove(commit);
+        continue;
+      }
+      Collections.sort(comments, PLC_ORDER);
+      // We allow comments for multiple commits to be written in the same
+      // update, even though the rest of the metadata update is associated with
+      // a single patch set.
+      noteMap.set(commit, inserter.insert(OBJ_BLOB, buildNote(comments)));
+    }
   }
 
-  public void removeNote(NoteMap noteMap, RevId commitId)
-      throws IOException {
-    noteMap.remove(ObjectId.fromString(commitId.get()));
+  static void addCommentToMap(Map<RevId, List<PatchLineComment>> map,
+      PatchLineComment c) {
+    List<PatchLineComment> list = map.get(c.getRevId());
+    if (list == null) {
+      list = new ArrayList<>();
+      map.put(c.getRevId(), list);
+    }
+    list.add(c);
   }
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 20d6c4a..a02c24d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -14,18 +14,14 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
-
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Table;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -70,8 +66,7 @@
   private final AllUsersName draftsProject;
   private final Account.Id author;
 
-  private final Table<PatchSet.Id, String, PatchLineComment> draftBaseComments;
-  private final Table<PatchSet.Id, String, PatchLineComment> draftPsComments;
+  private ImmutableListMultimap<RevId, PatchLineComment> comments;
   private NoteMap noteMap;
 
   DraftCommentNotes(GitRepositoryManager repoManager, NotesMigration migration,
@@ -79,9 +74,6 @@
     super(repoManager, migration, changeId);
     this.draftsProject = draftsProject;
     this.author = author;
-
-    this.draftBaseComments = HashBasedTable.create();
-    this.draftPsComments = HashBasedTable.create();
   }
 
   public NoteMap getNoteMap() {
@@ -92,32 +84,18 @@
     return author;
   }
 
-  /**
-   * @return a defensive copy of the table containing all draft comments
-   *    on this change with side == 0. The row key is the comment's PatchSet.Id,
-   *    the column key is the comment's UUID, and the value is the comment.
-   */
-  public Table<PatchSet.Id, String, PatchLineComment>
-      getDraftBaseComments() {
-    return HashBasedTable.create(draftBaseComments);
-  }
-
-  /**
-   * @return a defensive copy of the table containing all draft comments
-   *    on this change with side == 1. The row key is the comment's PatchSet.Id,
-   *    the column key is the comment's UUID, and the value is the comment.
-   */
-  public Table<PatchSet.Id, String, PatchLineComment>
-      getDraftPsComments() {
-    return HashBasedTable.create(draftPsComments);
+  public ImmutableListMultimap<RevId, PatchLineComment> getComments() {
+    // TODO(dborowitz): Defensive copy?
+    return comments;
   }
 
   public boolean containsComment(PatchLineComment c) {
-    Table<PatchSet.Id, String, PatchLineComment> t =
-        c.getSide() == (short) 0
-        ? getDraftBaseComments()
-        : getDraftPsComments();
-    return t.contains(getCommentPsId(c), c.getKey().get());
+    for (PatchLineComment existing : comments.values()) {
+      if (c.getKey().equals(existing.getKey())) {
+        return true;
+      }
+    }
+    return false;
   }
 
   @Override
@@ -129,6 +107,7 @@
   protected void onLoad() throws IOException, ConfigInvalidException {
     ObjectId rev = getRevision();
     if (rev == null) {
+      loadDefaults();
       return;
     }
 
@@ -137,8 +116,7 @@
           getChangeId(), walk, rev, repoManager, draftsProject, author)) {
       parser.parseDraftComments();
 
-      buildCommentTable(draftBaseComments, parser.draftBaseComments);
-      buildCommentTable(draftPsComments, parser.draftPsComments);
+      comments = ImmutableListMultimap.copyOf(parser.comments);
       noteMap = parser.noteMap;
     }
   }
@@ -152,20 +130,11 @@
 
   @Override
   protected void loadDefaults() {
-    // Do nothing; tables are final and initialized in constructor.
+    comments = ImmutableListMultimap.of();
   }
 
   @Override
   protected Project.NameKey getProjectName() {
     return draftsProject;
   }
-
-  private void buildCommentTable(
-      Table<PatchSet.Id, String, PatchLineComment> commentTable,
-      Multimap<PatchSet.Id, PatchLineComment> allComments) {
-    for (PatchLineComment c : allComments.values()) {
-      commentTable.put(getCommentPsId(c), c.getKey().get(), c);
-    }
-  }
-
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
index 4b3fbdf..ef8683f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 
@@ -34,8 +34,7 @@
 import java.io.IOException;
 
 class DraftCommentNotesParser implements AutoCloseable {
-  final Multimap<PatchSet.Id, PatchLineComment> draftBaseComments;
-  final Multimap<PatchSet.Id, PatchLineComment> draftPsComments;
+  final Multimap<RevId, PatchLineComment> comments;
   NoteMap noteMap;
 
   private final Change.Id changeId;
@@ -53,8 +52,7 @@
     this.repo = repoManager.openMetadataRepository(draftsProject);
     this.author = author;
 
-    draftBaseComments = ArrayListMultimap.create();
-    draftPsComments = ArrayListMultimap.create();
+    comments = ArrayListMultimap.create();
   }
 
   @Override
@@ -66,7 +64,6 @@
     walk.markStart(walk.parseCommit(tip));
     noteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
         RefNames.refsDraftComments(author, changeId),
-        walk, changeId, draftBaseComments,
-        draftPsComments, PatchLineComment.Status.DRAFT);
+        walk, changeId, comments, PatchLineComment.Status.DRAFT);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 4193fe4..75d926b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
 public class NoteDbModule extends FactoryModule {
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index e6d9ff8..d63f972 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -84,11 +84,15 @@
 
   public static Config allEnabledConfig() {
     Config cfg = new Config();
+    setAllEnabledConfig(cfg);
+    return cfg;
+  }
+
+  public static void setAllEnabledConfig(Config cfg) {
     for (Table t : Table.values()) {
       cfg.setBoolean(NOTEDB, t.key(), WRITE, true);
       cfg.setBoolean(NOTEDB, t.key(), READ, true);
     }
-    return cfg;
   }
 
   private final boolean writeChanges;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
new file mode 100644
index 0000000..4a61e2d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 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.server.patch;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.List;
+
+@AutoValue
+public abstract class IntraLineDiffArgs {
+  public static IntraLineDiffArgs create(Text aText, Text bText, List<Edit> edits,
+      Project.NameKey project, ObjectId commit, String path) {
+    return new AutoValue_IntraLineDiffArgs(aText, bText, edits,
+        project, commit, path);
+  }
+
+  public abstract Text aText();
+  public abstract Text bText();
+  public abstract List<Edit> edits();
+  public abstract Project.NameKey project();
+  public abstract ObjectId commit();
+  public abstract String path();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index 5b37b92..038ad51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -17,16 +17,12 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.gerrit.reviewdb.client.Project;
-
-import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
-import java.util.List;
 
 public class IntraLineDiffKey implements Serializable {
   static final long serialVersionUID = 4L;
@@ -35,45 +31,13 @@
   private transient ObjectId aId;
   private transient ObjectId bId;
 
-  // Transient data passed through on cache misses to the loader.
-
-  private transient Text aText;
-  private transient Text bText;
-  private transient List<Edit> edits;
-
-  private transient Project.NameKey projectKey;
-  private transient ObjectId commit;
-  private transient String path;
-
-  public IntraLineDiffKey(ObjectId aId, Text aText, ObjectId bId, Text bText,
-      List<Edit> edits, Project.NameKey projectKey, ObjectId commit, String path,
+  public IntraLineDiffKey(ObjectId aId, ObjectId bId,
       boolean ignoreWhitespace) {
     this.aId = aId;
     this.bId = bId;
-
-    this.aText = aText;
-    this.bText = bText;
-    this.edits = edits;
-
-    this.projectKey = projectKey;
-    this.commit = commit;
-    this.path = path;
-
     this.ignoreWhitespace = ignoreWhitespace;
   }
 
-  Text getTextA() {
-    return aText;
-  }
-
-  Text getTextB() {
-    return bText;
-  }
-
-  List<Edit> getEdits() {
-    return edits;
-  }
-
   public ObjectId getBlobA() {
     return aId;
   }
@@ -86,18 +50,6 @@
     return ignoreWhitespace;
   }
 
-  Project.NameKey getProject() {
-    return projectKey;
-  }
-
-  ObjectId getCommit() {
-    return commit;
-  }
-
-  String getPath() {
-    return path;
-  }
-
   @Override
   public int hashCode() {
     int h = 0;
@@ -124,9 +76,6 @@
   public String toString() {
     StringBuilder n = new StringBuilder();
     n.append("IntraLineDiffKey[");
-    if (projectKey != null) {
-      n.append(projectKey.get()).append(" ");
-    }
     n.append(aId.name());
     n.append("..");
     n.append(bId.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index ffcce14..b6b7fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -16,10 +16,10 @@
 package com.google.gerrit.server.patch;
 
 import com.google.common.base.Throwables;
-import com.google.common.cache.CacheLoader;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.MyersDiff;
@@ -28,7 +28,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -38,9 +37,13 @@
 import java.util.concurrent.TimeoutException;
 import java.util.regex.Pattern;
 
-class IntraLineLoader extends CacheLoader<IntraLineDiffKey, IntraLineDiff> {
+class IntraLineLoader implements Callable<IntraLineDiff> {
   static final Logger log = LoggerFactory.getLogger(IntraLineLoader.class);
 
+  static interface Factory {
+    IntraLineLoader create(IntraLineDiffKey key, IntraLineDiffArgs args);
+  }
+
   private static final Pattern BLANK_LINE_RE = Pattern
       .compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
 
@@ -49,32 +52,40 @@
 
   private final ExecutorService diffExecutor;
   private final long timeoutMillis;
+  private final IntraLineDiffKey key;
+  private final IntraLineDiffArgs args;
 
-  @Inject
+  @AssistedInject
   IntraLineLoader(@DiffExecutor ExecutorService diffExecutor,
-      @GerritServerConfig Config cfg) {
+      @GerritServerConfig Config cfg,
+      @Assisted IntraLineDiffKey key,
+      @Assisted IntraLineDiffArgs args) {
     this.diffExecutor = diffExecutor;
     timeoutMillis =
         ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.INTRA_NAME,
             "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
             TimeUnit.MILLISECONDS);
+    this.key = key;
+    this.args = args;
   }
 
   @Override
-  public IntraLineDiff load(final IntraLineDiffKey key) throws Exception {
-    Future<IntraLineDiff> result = diffExecutor.submit(new Callable<IntraLineDiff>() {
-      @Override
-      public IntraLineDiff call() throws Exception {
-        return IntraLineLoader.compute(key);
-      }
-    });
+  public IntraLineDiff call() throws Exception {
+    Future<IntraLineDiff> result = diffExecutor.submit(
+        new Callable<IntraLineDiff>() {
+          @Override
+          public IntraLineDiff call() throws Exception {
+            return IntraLineLoader.compute(args.aText(), args.bText(),
+                args.edits());
+          }
+        });
     try {
       return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | TimeoutException e) {
       log.warn(timeoutMillis + " ms timeout reached for IntraLineDiff"
-          + " in project " + key.getProject().get()
-          + " on commit " + key.getCommit().name()
-          + " for path " + key.getPath()
+          + " in project " + args.project()
+          + " on commit " + args.commit().name()
+          + " for path " + args.path()
           + " comparing " + key.getBlobA().name()
           + ".." + key.getBlobB().name());
       result.cancel(true);
@@ -87,18 +98,16 @@
     }
   }
 
-  static IntraLineDiff compute(IntraLineDiffKey key) throws Exception {
-    List<Edit> edits = new ArrayList<>(key.getEdits());
-    Text aContent = key.getTextA();
-    Text bContent = key.getTextB();
-    combineLineEdits(edits, aContent, bContent);
+  static IntraLineDiff compute(Text aText, Text bText, List<Edit> edits)
+      throws Exception {
+    combineLineEdits(edits, aText, bText);
 
     for (int i = 0; i < edits.size(); i++) {
       Edit e = edits.get(i);
 
       if (e.getType() == Edit.Type.REPLACE) {
-        CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
-        CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
+        CharText a = new CharText(aText, e.getBeginA(), e.getEndA());
+        CharText b = new CharText(bText, e.getBeginB(), e.getEndB());
         CharTextComparator cmp = new CharTextComparator();
 
         List<Edit> wordEdits = MyersDiff.INSTANCE.diff(cmp, a, b);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 8740a6b..2704be8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -26,6 +26,7 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
@@ -56,7 +57,11 @@
         if (patchList.isAgainstParent()) {
           a = Text.EMPTY;
         } else {
-          a = Text.forCommit(reader, patchList.getOldId());
+          // For the initial commit, we have an empty tree on Side A
+          RevObject object = rw.parseAny(patchList.getOldId());
+          a = object instanceof RevCommit
+              ? Text.forCommit(reader, object)
+              : Text.EMPTY;
         }
         b = Text.forCommit(reader, bCommit);
 
@@ -145,9 +150,13 @@
     if (tw == null) {
       return Text.EMPTY;
     }
-    if (tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
+    if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
+      return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
+    } else if (tw.getFileMode(0).getObjectType() == Constants.OBJ_COMMIT) {
+      String str = "Subproject commit " + ObjectId.toString(tw.getObjectId(0));
+      return new Text(str.getBytes());
+    } else {
       return Text.EMPTY;
     }
-    return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 4fff619..5f6113f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -163,8 +163,7 @@
 
   private void writeObject(final ObjectOutputStream output) throws IOException {
     final ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    final DeflaterOutputStream out = new DeflaterOutputStream(buf);
-    try {
+    try (DeflaterOutputStream out = new DeflaterOutputStream(buf)) {
       writeCanBeNull(out, oldId);
       writeNotNull(out, newId);
       writeVarInt32(out, againstParent ? 1 : 0);
@@ -174,16 +173,13 @@
       for (PatchListEntry p : patches) {
         p.writeTo(out);
       }
-    } finally {
-      out.close();
     }
     writeBytes(output, buf.toByteArray());
   }
 
   private void readObject(final ObjectInputStream input) throws IOException {
     final ByteArrayInputStream buf = new ByteArrayInputStream(readBytes(input));
-    final InflaterInputStream in = new InflaterInputStream(buf);
-    try {
+    try (InflaterInputStream in = new InflaterInputStream(buf)) {
       oldId = readCanBeNull(in);
       newId = readNotNull(in);
       againstParent = readVarInt32(in) != 0;
@@ -195,8 +191,6 @@
         all[i] = PatchListEntry.readFrom(in);
       }
       patches = all;
-    } finally {
-      in.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index fe77f5d..a6332f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -16,13 +16,16 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
-  public PatchList get(PatchListKey key) throws PatchListNotAvailableException;
+  public PatchList get(PatchListKey key, Project.NameKey project)
+      throws PatchListNotAvailableException;
 
   public PatchList get(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException;
 
-  public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key);
+  public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
+      IntraLineDiffArgs args);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 52856c6..da1e7b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,8 +15,8 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -43,14 +43,14 @@
     return new CacheModule() {
       @Override
       protected void configure() {
+        factory(PatchListLoader.Factory.class);
         persist(FILE_NAME, PatchListKey.class, PatchList.class)
             .maximumWeight(10 << 20)
-            .loader(PatchListLoader.class)
             .weigher(PatchListWeigher.class);
 
+        factory(IntraLineLoader.Factory.class);
         persist(INTRA_NAME, IntraLineDiffKey.class, IntraLineDiff.class)
             .maximumWeight(10 << 20)
-            .loader(IntraLineLoader.class)
             .weigher(IntraLineWeigher.class);
 
         bind(PatchListCacheImpl.class);
@@ -59,17 +59,23 @@
     };
   }
 
-  private final LoadingCache<PatchListKey, PatchList> fileCache;
-  private final LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache;
+  private final Cache<PatchListKey, PatchList> fileCache;
+  private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
+  private final PatchListLoader.Factory fileLoaderFactory;
+  private final IntraLineLoader.Factory intraLoaderFactory;
   private final boolean computeIntraline;
 
   @Inject
   PatchListCacheImpl(
-      @Named(FILE_NAME) LoadingCache<PatchListKey, PatchList> fileCache,
-      @Named(INTRA_NAME) LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache,
+      @Named(FILE_NAME) Cache<PatchListKey, PatchList> fileCache,
+      @Named(INTRA_NAME) Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
+      PatchListLoader.Factory fileLoaderFactory,
+      IntraLineLoader.Factory intraLoaderFactory,
       @GerritServerConfig Config cfg) {
     this.fileCache = fileCache;
     this.intraCache = intraCache;
+    this.fileLoaderFactory = fileLoaderFactory;
+    this.intraLoaderFactory = intraLoaderFactory;
 
     this.computeIntraline =
         cfg.getBoolean("cache", INTRA_NAME, "enabled",
@@ -77,9 +83,10 @@
   }
 
   @Override
-  public PatchList get(PatchListKey key) throws PatchListNotAvailableException {
+  public PatchList get(PatchListKey key, Project.NameKey project)
+      throws PatchListNotAvailableException {
     try {
-      return fileCache.get(key);
+      return fileCache.get(key, fileLoaderFactory.create(key, project));
     } catch (ExecutionException | LargeObjectException e) {
       PatchListLoader.log.warn("Error computing " + key, e);
       throw new PatchListNotAvailableException(e.getCause());
@@ -87,24 +94,25 @@
   }
 
   @Override
-  public PatchList get(final Change change, final PatchSet patchSet)
+  public PatchList get(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException {
-    final Project.NameKey projectKey = change.getProject();
-    final ObjectId a = null;
+    Project.NameKey project = change.getProject();
+    ObjectId a = null;
     if (patchSet.getRevision() == null) {
       throw new PatchListNotAvailableException(
           "revision is null for " + patchSet.getId());
     }
-    final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
-    final Whitespace ws = Whitespace.IGNORE_NONE;
-    return get(new PatchListKey(projectKey, a, b, ws));
+    ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
+    Whitespace ws = Whitespace.IGNORE_NONE;
+    return get(new PatchListKey(a, b, ws), project);
   }
 
   @Override
-  public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key) {
+  public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
+      IntraLineDiffArgs args) {
     if (computeIntraline) {
       try {
-        return intraCache.get(key);
+        return intraCache.get(key, intraLoaderFactory.create(key, args));
       } catch (ExecutionException | LargeObjectException e) {
         IntraLineLoader.log.warn("Error computing " + key, e);
         return new IntraLineDiff(IntraLineDiff.Status.ERROR);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index e7c56be..319483a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -16,10 +16,12 @@
 
 import static com.google.gerrit.server.ioutil.BasicSerialization.readBytes;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readEnum;
+import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
@@ -30,7 +32,6 @@
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.patch.CombinedFileHeader;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.util.IntList;
@@ -49,7 +50,7 @@
 
   static PatchListEntry empty(final String fileName) {
     return new PatchListEntry(ChangeType.MODIFIED, PatchType.UNIFIED, null,
-        fileName, EMPTY_HEADER, Collections.<Edit> emptyList(), 0, 0);
+        fileName, EMPTY_HEADER, Collections.<Edit> emptyList(), 0, 0, 0);
   }
 
   private final ChangeType changeType;
@@ -60,8 +61,11 @@
   private final List<Edit> edits;
   private final int insertions;
   private final int deletions;
+  private final long sizeDelta;
+  // Note: When adding new fields, the serialVersionUID in PatchListKey must be
+  // incremented so that entries from the cache are automatically invalidated.
 
-  PatchListEntry(final FileHeader hdr, List<Edit> editList) {
+  PatchListEntry(FileHeader hdr, List<Edit> editList, long sizeDelta) {
     changeType = toChangeType(hdr);
     patchType = toPatchType(hdr);
 
@@ -89,10 +93,7 @@
 
     header = compact(hdr);
 
-    if (hdr instanceof CombinedFileHeader
-        || hdr.getHunks().isEmpty() //
-        || hdr.getOldMode() == FileMode.GITLINK
-        || hdr.getNewMode() == FileMode.GITLINK) {
+    if (hdr instanceof CombinedFileHeader || hdr.getHunks().isEmpty()) {
       edits = Collections.emptyList();
     } else {
       edits = Collections.unmodifiableList(editList);
@@ -106,12 +107,12 @@
     }
     insertions = ins;
     deletions = del;
+    this.sizeDelta = sizeDelta;
   }
 
-  private PatchListEntry(final ChangeType changeType,
-      final PatchType patchType, final String oldName, final String newName,
-      final byte[] header, final List<Edit> edits, final int insertions,
-      final int deletions) {
+  private PatchListEntry(ChangeType changeType, PatchType patchType,
+      String oldName, String newName, byte[] header, List<Edit> edits,
+      int insertions, int deletions, long sizeDelta) {
     this.changeType = changeType;
     this.patchType = patchType;
     this.oldName = oldName;
@@ -120,6 +121,7 @@
     this.edits = edits;
     this.insertions = insertions;
     this.deletions = deletions;
+    this.sizeDelta = sizeDelta;
   }
 
   int weigh() {
@@ -166,6 +168,10 @@
     return deletions;
   }
 
+  public long getSizeDelta() {
+    return sizeDelta;
+  }
+
   public List<String> getHeaderLines() {
     final IntList m = RawParseUtils.lineMap(header, 0, header.length);
     final List<String> headerLines = new ArrayList<>(m.size() - 1);
@@ -190,7 +196,7 @@
     return p;
   }
 
-  void writeTo(final OutputStream out) throws IOException {
+  void writeTo(OutputStream out) throws IOException {
     writeEnum(out, changeType);
     writeEnum(out, patchType);
     writeString(out, oldName);
@@ -198,6 +204,7 @@
     writeBytes(out, header);
     writeVarInt32(out, insertions);
     writeVarInt32(out, deletions);
+    writeFixInt64(out, sizeDelta);
 
     writeVarInt32(out, edits.size());
     for (final Edit e : edits) {
@@ -208,17 +215,18 @@
     }
   }
 
-  static PatchListEntry readFrom(final InputStream in) throws IOException {
-    final ChangeType changeType = readEnum(in, ChangeType.values());
-    final PatchType patchType = readEnum(in, PatchType.values());
-    final String oldName = readString(in);
-    final String newName = readString(in);
-    final byte[] hdr = readBytes(in);
-    final int ins = readVarInt32(in);
-    final int del = readVarInt32(in);
+  static PatchListEntry readFrom(InputStream in) throws IOException {
+    ChangeType changeType = readEnum(in, ChangeType.values());
+    PatchType patchType = readEnum(in, PatchType.values());
+    String oldName = readString(in);
+    String newName = readString(in);
+    byte[] hdr = readBytes(in);
+    int ins = readVarInt32(in);
+    int del = readVarInt32(in);
+    long sizeDelta = readFixInt64(in);
 
-    final int editCount = readVarInt32(in);
-    final Edit[] editArray = new Edit[editCount];
+    int editCount = readVarInt32(in);
+    Edit[] editArray = new Edit[editCount];
     for (int i = 0; i < editCount; i++) {
       int beginA = readVarInt32(in);
       int endA = readVarInt32(in);
@@ -228,7 +236,7 @@
     }
 
     return new PatchListEntry(changeType, patchType, oldName, newName, hdr,
-        toList(editArray), ins, del);
+        toList(editArray), ins, del, sizeDelta);
   }
 
   private static List<Edit> toList(Edit[] l) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 9867b11..15277b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.server.patch;
 
-import static com.google.gerrit.server.ioutil.BasicSerialization.readEnum;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
+import static com.google.common.base.Preconditions.checkState;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -34,17 +34,23 @@
 import java.io.Serializable;
 
 public class PatchListKey implements Serializable {
-  static final long serialVersionUID = 17L;
+  static final long serialVersionUID = 18L;
+
+  public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
+      Whitespace.IGNORE_NONE, 'N',
+      Whitespace.IGNORE_TRAILING, 'E',
+      Whitespace.IGNORE_LEADING_AND_TRAILING, 'S',
+      Whitespace.IGNORE_ALL, 'A');
+
+  static {
+    checkState(WHITESPACE_TYPES.size() == Whitespace.values().length);
+  }
 
   private transient ObjectId oldId;
   private transient ObjectId newId;
   private transient Whitespace whitespace;
 
-  transient Project.NameKey projectKey; // not required to form the key
-
-  public PatchListKey(final Project.NameKey pk, final AnyObjectId a,
-      final AnyObjectId b, final Whitespace ws) {
-    projectKey = pk;
+  public PatchListKey(AnyObjectId a, AnyObjectId b, Whitespace ws) {
     oldId = a != null ? a.copy() : null;
     newId = b.copy();
     whitespace = ws;
@@ -94,10 +100,6 @@
   public String toString() {
     StringBuilder n = new StringBuilder();
     n.append("PatchListKey[");
-    if (projectKey != null) {
-      n.append(projectKey.get());
-      n.append(" ");
-    }
     n.append(oldId != null ? oldId.name() : "BASE");
     n.append("..");
     n.append(newId.name());
@@ -117,12 +119,20 @@
   private void writeObject(final ObjectOutputStream out) throws IOException {
     writeCanBeNull(out, oldId);
     writeNotNull(out, newId);
-    writeEnum(out, whitespace);
+    Character c = WHITESPACE_TYPES.get(whitespace);
+    if (c == null) {
+      throw new IOException("Invalid whitespace type: " + whitespace);
+    }
+    out.writeChar(c);
   }
 
   private void readObject(final ObjectInputStream in) throws IOException {
     oldId = readCanBeNull(in);
     newId = readNotNull(in);
-    whitespace = readEnum(in, Whitespace.values());
+    char t = in.readChar();
+    whitespace = WHITESPACE_TYPES.inverse().get(t);
+    if (whitespace == null) {
+      throw new IOException("Invalid whitespace type code: " + t);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 838b343..c10241b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,19 +15,22 @@
 
 package com.google.gerrit.server.patch;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
 import com.google.common.base.Function;
 import com.google.common.base.Throwables;
-import com.google.common.cache.CacheLoader;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
@@ -59,6 +62,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.slf4j.Logger;
@@ -79,24 +83,34 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-public class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+public class PatchListLoader implements Callable<PatchList> {
   static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
 
+  public interface Factory {
+    PatchListLoader create(PatchListKey key, Project.NameKey project);
+  }
+
   private final GitRepositoryManager repoManager;
   private final PatchListCache patchListCache;
   private final ThreeWayMergeStrategy mergeStrategy;
   private final ExecutorService diffExecutor;
+  private final PatchListKey key;
+  private final Project.NameKey project;
   private final long timeoutMillis;
 
-  @Inject
+  @AssistedInject
   PatchListLoader(GitRepositoryManager mgr,
       PatchListCache plc,
       @GerritServerConfig Config cfg,
-      @DiffExecutor ExecutorService de) {
+      @DiffExecutor ExecutorService de,
+      @Assisted PatchListKey k,
+      @Assisted Project.NameKey p) {
     repoManager = mgr;
     patchListCache = plc;
     mergeStrategy = MergeUtil.getMergeStrategy(cfg);
     diffExecutor = de;
+    key = k;
+    project = p;
     timeoutMillis =
         ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.FILE_NAME,
             "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
@@ -104,25 +118,22 @@
   }
 
   @Override
-  public PatchList load(final PatchListKey key) throws IOException,
+  public PatchList call() throws IOException,
       PatchListNotAvailableException {
-    final Repository repo = repoManager.openRepository(key.projectKey);
-    try {
+    try (Repository repo = repoManager.openRepository(project)) {
       return readPatchList(key, repo);
-    } finally {
-      repo.close();
     }
   }
 
   private static RawTextComparator comparatorFor(Whitespace ws) {
     switch (ws) {
-      case IGNORE_ALL_SPACE:
+      case IGNORE_ALL:
         return RawTextComparator.WS_IGNORE_ALL;
 
-      case IGNORE_SPACE_AT_EOL:
+      case IGNORE_TRAILING:
         return RawTextComparator.WS_IGNORE_TRAILING;
 
-      case IGNORE_SPACE_CHANGE:
+      case IGNORE_LEADING_AND_TRAILING:
         return RawTextComparator.WS_IGNORE_CHANGE;
 
       case IGNORE_NONE:
@@ -161,35 +172,39 @@
       df.setDetectRenames(true);
       List<DiffEntry> diffEntries = df.scan(aTree, bTree);
 
-      Set<String> paths = key.getOldId() != null
-          ? FluentIterable.from(
-                  Iterables.concat(
-                      patchListCache.get(
-                          new PatchListKey(key.projectKey, null,
-                              key.getNewId(), key.getWhitespace()))
-                          .getPatches(),
-                      patchListCache.get(
-                          new PatchListKey(key.projectKey, null,
-                              key.getOldId(), key.getWhitespace()))
-                          .getPatches()))
-              .transform(new Function<PatchListEntry, String>() {
-                @Override
-                public String apply(PatchListEntry entry) {
-                  return entry.getNewName();
-                }
-              })
-          .toSet()
-          : null;
+      Set<String> paths = null;
+      if (key.getOldId() != null) {
+        PatchListKey newKey =
+            new PatchListKey(null, key.getNewId(), key.getWhitespace());
+        PatchListKey oldKey =
+            new PatchListKey(null, key.getOldId(), key.getWhitespace());
+        paths = FluentIterable
+            .from(patchListCache.get(newKey, project).getPatches())
+            .append(patchListCache.get(oldKey, project).getPatches())
+            .transform(new Function<PatchListEntry, String>() {
+              @Override
+              public String apply(PatchListEntry entry) {
+                return entry.getNewName();
+              }
+            })
+            .toSet();
+      }
+
       int cnt = diffEntries.size();
       List<PatchListEntry> entries = new ArrayList<>();
       entries.add(newCommitMessage(cmp, reader,
           againstParent ? null : aCommit, b));
       for (int i = 0; i < cnt; i++) {
-        DiffEntry diffEntry = diffEntries.get(i);
-        if (paths == null || paths.contains(diffEntry.getNewPath())
-            || paths.contains(diffEntry.getOldPath())) {
-          FileHeader fh = toFileHeader(key, df, diffEntry);
-          entries.add(newEntry(aTree, fh));
+        DiffEntry e = diffEntries.get(i);
+        if (paths == null || paths.contains(e.getNewPath())
+            || paths.contains(e.getOldPath())) {
+
+          FileHeader fh = toFileHeader(key, df, e);
+          long oldSize =
+              getFileSize(repo, reader, e.getOldMode(), e.getOldPath(), aTree);
+          long newSize =
+              getFileSize(repo, reader, e.getNewMode(), e.getNewPath(), bTree);
+          entries.add(newEntry(aTree, fh, newSize - oldSize));
         }
       }
       return new PatchList(a, b, againstParent,
@@ -197,6 +212,23 @@
     }
   }
 
+  private static long getFileSize(Repository repo, ObjectReader reader,
+      FileMode mode, String path, RevTree t) throws IOException {
+    if (!isBlob(mode)) {
+      return 0;
+    }
+    try (TreeWalk tw = TreeWalk.forPath(reader, path, t)) {
+      return tw != null
+          ? repo.open(tw.getObjectId(0), OBJ_BLOB).getSize()
+          : 0;
+    }
+  }
+
+  private static boolean isBlob(FileMode mode) {
+    int t = mode.getBits() & FileMode.TYPE_MASK;
+    return t == FileMode.TYPE_FILE || t == FileMode.TYPE_SYMLINK;
+  }
+
   private FileHeader toFileHeader(PatchListKey key,
       final DiffFormatter diffFormatter, final DiffEntry diffEntry)
       throws IOException {
@@ -214,7 +246,7 @@
       return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | TimeoutException e) {
       log.warn(timeoutMillis + " ms timeout reached for Diff loader"
-                      + " in project " + key.projectKey.get()
+                      + " in project " + project
                       + " on commit " + key.getNewId().name()
                       + " on path " + diffEntry.getNewPath()
                       + " comparing " + diffEntry.getOldId().name()
@@ -264,33 +296,32 @@
         aCommit != null ? Text.forCommit(reader, aCommit) : Text.EMPTY;
     Text bText = Text.forCommit(reader, bCommit);
 
-    byte[] rawHdr = hdr.toString().getBytes("UTF-8");
-    RawText aRawText = new RawText(aText.getContent());
-    RawText bRawText = new RawText(bText.getContent());
+    byte[] rawHdr = hdr.toString().getBytes(UTF_8);
+    byte[] aContent = aText.getContent();
+    byte[] bContent = bText.getContent();
+    long sizeDelta = bContent.length - aContent.length;
+    RawText aRawText = new RawText(aContent);
+    RawText bRawText = new RawText(bContent);
     EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
     FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
-    return new PatchListEntry(fh, edits);
+    return new PatchListEntry(fh, edits, sizeDelta);
   }
 
-  private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader) {
-    final FileMode oldMode = fileHeader.getOldMode();
-    final FileMode newMode = fileHeader.getNewMode();
-
-    if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
-    }
-
+  private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader,
+      long sizeDelta) {
     if (aTree == null // want combined diff
         || fileHeader.getPatchType() != PatchType.UNIFIED
         || fileHeader.getHunks().isEmpty()) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
+          sizeDelta);
     }
 
     List<Edit> edits = fileHeader.toEditList();
     if (edits.isEmpty()) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
+          sizeDelta);
     } else {
-      return new PatchListEntry(fileHeader, edits);
+      return new PatchListEntry(fileHeader, edits, sizeDelta);
     }
   }
 
@@ -329,7 +360,7 @@
         + hash.substring(0, 2)
         + "/"
         + hash.substring(2);
-    Ref ref = repo.getRef(refName);
+    Ref ref = repo.getRefDatabase().exactRef(refName);
     if (ref != null && ref.getObjectId() != null) {
       return rw.parseTree(ref.getObjectId());
     }
@@ -389,20 +420,14 @@
         Map<String, ObjectId> resolved = new HashMap<>();
         for (Map.Entry<String, MergeResult<? extends Sequence>> entry : r.entrySet()) {
           MergeResult<? extends Sequence> p = entry.getValue();
-          TemporaryBuffer buf =
-              new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
-          try {
-            fmt.formatMerge(buf, p, "BASE", oursName, theirsName, "UTF-8");
+          try (TemporaryBuffer buf =
+              new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
+            fmt.formatMerge(buf, p, "BASE", oursName, theirsName, UTF_8.name());
             buf.close();
 
-            InputStream in = buf.openInputStream();
-            try {
+            try (InputStream in = buf.openInputStream()) {
               resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
-            } finally {
-              in.close();
             }
-          } finally {
-            buf.destroy();
           }
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index ea427eb..c26aea8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -65,7 +65,7 @@
   private Project.NameKey projectKey;
   private ObjectReader reader;
   private Change change;
-  private AccountDiffPreference diffPrefs;
+  private DiffPreferencesInfo diffPrefs;
   private boolean againstParent;
   private ObjectId aId;
   private ObjectId bId;
@@ -95,11 +95,11 @@
     this.change = c;
   }
 
-  void setDiffPrefs(final AccountDiffPreference dp) {
+  void setDiffPrefs(final DiffPreferencesInfo dp) {
     diffPrefs = dp;
 
-    context = diffPrefs.getContext();
-    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+    context = diffPrefs.context;
+    if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
       context = MAX_CONTEXT;
     } else if (context > MAX_CONTEXT) {
       context = MAX_CONTEXT;
@@ -140,11 +140,14 @@
 
     if (!isModify(content)) {
       intralineDifferenceIsPossible = false;
-    } else if (diffPrefs.isIntralineDifference()) {
+    } else if (diffPrefs.intralineDifference) {
       IntraLineDiff d =
-          patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
-              b.id, b.src, edits, projectKey, bId, b.path,
-              diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
+          patchListCache.getIntraLineDiff(
+              new IntraLineDiffKey(
+                a.id, b.id,
+                diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE),
+              IntraLineDiffArgs.create(
+                a.src, b.src, edits, projectKey, bId, b.path));
       if (d != null) {
         switch (d.getStatus()) {
           case EDIT_LIST:
@@ -176,9 +179,7 @@
     }
 
     boolean hugeFile = false;
-    if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
-
-    } else if (a.src == b.src && a.size() <= context
+    if (a.src == b.src && a.size() <= context
         && content.getEdits().isEmpty()) {
       // Odd special case; the files are identical (100% rename or copy)
       // and the user has asked for context that is larger than the file.
@@ -205,7 +206,7 @@
       //
       context = MAX_CONTEXT;
 
-      packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
+      packContent(diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE);
     }
 
     return new PatchScript(change.getKey(), content.getChangeType(),
@@ -214,7 +215,9 @@
         a.displayMethod, b.displayMethod, a.mimeType.toString(),
         b.mimeType.toString(), comments, history, hugeFile,
         intralineDifferenceIsPossible, intralineFailure, intralineTimeout,
-        content.getPatchType() == Patch.PatchType.BINARY);
+        content.getPatchType() == Patch.PatchType.BINARY,
+        aId == null ? null : aId.getName(),
+        bId == null ? null : bId.getName());
   }
 
   private static boolean isModify(PatchListEntry content) {
@@ -463,6 +466,10 @@
           } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
             srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
 
+          } else if (mode.getObjectType() == Constants.OBJ_COMMIT) {
+            String strContent = "Subproject commit " + ObjectId.toString(id);
+            srcContent = strContent.getBytes();
+
           } else {
             srcContent = Text.NO_BYTES;
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 3bdb6b2..5836df5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -30,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.edit.ChangeEdit;
@@ -67,7 +66,7 @@
         String fileName,
         @Assisted("patchSetA") PatchSet.Id patchSetA,
         @Assisted("patchSetB") PatchSet.Id patchSetB,
-        AccountDiffPreference diffPrefs);
+        DiffPreferencesInfo diffPrefs);
   }
 
   private static final Logger log =
@@ -84,7 +83,7 @@
   @Nullable
   private final PatchSet.Id psa;
   private final PatchSet.Id psb;
-  private final AccountDiffPreference diffPrefs;
+  private final DiffPreferencesInfo diffPrefs;
   private final ChangeEditUtil editReader;
   private Optional<ChangeEdit> edit;
 
@@ -93,7 +92,7 @@
   private boolean loadComments = true;
 
   private Change change;
-  private Project.NameKey projectKey;
+  private Project.NameKey project;
   private ChangeControl control;
   private ObjectId aId;
   private ObjectId bId;
@@ -111,7 +110,7 @@
       @Assisted final String fileName,
       @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
       @Assisted("patchSetB") final PatchSet.Id patchSetB,
-      @Assisted final AccountDiffPreference diffPrefs) {
+      @Assisted DiffPreferencesInfo diffPrefs) {
     this.repoManager = grm;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
@@ -145,7 +144,7 @@
     validatePatchSetId(psb);
 
     change = control.getChange();
-    projectKey = change.getProject();
+    project = change.getProject();
 
     aId = psa != null ? toObjectId(db, psa) : null;
     bId = toObjectId(db, psb);
@@ -155,53 +154,48 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    final Repository git;
-    try {
-      git = repoManager.openRepository(projectKey);
+    try (Repository git = repoManager.openRepository(project)) {
+      try {
+        final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace));
+        final PatchScriptBuilder b = newBuilder(list, git);
+        final PatchListEntry content = list.get(fileName);
+
+        loadCommentsAndHistory(content.getChangeType(), //
+            content.getOldName(), //
+            content.getNewName());
+
+        return b.toPatchScript(content, comments, history);
+      } catch (PatchListNotAvailableException e) {
+        throw new NoSuchChangeException(changeId, e);
+      } catch (IOException e) {
+        log.error("File content unavailable", e);
+        throw new NoSuchChangeException(changeId, e);
+      } catch (org.eclipse.jgit.errors.LargeObjectException err) {
+        throw new LargeObjectException("File content is too large", err);
+      }
     } catch (RepositoryNotFoundException e) {
-      log.error("Repository " + projectKey + " not found", e);
+      log.error("Repository " + project + " not found", e);
       throw new NoSuchChangeException(changeId, e);
     } catch (IOException e) {
-      log.error("Cannot open repository " + projectKey, e);
+      log.error("Cannot open repository " + project, e);
       throw new NoSuchChangeException(changeId, e);
     }
-    try {
-      final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
-      final PatchScriptBuilder b = newBuilder(list, git);
-      final PatchListEntry content = list.get(fileName);
-
-      loadCommentsAndHistory(content.getChangeType(), //
-          content.getOldName(), //
-          content.getNewName());
-
-      return b.toPatchScript(content, comments, history);
-    } catch (PatchListNotAvailableException e) {
-      throw new NoSuchChangeException(changeId, e);
-    } catch (IOException e) {
-      log.error("File content unavailable", e);
-      throw new NoSuchChangeException(changeId, e);
-    } catch (org.eclipse.jgit.errors.LargeObjectException err) {
-      throw new LargeObjectException("File content is too large", err);
-    } finally {
-      git.close();
-    }
   }
 
   private PatchListKey keyFor(final Whitespace whitespace) {
-    return new PatchListKey(projectKey, aId, bId, whitespace);
+    return new PatchListKey(aId, bId, whitespace);
   }
 
   private PatchList listFor(final PatchListKey key)
       throws PatchListNotAvailableException {
-    return patchListCache.get(key);
+    return patchListCache.get(key, project);
   }
 
   private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
-    final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
     final PatchScriptBuilder b = builderFactory.get();
-    b.setRepository(git, projectKey);
+    b.setRepository(git, project);
     b.setChange(change);
-    b.setDiffPrefs(dp);
+    b.setDiffPrefs(diffPrefs);
     b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
     return b;
   }
@@ -318,9 +312,9 @@
           break;
       }
 
-      final CurrentUser user = control.getCurrentUser();
+      final CurrentUser user = control.getUser();
       if (user.isIdentifiedUser()) {
-        final Account.Id me = ((IdentifiedUser) user).getAccountId();
+        final Account.Id me = user.getAccountId();
         switch (changeType) {
           case ADDED:
           case MODIFIED:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 1e4ffce..16598b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -56,7 +56,9 @@
     this.byEmailCache = byEmailCache;
   }
 
-  public PatchSetInfo get(RevCommit src, PatchSet.Id psi) {
+  public PatchSetInfo get(RevWalk rw, RevCommit src, PatchSet.Id psi)
+      throws IOException {
+    rw.parseBody(src);
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
     info.setMessage(src.getFullMessage());
@@ -79,22 +81,15 @@
 
   public PatchSetInfo get(Change change, PatchSet patchSet)
       throws PatchSetInfoNotAvailableException {
-    Repository repo;
-    try {
-      repo = repoManager.openRepository(change.getProject());
-    } catch (IOException e) {
-      throw new PatchSetInfoNotAvailableException(e);
-    }
-    try (RevWalk rw = new RevWalk(repo)) {
+    try (Repository repo = repoManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(repo)) {
       final RevCommit src =
           rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-      PatchSetInfo info = get(src, patchSet.getId());
+      PatchSetInfo info = get(rw, src, patchSet.getId());
       info.setParents(toParentInfos(src.getParents(), rw));
       return info;
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
-    } finally {
-      repo.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
index 882e25f..7982479 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.patch;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -37,7 +40,6 @@
 
 public class Text extends RawText {
   private static final Logger log = LoggerFactory.getLogger(Text.class);
-  private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
   private static final int bigFileThreshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
 
   public static final byte[] NO_BYTES = {};
@@ -81,7 +83,7 @@
       appendPersonIdent(b, "Commit", c.getCommitterIdent());
       b.append("\n");
       b.append(c.getFullMessage());
-      return new Text(b.toString().getBytes("UTF-8"));
+      return new Text(b.toString().getBytes(UTF_8));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 593f2c9..9827812 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -14,17 +14,17 @@
 
 package com.google.gerrit.server.plugins;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.jar.JarFile;
 
 class CleanupHandle {
-  private final File tmpFile;
+  private final Path tmp;
   private final JarFile jarFile;
 
-  CleanupHandle(File tmpFile,
-      JarFile jarFile) {
-    this.tmpFile = tmpFile;
+  CleanupHandle(Path tmp, JarFile jarFile) {
+    this.tmp = tmp;
     this.jarFile = jarFile;
   }
 
@@ -34,12 +34,13 @@
     } catch (IOException err) {
       PluginLoader.log.error("Cannot close " + jarFile.getName(), err);
     }
-    if (!tmpFile.delete() && tmpFile.exists()) {
-      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath()
-          + ", retrying to delete it on termination of the virtual machine");
-      tmpFile.deleteOnExit();
-    } else {
-      PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
+    try {
+      Files.deleteIfExists(tmp);
+      PluginLoader.log.info("Cleaned plugin " + tmp.getFileName());
+    } catch (IOException e) {
+      PluginLoader.log.warn("Cannot delete " + tmp.toAbsolutePath()
+          + ", retrying to delete it on termination of the virtual machine", e);
+      tmp.toFile().deleteOnExit();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index 7252617..1d4233a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -33,7 +33,7 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
-import java.io.File;
+import java.nio.file.Path;
 
 /**
  * Copies critical objects from the {@code dbInjector} into a plugin.
@@ -47,11 +47,11 @@
 class CopyConfigModule extends AbstractModule {
   @Inject
   @SitePath
-  private File sitePath;
+  private Path sitePath;
 
   @Provides
   @SitePath
-  File getSitePath() {
+  Path getSitePath() {
     return sitePath;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
index 33e0d12..2f780bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
@@ -31,7 +31,6 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.zip.ZipException;
 
@@ -60,22 +59,13 @@
       throw new MethodNotAllowedException("remote installation is disabled");
     }
     try {
-      InputStream in;
-      if (input.raw != null) {
-        in = input.raw.getInputStream();
-      } else {
-        try {
-          in = new URL(input.url).openStream();
-        } catch (MalformedURLException e) {
-          throw new BadRequestException(e.getMessage());
-        } catch (IOException e) {
-          throw new BadRequestException(e.getMessage());
-        }
-      }
-      try {
-        loader.installPluginFromStream(name, in);
-      } finally {
-        in.close();
+      try (InputStream in = openStream(input)) {
+        String pluginName = loader.installPluginFromStream(name, in);
+        ListPlugins.PluginInfo info =
+            new ListPlugins.PluginInfo(loader.get(pluginName));
+        return created
+            ? Response.created(info)
+            : Response.ok(info);
       }
     } catch (PluginInstallException e) {
       StringWriter buf = new StringWriter();
@@ -91,9 +81,18 @@
       }
       throw new BadRequestException(buf.toString());
     }
+  }
 
-    ListPlugins.PluginInfo info = new ListPlugins.PluginInfo(loader.get(name));
-    return created ? Response.created(info) : Response.ok(info);
+  private InputStream openStream(Input input)
+      throws IOException, BadRequestException {
+    if (input.raw != null) {
+      return input.raw.getInputStream();
+    }
+    try {
+      return new URL(input.url).openStream();
+    } catch (IOException e) {
+      throw new BadRequestException(e.getMessage());
+    }
   }
 
   @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 3406080..926ef44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -24,13 +24,14 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -43,7 +44,7 @@
   static final String JAR_EXTENSION = ".jar";
   static final Logger log = LoggerFactory.getLogger(JarPluginProvider.class);
 
-  private final File tmpDir;
+  private final Path tmpDir;
 
   @Inject
   JarPluginProvider(SitePaths sitePaths) {
@@ -51,42 +52,42 @@
   }
 
   @Override
-  public boolean handles(File srcFile) {
-    String fileName = srcFile.getName();
+  public boolean handles(Path srcPath) {
+    String fileName = srcPath.getFileName().toString();
     return fileName.endsWith(JAR_EXTENSION)
         || fileName.endsWith(JAR_EXTENSION + ".disabled");
   }
 
   @Override
-  public String getPluginName(File srcFile) {
+  public String getPluginName(Path srcPath) {
     try {
-      return MoreObjects.firstNonNull(getJarPluginName(srcFile),
-          PluginLoader.nameOf(srcFile));
+      return MoreObjects.firstNonNull(getJarPluginName(srcPath),
+          PluginLoader.nameOf(srcPath));
     } catch (IOException e) {
-      throw new IllegalArgumentException("Invalid plugin file " + srcFile
+      throw new IllegalArgumentException("Invalid plugin file " + srcPath
           + ": cannot get plugin name", e);
     }
   }
 
-  public static String getJarPluginName(File srcFile) throws IOException {
-    try (JarFile jarFile = new JarFile(srcFile)) {
+  public static String getJarPluginName(Path srcPath) throws IOException {
+    try (JarFile jarFile = new JarFile(srcPath.toFile())) {
       return jarFile.getManifest().getMainAttributes()
           .getValue("Gerrit-PluginName");
     }
   }
 
   @Override
-  public ServerPlugin get(File srcFile, FileSnapshot snapshot,
+  public ServerPlugin get(Path srcPath, FileSnapshot snapshot,
       PluginDescription description) throws InvalidPluginException {
     try {
-      String name = getPluginName(srcFile);
-      String extension = getExtension(srcFile);
-      try (FileInputStream in = new FileInputStream(srcFile)) {
-        File tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
-        return loadJarPlugin(name, srcFile, snapshot, tmp, description);
+      String name = getPluginName(srcPath);
+      String extension = getExtension(srcPath);
+      try (InputStream in = Files.newInputStream(srcPath)) {
+        Path tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
+        return loadJarPlugin(name, srcPath, snapshot, tmp, description);
       }
     } catch (IOException e) {
-      throw new InvalidPluginException("Cannot load Jar plugin " + srcFile, e);
+      throw new InvalidPluginException("Cannot load Jar plugin " + srcPath, e);
     }
   }
 
@@ -95,8 +96,8 @@
     return "gerrit";
   }
 
-  private static String getExtension(File file) {
-    return getExtension(file.getName());
+  private static String getExtension(Path path) {
+    return getExtension(path.getFileName().toString());
   }
 
   private static String getExtension(String name) {
@@ -109,18 +110,18 @@
     return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
   }
 
-  public static File storeInTemp(String pluginName, InputStream in,
+  public static Path storeInTemp(String pluginName, InputStream in,
       SitePaths sitePaths) throws IOException {
-    if (!sitePaths.tmp_dir.exists()) {
-      sitePaths.tmp_dir.mkdirs();
+    if (!Files.exists(sitePaths.tmp_dir)) {
+      Files.createDirectories(sitePaths.tmp_dir);
     }
     return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
   }
 
-  private ServerPlugin loadJarPlugin(String name, File srcJar,
-      FileSnapshot snapshot, File tmp, PluginDescription description)
+  private ServerPlugin loadJarPlugin(String name, Path srcJar,
+      FileSnapshot snapshot, Path tmp, PluginDescription description)
       throws IOException, InvalidPluginException, MalformedURLException {
-    JarFile jarFile = new JarFile(tmp);
+    JarFile jarFile = new JarFile(tmp.toFile());
     boolean keep = false;
     try {
       Manifest manifest = jarFile.getManifest();
@@ -129,24 +130,22 @@
       List<URL> urls = new ArrayList<>(2);
       String overlay = System.getProperty("gerrit.plugin-classes");
       if (overlay != null) {
-        File classes = new File(new File(new File(overlay), name), "main");
-        if (classes.isDirectory()) {
-          log.info(String.format("plugin %s: including %s", name,
-              classes.getPath()));
-          urls.add(classes.toURI().toURL());
+        Path classes = Paths.get(overlay).resolve(name).resolve("main");
+        if (Files.isDirectory(classes)) {
+          log.info(String.format("plugin %s: including %s", name, classes));
+          urls.add(classes.toUri().toURL());
         }
       }
-      urls.add(tmp.toURI().toURL());
+      urls.add(tmp.toUri().toURL());
 
       ClassLoader pluginLoader =
           new URLClassLoader(urls.toArray(new URL[urls.size()]),
               PluginLoader.parentFor(type));
 
       JarScanner jarScanner = createJarScanner(tmp);
-      ServerPlugin plugin =
-          new ServerPlugin(name, description.canonicalUrl, description.user,
-              srcJar, snapshot, jarScanner, description.dataDir,
-              pluginLoader);
+      ServerPlugin plugin = new ServerPlugin(name, description.canonicalUrl,
+          description.user, srcJar, snapshot, jarScanner,
+          description.dataDir, pluginLoader);
       plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
       keep = true;
       return plugin;
@@ -157,7 +156,7 @@
     }
   }
 
-  private JarScanner createJarScanner(File srcJar)
+  private JarScanner createJarScanner(Path srcJar)
       throws InvalidPluginException {
     try {
       return new JarScanner(srcJar);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index b01bc9e..d94df9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -39,10 +39,10 @@
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -69,8 +69,8 @@
 
   private final JarFile jarFile;
 
-  public JarScanner(File srcFile) throws IOException {
-    this.jarFile = new JarFile(srcFile);
+  public JarScanner(Path src) throws IOException {
+    this.jarFile = new JarFile(src.toFile());
   }
 
   @Override
@@ -185,11 +185,8 @@
   private static byte[] read(JarFile jarFile, JarEntry entry)
       throws IOException {
     byte[] data = new byte[(int) entry.getSize()];
-    InputStream in = jarFile.getInputStream(entry);
-    try {
+    try (InputStream in = jarFile.getInputStream(entry)) {
       IO.readFully(in, data, 0, data.length);
-    } finally {
-      in.close();
     }
     return data;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
index 63f69b5..ea81f17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
@@ -27,12 +27,12 @@
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
-import java.io.File;
+import java.nio.file.Path;
 
 class JsPlugin extends Plugin {
   private Injector httpInjector;
 
-  JsPlugin(String name, File srcFile, PluginUser pluginUser,
+  JsPlugin(String name, Path srcFile, PluginUser pluginUser,
       FileSnapshot snapshot) {
     super(name, srcFile, pluginUser, snapshot, ApiType.JS);
   }
@@ -40,7 +40,7 @@
   @Override
   @Nullable
   public String getVersion() {
-    String fileName = getSrcFile().getName();
+    String fileName = getSrcFile().getFileName().toString();
     int firstDash = fileName.indexOf("-");
     if (firstDash > 0) {
       return fileName.substring(firstDash + 1, fileName.lastIndexOf(".js"));
@@ -51,14 +51,14 @@
   @Override
   public void start(PluginGuiceEnvironment env) throws Exception {
     manager = new LifecycleManager();
-    String fileName = getSrcFile().getName();
+    String fileName = getSrcFile().getFileName().toString();
     httpInjector =
         Guice.createInjector(new StandaloneJsPluginModule(getName(), fileName));
     manager.start();
   }
 
   @Override
-  void stop(PluginGuiceEnvironment env) {
+  protected void stop(PluginGuiceEnvironment env) {
     if (manager != null) {
       manager.stop();
       httpInjector = null;
@@ -83,7 +83,7 @@
   }
 
   @Override
-  boolean canReload() {
+  protected boolean canReload() {
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index 54f05f0..1d717ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -90,7 +90,7 @@
         stdout.format("%-30s %-10s %-8s %s\n", p.getName(),
             Strings.nullToEmpty(info.version),
             p.isDisabled() ? "DISABLED" : "ENABLED",
-            p.getSrcFile().getName());
+            p.getSrcFile().getFileName());
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
index 82a6ad9..cf38310 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
@@ -18,14 +18,14 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Iterables;
 
-import java.io.File;
+import java.nio.file.Path;
 
 class MultipleProvidersForPluginException extends IllegalArgumentException {
   private static final long serialVersionUID = 1L;
 
-  MultipleProvidersForPluginException(File pluginSrcFile,
+  MultipleProvidersForPluginException(Path pluginSrcPath,
       Iterable<ServerPluginProvider> providersHandlers) {
-    super(pluginSrcFile.getAbsolutePath()
+    super(pluginSrcPath.toAbsolutePath()
         + " is claimed to be handled by more than one plugin provider: "
         + providersListToString(providersHandlers));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index b227909..588bc6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -25,7 +25,7 @@
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
-import java.io.File;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 import java.util.jar.Attributes;
@@ -67,7 +67,7 @@
   }
 
   private final String name;
-  private final File srcFile;
+  private final Path srcFile;
   private final ApiType apiType;
   private final boolean disabled;
   private final CacheKey cacheKey;
@@ -80,17 +80,17 @@
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
   public Plugin(String name,
-      File srcFile,
+      Path srcPath,
       PluginUser pluginUser,
       FileSnapshot snapshot,
       ApiType apiType) {
     this.name = name;
-    this.srcFile = srcFile;
+    this.srcFile = srcPath;
     this.apiType = apiType;
     this.snapshot = snapshot;
     this.pluginUser = pluginUser;
     this.cacheKey = new Plugin.CacheKey(name);
-    this.disabled = srcFile.getName().endsWith(".disabled");
+    this.disabled = srcPath.getFileName().toString().endsWith(".disabled");
   }
 
   public CleanupHandle getCleanupHandle() {
@@ -105,7 +105,7 @@
     return pluginUser;
   }
 
-  public File getSrcFile() {
+  public Path getSrcFile() {
     return srcFile;
   }
 
@@ -128,9 +128,9 @@
     return disabled;
   }
 
-  abstract void start(PluginGuiceEnvironment env) throws Exception;
+  protected abstract void start(PluginGuiceEnvironment env) throws Exception;
 
-  abstract void stop(PluginGuiceEnvironment env);
+  protected abstract void stop(PluginGuiceEnvironment env);
 
   public abstract PluginContentScanner getContentScanner();
 
@@ -166,9 +166,9 @@
     return "Plugin [" + name + "]";
   }
 
-  abstract boolean canReload();
+  protected abstract boolean canReload();
 
-  boolean isModified(File jar) {
-    return snapshot.lastModified() != jar.lastModified();
+  boolean isModified(Path jar) {
+    return snapshot.isModified(jar.toFile());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
index c13c533..6c4c451 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -45,6 +45,7 @@
         Thread.sleep(50);
       }
     } catch (InterruptedException e) {
+      // Ignored
     }
 
     int left = loader.processPendingCleanups();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
index 0228509..1d9cd0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -11,14 +11,15 @@
 // 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.server.plugins;
 
 import com.google.common.base.Optional;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.nio.file.NoSuchFileException;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Map;
@@ -57,7 +58,7 @@
 
     @Override
     public InputStream getInputStream(PluginEntry entry) throws IOException {
-      throw new FileNotFoundException("Empty plugin");
+      throw new NoSuchFileException("Empty plugin");
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
index 7242e98..74ded73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
@@ -23,7 +23,7 @@
  * Plugin static resource entry
  *
  * Bean representing a static resource inside a plugin.
- * All static resources are available at <plugin web url>/static
+ * All static resources are available at {@code <plugin web url>/static}
  * and served by the HttpPluginServlet.
  */
 public class PluginEntry {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 5f33e38..04865a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -145,7 +145,7 @@
         || (httpMaps != null && httpMaps.containsKey(type));
   }
 
-  Module getSysModule() {
+  public Module getSysModule() {
     return sysModule;
   }
 
@@ -210,15 +210,15 @@
     return httpGen.get();
   }
 
-  RequestContext enter(Plugin plugin) {
+  public RequestContext enter(Plugin plugin) {
     return local.setContext(new PluginRequestContext(plugin.getPluginUser()));
   }
 
-  void exit(RequestContext old) {
+  public void exit(RequestContext old) {
     local.setContext(old);
   }
 
-  void onStartPlugin(Plugin plugin) {
+  public void onStartPlugin(Plugin plugin) {
     RequestContext oldContext = enter(plugin);
     try {
       attachItem(sysItems, plugin.getSysInjector(), plugin);
@@ -518,10 +518,12 @@
     bindings.remove(Key.get(Injector.class));
     bindings.remove(Key.get(java.util.logging.Logger.class));
 
-    final @Nullable Binding<HttpServletRequest> requestBinding =
+    @Nullable
+    final Binding<HttpServletRequest> requestBinding =
         src.getExistingBinding(Key.get(HttpServletRequest.class));
 
-    final @Nullable Binding<HttpServletResponse> responseBinding =
+    @Nullable
+    final Binding<HttpServletResponse> responseBinding =
         src.getExistingBinding(Key.get(HttpServletResponse.class));
 
     return new AbstractModule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index b51359d..5006401 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -18,6 +18,8 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
@@ -25,6 +27,7 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -45,14 +48,15 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.AbstractMap;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -71,13 +75,13 @@
   static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
-  public String getPluginName(File srcFile) {
-    return MoreObjects.firstNonNull(getGerritPluginName(srcFile),
-        nameOf(srcFile));
+  public String getPluginName(Path srcPath) {
+    return MoreObjects.firstNonNull(getGerritPluginName(srcPath),
+        nameOf(srcPath));
   }
 
-  private final File pluginsDir;
-  private final File dataDir;
+  private final Path pluginsDir;
+  private final Path dataDir;
   private final PluginGuiceEnvironment env;
   private final ServerInformationImpl srvInfoImpl;
   private final PluginUser.Factory pluginUserFactory;
@@ -153,12 +157,12 @@
     }
   }
 
-  public void installPluginFromStream(String originalName, InputStream in)
+  public String installPluginFromStream(String originalName, InputStream in)
       throws IOException, PluginInstallException {
     checkRemoteInstall();
 
     String fileName = originalName;
-    File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
+    Path tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
     String name = MoreObjects.firstNonNull(getGerritPluginName(tmp),
         nameOf(fileName));
     if (!originalName.equals(name)) {
@@ -168,48 +172,46 @@
     }
 
     String fileExtension = getExtension(fileName);
-    File dst = new File(pluginsDir, name + fileExtension);
+    Path dst = pluginsDir.resolve(name + fileExtension);
     synchronized (this) {
       Plugin active = running.get(name);
       if (active != null) {
-        fileName = active.getSrcFile().getName();
+        fileName = active.getSrcFile().getFileName().toString();
         log.info(String.format("Replacing plugin %s", active.getName()));
-        File old = new File(pluginsDir, ".last_" + fileName);
-        old.delete();
-        active.getSrcFile().renameTo(old);
+        Path old = pluginsDir.resolve(".last_" + fileName);
+        Files.deleteIfExists(old);
+        Files.move(active.getSrcFile(), old);
       }
 
-      new File(pluginsDir, fileName + ".disabled").delete();
-      tmp.renameTo(dst);
+      Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled"));
+      Files.move(tmp, dst);
       try {
         Plugin plugin = runPlugin(name, dst, active);
         if (active == null) {
           log.info(String.format("Installed plugin %s", plugin.getName()));
         }
       } catch (PluginInstallException e) {
-        dst.delete();
+        Files.deleteIfExists(dst);
         throw e;
       }
 
       cleanInBackground();
     }
+
+    return name;
   }
 
-  static File asTemp(InputStream in, String prefix, String suffix, File dir)
+  static Path asTemp(InputStream in, String prefix, String suffix, Path dir)
       throws IOException {
-    File tmp = File.createTempFile(prefix, suffix, dir);
+    Path tmp = Files.createTempFile(dir, prefix, suffix);
     boolean keep = false;
-    try (FileOutputStream out = new FileOutputStream(tmp)) {
-      byte[] data = new byte[8192];
-      int n;
-      while ((n = in.read(data)) > 0) {
-        out.write(data, 0, n);
-      }
+    try (OutputStream out = Files.newOutputStream(tmp)) {
+      ByteStreams.copy(in, out);
       keep = true;
       return tmp;
     } finally {
       if (!keep) {
-        tmp.delete();
+        Files.delete(tmp);
       }
     }
   }
@@ -217,7 +219,8 @@
   private synchronized void unloadPlugin(Plugin plugin) {
     persistentCacheFactory.onStop(plugin);
     String name = plugin.getName();
-    log.info(String.format("Unloading plugin %s", name));
+    log.info(String.format("Unloading plugin %s, version %s",
+        name, plugin.getVersion()));
     plugin.stop(env);
     env.onStopPlugin(plugin);
     running.remove(name);
@@ -240,12 +243,21 @@
         }
 
         log.info(String.format("Disabling plugin %s", active.getName()));
-        File off = new File(active.getSrcFile() + ".disabled");
-        active.getSrcFile().renameTo(off);
+        Path off = active.getSrcFile().resolveSibling(
+            active.getSrcFile().getFileName() + ".disabled");
+        try {
+          Files.move(active.getSrcFile(), off);
+        } catch (IOException e) {
+          log.error("Failed to disable plugin", e);
+          // In theory we could still unload the plugin even if the rename
+          // failed. However, it would be reloaded on the next server startup,
+          // which is probably not what the user expects.
+          continue;
+        }
 
         unloadPlugin(active);
         try {
-          FileSnapshot snapshot = FileSnapshot.save(off);
+          FileSnapshot snapshot = FileSnapshot.save(off.toFile());
           Plugin offPlugin = loadPlugin(name, off, snapshot);
           disabled.put(name, offPlugin);
         } catch (Throwable e) {
@@ -274,13 +286,17 @@
         }
 
         log.info(String.format("Enabling plugin %s", name));
-        String n = off.getSrcFile().getName();
+        String n = off.getSrcFile().toFile().getName();
         if (n.endsWith(".disabled")) {
           n = n.substring(0, n.lastIndexOf('.'));
         }
-        File on = new File(pluginsDir, n);
-        off.getSrcFile().renameTo(on);
-
+        Path on = pluginsDir.resolve(n);
+        try {
+          Files.move(off.getSrcFile(), on);
+        } catch (IOException e) {
+          log.error("Failed to move plugin " + name + " into place", e);
+          continue;
+        }
         disabled.remove(name);
         runPlugin(name, on, null);
       }
@@ -290,7 +306,7 @@
 
   @Override
   public synchronized void start() {
-    log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+    log.info("Loading plugins from " + pluginsDir.toAbsolutePath());
     srvInfoImpl.state = ServerInformation.State.STARTUP;
     rescan();
     srvInfoImpl.state = ServerInformation.State.RUNNING;
@@ -342,7 +358,9 @@
         String name = active.getName();
         try {
           log.info(String.format("Reloading plugin %s", name));
-          runPlugin(name, active.getSrcFile(), active);
+          Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
+          log.info(String.format("Reloaded plugin %s, version %s",
+              newPlugin.getName(), newPlugin.getVersion()));
         } catch (PluginInstallException e) {
           log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
           throw e;
@@ -354,42 +372,42 @@
   }
 
   public synchronized void rescan() {
-    Multimap<String, File> pluginsFiles = prunePlugins(pluginsDir);
+    Multimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
     if (pluginsFiles.isEmpty()) {
       return;
     }
 
     syncDisabledPlugins(pluginsFiles);
 
-    Map<String, File> activePlugins = filterDisabled(pluginsFiles);
-    for (Map.Entry<String, File> entry : jarsFirstSortedPluginsSet(activePlugins)) {
+    Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
+    for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
       String name = entry.getKey();
-      File file = entry.getValue();
-      String fileName = file.getName();
-      if (!isJsPlugin(fileName) && !serverPluginFactory.handles(file)) {
+      Path path = entry.getValue();
+      String fileName = path.getFileName().toString();
+      if (!isJsPlugin(fileName) && !serverPluginFactory.handles(path)) {
         log.warn("No Plugin provider was found that handles this file format: {}", fileName);
         continue;
       }
 
       FileSnapshot brokenTime = broken.get(name);
-      if (brokenTime != null && !brokenTime.isModified(file)) {
+      if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
         continue;
       }
 
       Plugin active = running.get(name);
-      if (active != null && !active.isModified(file)) {
+      if (active != null && !active.isModified(path)) {
         continue;
       }
 
       if (active != null) {
-        log.info(String.format("Reloading plugin %s, version %s",
-            active.getName(), active.getVersion()));
+        log.info(String.format("Reloading plugin %s", active.getName()));
       }
 
       try {
-        Plugin loadedPlugin = runPlugin(name, file, active);
-        if (active == null && !loadedPlugin.isDisabled()) {
-          log.info(String.format("Loaded plugin %s, version %s",
+        Plugin loadedPlugin = runPlugin(name, path, active);
+        if (!loadedPlugin.isDisabled()) {
+          log.info(String.format("%s plugin %s, version %s",
+              active == null ? "Loaded" : "Reloaded",
               loadedPlugin.getName(), loadedPlugin.getVersion()));
         }
       } catch (PluginInstallException e) {
@@ -400,31 +418,32 @@
     cleanInBackground();
   }
 
-  private void addAllEntries(Map<String, File> from,
-      TreeSet<Entry<String, File>> to) {
-    Iterator<Entry<String, File>> it = from.entrySet().iterator();
+  private void addAllEntries(Map<String, Path> from,
+      TreeSet<Entry<String, Path>> to) {
+    Iterator<Entry<String, Path>> it = from.entrySet().iterator();
     while (it.hasNext()) {
-      Entry<String,File> entry = it.next();
+      Entry<String,Path> entry = it.next();
       to.add(new AbstractMap.SimpleImmutableEntry<>(
           entry.getKey(), entry.getValue()));
     }
   }
 
-  private TreeSet<Entry<String, File>> jarsFirstSortedPluginsSet(
-      Map<String, File> activePlugins) {
-    TreeSet<Entry<String, File>> sortedPlugins =
-        Sets.newTreeSet(new Comparator<Entry<String, File>>() {
+  private TreeSet<Entry<String, Path>> jarsFirstSortedPluginsSet(
+      Map<String, Path> activePlugins) {
+    TreeSet<Entry<String, Path>> sortedPlugins =
+        Sets.newTreeSet(new Comparator<Entry<String, Path>>() {
           @Override
-          public int compare(Entry<String, File> entry1,
-              Entry<String, File> entry2) {
-            String file1 = entry1.getValue().getName();
-            String file2 = entry2.getValue().getName();
-            int cmp = file1.compareTo(file2);
-            if (file1.endsWith(".jar")) {
-              return (file2.endsWith(".jar") ? cmp : -1);
-            } else {
-              return (file2.endsWith(".jar") ? +1 : cmp);
-            }
+          public int compare(Entry<String, Path> e1, Entry<String, Path> e2) {
+            Path n1 = e1.getValue().getFileName();
+            Path n2 = e2.getValue().getFileName();
+            return ComparisonChain.start()
+                .compareTrueFirst(isJar(n1), isJar(n2))
+                .compare(n1, n2)
+                .result();
+          }
+
+          private boolean isJar(Path n1) {
+            return n1.toString().endsWith(".jar");
           }
         });
 
@@ -432,14 +451,14 @@
     return sortedPlugins;
   }
 
-  private void syncDisabledPlugins(Multimap<String, File> jars) {
+  private void syncDisabledPlugins(Multimap<String, Path> jars) {
     stopRemovedPlugins(jars);
     dropRemovedDisabledPlugins(jars);
   }
 
-  private Plugin runPlugin(String name, File plugin, Plugin oldPlugin)
+  private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
       throws PluginInstallException {
-    FileSnapshot snapshot = FileSnapshot.save(plugin);
+    FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
     try {
       Plugin newPlugin = loadPlugin(name, plugin, snapshot);
       if (newPlugin.getCleanupHandle() != null) {
@@ -479,11 +498,11 @@
     }
   }
 
-  private void stopRemovedPlugins(Multimap<String, File> jars) {
+  private void stopRemovedPlugins(Multimap<String, Path> jars) {
     Set<String> unload = Sets.newHashSet(running.keySet());
-    for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
-      for (File file : entry.getValue()) {
-        if (!file.getName().endsWith(".disabled")) {
+    for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
+      for (Path path : entry.getValue()) {
+        if (!path.getFileName().toString().endsWith(".disabled")) {
           unload.remove(entry.getKey());
         }
       }
@@ -493,11 +512,11 @@
     }
   }
 
-  private void dropRemovedDisabledPlugins(Multimap<String, File> jars) {
+  private void dropRemovedDisabledPlugins(Multimap<String, Path> jars) {
     Set<String> unload = Sets.newHashSet(disabled.keySet());
-    for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) {
-      for (File file : entry.getValue()) {
-        if (file.getName().endsWith(".disabled")) {
+    for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
+      for (Path path : entry.getValue()) {
+        if (path.getFileName().toString().endsWith(".disabled")) {
           unload.remove(entry.getKey());
         }
       }
@@ -528,8 +547,8 @@
     }
   }
 
-  public static String nameOf(File plugin) {
-    return nameOf(plugin.getName());
+  public static String nameOf(Path plugin) {
+    return nameOf(plugin.getFileName().toString());
   }
 
   private static String nameOf(String name) {
@@ -545,21 +564,21 @@
     return 0 < ext ? name.substring(ext) : "";
   }
 
-  private Plugin loadPlugin(String name, File srcPlugin, FileSnapshot snapshot)
+  private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
       throws InvalidPluginException {
-    String pluginName = srcPlugin.getName();
+    String pluginName = srcPlugin.getFileName().toString();
     if (isJsPlugin(pluginName)) {
       return loadJsPlugin(name, srcPlugin, snapshot);
     } else if (serverPluginFactory.handles(srcPlugin)) {
       return loadServerPlugin(srcPlugin, snapshot);
     } else {
       throw new InvalidPluginException(String.format(
-          "Unsupported plugin type: %s", srcPlugin.getName()));
+          "Unsupported plugin type: %s", srcPlugin.getFileName()));
     }
   }
 
-  private File getPluginDataDir(String name) {
-    return new File(dataDir, name);
+  private Path getPluginDataDir(String name) {
+    return dataDir.resolve(name);
   }
 
   private String getPluginCanonicalWebUrl(String name) {
@@ -569,11 +588,11 @@
     return url;
   }
 
-  private Plugin loadJsPlugin(String name, File srcJar, FileSnapshot snapshot) {
+  private Plugin loadJsPlugin(String name, Path srcJar, FileSnapshot snapshot) {
     return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
   }
 
-  private ServerPlugin loadServerPlugin(File scriptFile,
+  private ServerPlugin loadServerPlugin(Path scriptFile,
       FileSnapshot snapshot) throws InvalidPluginException {
     String name = serverPluginFactory.getPluginName(scriptFile);
     return serverPluginFactory.get(scriptFile, snapshot, new PluginDescription(
@@ -597,15 +616,15 @@
 
   // Only one active plugin per plugin name can exist for each plugin name.
   // Filter out disabled plugins and transform the multimap to a map
-  private static Map<String, File> filterDisabled(
-      Multimap<String, File> pluginFiles) {
-    Map<String, File> activePlugins = Maps.newHashMapWithExpectedSize(
-        pluginFiles.keys().size());
-    for (String name : pluginFiles.keys()) {
-      for (File pluginFile : pluginFiles.asMap().get(name)) {
-        if (!pluginFile.getName().endsWith(".disabled")) {
+  private static Map<String, Path> filterDisabled(
+      Multimap<String, Path> pluginPaths) {
+    Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize(
+        pluginPaths.keys().size());
+    for (String name : pluginPaths.keys()) {
+      for (Path pluginPath : pluginPaths.asMap().get(name)) {
+        if (!pluginPath.getFileName().toString().endsWith(".disabled")) {
           assert(!activePlugins.containsKey(name));
-          activePlugins.put(name, pluginFile);
+          activePlugins.put(name, pluginPath);
         }
       }
     }
@@ -621,37 +640,40 @@
   //
   // NOTE: Bear in mind that the plugin name can be reassigned after load by the
   //       Server plugin provider.
-  public Multimap<String, File> prunePlugins(File pluginsDir) {
-    List<File> pluginFiles = scanFilesInPluginsDirectory(pluginsDir);
-    Multimap<String, File> map;
-    map = asMultimap(pluginFiles);
+  public Multimap<String, Path> prunePlugins(Path pluginsDir) {
+    List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir);
+    Multimap<String, Path> map;
+    map = asMultimap(pluginPaths);
     for (String plugin : map.keySet()) {
-      Collection<File> files = map.asMap().get(plugin);
+      Collection<Path> files = map.asMap().get(plugin);
       if (files.size() == 1) {
         continue;
       }
       // retrieve enabled plugins
-      Iterable<File> enabled = filterDisabledPlugins(
-          files);
+      Iterable<Path> enabled = filterDisabledPlugins(files);
       // If we have only one (the winner) plugin, nothing to do
       if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
         continue;
       }
-      File winner = Iterables.getFirst(enabled, null);
+      Path winner = Iterables.getFirst(enabled, null);
       assert(winner != null);
       // Disable all loser plugins by renaming their file names to
       // "file.disabled" and replace the disabled files in the multimap.
-      Collection<File> elementsToRemove = Lists.newArrayList();
-      Collection<File> elementsToAdd = Lists.newArrayList();
-      for (File loser : Iterables.skip(enabled, 1)) {
+      Collection<Path> elementsToRemove = Lists.newArrayList();
+      Collection<Path> elementsToAdd = Lists.newArrayList();
+      for (Path loser : Iterables.skip(enabled, 1)) {
         log.warn(String.format("Plugin <%s> was disabled, because"
              + " another plugin <%s>"
              + " with the same name <%s> already exists",
              loser, winner, plugin));
-        File disabledPlugin = new File(loser + ".disabled");
+        Path disabledPlugin = Paths.get(loser + ".disabled");
         elementsToAdd.add(disabledPlugin);
         elementsToRemove.add(loser);
-        loser.renameTo(disabledPlugin);
+        try {
+          Files.move(loser, disabledPlugin);
+        } catch (IOException e) {
+          log.warn("Failed to fully disable plugin " + loser, e);
+        }
       }
       Iterables.removeAll(files, elementsToRemove);
       Iterables.addAll(files, elementsToAdd);
@@ -659,50 +681,52 @@
     return map;
   }
 
-  private List<File> scanFilesInPluginsDirectory(File pluginsDir) {
-    if (pluginsDir == null || !pluginsDir.exists()) {
+  private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
+    if (pluginsDir == null || !Files.exists(pluginsDir)) {
       return Collections.emptyList();
     }
-    File[] matches = pluginsDir.listFiles(new FileFilter() {
+    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
       @Override
-      public boolean accept(File pathname) {
-        String n = pathname.getName();
+      public boolean accept(Path entry) throws IOException {
+        String n = entry.getFileName().toString();
         return !n.startsWith(".last_")
             && !n.startsWith(".next_");
       }
-    });
-    if (matches == null) {
-      log.error("Cannot list " + pluginsDir.getAbsolutePath());
-      return Collections.emptyList();
+    };
+    try (DirectoryStream<Path> files
+        = Files.newDirectoryStream(pluginsDir, filter)) {
+      return ImmutableList.copyOf(files);
+    } catch (IOException e) {
+      log.error("Cannot list " + pluginsDir.toAbsolutePath(), e);
+      return ImmutableList.of();
     }
-    return Arrays.asList(matches);
   }
 
-  private static Iterable<File> filterDisabledPlugins(
-      Collection<File> files) {
-    return Iterables.filter(files, new Predicate<File>() {
+  private static Iterable<Path> filterDisabledPlugins(
+      Collection<Path> paths) {
+    return Iterables.filter(paths, new Predicate<Path>() {
       @Override
-      public boolean apply(File file) {
-        return !file.getName().endsWith(".disabled");
+      public boolean apply(Path p) {
+        return !p.getFileName().toString().endsWith(".disabled");
       }
     });
   }
 
-  public String getGerritPluginName(File srcFile) {
-    String fileName = srcFile.getName();
+  public String getGerritPluginName(Path srcPath) {
+    String fileName = srcPath.getFileName().toString();
     if (isJsPlugin(fileName)) {
       return fileName.substring(0, fileName.length() - 3);
     }
-    if (serverPluginFactory.handles(srcFile)) {
-      return serverPluginFactory.getPluginName(srcFile);
+    if (serverPluginFactory.handles(srcPath)) {
+      return serverPluginFactory.getPluginName(srcPath);
     }
     return null;
   }
 
-  private Multimap<String, File> asMultimap(List<File> plugins) {
-    Multimap<String, File> map = LinkedHashMultimap.create();
-    for (File srcFile : plugins) {
-      map.put(getPluginName(srcFile), srcFile);
+  private Multimap<String, Path> asMultimap(List<Path> plugins) {
+    Multimap<String, Path> map = LinkedHashMultimap.create();
+    for (Path srcPath : plugins) {
+      map.put(getPluginName(srcPath), srcPath);
     }
     return map;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
index a484c5d..e6c3dbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -37,6 +37,7 @@
           return;
         }
       } catch (InterruptedException e) {
+        // Ignored
       }
       loader.rescan();
     }
@@ -47,6 +48,7 @@
     try {
       join();
     } catch (InterruptedException e) {
+      // Ignored
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 0b037fb..14c1185 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -17,25 +17,19 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
-import com.google.gerrit.extensions.annotations.PluginData;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.util.RequestContext;
-import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
@@ -59,7 +53,7 @@
 
   private final Manifest manifest;
   private final PluginContentScanner scanner;
-  private final File dataDir;
+  private final Path dataDir;
   private final String pluginCanonicalWebUrl;
   private final ClassLoader classLoader;
   private Class<? extends Module> sysModule;
@@ -75,12 +69,13 @@
   public ServerPlugin(String name,
       String pluginCanonicalWebUrl,
       PluginUser pluginUser,
-      File srcJar,
+      Path srcJar,
       FileSnapshot snapshot,
       PluginContentScanner scanner,
-      File dataDir,
+      Path dataDir,
       ClassLoader classLoader) throws InvalidPluginException {
-    super(name, srcJar, pluginUser, snapshot, Plugin.getApiType(getPluginManifest(scanner)));
+    super(name, srcJar, pluginUser, snapshot,
+        Plugin.getApiType(getPluginManifest(scanner)));
     this.pluginCanonicalWebUrl = pluginCanonicalWebUrl;
     this.scanner = scanner;
     this.dataDir = dataDir;
@@ -127,10 +122,18 @@
     return (Class<? extends Module>) clazz;
   }
 
-  File getSrcJar() {
+  Path getSrcJar() {
     return getSrcFile();
   }
 
+  Path getDataDir() {
+    return dataDir;
+  }
+
+  String getPluginCanonicalWebUrl() {
+    return pluginCanonicalWebUrl;
+  }
+
   private static Manifest getPluginManifest(PluginContentScanner scanner)
       throws InvalidPluginException {
     try {
@@ -148,7 +151,7 @@
   }
 
   @Override
-  boolean canReload() {
+  protected boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
     if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
@@ -164,7 +167,7 @@
   }
 
   @Override
-  void start(PluginGuiceEnvironment env) throws Exception {
+  protected void start(PluginGuiceEnvironment env) throws Exception {
     RequestContext oldContext = env.enter(this);
     try {
       startPlugin(env);
@@ -229,50 +232,16 @@
   }
 
   private Injector newRootInjector(final PluginGuiceEnvironment env) {
-    List<Module> modules = Lists.newArrayListWithCapacity(4);
+    List<Module> modules = Lists.newArrayListWithCapacity(2);
     if (getApiType() == ApiType.PLUGIN) {
       modules.add(env.getSysModule());
     }
-    modules.add(new AbstractModule() {
-      @Override
-      protected void configure() {
-        bind(PluginUser.class).toInstance(getPluginUser());
-        bind(String.class)
-          .annotatedWith(PluginName.class)
-          .toInstance(getName());
-        bind(String.class)
-          .annotatedWith(PluginCanonicalWebUrl.class)
-          .toInstance(pluginCanonicalWebUrl);
-
-        bind(File.class)
-          .annotatedWith(PluginData.class)
-          .toProvider(new Provider<File>() {
-            private volatile boolean ready;
-
-            @Override
-            public File get() {
-              if (!ready) {
-                synchronized (dataDir) {
-                  if (!ready) {
-                    if (!dataDir.exists() && !dataDir.mkdirs()) {
-                      throw new ProvisionException(String.format(
-                          "Cannot create %s for plugin %s",
-                          dataDir.getAbsolutePath(), getName()));
-                    }
-                    ready = true;
-                  }
-                }
-              }
-              return dataDir;
-            }
-          });
-      }
-    });
+    modules.add(new ServerPluginInfoModule(this));
     return Guice.createInjector(modules);
   }
 
   @Override
-  void stop(PluginGuiceEnvironment env) {
+  protected void stop(PluginGuiceEnvironment env) {
     if (serverManager != null) {
       RequestContext oldContext = env.enter(this);
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
new file mode 100644
index 0000000..b0e9453
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2015 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.server.plugins;
+
+import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.PluginUser;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class ServerPluginInfoModule extends AbstractModule {
+  private final ServerPlugin plugin;
+  private final Path dataDir;
+
+  private volatile boolean ready;
+
+  ServerPluginInfoModule(ServerPlugin plugin) {
+    this.plugin = plugin;
+    this.dataDir = plugin.getDataDir();
+  }
+
+  @Override
+  protected void configure() {
+    bind(PluginUser.class).toInstance(plugin.getPluginUser());
+    bind(String.class)
+      .annotatedWith(PluginName.class)
+      .toInstance(plugin.getName());
+    bind(String.class)
+      .annotatedWith(PluginCanonicalWebUrl.class)
+      .toInstance(plugin.getPluginCanonicalWebUrl());
+  }
+
+  @Provides
+  @PluginData
+  Path getPluginData() {
+    if (!ready) {
+      synchronized (dataDir) {
+        if (!ready) {
+          try {
+            Files.createDirectories(dataDir);
+          } catch (IOException e) {
+            throw new ProvisionException(String.format(
+                "Cannot create %s for plugin %s",
+                dataDir.toAbsolutePath(), plugin.getName()), e);
+          }
+          ready = true;
+        }
+      }
+    }
+    return dataDir;
+  }
+
+  @Provides
+  @PluginData
+  File getPluginDataAsFile(@PluginData Path pluginData) {
+    return pluginData.toFile();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
index 37fed9b..bc2432b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginProvider.java
@@ -19,7 +19,7 @@
 
 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
-import java.io.File;
+import java.nio.file.Path;
 
 /**
  * Provider of one Server plugin from one external file
@@ -40,7 +40,7 @@
   public class PluginDescription {
     public final PluginUser user;
     public final String canonicalUrl;
-    public final File dataDir;
+    public final Path dataDir;
 
     /**
      * Creates a new PluginDescription for ServerPluginProvider.
@@ -49,7 +49,7 @@
      * @param canonicalUrl plugin root Web URL
      * @param dataDir directory for plugin data
      */
-    public PluginDescription(PluginUser user, String canonicalUrl, File dataDir) {
+    public PluginDescription(PluginUser user, String canonicalUrl, Path dataDir) {
       this.user = user;
       this.canonicalUrl = canonicalUrl;
       this.dataDir = dataDir;
@@ -59,39 +59,39 @@
   /**
    * Declares the availability to manage an external file or directory
    *
-   * @param srcFile the external file or directory
+   * @param srcPath the external file or directory
    * @return true if file or directory can be loaded into a Server Plugin
    */
-  boolean handles(File srcFile);
+  boolean handles(Path srcPath);
 
   /**
    * Returns the plugin name of an external file or directory
    *
-   * Should be called only if {@link #handles(File) handles(srcFile)}
+   * Should be called only if {@link #handles(Path) handles(srcFile)}
    * returns true and thus srcFile is a supported plugin format.
    * An IllegalArgumentException is thrown otherwise as srcFile
    * is not a valid file format for extracting its plugin name.
    *
-   * @param srcFile external file or directory
+   * @param srcPath external file or directory
    * @return plugin name
    */
-  String getPluginName(File srcFile);
+  String getPluginName(Path srcPath);
 
   /**
    * Loads an external file or directory into a Server plugin.
    *
-   * Should be called only if {@link #handles(File) handles(srcFile)}
+   * Should be called only if {@link #handles(Path) handles(srcFile)}
    * returns true and thus srcFile is a supported plugin format.
    * An IllegalArgumentException is thrown otherwise as srcFile
    * is not a valid file format for extracting its plugin name.
    *
-   * @param srcFile external file or directory
+   * @param srcPath external file or directory
    * @param snapshot snapshot of the external file
    * @param pluginDescriptor descriptor of the ServerPlugin to load
    * @throws InvalidPluginException if plugin is supposed to be handled
    *         but cannot be loaded for any other reason
    */
-  ServerPlugin get(File srcFile, FileSnapshot snapshot,
+  ServerPlugin get(Path srcPath, FileSnapshot snapshot,
       PluginDescription pluginDescriptor) throws InvalidPluginException;
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
index 0e8bd87..afdc5b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -22,7 +22,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -38,27 +38,26 @@
   }
 
   @Override
-  public ServerPlugin get(File srcFile, FileSnapshot snapshot,
+  public ServerPlugin get(Path srcPath, FileSnapshot snapshot,
       PluginDescription pluginDescription) throws InvalidPluginException {
-    return providerOf(srcFile).get(srcFile, snapshot, pluginDescription);
+    return providerOf(srcPath).get(srcPath, snapshot, pluginDescription);
   }
 
   @Override
-  public String getPluginName(File srcFile) {
-    return providerOf(srcFile).getPluginName(srcFile);
+  public String getPluginName(Path srcPath) {
+    return providerOf(srcPath).getPluginName(srcPath);
   }
 
   @Override
-  public boolean handles(File srcFile) {
-    List<ServerPluginProvider> providers =
-        providersForHandlingPlugin(srcFile);
+  public boolean handles(Path srcPath) {
+    List<ServerPluginProvider> providers = providersForHandlingPlugin(srcPath);
     switch (providers.size()) {
       case 1:
         return true;
       case 0:
         return false;
       default:
-        throw new MultipleProvidersForPluginException(srcFile, providers);
+        throw new MultipleProvidersForPluginException(srcPath, providers);
     }
   }
 
@@ -67,27 +66,27 @@
     return "gerrit";
   }
 
-  private ServerPluginProvider providerOf(File srcFile) {
+  private ServerPluginProvider providerOf(Path srcPath) {
     List<ServerPluginProvider> providers =
-        providersForHandlingPlugin(srcFile);
+        providersForHandlingPlugin(srcPath);
     switch (providers.size()) {
       case 1:
         return providers.get(0);
       case 0:
         throw new IllegalArgumentException(
             "No ServerPluginProvider found/loaded to handle plugin file "
-                + srcFile.getAbsolutePath());
+                + srcPath.toAbsolutePath());
       default:
-        throw new MultipleProvidersForPluginException(srcFile, providers);
+        throw new MultipleProvidersForPluginException(srcPath, providers);
     }
   }
 
   private List<ServerPluginProvider> providersForHandlingPlugin(
-      final File srcFile) {
+      final Path srcPath) {
     List<ServerPluginProvider> providers = new ArrayList<>();
     for (ServerPluginProvider serverPluginProvider : serverPluginProviders) {
-      boolean handles = serverPluginProvider.handles(srcFile);
-      log.debug("File {} handled by {} ? => {}", srcFile,
+      boolean handles = serverPluginProvider.handles(srcPath);
+      log.debug("File {} handled by {} ? => {}", srcPath,
           serverPluginProvider.getProviderPluginName(), handles);
       if (handles) {
         providers.add(serverPluginProvider);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
index 7215d18..29ab220 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -16,8 +16,8 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitReceivePackGroupsProvider;
 import com.google.gerrit.server.config.GitUploadPackGroups;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index ef153ce..f0c2b78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -60,7 +60,7 @@
   @Override
   public BanResultInfo apply(ProjectResource rsrc, Input input)
       throws UnprocessableEntityException, AuthException,
-      ResourceConflictException, IOException, InterruptedException {
+      ResourceConflictException, IOException {
     BanResultInfo r = new BanResultInfo();
     if (input != null && input.commits != null && !input.commits.isEmpty()) {
       List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
index 98531ce..7168b1b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.TypeLiteral;
 
 public class BranchResource extends ProjectResource {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
index e96d3a5..41d4920 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -21,7 +22,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,9 +57,8 @@
   public BranchResource parse(ProjectResource parent, IdString id)
       throws ResourceNotFoundException, IOException, BadRequestException {
     String branchName = id.get();
-    if (!branchName.startsWith(Constants.R_REFS)
-        && !branchName.equals(Constants.HEAD)) {
-      branchName = Constants.R_HEADS + branchName;
+    if (!branchName.equals(Constants.HEAD)) {
+      branchName = RefNames.fullName(branchName);
     }
     List<BranchInfo> branches = list.get().apply(parent);
     for (BranchInfo b : branches) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 08c7963..3ab6ff5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
@@ -27,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -124,7 +125,7 @@
   }
 
   public ChangeControl forUser(final CurrentUser who) {
-    if (getCurrentUser().equals(who)) {
+    if (getUser().equals(who)) {
       return this;
     }
     return new ChangeControl(changeDataFactory,
@@ -135,8 +136,8 @@
     return refControl;
   }
 
-  public CurrentUser getCurrentUser() {
-    return getRefControl().getCurrentUser();
+  public CurrentUser getUser() {
+    return getRefControl().getUser();
   }
 
   public ProjectControl getProjectControl() {
@@ -177,12 +178,23 @@
     return isVisible(db);
   }
 
+  /** Can this user see the given patchset? */
+  public boolean isPatchVisible(PatchSet ps, ChangeData cd)
+      throws OrmException {
+    checkArgument(cd.getId().equals(ps.getId().getParentKey()),
+        "%s not for change %s", ps, cd.getId());
+    if (ps.isDraft() && !isDraftVisible(cd.db(), cd)) {
+      return false;
+    }
+    return isVisible(cd.db());
+  }
+
   /** Can this user abandon this change? */
   public boolean canAbandon() {
     return isOwner() // owner (aka creator) of the change can abandon
         || getRefControl().isOwner() // branch owner can abandon
         || getProjectControl().isOwner() // project owner can abandon
-        || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+        || getUser().getCapabilities().canAdministrateServer() // site administers are god
         || getRefControl().canAbandon() // user can abandon a specific ref
     ;
   }
@@ -252,9 +264,9 @@
 
   /** Is this user the owner of the change? */
   public boolean isOwner() {
-    if (getCurrentUser().isIdentifiedUser()) {
-      final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
-      return i.getAccountId().equals(getChange().getOwner());
+    if (getUser().isIdentifiedUser()) {
+      Account.Id id = getUser().asIdentifiedUser().getAccountId();
+      return id.equals(getChange().getOwner());
     }
     return false;
   }
@@ -267,10 +279,9 @@
   /** Is this user a reviewer for the change? */
   public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
       throws OrmException {
-    if (getCurrentUser().isIdentifiedUser()) {
+    if (getUser().isIdentifiedUser()) {
       Collection<Account.Id> results = changeData(db, cd).reviewers().values();
-      IdentifiedUser user = (IdentifiedUser) getCurrentUser();
-      return results.contains(user.getAccountId());
+      return results.contains(getUser().getAccountId());
     }
     return false;
   }
@@ -284,9 +295,8 @@
     if (getChange().getStatus().isOpen()) {
       // A user can always remove themselves.
       //
-      if (getCurrentUser().isIdentifiedUser()) {
-        final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
-        if (i.getAccountId().equals(reviewer)) {
+      if (getUser().isIdentifiedUser()) {
+        if (getUser().getAccountId().equals(reviewer)) {
           return true; // can remove self
         }
       }
@@ -302,7 +312,7 @@
       if (getRefControl().canRemoveReviewer() // has removal permissions
           || getRefControl().isOwner() // branch owner
           || getProjectControl().isOwner() // project owner
-          || getCurrentUser().getCapabilities().canAdministrateServer()) {
+          || getUser().getCapabilities().canAdministrateServer()) {
         return true;
       }
     }
@@ -316,7 +326,7 @@
       return isOwner() // owner (aka creator) of the change can edit topic
           || getRefControl().isOwner() // branch owner can edit topic
           || getProjectControl().isOwner() // project owner can edit topic
-          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+          || getUser().getCapabilities().canAdministrateServer() // site administers are god
           || getRefControl().canEditTopicName() // user can edit topic on a specific ref
       ;
     } else {
@@ -329,7 +339,7 @@
     return isOwner() // owner (aka creator) of the change can edit hashtags
           || getRefControl().isOwner() // branch owner can edit hashtags
           || getProjectControl().isOwner() // project owner can edit hashtags
-          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+          || getUser().getCapabilities().canAdministrateServer() // site administers are god
           || getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref
   }
 
@@ -343,7 +353,7 @@
 
   private boolean match(String destBranch, String refPattern) {
     return RefPatternMatcher.getMatcher(refPattern).match(destBranch,
-        this.getRefControl().getCurrentUser().getUserName());
+        getUser().getUserName());
   }
 
   private ChangeData changeData(ReviewDb db, @Nullable ChangeData cd) {
@@ -353,6 +363,6 @@
   public boolean isDraftVisible(ReviewDb db, ChangeData cd)
       throws OrmException {
     return isOwner() || isReviewer(db, cd) || getRefControl().canViewDrafts()
-        || getCurrentUser().isInternalUser();
+        || getUser().isInternalUser();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
index f68acdd..4257825 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -15,28 +15,34 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
 
-public class ChildProjectResource extends ProjectResource {
+public class ChildProjectResource implements RestResource {
   public static final TypeLiteral<RestView<ChildProjectResource>> CHILD_PROJECT_KIND =
       new TypeLiteral<RestView<ChildProjectResource>>() {};
 
+  private final ProjectResource parent;
   private final ProjectControl child;
 
-  public ChildProjectResource(ProjectResource project, ProjectControl child) {
-    super(project);
+  public ChildProjectResource(ProjectResource parent, ProjectControl child) {
+    this.parent = parent;
     this.child = child;
   }
 
+  public ProjectResource getParent() {
+    return parent;
+  }
+
   public ProjectControl getChild() {
     return child;
   }
 
   public boolean isDirectChild() {
-    ProjectState parent =
+    ProjectState firstParent =
         Iterables.getFirst(child.getProjectState().parents(), null);
-    return parent != null
-        && getNameKey().equals(parent.getProject().getNameKey());
+    return firstParent != null
+        && parent.getNameKey().equals(firstParent.getProject().getNameKey());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
index ca98cf6..faba87a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
@@ -44,7 +44,7 @@
   }
 
   @Override
-  public RestView<ProjectResource> list() throws ResourceNotFoundException,
+  public ListChildProjects list() throws ResourceNotFoundException,
       AuthException {
     return list.get();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 114ab90..b20c461 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -20,14 +20,18 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
 
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.List;
 import java.util.Set;
 
 public class CommentLinkProvider implements Provider<List<CommentLinkInfo>> {
+  private static final Logger log =
+      LoggerFactory.getLogger(CommentLinkProvider.class);
+
   private final Config cfg;
 
   @Inject
@@ -41,12 +45,16 @@
     List<CommentLinkInfo> cls =
         Lists.newArrayListWithCapacity(subsections.size());
     for (String name : subsections) {
-      CommentLinkInfo cl = ProjectConfig.buildCommentLink(cfg, name, true);
-      if (cl.isOverrideOnly()) {
-        throw new ProvisionException(
-            "commentlink " + name + " empty except for \"enabled\"");
+      try {
+        CommentLinkInfo cl = ProjectConfig.buildCommentLink(cfg, name, true);
+        if (cl.isOverrideOnly()) {
+          log.warn("commentlink " + name + " empty except for \"enabled\"");
+          continue;
+        }
+        cls.add(cl);
+      } catch (IllegalArgumentException e) {
+        log.warn("invalid commentlink: " + e.getMessage());
       }
-      cls.add(cl);
     }
     return ImmutableList.copyOf(cls);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 1c6782c..6cbf7c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -45,6 +45,8 @@
   public InheritedBooleanInfo useSignedOffBy;
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
+  public InheritedBooleanInfo enableSignedPush;
+  public InheritedBooleanInfo requireSignedPush;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
   public com.google.gerrit.extensions.client.ProjectState state;
@@ -54,7 +56,8 @@
   public Map<String, CommentLinkInfo> commentlinks;
   public ThemeInfo theme;
 
-  public ConfigInfo(ProjectControl control,
+  public ConfigInfo(boolean serverEnableSignedPush,
+      ProjectControl control,
       TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
@@ -71,6 +74,8 @@
     InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
     InheritedBooleanInfo createNewChangeForAllNotInTarget =
         new InheritedBooleanInfo();
+    InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
+    InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo();
 
     useContributorAgreements.value = projectState.isUseContributorAgreements();
     useSignedOffBy.value = projectState.isUseSignedOffBy();
@@ -86,6 +91,8 @@
     requireChangeId.configuredValue = p.getRequireChangeID();
     createNewChangeForAllNotInTarget.configuredValue =
         p.getCreateNewChangeForAllNotInTarget();
+    enableSignedPush.configuredValue = p.getEnableSignedPush();
+    requireSignedPush.configuredValue = p.getRequireSignedPush();
 
     ProjectState parentState = Iterables.getFirst(projectState
         .parents(), null);
@@ -97,6 +104,8 @@
       requireChangeId.inheritedValue = parentState.isRequireChangeID();
       createNewChangeForAllNotInTarget.inheritedValue =
           parentState.isCreateNewChangeForAllNotInTarget();
+      enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
+      requireSignedPush.inheritedValue = projectState.isRequireSignedPush();
     }
 
     this.useContributorAgreements = useContributorAgreements;
@@ -104,6 +113,10 @@
     this.useContentMerge = useContentMerge;
     this.requireChangeId = requireChangeId;
     this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
+    if (serverEnableSignedPush) {
+      this.enableSignedPush = enableSignedPush;
+      this.requireSignedPush = requireSignedPush;
+    }
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
     maxObjectSizeLimit.value =
@@ -130,7 +143,7 @@
     actions = Maps.newTreeMap();
     for (UiAction.Description d : UiActions.from(
         views, new ProjectResource(control),
-        Providers.of(control.getCurrentUser()))) {
+        Providers.of(control.getUser()))) {
       actions.put(d.getId(), new ActionInfo(d));
     }
     this.theme = projectState.getTheme();
@@ -158,14 +171,19 @@
             cfgFactory.getFromProjectConfigWithInheritance(project,
                 e.getPluginName());
         p.inheritable = true;
-        p.value = cfgWithInheritance.getString(e.getExportName(), configEntry.getDefaultValue());
+        p.value = configEntry.onRead(project,
+            cfgWithInheritance.getString(e.getExportName(),
+                configEntry.getDefaultValue()));
         p.configuredValue = configuredValue;
         p.inheritedValue = getInheritedValue(project, cfgFactory, e);
       } else {
         if (configEntry.getType() == ProjectConfigEntry.Type.ARRAY) {
-          p.values = Arrays.asList(cfg.getStringList(e.getExportName()));
+          p.values = configEntry.onRead(project,
+              Arrays.asList(cfg.getStringList(e.getExportName())));
         } else {
-          p.value = configuredValue != null ? configuredValue : configEntry.getDefaultValue();
+          p.value = configEntry.onRead(project, configuredValue != null
+              ? configuredValue
+              : configEntry.getDefaultValue());
         }
       }
       Map<String, ConfigParameterInfo> pc = pluginConfig.get(e.getPluginName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index ac40df0..252b44a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.errors.InvalidRevisionException;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -30,7 +31,6 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.CreateBranch.Input;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,6 +48,7 @@
 import org.eclipse.jgit.revwalk.ObjectWalk;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -105,9 +106,7 @@
     while (ref.startsWith("/")) {
       ref = ref.substring(1);
     }
-    if (!ref.startsWith(Constants.R_REFS)) {
-      ref = Constants.R_HEADS + ref;
-    }
+    ref = RefNames.fullName(ref);
     if (!Repository.isValidRefName(ref)) {
       throw new BadRequestException("invalid branch name \"" + ref + "\"");
     }
@@ -118,8 +117,7 @@
 
     final Branch.NameKey name = new Branch.NameKey(rsrc.getNameKey(), ref);
     final RefControl refControl = rsrc.getControl().controlForRef(name);
-    final Repository repo = repoManager.openRepository(rsrc.getNameKey());
-    try {
+    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       final ObjectId revid = parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
       final RevWalk rw = verifyConnected(repo, revid);
       RevObject object = rw.parseAny(revid);
@@ -151,17 +149,17 @@
           case FAST_FORWARD:
           case NEW:
           case NO_CHANGE:
-            referenceUpdated.fire(name.getParentKey(), u);
+            referenceUpdated.fire(name.getParentKey(), u, ReceiveCommand.Type.CREATE);
             hooks.doRefUpdatedHook(name, u, identifiedUser.get().getAccount());
             break;
           case LOCK_FAILURE:
-            if (repo.getRef(ref) != null) {
+            if (repo.getRefDatabase().exactRef(ref) != null) {
               throw new ResourceConflictException("branch \"" + ref
                   + "\" already exists");
             }
             String refPrefix = getRefPrefix(ref);
             while (!Constants.R_HEADS.equals(refPrefix)) {
-              if (repo.getRef(refPrefix) != null) {
+              if (repo.getRefDatabase().exactRef(refPrefix) != null) {
                 throw new ResourceConflictException("Cannot create branch \""
                     + ref + "\" since it conflicts with branch \"" + refPrefix
                     + "\".");
@@ -174,15 +172,17 @@
           }
         }
 
-        return new BranchInfo(ref, revid.getName(), refControl.canDelete());
+        BranchInfo info = new BranchInfo();
+        info.ref = ref;
+        info.revision = revid.getName();
+        info.canDelete = refControl.canDelete() ? true : null;
+        return info;
       } catch (IOException err) {
         log.error("Cannot create branch \"" + name + "\"", err);
         throw err;
       }
     } catch (InvalidRevisionException e) {
       throw new BadRequestException("invalid revision \"" + input.revision + "\"");
-    } finally {
-      repo.close();
     }
   }
 
@@ -226,7 +226,7 @@
       Iterable<Ref> refs = Iterables.concat(
           refDb.getRefs(Constants.R_HEADS).values(),
           refDb.getRefs(Constants.R_TAGS).values());
-      Ref rc = refDb.getRef(RefNames.REFS_CONFIG);
+      Ref rc = refDb.exactRef(RefNames.REFS_CONFIG);
       if (rc != null) {
         refs = Iterables.concat(refs, Collections.singleton(rc));
       }
@@ -239,9 +239,7 @@
       }
       rw.checkConnectivity();
       return rw;
-    } catch (IncorrectObjectTypeException err) {
-      throw new InvalidRevisionException();
-    } catch (MissingObjectException err) {
+    } catch (IncorrectObjectTypeException | MissingObjectException err) {
       throw new InvalidRevisionException();
     } catch (IOException err) {
       log.error("Repository \"" + repo.getDirectory()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 964235d..c04878f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -17,13 +17,19 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.ProjectUtil;
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -34,9 +40,18 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
+import com.google.gerrit.server.config.RepositoryConfig;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
@@ -45,8 +60,22 @@
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
@@ -55,34 +84,60 @@
     CreateProject create(String name);
   }
 
-  private final PerformCreateProject.Factory createProjectFactory;
+  private static final Logger log = LoggerFactory
+      .getLogger(CreateProject.class);
+
   private final Provider<ProjectsCollection> projectsCollection;
   private final Provider<GroupsCollection> groupsCollection;
   private final DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
   private final ProjectJson json;
   private final ProjectControl.GenericFactory projectControlFactory;
+  private final GitRepositoryManager repoManager;
+  private final DynamicSet<NewProjectCreatedListener> createdListener;
+  private final ProjectCache projectCache;
+  private final GroupBackend groupBackend;
+  private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final GitReferenceUpdated referenceUpdated;
+  private final RepositoryConfig repositoryCfg;
+  private final PersonIdent serverIdent;
   private final Provider<CurrentUser> currentUser;
   private final Provider<PutConfig> putConfig;
   private final AllProjectsName allProjects;
   private final String name;
 
   @Inject
-  CreateProject(PerformCreateProject.Factory performCreateProjectFactory,
-      Provider<ProjectsCollection> projectsCollection,
-      Provider<GroupsCollection> groupsCollection,
-      ProjectJson json,
+  CreateProject(Provider<ProjectsCollection> projectsCollection,
+      Provider<GroupsCollection> groupsCollection, ProjectJson json,
       DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
       ProjectControl.GenericFactory projectControlFactory,
+      GitRepositoryManager repoManager,
+      DynamicSet<NewProjectCreatedListener> createdListener,
+      ProjectCache projectCache,
+      GroupBackend groupBackend,
+      ProjectOwnerGroupsProvider.Factory projectOwnerGroups,
+      MetaDataUpdate.User metaDataUpdateFactory,
+      GitReferenceUpdated referenceUpdated,
+      RepositoryConfig repositoryCfg,
+      @GerritPersonIdent PersonIdent serverIdent,
       Provider<CurrentUser> currentUser,
       Provider<PutConfig> putConfig,
       AllProjectsName allProjects,
       @Assisted String name) {
-    this.createProjectFactory = performCreateProjectFactory;
     this.projectsCollection = projectsCollection;
     this.groupsCollection = groupsCollection;
     this.projectCreationValidationListeners = projectCreationValidationListeners;
     this.json = json;
     this.projectControlFactory = projectControlFactory;
+    this.repoManager = repoManager;
+    this.createdListener = createdListener;
+    this.projectCache = projectCache;
+    this.groupBackend = groupBackend;
+    this.projectOwnerGroups = projectOwnerGroups;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.referenceUpdated = referenceUpdated;
+    this.repositoryCfg = repositoryCfg;
+    this.serverIdent = serverIdent;
     this.currentUser = currentUser;
     this.putConfig = putConfig;
     this.allProjects = allProjects;
@@ -90,10 +145,10 @@
   }
 
   @Override
-  public Response<ProjectInfo> apply(TopLevelResource resource, ProjectInput input)
-      throws BadRequestException, UnprocessableEntityException,
-      ResourceConflictException, ProjectCreationFailedException,
-      ResourceNotFoundException, IOException {
+  public Response<ProjectInfo> apply(TopLevelResource resource,
+      ProjectInput input) throws BadRequestException,
+      UnprocessableEntityException, ResourceConflictException,
+      ResourceNotFoundException, IOException, ConfigInvalidException {
     if (input == null) {
       input = new ProjectInput();
     }
@@ -101,23 +156,27 @@
       throw new BadRequestException("name must match URL");
     }
 
-    final CreateProjectArgs args = new CreateProjectArgs();
-    args.setProjectName(name);
+    CreateProjectArgs args = new CreateProjectArgs();
+    args.setProjectName(ProjectUtil.stripGitSuffix(name));
+
     String parentName = MoreObjects.firstNonNull(
         Strings.emptyToNull(input.parent), allProjects.get());
-    args.newParent = projectsCollection.get().parse(parentName).getControl();
+    args.newParent =
+        projectsCollection.get().parse(parentName, false).getControl();
     args.createEmptyCommit = input.createEmptyCommit;
     args.permissionsOnly = input.permissionsOnly;
     args.projectDescription = Strings.emptyToNull(input.description);
     args.submitType = input.submitType;
-    args.branch = input.branches;
-    if (input.owners != null) {
-      List<AccountGroup.UUID> ownerIds =
-          Lists.newArrayListWithCapacity(input.owners.size());
+    args.branch = normalizeBranchNames(input.branches);
+    if (input.owners == null || input.owners.isEmpty()) {
+      args.ownerIds =
+          new ArrayList<>(projectOwnerGroups.create(args.getProject()).get());
+    } else {
+      args.ownerIds =
+        Lists.newArrayListWithCapacity(input.owners.size());
       for (String owner : input.owners) {
-        ownerIds.add(groupsCollection.get().parse(owner).getGroupUUID());
+        args.ownerIds.add(groupsCollection.get().parse(owner).getGroupUUID());
       }
-      args.ownerIds = ownerIds;
     }
     args.contributorAgreements =
         MoreObjects.firstNonNull(input.useContributorAgreements,
@@ -150,7 +209,7 @@
       }
     }
 
-    Project p = createProjectFactory.create(args).createProject();
+    Project p = createProject(args);
 
     if (input.pluginConfigValues != null) {
       try {
@@ -166,4 +225,169 @@
 
     return Response.created(json.format(p));
   }
+
+  public Project createProject(CreateProjectArgs args)
+      throws BadRequestException, ResourceConflictException, IOException,
+      ConfigInvalidException {
+    final Project.NameKey nameKey = args.getProject();
+    try {
+      final String head =
+          args.permissionsOnly ? RefNames.REFS_CONFIG
+              : args.branch.get(0);
+      try (Repository repo = repoManager.openRepository(nameKey)) {
+        if (repo.getObjectDatabase().exists()) {
+          throw new ResourceConflictException("project \"" + nameKey + "\" exists");
+        }
+      } catch (RepositoryNotFoundException e) {
+        // It does not exist, safe to ignore.
+      }
+      try (Repository repo = repoManager.createRepository(nameKey)) {
+        RefUpdate u = repo.updateRef(Constants.HEAD);
+        u.disableRefLog();
+        u.link(head);
+
+        createProjectConfig(args);
+
+        if (!args.permissionsOnly
+            && args.createEmptyCommit) {
+          createEmptyCommits(repo, nameKey, args.branch);
+        }
+
+        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return nameKey.get();
+          }
+
+          @Override
+          public String getHeadName() {
+            return head;
+          }
+        };
+        for (NewProjectCreatedListener l : createdListener) {
+          try {
+            l.onNewProjectCreated(event);
+          } catch (RuntimeException e) {
+            log.warn("Failure in NewProjectCreatedListener", e);
+          }
+        }
+
+        return projectCache.get(nameKey).getProject();
+      }
+    } catch (RepositoryCaseMismatchException e) {
+      throw new ResourceConflictException("Cannot create " + nameKey.get()
+          + " because the name is already occupied by another project."
+          + " The other project has the same name, only spelled in a"
+          + " different case.");
+    } catch (RepositoryNotFoundException badName) {
+      throw new BadRequestException("invalid project name: " + nameKey);
+    } catch (ConfigInvalidException e) {
+      String msg = "Cannot create " + nameKey;
+      log.error(msg, e);
+      throw e;
+    }
+  }
+
+  private void createProjectConfig(CreateProjectArgs args) throws IOException, ConfigInvalidException {
+    MetaDataUpdate md =
+        metaDataUpdateFactory.create(args.getProject());
+    try {
+      ProjectConfig config = ProjectConfig.read(md);
+      config.load(md);
+
+      Project newProject = config.getProject();
+      newProject.setDescription(args.projectDescription);
+      newProject.setSubmitType(MoreObjects.firstNonNull(args.submitType,
+          repositoryCfg.getDefaultSubmitType(args.getProject())));
+      newProject
+          .setUseContributorAgreements(args.contributorAgreements);
+      newProject.setUseSignedOffBy(args.signedOffBy);
+      newProject.setUseContentMerge(args.contentMerge);
+      newProject.setCreateNewChangeForAllNotInTarget(args.newChangeForAllNotInTarget);
+      newProject.setRequireChangeID(args.changeIdRequired);
+      newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
+      if (args.newParent != null) {
+        newProject.setParentName(args.newParent.getProject()
+            .getNameKey());
+      }
+
+      if (!args.ownerIds.isEmpty()) {
+        AccessSection all =
+            config.getAccessSection(AccessSection.ALL, true);
+        for (AccountGroup.UUID ownerId : args.ownerIds) {
+          GroupDescription.Basic g = groupBackend.get(ownerId);
+          if (g != null) {
+            GroupReference group = config.resolve(GroupReference.forGroup(g));
+            all.getPermission(Permission.OWNER, true).add(
+                new PermissionRule(group));
+          }
+        }
+      }
+
+      md.setMessage("Created project\n");
+      config.commit(md);
+    } finally {
+      md.close();
+    }
+    projectCache.onCreateProject(args.getProject());
+    repoManager.setProjectDescription(args.getProject(),
+        args.projectDescription);
+  }
+
+  private List<String> normalizeBranchNames(List<String> branches)
+      throws BadRequestException {
+    if (branches == null || branches.isEmpty()) {
+      return Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
+    }
+
+    List<String> normalizedBranches = new ArrayList<>();
+    for (String branch : branches) {
+      while (branch.startsWith("/")) {
+        branch = branch.substring(1);
+      }
+      branch = RefNames.fullName(branch);
+      if (!Repository.isValidRefName(branch)) {
+        throw new BadRequestException(String.format(
+            "Branch \"%s\" is not a valid name.", branch));
+      }
+      if (!normalizedBranches.contains(branch)) {
+        normalizedBranches.add(branch);
+      }
+    }
+    return normalizedBranches;
+  }
+
+  private void createEmptyCommits(Repository repo, Project.NameKey project,
+      List<String> refs) throws IOException {
+    try (ObjectInserter oi = repo.newObjectInserter()) {
+      CommitBuilder cb = new CommitBuilder();
+      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
+      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
+      cb.setCommitter(serverIdent);
+      cb.setMessage("Initial empty repository\n");
+
+      ObjectId id = oi.insert(cb);
+      oi.flush();
+
+      for (String ref : refs) {
+        RefUpdate ru = repo.updateRef(ref);
+        ru.setNewObjectId(id);
+        Result result = ru.update();
+        switch (result) {
+          case NEW:
+            referenceUpdated.fire(project, ru, ReceiveCommand.Type.CREATE);
+            break;
+          default: {
+            throw new IOException(String.format(
+              "Failed to create ref \"%s\": %s", ref, result.name()));
+          }
+        }
+      }
+    } catch (IOException e) {
+      log.error(
+          "Cannot create empty commit for "
+              + project.get(), e);
+      throw e;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 7822fa6..4646e3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -100,17 +100,14 @@
       throw new ResourceNotFoundException(id);
     }
 
-    CurrentUser user = myCtl.getCurrentUser();
+    CurrentUser user = myCtl.getUser();
     String ref = parts.get(0);
     String path = parts.get(1);
     for (ProjectState ps : myCtl.getProjectState().tree()) {
       try {
         return parse(ps.controlFor(user), ref, path, myCtl);
-      } catch (AmbiguousObjectException e) {
-        throw new ResourceNotFoundException(id);
-      } catch (IncorrectObjectTypeException e) {
-        throw new ResourceNotFoundException(id);
-      } catch (ConfigInvalidException e) {
+      } catch (AmbiguousObjectException | ConfigInvalidException
+          | IncorrectObjectTypeException e) {
         throw new ResourceNotFoundException(id);
       } catch (ResourceNotFoundException e) {
         continue;
@@ -132,21 +129,15 @@
       throw new ResourceNotFoundException(id);
     }
 
-    Repository git;
-    try {
-      git = gitManager.openRepository(ctl.getProject().getNameKey());
-    } catch (RepositoryNotFoundException e) {
-      throw new ResourceNotFoundException(id);
-    }
-    try {
+    try (Repository git = gitManager.openRepository(ctl.getProject().getNameKey())) {
       ObjectId objId = git.resolve(ref + ":" + path);
       if (objId == null) {
         throw new ResourceNotFoundException(id);
       }
       BlobBasedConfig cfg = new BlobBasedConfig(null, git, objId);
       return new DashboardResource(myCtl, ref, path, cfg, false);
-    } finally {
-      git.close();
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException(id);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 4aba333..2aa6b0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -35,6 +35,7 @@
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,7 +47,7 @@
   private static final int MAX_LOCK_FAILURE_CALLS = 10;
   private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
 
-  static class Input {
+  public static class Input {
   }
 
   private final Provider<IdentifiedUser> identifiedUser;
@@ -81,8 +82,7 @@
           + " has open changes");
     }
 
-    Repository r = repoManager.openRepository(rsrc.getNameKey());
-    try {
+    try (Repository r = repoManager.openRepository(rsrc.getNameKey())) {
       RefUpdate.Result result;
       RefUpdate u = r.updateRef(rsrc.getRef());
       u.setForceUpdate(true);
@@ -113,7 +113,7 @@
         case NO_CHANGE:
         case FAST_FORWARD:
         case FORCED:
-          referenceUpdated.fire(rsrc.getNameKey(), u);
+          referenceUpdated.fire(rsrc.getNameKey(), u, ReceiveCommand.Type.DELETE);
           hooks.doRefUpdatedHook(rsrc.getBranchKey(), u, identifiedUser.get().getAccount());
           ResultSet<SubmoduleSubscription> submoduleSubscriptions =
             dbProvider.get().submoduleSubscriptions().bySuperProject(rsrc.getBranchKey());
@@ -128,8 +128,6 @@
           log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
           throw new ResourceConflictException("cannot delete branch: " + result.name());
       }
-    } finally {
-      r.close();
     }
     return Response.none();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index d6e93f0..e420771 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -93,8 +93,7 @@
   public Response<?> apply(ProjectResource project, Input input)
       throws OrmException, IOException, ResourceConflictException {
     input = Input.init(input);
-    Repository r = repoManager.openRepository(project.getNameKey());
-    try {
+    try (Repository r = repoManager.openRepository(project.getNameKey())) {
       BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate();
       for (String branch : input.branches) {
         batchUpdate.addCommand(createDeleteCommand(project, r, branch));
@@ -113,8 +112,6 @@
       if (errorMessages.length() > 0) {
         throw new ResourceConflictException(errorMessages.toString());
       }
-    } finally {
-      r.close();
     }
     return Response.none();
   }
@@ -165,8 +162,7 @@
 
   private void postDeletion(ProjectResource project, ReceiveCommand cmd)
       throws OrmException {
-    referenceUpdated.fire(project.getNameKey(), cmd.getRefName(),
-        cmd.getOldId(), cmd.getNewId());
+    referenceUpdated.fire(project.getNameKey(), cmd);
     Branch.NameKey branchKey =
         new Branch.NameKey(project.getNameKey(), cmd.getRefName());
     hooks.doRefUpdatedHook(branchKey, cmd.getOldId(), cmd.getNewId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
index b525399..7702b7d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
-import com.google.gerrit.server.project.DeleteDashboard.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -30,11 +29,7 @@
 import java.io.IOException;
 
 @Singleton
-class DeleteDashboard implements RestModifyView<DashboardResource, Input> {
-  static class Input {
-    String commitMessage;
-  }
-
+class DeleteDashboard implements RestModifyView<DashboardResource, SetDashboard.Input> {
   private final Provider<SetDefaultDashboard> defaultSetter;
 
   @Inject
@@ -43,7 +38,7 @@
   }
 
   @Override
-  public Response<DashboardInfo> apply(DashboardResource resource, Input input)
+  public Response<DashboardInfo> apply(DashboardResource resource, SetDashboard.Input input)
       throws AuthException, BadRequestException, ResourceConflictException,
       ResourceNotFoundException, MethodNotAllowedException, IOException {
     if (resource.isProjectDefault()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
index fe1086b..03dc97c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
@@ -41,6 +41,7 @@
     UiAction<ProjectResource> {
   public static class Input {
     public boolean showProgress;
+    public boolean aggressive;
   }
 
   private final boolean canGC;
@@ -68,8 +69,10 @@
           }
         };
         try {
-          GarbageCollectionResult result = garbageCollectionFactory.create().run(
-              Collections.singletonList(rsrc.getNameKey()), input.showProgress ? writer : null);
+          GarbageCollectionResult result =
+              garbageCollectionFactory.create().run(
+                  Collections.singletonList(rsrc.getNameKey()), input.aggressive,
+                  input.showProgress ? writer : null);
           String msg = "Garbage collection completed successfully.";
           if (result.hasErrors()) {
             for (GarbageCollectionResult.Error e : result.getErrors()) {
@@ -97,7 +100,7 @@
         }
       }
     }.setContentType("text/plain")
-     .setCharacterEncoding(UTF_8.name())
+     .setCharacterEncoding(UTF_8)
      .disableGzip();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
index 59b15d8..78878a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.Singleton;
 
 @Singleton
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
index 815653f..7737d8c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
@@ -23,9 +23,12 @@
 
 public class GetChildProject implements RestReadView<ChildProjectResource> {
   @Option(name = "--recursive", usage = "to list child projects recursively")
-  private boolean recursive;
+  public void setRecursive(boolean recursive) {
+    this.recursive = recursive;
+  }
 
   private final ProjectJson json;
+  private boolean recursive;
 
   @Inject
   GetChildProject(ProjectJson json) {
@@ -38,6 +41,6 @@
     if (recursive || rsrc.isDirectChild()) {
       return json.format(rsrc.getChild().getProject());
     }
-    throw new ResourceNotFoundException(rsrc.getName());
+    throw new ResourceNotFoundException(rsrc.getChild().getProject().getName());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index bb91097..469da93 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
@@ -26,7 +27,7 @@
 
 @Singleton
 public class GetConfig implements RestReadView<ProjectResource> {
-
+  private final boolean serverEnableSignedPush;
   private final TransferConfig config;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
@@ -34,11 +35,13 @@
   private final DynamicMap<RestView<ProjectResource>> views;
 
   @Inject
-  public GetConfig(TransferConfig config,
+  public GetConfig(@EnableSignedPush boolean serverEnableSignedPush,
+      TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsNameProvider allProjects,
       DynamicMap<RestView<ProjectResource>> views) {
+    this.serverEnableSignedPush = serverEnableSignedPush;
     this.config = config;
     this.pluginConfigEntries = pluginConfigEntries;
     this.allProjects = allProjects;
@@ -48,7 +51,7 @@
 
   @Override
   public ConfigInfo apply(ProjectResource resource) {
-    return new ConfigInfo(resource.getControl(), config,
+    return new ConfigInfo(serverEnableSignedPush, resource.getControl(), config,
         pluginConfigEntries, cfgFactory, allProjects, views);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
index 8acc29e..09555b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -90,7 +90,7 @@
       if ("default".equals(id)) {
         throw new ResourceNotFoundException();
       } else if (!Strings.isNullOrEmpty(id)) {
-        ctl = ps.controlFor(ctl.getCurrentUser());
+        ctl = ps.controlFor(ctl.getUser());
         return parse(ctl, id);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
index 5241c69..bace0a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
@@ -20,7 +20,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class GetDescription implements RestReadView<ProjectResource> {
+public class GetDescription implements RestReadView<ProjectResource> {
   @Override
   public String apply(ProjectResource resource) {
     Project project = resource.getControl().getProject();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
index 2efd257..f2b5fb8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -50,7 +50,7 @@
   public String apply(ProjectResource rsrc) throws AuthException,
       ResourceNotFoundException, IOException {
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
-      Ref head = repo.getRef(Constants.HEAD);
+      Ref head = repo.getRefDatabase().exactRef(Constants.HEAD);
       if (head == null) {
         throw new ResourceNotFoundException(Constants.HEAD);
       } else if (head.isSymbolic()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
index 7e52381..b957ba1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -78,8 +78,7 @@
       throw new AuthException("not project owner");
     }
 
-    Repository repo = repoManager.openRepository(rsrc.getNameKey());
-    try {
+    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       ReflogReader r = repo.getReflogReader(rsrc.getRef());
       if (r == null) {
         throw new ResourceNotFoundException(rsrc.getRef());
@@ -107,9 +106,8 @@
         @Override
         public ReflogEntryInfo apply(ReflogEntry e) {
           return new ReflogEntryInfo(e);
-        }});
-    } finally {
-      repo.close();
+        }
+      });
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
index 2b9d11f..723ee63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
@@ -45,18 +45,11 @@
   @Override
   public RepositoryStatistics apply(ProjectResource rsrc)
       throws ResourceNotFoundException, ResourceConflictException {
-    try {
-      Repository repo = repoManager.openRepository(rsrc.getNameKey());
-      try {
-        GarbageCollectCommand gc = Git.wrap(repo).gc();
-        return new RepositoryStatistics(gc.getStatistics());
-      } catch (GitAPIException e) {
-        throw new ResourceConflictException(e.getMessage());
-      } catch (JGitInternalException e) {
-        throw new ResourceConflictException(e.getMessage());
-      } finally {
-        repo.close();
-      }
+    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
+      GarbageCollectCommand gc = Git.wrap(repo).gc();
+      return new RepositoryStatistics(gc.getStatistics());
+    } catch (GitAPIException | JGitInternalException e) {
+      throw new ResourceConflictException(e.getMessage());
     } catch (IOException e) {
       throw new ResourceNotFoundException(rsrc.getName());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
index 5b78e08..a94d17e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index a8eda97..a50705d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -34,9 +33,6 @@
 import com.google.inject.Inject;
 import com.google.inject.util.Providers;
 
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -45,11 +41,10 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -59,15 +54,28 @@
   private final WebLinks webLinks;
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of branches to list")
-  private int limit;
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
 
   @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of branches to skip")
-  private int start;
+  public void setStart(int start) {
+    this.start = start;
+  }
 
   @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match branches substring")
-  private String matchSubstring;
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
 
   @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match branches regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
+  private int limit;
+  private int start;
+  private String matchSubstring;
   private String matchRegex;
 
   @Inject
@@ -82,175 +90,102 @@
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, IOException, BadRequestException {
-    List<BranchInfo> branches = Lists.newArrayList();
+    return new RefFilter<BranchInfo>(Constants.R_HEADS)
+        .subString(matchSubstring)
+        .regex(matchRegex)
+        .start(start)
+        .limit(limit)
+        .filter(allBranches(rsrc));
+  }
 
-    BranchInfo headBranch = null;
-    BranchInfo configBranch = null;
-    final Set<String> targets = Sets.newHashSet();
-
-    final Repository db;
-    try {
-      db = repoManager.openRepository(rsrc.getNameKey());
+  private List<BranchInfo> allBranches(ProjectResource rsrc)
+      throws IOException, ResourceNotFoundException {
+    List<Ref> refs;
+    try (Repository db = repoManager.openRepository(rsrc.getNameKey())) {
+      Collection<Ref> heads =
+          db.getRefDatabase().getRefs(Constants.R_HEADS).values();
+      refs = new ArrayList<>(heads.size() + 3);
+      refs.addAll(heads);
+      refs.addAll(db.getRefDatabase().exactRef(
+          Constants.HEAD,
+          RefNames.REFS_CONFIG,
+          RefNames.REFS_USERS_DEFAULT).values());
     } catch (RepositoryNotFoundException noGitRepository) {
       throw new ResourceNotFoundException();
     }
 
-    try {
-      List<Ref> refs =
-          new ArrayList<>(db.getRefDatabase().getRefs(Constants.R_HEADS)
-              .values());
-
-        try {
-          Ref head = db.getRef(Constants.HEAD);
-          if (head != null) {
-            refs.add(head);
-          }
-        } catch (IOException e) {
-          // Ignore the failure reading HEAD.
-        }
-        try {
-          Ref config = db.getRef(RefNames.REFS_CONFIG);
-          if (config != null) {
-            refs.add(config);
-          }
-        } catch (IOException e) {
-          // Ignore the failure reading refs/meta/config.
-        }
-
-      for (Ref ref : refs) {
-        if (ref.isSymbolic()) {
-          targets.add(ref.getTarget().getName());
-        }
+    Set<String> targets = Sets.newHashSetWithExpectedSize(1);
+    for (Ref ref : refs) {
+      if (ref.isSymbolic()) {
+        targets.add(ref.getTarget().getName());
       }
+    }
 
-      for (Ref ref : refs) {
-        if (ref.isSymbolic()) {
-          // A symbolic reference to another branch, instead of
-          // showing the resolved value, show the name it references.
-          //
-          String target = ref.getTarget().getName();
-          RefControl targetRefControl = rsrc.getControl().controlForRef(target);
-          if (!targetRefControl.isVisible()) {
-            continue;
-          }
-          if (target.startsWith(Constants.R_HEADS)) {
-            target = target.substring(Constants.R_HEADS.length());
-          }
-
-          BranchInfo b = new BranchInfo(ref.getName(), target, false);
-
-          if (Constants.HEAD.equals(ref.getName())) {
-            headBranch = b;
-          } else {
-            b.setCanDelete(targetRefControl.canDelete());
-            branches.add(b);
-          }
+    List<BranchInfo> branches = new ArrayList<>(refs.size());
+    for (Ref ref : refs) {
+      if (ref.isSymbolic()) {
+        // A symbolic reference to another branch, instead of
+        // showing the resolved value, show the name it references.
+        //
+        String target = ref.getTarget().getName();
+        RefControl targetRefControl = rsrc.getControl().controlForRef(target);
+        if (!targetRefControl.isVisible()) {
           continue;
         }
-
-        final RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
-        if (refControl.isVisible()) {
-          if (RefNames.REFS_CONFIG.equals(ref.getName())) {
-            configBranch = createBranchInfo(ref, refControl, targets);
-          } else {
-            branches.add(createBranchInfo(ref, refControl, targets));
-          }
+        if (target.startsWith(Constants.R_HEADS)) {
+          target = target.substring(Constants.R_HEADS.length());
         }
-      }
-    } finally {
-      db.close();
-    }
-    Collections.sort(branches, new Comparator<BranchInfo>() {
-      @Override
-      public int compare(final BranchInfo a, final BranchInfo b) {
-        return a.ref.compareTo(b.ref);
-      }
-    });
-    if (configBranch != null) {
-      branches.add(0, configBranch);
-    }
-    if (headBranch != null) {
-      branches.add(0, headBranch);
-    }
 
-    List<BranchInfo> filteredBranches;
-    if ((matchSubstring != null && !matchSubstring.isEmpty())
-        || (matchRegex != null && !matchRegex.isEmpty())) {
-      filteredBranches = filterBranches(branches);
-    } else {
-      filteredBranches = branches;
-    }
-    if (!filteredBranches.isEmpty()) {
-      int end = filteredBranches.size();
-      if (limit > 0 && start + limit < end) {
-        end = start + limit;
+        BranchInfo b = new BranchInfo();
+        b.ref = ref.getName();
+        b.revision = target;
+        branches.add(b);
+
+        if (!Constants.HEAD.equals(ref.getName())) {
+          b.canDelete = targetRefControl.canDelete() ? true : null;
+        }
+        continue;
       }
-      if (start <= end) {
-        filteredBranches = filteredBranches.subList(start, end);
-      } else {
-        filteredBranches = Collections.emptyList();
+
+      RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
+      if (refControl.isVisible()) {
+        branches.add(createBranchInfo(ref, refControl, targets));
       }
     }
-    return filteredBranches;
+    Collections.sort(branches, new BranchComparator());
+    return branches;
   }
 
-  private List<BranchInfo> filterBranches(List<BranchInfo> branches)
-      throws BadRequestException {
-    if (matchSubstring != null) {
-      return Lists.newArrayList(Iterables.filter(branches,
-          new Predicate<BranchInfo>() {
-            @Override
-            public boolean apply(BranchInfo in) {
-              if (!in.ref.startsWith(Constants.R_HEADS)){
-                return in.ref.toLowerCase(Locale.US).contains(
-                    matchSubstring.toLowerCase(Locale.US));
-              } else {
-                return in.ref.substring(Constants.R_HEADS.length())
-                    .toLowerCase(Locale.US)
-                    .contains(matchSubstring.toLowerCase(Locale.US));
-              }
-            }
-          }));
-    } else if (matchRegex != null) {
-      if (matchRegex.startsWith("^")) {
-        matchRegex = matchRegex.substring(1);
-        if (matchRegex.endsWith("$") && !matchRegex.endsWith("\\$")) {
-          matchRegex = matchRegex.substring(0, matchRegex.length() - 1);
-        }
-      }
-      if (matchRegex.equals(".*")) {
-        return branches;
-      }
-      try {
-        final RunAutomaton a =
-            new RunAutomaton(new RegExp(matchRegex).toAutomaton());
-        return Lists.newArrayList(Iterables.filter(
-            branches, new Predicate<BranchInfo>() {
-              @Override
-              public boolean apply(BranchInfo in) {
-                if (!in.ref.startsWith(Constants.R_HEADS)){
-                  return a.run(in.ref);
-                } else {
-                  return a.run(in.ref.substring(Constants.R_HEADS.length()));
-                }
-              }
-            }));
-      } catch (IllegalArgumentException e) {
-        throw new BadRequestException(e.getMessage());
-      }
+  private static class BranchComparator implements Comparator<BranchInfo> {
+    @Override
+    public int compare(BranchInfo a, BranchInfo b) {
+      return ComparisonChain.start()
+          .compareTrueFirst(isHead(a), isHead(b))
+          .compareTrueFirst(isConfig(a), isConfig(b))
+          .compare(a.ref, b.ref)
+          .result();
     }
-    return branches;
+
+    private static boolean isHead(BranchInfo i) {
+      return Constants.HEAD.equals(i.ref);
+    }
+
+    private static boolean isConfig(BranchInfo i) {
+      return RefNames.REFS_CONFIG.equals(i.ref);
+    }
   }
 
   private BranchInfo createBranchInfo(Ref ref, RefControl refControl,
       Set<String> targets) {
-    BranchInfo info = new BranchInfo(ref.getName(),
-        ref.getObjectId() != null ? ref.getObjectId().name() : null,
-        !targets.contains(ref.getName()) && refControl.canDelete());
+    BranchInfo info = new BranchInfo();
+    info.ref = ref.getName();
+    info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
+    info.canDelete = !targets.contains(ref.getName()) && refControl.canDelete()
+        ? true : null;
     for (UiAction.Description d : UiActions.from(
         branchViews,
         new BranchResource(refControl.getProjectControl(), info),
-        Providers.of(refControl.getCurrentUser()))) {
+        Providers.of(refControl.getUser()))) {
       if (info.actions == null) {
         info.actions = new TreeMap<>();
       }
@@ -262,22 +197,4 @@
     info.webLinks = links.isEmpty() ? null : links.toList();
     return info;
   }
-
-  public static class BranchInfo {
-    public String ref;
-    public String revision;
-    public Boolean canDelete;
-    public Map<String, ActionInfo> actions;
-    public List<WebLinkInfo> webLinks;
-
-    public BranchInfo(String ref, String revision, boolean canDelete) {
-      this.ref = ref;
-      this.revision = revision;
-      this.canDelete = canDelete;
-    }
-
-    void setCanDelete(boolean canDelete) {
-      this.canDelete = canDelete ? true : null;
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
index 8d1e95f..b2596be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -58,7 +58,7 @@
   public List<ProjectInfo> apply(ProjectResource rsrc) {
     if (recursive) {
       return getChildProjectsRecursively(rsrc.getNameKey(),
-          rsrc.getControl().getCurrentUser());
+          rsrc.getControl().getUser());
     } else {
       return getDirectChildProjects(rsrc.getNameKey());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index 0208829..b4bd9a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -64,7 +64,7 @@
     List<List<DashboardInfo>> all = Lists.newArrayList();
     boolean setDefault = true;
     for (ProjectState ps : ctl.getProjectState().tree()) {
-      ctl = ps.controlFor(ctl.getCurrentUser());
+      ctl = ps.controlFor(ctl.getUser());
       if (ctl.isVisible()) {
         List<DashboardInfo> list = scan(ctl, project, setDefault);
         for (DashboardInfo d : list) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 1f17e70..62e18fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
@@ -59,13 +61,13 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -90,7 +92,7 @@
     PERMISSIONS {
       @Override
       boolean matches(Repository git) throws IOException {
-        Ref head = git.getRef(Constants.HEAD);
+        Ref head = git.getRefDatabase().exactRef(Constants.HEAD);
         return head != null
           && head.isSymbolic()
           && RefNames.REFS_CONFIG.equals(head.getLeaf().getName());
@@ -234,31 +236,27 @@
       display(buf);
       return BinaryResult.create(buf.toByteArray())
           .setContentType("text/plain")
-          .setCharacterEncoding("UTF-8");
+          .setCharacterEncoding(UTF_8);
     }
     return apply();
   }
 
-  public Map<String, ProjectInfo> apply() throws BadRequestException {
+  public SortedMap<String, ProjectInfo> apply() throws BadRequestException {
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public Map<String, ProjectInfo> display(OutputStream displayOutputStream)
+  public SortedMap<String, ProjectInfo> display(OutputStream displayOutputStream)
       throws BadRequestException {
     PrintWriter stdout = null;
     if (displayOutputStream != null) {
-      try {
-        stdout = new PrintWriter(new BufferedWriter(
-            new OutputStreamWriter(displayOutputStream, "UTF-8")));
-      } catch (UnsupportedEncodingException e) {
-        throw new RuntimeException("JVM lacks UTF-8 encoding", e);
-      }
+      stdout = new PrintWriter(new BufferedWriter(
+          new OutputStreamWriter(displayOutputStream, UTF_8)));
     }
 
     int foundIndex = 0;
     int found = 0;
-    Map<String, ProjectInfo> output = Maps.newTreeMap();
+    TreeMap<String, ProjectInfo> output = Maps.newTreeMap();
     Map<String, String> hiddenNames = Maps.newHashMap();
     Set<String> rejected = new HashSet<>();
 
@@ -345,8 +343,7 @@
 
           try {
             if (!showBranch.isEmpty()) {
-              Repository git = repoManager.openRepository(projectName);
-              try {
+              try (Repository git = repoManager.openRepository(projectName)) {
                 if (!type.matches(git)) {
                   continue;
                 }
@@ -365,17 +362,12 @@
                     info.branches.put(showBranch.get(i), ref.getObjectId().name());
                   }
                 }
-              } finally {
-                git.close();
               }
             } else if (!showTree && type != FilterType.ALL) {
-              Repository git = repoManager.openRepository(projectName);
-              try {
+              try (Repository git = repoManager.openRepository(projectName)) {
                 if (!type.matches(git)) {
                   continue;
                 }
-              } finally {
-                git.close();
               }
             }
 
@@ -513,20 +505,15 @@
   private List<Ref> getBranchRefs(Project.NameKey projectName,
       ProjectControl projectControl) {
     Ref[] result = new Ref[showBranch.size()];
-    try {
-      Repository git = repoManager.openRepository(projectName);
-      try {
-        for (int i = 0; i < showBranch.size(); i++) {
-          Ref ref = git.getRef(showBranch.get(i));
-          if (ref != null
-            && ref.getObjectId() != null
-            && (projectControl.controlForRef(ref.getLeaf().getName()).isVisible())
-                || (all && projectControl.isOwner())) {
-            result[i] = ref;
-          }
+    try (Repository git = repoManager.openRepository(projectName)) {
+      for (int i = 0; i < showBranch.size(); i++) {
+        Ref ref = git.findRef(showBranch.get(i));
+        if (ref != null
+          && ref.getObjectId() != null
+          && (projectControl.controlForRef(ref.getLeaf().getName()).isVisible())
+              || (all && projectControl.isOwner())) {
+          result[i] = ref;
         }
-      } finally {
-        git.close();
       }
     } catch (IOException ioe) {
       // Fall through and return what is available.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 1a359c1..927d205 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -16,7 +16,8 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -40,6 +40,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -47,13 +48,37 @@
 import java.util.List;
 import java.util.Map;
 
-@Singleton
 public class ListTags implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final Provider<ReviewDb> dbProvider;
   private final TagCache tagCache;
   private final ChangeCache changeCache;
 
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of tags to list")
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
+
+  @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of tags to skip")
+  public void setStart(int start) {
+    this.start = start;
+  }
+
+  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match tags substring")
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
+
+  @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match tags regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
+  private int limit;
+  private int start;
+  private String matchSubstring;
+  private String matchRegex;
+
   @Inject
   public ListTags(GitRepositoryManager repoManager,
       Provider<ReviewDb> dbProvider,
@@ -67,24 +92,16 @@
 
   @Override
   public List<TagInfo> apply(ProjectResource resource) throws IOException,
-      ResourceNotFoundException {
+      ResourceNotFoundException, BadRequestException {
     List<TagInfo> tags = Lists.newArrayList();
 
-    Repository repo = getRepository(resource.getNameKey());
-
-    try {
-      RevWalk rw = new RevWalk(repo);
-      try {
-        Map<String, Ref> all = visibleTags(resource.getControl(), repo,
-            repo.getRefDatabase().getRefs(Constants.R_TAGS));
-        for (Ref ref : all.values()) {
-          tags.add(createTagInfo(ref, rw));
-        }
-      } finally {
-        rw.dispose();
+    try (Repository repo = getRepository(resource.getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      Map<String, Ref> all = visibleTags(resource.getControl(), repo,
+          repo.getRefDatabase().getRefs(Constants.R_TAGS));
+      for (Ref ref : all.values()) {
+        tags.add(createTagInfo(ref, rw));
       }
-    } finally {
-      repo.close();
     }
 
     Collections.sort(tags, new Comparator<TagInfo>() {
@@ -94,7 +111,12 @@
       }
     });
 
-    return tags;
+    return new RefFilter<TagInfo>(Constants.R_TAGS)
+        .start(start)
+        .limit(limit)
+        .subString(matchSubstring)
+        .regex(matchRegex)
+        .filter(tags);
   }
 
   public TagInfo get(ProjectResource resource, IdString id)
@@ -105,7 +127,7 @@
       if (!tagName.startsWith(Constants.R_TAGS)) {
         tagName = Constants.R_TAGS + tagName;
       }
-      Ref ref = repo.getRefDatabase().getRef(tagName);
+      Ref ref = repo.getRefDatabase().exactRef(tagName);
       if (ref != null && !visibleTags(resource.getControl(), repo,
           ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
         return createTagInfo(ref, rw);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 430d8f5..3ad6f8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -24,7 +24,6 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
   @Override
@@ -65,7 +64,7 @@
     get(BRANCH_KIND).to(GetBranch.class);
     delete(BRANCH_KIND).to(DeleteBranch.class);
     post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
-    install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
+    factory(CreateBranch.Factory.class);
     get(BRANCH_KIND, "reflog").to(GetReflog.class);
     child(BRANCH_KIND, "files").to(FilesCollection.class);
     get(FILE_KIND, "content").to(GetContent.class);
@@ -81,7 +80,7 @@
     get(DASHBOARD_KIND).to(GetDashboard.class);
     put(DASHBOARD_KIND).to(SetDashboard.class);
     delete(DASHBOARD_KIND).to(DeleteDashboard.class);
-    install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
+    factory(CreateProject.Factory.class);
 
     get(PROJECT_KIND, "config").to(GetConfig.class);
     put(PROJECT_KIND, "config").to(PutConfig.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
index aa6fb16..61b5c05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
@@ -21,6 +21,7 @@
   private static final long serialVersionUID = 1L;
 
   private static final String MESSAGE = "Project not found: ";
+  private final Project.NameKey project;
 
   public NoSuchProjectException(final Project.NameKey key) {
     this(key, null);
@@ -28,5 +29,10 @@
 
   public NoSuchProjectException(final Project.NameKey key, final Throwable why) {
     super(MESSAGE + key.toString(), why);
+    project = key;
+  }
+
+  public Project.NameKey project() {
+    return project;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
deleted file mode 100644
index 8b5e084..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
+++ /dev/null
@@ -1,304 +0,0 @@
-// Copyright (C) 2011 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.server.project;
-
-import com.google.common.base.MoreObjects;
-import com.google.gerrit.common.ProjectUtil;
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.ProjectCreationFailedException;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.events.NewProjectCreatedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.ProjectOwnerGroups;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.RepositoryCaseMismatchException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-
-/** Common class that holds the code to create projects */
-public class PerformCreateProject {
-  private static final Logger log = LoggerFactory
-      .getLogger(PerformCreateProject.class);
-
-  public interface Factory {
-    PerformCreateProject create(CreateProjectArgs createProjectArgs);
-  }
-
-  private final Config cfg;
-  private final Set<AccountGroup.UUID> projectOwnerGroups;
-  private final IdentifiedUser currentUser;
-  private final GitRepositoryManager repoManager;
-  private final GitReferenceUpdated referenceUpdated;
-  private final DynamicSet<NewProjectCreatedListener> createdListener;
-  private final PersonIdent serverIdent;
-  private final CreateProjectArgs createProjectArgs;
-  private final ProjectCache projectCache;
-  private final GroupBackend groupBackend;
-  private final MetaDataUpdate.User metaDataUpdateFactory;
-
-  @Inject
-  PerformCreateProject(@GerritServerConfig Config cfg,
-      @ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
-      IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
-      GitReferenceUpdated referenceUpdated,
-      DynamicSet<NewProjectCreatedListener> createdListener,
-      @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
-      MetaDataUpdate.User metaDataUpdateFactory,
-      @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
-    this.cfg = cfg;
-    this.projectOwnerGroups = pOwnerGroups;
-    this.currentUser = identifiedUser;
-    this.repoManager = gitRepoManager;
-    this.referenceUpdated = referenceUpdated;
-    this.createdListener = createdListener;
-    this.serverIdent = personIdent;
-    this.createProjectArgs = createPArgs;
-    this.projectCache = pCache;
-    this.groupBackend = groupBackend;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-  }
-
-  public Project createProject() throws ProjectCreationFailedException {
-    validateParameters();
-    final Project.NameKey nameKey = createProjectArgs.getProject();
-    try {
-      final String head =
-          createProjectArgs.permissionsOnly ? RefNames.REFS_CONFIG
-              : createProjectArgs.branch.get(0);
-      final Repository repo = repoManager.createRepository(nameKey);
-      try {
-        final RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-
-        createProjectConfig();
-
-        if (!createProjectArgs.permissionsOnly
-            && createProjectArgs.createEmptyCommit) {
-          createEmptyCommits(repo, nameKey, createProjectArgs.branch);
-        }
-
-        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
-          @Override
-          public String getProjectName() {
-            return nameKey.get();
-          }
-
-          @Override
-          public String getHeadName() {
-            return head;
-          }
-        };
-        for (NewProjectCreatedListener l : createdListener) {
-          try {
-            l.onNewProjectCreated(event);
-          } catch (RuntimeException e) {
-            log.warn("Failure in NewProjectCreatedListener", e);
-          }
-        }
-
-        return projectCache.get(nameKey).getProject();
-      } finally {
-        repo.close();
-      }
-    } catch (RepositoryCaseMismatchException e) {
-      throw new ProjectCreationFailedException("Cannot create " + nameKey.get()
-          + " because the name is already occupied by another project."
-          + " The other project has the same name, only spelled in a"
-          + " different case.", e);
-    } catch (RepositoryNotFoundException badName) {
-      throw new ProjectCreationFailedException("Cannot create " + nameKey, badName);
-    } catch (IllegalStateException err) {
-      try {
-        final Repository repo = repoManager.openRepository(nameKey);
-        try {
-          if (repo.getObjectDatabase().exists()) {
-            throw new ProjectCreationFailedException("project \"" + nameKey + "\" exists");
-          }
-          throw err;
-        } finally {
-          repo.close();
-        }
-      } catch (IOException ioErr) {
-        final String msg = "Cannot create " + nameKey;
-        log.error(msg, err);
-        throw new ProjectCreationFailedException(msg, ioErr);
-      }
-    } catch (Exception e) {
-      final String msg = "Cannot create " + nameKey;
-      log.error(msg, e);
-      throw new ProjectCreationFailedException(msg, e);
-    }
-  }
-
-  private void createProjectConfig() throws IOException, ConfigInvalidException {
-    final MetaDataUpdate md =
-        metaDataUpdateFactory.create(createProjectArgs.getProject());
-    try {
-      final ProjectConfig config = ProjectConfig.read(md);
-      config.load(md);
-
-      Project newProject = config.getProject();
-      newProject.setDescription(createProjectArgs.projectDescription);
-      newProject.setSubmitType(MoreObjects.firstNonNull(createProjectArgs.submitType,
-          cfg.getEnum("repository", "*", "defaultSubmitType", SubmitType.MERGE_IF_NECESSARY)));
-      newProject
-          .setUseContributorAgreements(createProjectArgs.contributorAgreements);
-      newProject.setUseSignedOffBy(createProjectArgs.signedOffBy);
-      newProject.setUseContentMerge(createProjectArgs.contentMerge);
-      newProject.setCreateNewChangeForAllNotInTarget(createProjectArgs.newChangeForAllNotInTarget);
-      newProject.setRequireChangeID(createProjectArgs.changeIdRequired);
-      newProject.setMaxObjectSizeLimit(createProjectArgs.maxObjectSizeLimit);
-      if (createProjectArgs.newParent != null) {
-        newProject.setParentName(createProjectArgs.newParent.getProject()
-            .getNameKey());
-      }
-
-      if (!createProjectArgs.ownerIds.isEmpty()) {
-        final AccessSection all =
-            config.getAccessSection(AccessSection.ALL, true);
-        for (AccountGroup.UUID ownerId : createProjectArgs.ownerIds) {
-          GroupDescription.Basic g = groupBackend.get(ownerId);
-          if (g != null) {
-            GroupReference group = config.resolve(GroupReference.forGroup(g));
-            all.getPermission(Permission.OWNER, true).add(
-                new PermissionRule(group));
-          }
-        }
-      }
-
-      md.setMessage("Created project\n");
-      config.commit(md);
-    } finally {
-      md.close();
-    }
-    projectCache.onCreateProject(createProjectArgs.getProject());
-    repoManager.setProjectDescription(createProjectArgs.getProject(),
-        createProjectArgs.projectDescription);
-  }
-
-  private void validateParameters() throws ProjectCreationFailedException {
-    if (createProjectArgs.getProjectName() == null
-        || createProjectArgs.getProjectName().isEmpty()) {
-      throw new ProjectCreationFailedException("Project name is required");
-    }
-
-    String nameWithoutSuffix = ProjectUtil.stripGitSuffix(createProjectArgs.getProjectName());
-    createProjectArgs.setProjectName(nameWithoutSuffix);
-
-    if (!currentUser.getCapabilities().canCreateProject()) {
-      throw new ProjectCreationFailedException(String.format(
-          "%s does not have \"Create Project\" capability.",
-          currentUser.getUserName()));
-    }
-
-    if (createProjectArgs.ownerIds == null
-        || createProjectArgs.ownerIds.isEmpty()) {
-      createProjectArgs.ownerIds = new ArrayList<>(projectOwnerGroups);
-    }
-
-    List<String> transformedBranches = new ArrayList<>();
-    if (createProjectArgs.branch == null ||
-        createProjectArgs.branch.isEmpty()) {
-      createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
-    }
-    for (String branch : createProjectArgs.branch) {
-      while (branch.startsWith("/")) {
-        branch = branch.substring(1);
-      }
-      if (!branch.startsWith(Constants.R_HEADS)) {
-        branch = Constants.R_HEADS + branch;
-      }
-      if (!Repository.isValidRefName(branch)) {
-        throw new ProjectCreationFailedException(String.format(
-            "Branch \"%s\" is not a valid name.", branch));
-      }
-      if (!transformedBranches.contains(branch)) {
-        transformedBranches.add(branch);
-      }
-    }
-    createProjectArgs.branch = transformedBranches;
-  }
-
-  private void createEmptyCommits(final Repository repo,
-      final Project.NameKey project, final List<String> refs)
-      throws IOException {
-    try (ObjectInserter oi = repo.newObjectInserter()) {
-      CommitBuilder cb = new CommitBuilder();
-      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
-      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
-      cb.setCommitter(serverIdent);
-      cb.setMessage("Initial empty repository\n");
-
-      ObjectId id = oi.insert(cb);
-      oi.flush();
-
-      for (String ref : refs) {
-        RefUpdate ru = repo.updateRef(ref);
-        ru.setNewObjectId(id);
-        final Result result = ru.update();
-        switch (result) {
-          case NEW:
-            referenceUpdated.fire(project, ru);
-            break;
-          default: {
-            throw new IOException(String.format(
-              "Failed to create ref \"%s\": %s", ref, result.name()));
-          }
-        }
-      }
-    } catch (IOException e) {
-      log.error(
-          "Cannot create empty commit for "
-              + createProjectArgs.getProjectName(), e);
-      throw e;
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 3b2dfbc..ad94d64 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -288,13 +288,10 @@
     @Override
     public ProjectState load(String projectName) throws Exception {
       Project.NameKey key = new Project.NameKey(projectName);
-      Repository git = mgr.openRepository(key);
-      try {
+      try (Repository git = mgr.openRepository(key)) {
         ProjectConfig cfg = new ProjectConfig(key);
         cfg.load(git);
         return projectStateFactory.create(cfg);
-      } finally {
-        git.close();
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 3bbcf71..9116dcc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -212,7 +211,7 @@
         public List<String> get() {
           List<String> r;
           if (user.isIdentifiedUser()) {
-            Set<String> emails = ((IdentifiedUser) user).getEmailAddresses();
+            Set<String> emails = user.asIdentifiedUser().getEmailAddresses();
             r = new ArrayList<>(emails.size() + 1);
             r.addAll(emails);
           } else {
@@ -232,7 +231,7 @@
     return ctl;
   }
 
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
@@ -258,7 +257,7 @@
 
   /** Can this user see this project exists? */
   public boolean isVisible() {
-    return (user instanceof InternalUser
+    return (user.isInternalUser()
         || canPerformOnAnyRef(Permission.READ)) && !isHidden();
   }
 
@@ -287,7 +286,7 @@
   }
 
   public boolean allRefsAreVisible(Set<String> ignore) {
-    return user instanceof InternalUser
+    return user.isInternalUser()
         || canPerformOnAllRefs(Permission.READ, ignore);
   }
 
@@ -350,21 +349,12 @@
     if (! (user.isIdentifiedUser())) {
       return new Capable("Must be logged in to verify Contributor Agreement");
     }
-    final IdentifiedUser iUser = (IdentifiedUser) user;
-
-    boolean hasContactInfo = !missing(iUser.getAccount().getFullName())
-        && !missing(iUser.getAccount().getPreferredEmail())
-        && iUser.getAccount().isContactFiled();
+    final IdentifiedUser iUser = user.asIdentifiedUser();
 
     List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
-    List<AccountGroup.UUID> missingInfoGroupIds = Lists.newArrayList();
     for (ContributorAgreement ca : contributorAgreements) {
       List<AccountGroup.UUID> groupIds;
-      if (hasContactInfo || !ca.isRequireContactInformation()) {
-        groupIds = okGroupIds;
-      } else {
-        groupIds = missingInfoGroupIds;
-      }
+      groupIds = okGroupIds;
 
       for (PermissionRule rule : ca.getAccepted()) {
         if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)
@@ -378,28 +368,6 @@
       return Capable.OK;
     }
 
-    if (iUser.getEffectiveGroups().containsAnyOf(missingInfoGroupIds)) {
-      final StringBuilder msg = new StringBuilder();
-      for (ContributorAgreement ca : contributorAgreements) {
-        if (ca.isRequireContactInformation()) {
-          msg.append(ca.getName());
-          break;
-        }
-      }
-      msg.append(" contributor agreement requires");
-      msg.append(" current contact information.\n");
-      if (canonicalWebUrl != null) {
-        msg.append("\nPlease review your contact information");
-        msg.append(":\n\n  ");
-        msg.append(canonicalWebUrl);
-        msg.append("#");
-        msg.append(PageLinks.SETTINGS_CONTACT);
-        msg.append("\n");
-      }
-      msg.append("\n");
-      return new Capable(msg.toString());
-    }
-
     final StringBuilder msg = new StringBuilder();
     msg.append(" A Contributor Agreement must be completed before uploading");
     if (canonicalWebUrl != null) {
@@ -415,10 +383,6 @@
     return new Capable(msg.toString());
   }
 
-  private static boolean missing(final String value) {
-    return value == null || value.trim().equals("");
-  }
-
   private boolean canPerformOnAnyRef(String permissionName) {
     for (SectionMatcher matcher : access()) {
       AccessSection section = matcher.section;
@@ -535,14 +499,9 @@
   }
 
   public boolean canReadCommit(ReviewDb db, RevWalk rw, RevCommit commit) {
-    try {
-      Repository repo = openRepository();
-      try {
-        return isMergedIntoVisibleRef(repo, db, rw, commit,
-            repo.getAllRefs().values());
-      } finally {
-        repo.close();
-      }
+    try (Repository repo = openRepository()) {
+      return isMergedIntoVisibleRef(repo, db, rw, commit,
+          repo.getAllRefs().values());
     } catch (IOException e) {
       String msg = String.format(
           "Cannot verify permissions to commit object %s in repository %s",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 558b572..7094828 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.io.Files;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
@@ -46,7 +45,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
 import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -55,9 +54,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -163,17 +163,12 @@
   }
 
   private boolean isRevisionOutOfDate() {
-    try {
-      Repository git = gitMgr.openRepository(getProject().getNameKey());
-      try {
-        Ref ref = git.getRef(RefNames.REFS_CONFIG);
-        if (ref == null || ref.getObjectId() == null) {
-          return true;
-        }
-        return !ref.getObjectId().equals(config.getRevision());
-      } finally {
-        git.close();
+    try (Repository git = gitMgr.openRepository(getProject().getNameKey())) {
+      Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG);
+      if (ref == null || ref.getObjectId() == null) {
+        return true;
       }
+      return !ref.getObjectId().equals(config.getRevision());
     } catch (IOException gone) {
       return true;
     }
@@ -205,10 +200,10 @@
    * read the provided input stream.
    *
    * @param name a name of the input stream. Could be any name.
-   * @param in InputStream to read prolog rules from
+   * @param in stream to read prolog rules from
    * @throws CompileException
    */
-  public PrologEnvironment newPrologEnvironment(String name, InputStream in)
+  public PrologEnvironment newPrologEnvironment(String name, Reader in)
       throws CompileException {
     PrologMachineCopy pmc = rulesCache.loadMachine(name, in);
     return envFactory.create(pmc);
@@ -228,16 +223,9 @@
     }
 
     ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
-    try {
-      Repository git = gitMgr.openRepository(getProject().getNameKey());
-      try {
-        cfg.load(git);
-      } finally {
-        git.close();
-      }
-    } catch (IOException e) {
-      log.warn("Failed to load " + fileName + " for " + getProject().getName(), e);
-    } catch (ConfigInvalidException e) {
+    try (Repository git = gitMgr.openRepository(getProject().getNameKey())) {
+      cfg.load(git);
+    } catch (IOException | ConfigInvalidException e) {
       log.warn("Failed to load " + fileName + " for " + getProject().getName(), e);
     }
 
@@ -416,6 +404,24 @@
     });
   }
 
+  public boolean isEnableSignedPush() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getEnableSignedPush();
+      }
+    });
+  }
+
+  public boolean isRequireSignedPush() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getRequireSignedPush();
+      }
+    });
+  }
+
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
     for (ProjectState s : treeInOrder()) {
@@ -488,25 +494,25 @@
 
   private ThemeInfo loadTheme() {
     String name = getConfig().getProject().getName();
-    File dir = new File(sitePaths.themes_dir, name);
-    if (!dir.exists()) {
+    Path dir = sitePaths.themes_dir.resolve(name);
+    if (!Files.exists(dir)) {
       return ThemeInfo.INHERIT;
-    } else if (!dir.isDirectory()) {
+    } else if (!Files.isDirectory(dir)) {
       log.warn("Bad theme for {}: not a directory", name);
       return ThemeInfo.INHERIT;
     }
     try {
-      return new ThemeInfo(readFile(new File(dir, SitePaths.CSS_FILENAME)),
-          readFile(new File(dir, SitePaths.HEADER_FILENAME)),
-          readFile(new File(dir, SitePaths.FOOTER_FILENAME)));
+      return new ThemeInfo(readFile(dir.resolve(SitePaths.CSS_FILENAME)),
+          readFile(dir.resolve(SitePaths.HEADER_FILENAME)),
+          readFile(dir.resolve(SitePaths.FOOTER_FILENAME)));
     } catch (IOException e) {
       log.error("Error reading theme for " + name, e);
       return ThemeInfo.INHERIT;
     }
   }
 
-  private String readFile(File f) throws IOException {
-    return f.exists() ? Files.toString(f, UTF_8) : null;
+  private String readFile(Path p) throws IOException {
+    return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
   }
 
   private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
index 0ffbe3e..51603fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -63,7 +63,7 @@
   @Override
   public ProjectResource parse(TopLevelResource parent, IdString id)
       throws ResourceNotFoundException, IOException {
-    ProjectResource rsrc = _parse(id.get());
+    ProjectResource rsrc = _parse(id.get(), true);
     if (rsrc == null) {
       throw new ResourceNotFoundException(id);
     }
@@ -81,7 +81,24 @@
    */
   public ProjectResource parse(String id)
       throws UnprocessableEntityException, IOException {
-    ProjectResource rsrc = _parse(id);
+    return parse(id, true);
+  }
+
+  /**
+   * Parses a project ID from a request body and returns the project.
+   *
+   * @param id ID of the project, can be a project name
+   * @param checkVisibility Whether to check or not that project is visible to
+   *        the calling user
+   * @return the project
+   * @throws UnprocessableEntityException thrown if the project ID cannot be
+   *         resolved or if the project is not visible to the calling user and
+   *         checkVisibility is true.
+   * @throws IOException thrown when there is an error.
+   */
+  public ProjectResource parse(String id, boolean checkVisibility)
+      throws UnprocessableEntityException, IOException {
+    ProjectResource rsrc = _parse(id, checkVisibility);
     if (rsrc == null) {
       throw new UnprocessableEntityException(String.format(
           "Project Not Found: %s", id));
@@ -89,7 +106,8 @@
     return rsrc;
   }
 
-  private ProjectResource _parse(String id) throws IOException {
+  private ProjectResource _parse(String id, boolean checkVisibility)
+      throws IOException {
     if (id.endsWith(Constants.DOT_GIT_EXT)) {
       id = id.substring(0, id.length() - Constants.DOT_GIT_EXT.length());
     }
@@ -101,7 +119,7 @@
     } catch (NoSuchProjectException e) {
       return null;
     }
-    if (!ctl.isVisible() && !ctl.isOwner()) {
+    if (checkVisibility && !ctl.isVisible() && !ctl.isOwner()) {
       return null;
     }
     return new ProjectResource(ctl);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index f22fb1e..76ad2f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -31,7 +31,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -61,6 +61,7 @@
 @Singleton
 public class PutConfig implements RestModifyView<ProjectResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(PutConfig.class);
+
   public static class Input {
     public String description;
     public InheritableBoolean useContributorAgreements;
@@ -68,12 +69,15 @@
     public InheritableBoolean useSignedOffBy;
     public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
+    public InheritableBoolean enableSignedPush;
+    public InheritableBoolean requireSignedPush;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
     public com.google.gerrit.extensions.client.ProjectState state;
     public Map<String, Map<String, ConfigValue>> pluginConfigValues;
   }
 
+  private final boolean serverEnableSignedPush;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final GitRepositoryManager gitMgr;
@@ -83,11 +87,12 @@
   private final PluginConfigFactory cfgFactory;
   private final AllProjectsNameProvider allProjects;
   private final DynamicMap<RestView<ProjectResource>> views;
-  private final Provider<CurrentUser> currentUser;
+  private final Provider<CurrentUser> user;
   private final ChangeHooks hooks;
 
   @Inject
-  PutConfig(Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+  PutConfig(@EnableSignedPush boolean serverEnableSignedPush,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       GitRepositoryManager gitMgr,
       ProjectState.Factory projectStateFactory,
@@ -97,7 +102,8 @@
       AllProjectsNameProvider allProjects,
       DynamicMap<RestView<ProjectResource>> views,
       ChangeHooks hooks,
-      Provider<CurrentUser> currentUser) {
+      Provider<CurrentUser> user) {
+    this.serverEnableSignedPush = serverEnableSignedPush;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.gitMgr = gitMgr;
@@ -108,7 +114,7 @@
     this.allProjects = allProjects;
     this.views = views;
     this.hooks = hooks;
-    this.currentUser = currentUser;
+    this.user = user;
   }
 
   @Override
@@ -161,6 +167,15 @@
         p.setRequireChangeID(input.requireChangeId);
       }
 
+      if (serverEnableSignedPush) {
+        if (input.enableSignedPush != null) {
+          p.setEnableSignedPush(input.enableSignedPush);
+        }
+        if (input.requireSignedPush != null) {
+          p.setRequireSignedPush(input.requireSignedPush);
+        }
+      }
+
       if (input.maxObjectSizeLimit != null) {
         p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
       }
@@ -184,10 +199,9 @@
         ObjectId commitRev = projectConfig.commit(md);
         // Only fire hook if project was actually changed.
         if (!Objects.equals(baseRev, commitRev)) {
-          IdentifiedUser user = (IdentifiedUser) currentUser.get();
           hooks.doRefUpdatedHook(
             new Branch.NameKey(projectName, RefNames.REFS_CONFIG),
-            baseRev, commitRev, user.getAccount());
+            baseRev, commitRev, user.get().asIdentifiedUser().getAccount());
         }
         projectCache.evict(projectConfig.getProject());
         gitMgr.setProjectDescription(projectName, p.getDescription());
@@ -203,8 +217,9 @@
       }
 
       ProjectState state = projectStateFactory.create(projectConfig);
-      return new ConfigInfo(state.controlFor(currentUser.get()), config,
-          pluginConfigEntries, cfgFactory, allProjects, views);
+      return new ConfigInfo(serverEnableSignedPush,
+          state.controlFor(user.get()), config, pluginConfigEntries,
+          cfgFactory, allProjects, views);
     } catch (ConfigInvalidException err) {
       throw new ResourceConflictException("Cannot read project " + projectName, err);
     } catch (IOException err) {
@@ -239,7 +254,9 @@
           if (Strings.emptyToNull(value) != null) {
             if (!value.equals(oldValue)) {
               validateProjectConfigEntryIsEditable(projectConfigEntry,
-                  projectState, e.getKey(), pluginName);
+                  projectState, v.getKey(), pluginName);
+              v.setValue(projectConfigEntry.preUpdate(v.getValue()));
+              value = v.getValue().value;
               try {
                 switch (projectConfigEntry.getType()) {
                   case BOOLEAN:
@@ -281,7 +298,7 @@
           } else {
             if (oldValue != null) {
               validateProjectConfigEntryIsEditable(projectConfigEntry,
-                  projectState, e.getKey(), pluginName);
+                  projectState, v.getKey(), pluginName);
               cfg.unset(v.getKey());
             }
           }
@@ -305,7 +322,7 @@
   }
 
   private static boolean isValidParameterName(String name) {
-    return CharMatcher.JAVA_LETTER_OR_DIGIT
+    return CharMatcher.javaLetterOrDigit()
         .or(CharMatcher.is('-'))
         .matchesAllOf(name) && !name.startsWith("-");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
index 536bfa7..d589865 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -17,8 +17,8 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.PutDescription.Input;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -42,13 +41,7 @@
 import java.util.Objects;
 
 @Singleton
-class PutDescription implements RestModifyView<ProjectResource, Input> {
-  static class Input {
-    @DefaultInput
-    String description;
-    String commitMessage;
-  }
-
+public class PutDescription implements RestModifyView<ProjectResource, PutDescriptionInput> {
   private final ProjectCache cache;
   private final MetaDataUpdate.Server updateFactory;
   private final GitRepositoryManager gitMgr;
@@ -66,15 +59,15 @@
   }
 
   @Override
-  public Response<String> apply(ProjectResource resource, Input input)
-      throws AuthException, ResourceConflictException,
-      ResourceNotFoundException, IOException {
+  public Response<String> apply(ProjectResource resource,
+      PutDescriptionInput input) throws AuthException,
+      ResourceConflictException, ResourceNotFoundException, IOException {
     if (input == null) {
-      input = new Input(); // Delete would set description to null.
+      input = new PutDescriptionInput(); // Delete would set description to null.
     }
 
     ProjectControl ctl = resource.getControl();
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     if (!ctl.isOwner()) {
       throw new AuthException("not project owner");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 28cd868..964554d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -26,8 +26,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
 
 import dk.brics.automaton.RegExp;
@@ -88,8 +86,8 @@
     return projectControl;
   }
 
-  public CurrentUser getCurrentUser() {
-    return projectControl.getCurrentUser();
+  public CurrentUser getUser() {
+    return projectControl.getUser();
   }
 
   public RefControl forUser(CurrentUser who) {
@@ -118,7 +116,7 @@
   public boolean isVisible() {
     if (isVisible == null) {
       isVisible =
-          (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
+          (getUser().isInternalUser() || canPerform(Permission.READ))
               && canRead();
     }
     return isVisible;
@@ -207,7 +205,7 @@
       // this why for the AllProjects project we allow administrators to push
       // configuration changes if they have push without being project owner.
       if (!(projectControl.getProjectState().isAllProjects() &&
-          getCurrentUser().getCapabilities().canAdministrateServer())) {
+          getUser().getCapabilities().canAdministrateServer())) {
         return false;
       }
     }
@@ -258,11 +256,12 @@
     }
     boolean owner;
     boolean admin;
-    switch (getCurrentUser().getAccessPath()) {
+    switch (getUser().getAccessPath()) {
       case REST_API:
       case JSON_RPC:
+      case UNKNOWN:
         owner = isOwner();
-        admin = getCurrentUser().getCapabilities().canAdministrateServer();
+        admin = getUser().getCapabilities().canAdministrateServer();
         break;
 
       default:
@@ -302,10 +301,9 @@
       final PersonIdent tagger = tag.getTaggerIdent();
       if (tagger != null) {
         boolean valid;
-        if (getCurrentUser().isIdentifiedUser()) {
-          final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
+        if (getUser().isIdentifiedUser()) {
           final String addr = tagger.getEmailAddress();
-          valid = user.hasEmailAddress(addr);
+          valid = getUser().asIdentifiedUser().hasEmailAddress(addr);
         } else {
           valid = false;
         }
@@ -329,17 +327,12 @@
 
   private boolean isMergedIntoBranchOrTag(ReviewDb db, RevWalk rw,
       RevCommit commit) {
-    try {
-      Repository repo = projectControl.openRepository();
-      try {
-        List<Ref> refs = new ArrayList<>(
-            repo.getRefDatabase().getRefs(Constants.R_HEADS).values());
-        refs.addAll(repo.getRefDatabase().getRefs(Constants.R_TAGS).values());
-        return projectControl.isMergedIntoVisibleRef(
-            repo, db, rw, commit, refs);
-      } finally {
-        repo.close();
-      }
+    try (Repository repo = projectControl.openRepository()) {
+      List<Ref> refs = new ArrayList<>(
+          repo.getRefDatabase().getRefs(Constants.R_HEADS).values());
+      refs.addAll(repo.getRefDatabase().getRefs(Constants.R_TAGS).values());
+      return projectControl.isMergedIntoVisibleRef(
+          repo, db, rw, commit, refs);
     } catch (IOException e) {
       String msg = String.format(
           "Cannot verify permissions to commit object %s in repository %s",
@@ -365,19 +358,14 @@
       return false;
     }
 
-    switch (getCurrentUser().getAccessPath()) {
-      case REST_API:
-      case JSON_RPC:
-      case SSH_COMMAND:
-        return getCurrentUser().getCapabilities().canAdministrateServer()
-            || (isOwner() && !isForceBlocked(Permission.PUSH))
-            || canPushWithForce();
-
+    switch (getUser().getAccessPath()) {
       case GIT:
         return canPushWithForce();
 
       default:
-        return false;
+        return getUser().getCapabilities().canAdministrateServer()
+            || (isOwner() && !isForceBlocked(Permission.PUSH))
+            || canPushWithForce();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java
new file mode 100644
index 0000000..63fb595
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefFilter.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2015 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.server.project;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+import com.google.gerrit.extensions.api.projects.RefInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+import java.util.List;
+import java.util.Locale;
+
+public class RefFilter<T extends RefInfo> {
+  private final String prefix;
+  private String matchSubstring;
+  private String matchRegex;
+  private int start;
+  private int limit;
+
+  public RefFilter(String prefix) {
+    this.prefix = prefix;
+  }
+
+  public RefFilter<T> subString(String subString) {
+    this.matchSubstring = subString;
+    return this;
+  }
+
+  public RefFilter<T> regex(String regex) {
+    this.matchRegex = regex;
+    return this;
+  }
+
+  public RefFilter<T> start(int start) {
+    this.start = start;
+    return this;
+  }
+
+  public RefFilter<T> limit(int limit) {
+    this.limit = limit;
+    return this;
+  }
+
+  public List<T> filter(List<T> refs) throws BadRequestException {
+    FluentIterable<T> results = FluentIterable.from(refs);
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      results = results.filter(new SubstringPredicate(matchSubstring));
+    } else if (!Strings.isNullOrEmpty(matchRegex)) {
+      results = results.filter(new RegexPredicate(matchRegex));
+    }
+    if (start > 0) {
+      results = results.skip(start);
+    }
+    if (limit > 0) {
+      results = results.limit(limit);
+    }
+    return results.toList();
+  }
+
+  private class SubstringPredicate implements Predicate<T> {
+    private final String substring;
+
+    private SubstringPredicate(String substring) {
+      this.substring = substring.toLowerCase(Locale.US);
+    }
+
+    @Override
+    public boolean apply(T in) {
+      String ref = in.ref;
+      if (ref.startsWith(prefix)) {
+        ref = ref.substring(prefix.length());
+      }
+      ref = ref.toLowerCase(Locale.US);
+      return ref.contains(substring);
+    }
+  }
+
+  private class RegexPredicate implements Predicate<T> {
+    private final RunAutomaton a;
+
+    private RegexPredicate(String regex) throws BadRequestException {
+      if (regex.startsWith("^")) {
+        regex = regex.substring(1);
+        if (regex.endsWith("$") && !regex.endsWith("\\$")) {
+          regex = regex.substring(0, regex.length() - 1);
+        }
+      }
+      try {
+        a = new RunAutomaton(new RegExp(regex).toAutomaton());
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
+
+    @Override
+    public boolean apply(T in) {
+      String ref = in.ref;
+      if (ref.startsWith(prefix)) {
+        ref = ref.substring(prefix.length());
+      }
+      return a.run(ref);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index 9009aad..7b6b5c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -86,7 +86,7 @@
 
       Collections.sort(sections, new MostSpecificComparator(ref));
 
-      int srcIdx[];
+      int[] srcIdx;
       if (isIdentityTransform(sections, srcMap)) {
         srcIdx = null;
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index b18b8ec..ac01de5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
@@ -68,7 +67,6 @@
     input.id = Strings.emptyToNull(input.id);
 
     ProjectControl ctl = resource.getControl();
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
     if (!ctl.isOwner()) {
       throw new AuthException("not project owner");
     }
@@ -105,7 +103,7 @@
         if (!msg.endsWith("\n")) {
           msg += "\n";
         }
-        md.setAuthor(user);
+        md.setAuthor(ctl.getUser().asIdentifiedUser());
         md.setMessage(msg);
         config.commit(md);
         cache.evict(ctl.getProject());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index 77c221c..25e2db3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.SetHead.Input;
@@ -32,20 +33,22 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.Map;
 
 @Singleton
 public class SetHead implements RestModifyView<ProjectResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(SetHead.class);
 
-  static class Input {
+  public static class Input {
     @DefaultInput
-    String ref;
+    public String ref;
   }
 
   private final GitRepositoryManager repoManager;
@@ -71,20 +74,17 @@
     if (input == null || Strings.isNullOrEmpty(input.ref)) {
       throw new BadRequestException("ref required");
     }
-    String ref = input.ref;
-    if (!ref.startsWith(Constants.R_REFS)) {
-      ref = Constants.R_HEADS + ref;
-    }
+    String ref = RefNames.fullName(input.ref);
 
-    Repository repo = null;
-    try {
-      repo = repoManager.openRepository(rsrc.getNameKey());
-      if (repo.getRef(ref) == null) {
+    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
+      Map<String, Ref> cur =
+          repo.getRefDatabase().exactRef(Constants.HEAD, ref);
+      if (!cur.containsKey(ref)) {
         throw new UnprocessableEntityException(String.format(
             "Ref Not Found: %s", ref));
       }
 
-      final String oldHead = repo.getRef(Constants.HEAD).getTarget().getName();
+      final String oldHead = cur.get(Constants.HEAD).getTarget().getName();
       final String newHead = ref;
       if (!oldHead.equals(newHead)) {
         final RefUpdate u = repo.updateRef(Constants.HEAD, true);
@@ -127,10 +127,6 @@
       return ref;
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(rsrc.getName());
-    } finally {
-      if (repo != null) {
-        repo.close();
-      }
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 5ec7dc8..284d419 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -62,14 +62,19 @@
   }
 
   @Override
-  public String apply(final ProjectResource rsrc, Input input)
+  public String apply(ProjectResource rsrc, Input input) throws AuthException,
+      ResourceConflictException, ResourceNotFoundException,
+      UnprocessableEntityException, IOException {
+    return apply(rsrc, input, true);
+  }
+
+  public String apply(ProjectResource rsrc, Input input, boolean checkIfAdmin)
       throws AuthException, ResourceConflictException,
       ResourceNotFoundException, UnprocessableEntityException, IOException {
     ProjectControl ctl = rsrc.getControl();
     String parentName = MoreObjects.firstNonNull(
         Strings.emptyToNull(input.parent), allProjects.get());
-    validateParentUpdate(ctl, parentName, true);
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    validateParentUpdate(ctl, parentName, checkIfAdmin);
     try {
       MetaDataUpdate md = updateFactory.create(rsrc.getNameKey());
       try {
@@ -84,7 +89,7 @@
         } else if (!msg.endsWith("\n")) {
           msg += "\n";
         }
-        md.setAuthor(user);
+        md.setAuthor(ctl.getUser().asIdentifiedUser());
         md.setMessage(msg);
         config.commit(md);
         cache.evict(ctl.getProject());
@@ -106,7 +111,7 @@
   public void validateParentUpdate(final ProjectControl ctl, String newParent,
       boolean checkIfAdmin) throws AuthException, ResourceConflictException,
       UnprocessableEntityException {
-    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     if (checkIfAdmin && !user.getCapabilities().canAdministrateServer()) {
       throw new AuthException("not administrator");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 6c6e3c2..da90566 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
@@ -27,13 +26,13 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.rules.PrologEnvironment;
-import com.google.gerrit.rules.ReductionLimitException;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
+import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
@@ -45,7 +44,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.ByteArrayInputStream;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -107,7 +106,7 @@
   private boolean skipFilters;
   private String rule;
   private boolean logErrors = true;
-  private int reductionsConsumed;
+  private long reductionsConsumed;
 
   private Term submitRule;
 
@@ -185,7 +184,7 @@
   }
 
   /** @return Prolog reductions consumed during evaluation. */
-  public int getReductionsConsumed() {
+  public long getReductionsConsumed() {
     return reductionsConsumed;
   }
 
@@ -221,7 +220,7 @@
     try {
       results = evaluateImpl("locate_submit_rule", "can_submit",
           "locate_submit_filter", "filter_submit_results",
-          control.getCurrentUser());
+          control.getUser());
     } catch (RuleEvalException e) {
       return ruleError(e.getMessage(), e);
     }
@@ -270,7 +269,7 @@
       SubmitRecord rec = new SubmitRecord();
       out.add(rec);
 
-      if (!submitRecord.isStructure() || 1 != submitRecord.arity()) {
+      if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
         return invalidResult(submitRule, submitRecord);
       }
 
@@ -289,14 +288,16 @@
       //
       submitRecord = submitRecord.arg(0);
 
-      if (!submitRecord.isStructure()) {
+      if (!(submitRecord instanceof StructureTerm)) {
         return invalidResult(submitRule, submitRecord);
       }
 
       rec.labels = new ArrayList<>(submitRecord.arity());
 
       for (Term state : ((StructureTerm) submitRecord).args()) {
-        if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
+        if (!(state instanceof StructureTerm)
+            || 2 != state.arity()
+            || !"label".equals(state.name())) {
           return invalidResult(submitRule, submitRecord);
         }
 
@@ -414,7 +415,7 @@
     }
 
     Term typeTerm = results.get(0);
-    if (!typeTerm.isSymbol()) {
+    if (!(typeTerm instanceof SymbolTerm)) {
       return typeError("Submit rule '" + getSubmitRule() + "' for change "
           + cd.getId() + " of " + getProjectName()
           + " did not return a symbol.");
@@ -485,9 +486,9 @@
             resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
       }
       List<Term> r;
-      if (resultsTerm.isList()) {
+      if (resultsTerm instanceof ListTerm) {
         r = Lists.newArrayList();
-        for (Term t = resultsTerm; t.isList();) {
+        for (Term t = resultsTerm; t instanceof ListTerm;) {
           ListTerm l = (ListTerm) t;
           r.add(l.car().dereference());
           t = l.cdr().dereference();
@@ -510,12 +511,20 @@
       if (rule == null) {
         env = projectState.newPrologEnvironment();
       } else {
-        env = projectState.newPrologEnvironment(
-            "stdin", new ByteArrayInputStream(rule.getBytes(UTF_8)));
+        env = projectState.newPrologEnvironment("stdin", new StringReader(rule));
       }
     } catch (CompileException err) {
-      throw new RuleEvalException("Cannot consult rules.pl for "
-          + getProjectName(), err);
+      String msg;
+      if (rule == null && control.getProjectControl().isOwner()) {
+        msg = String.format(
+            "Cannot load rules.pl for %s: %s",
+            getProjectName(), err.getMessage());
+      } else if (rule != null) {
+        msg = err.getMessage();
+      } else {
+        msg = String.format("Cannot load rules.pl for %s", getProjectName());
+      }
+      throw new RuleEvalException(msg, err);
     }
     env.set(StoredValues.REVIEW_DB, cd.db());
     env.set(StoredValues.CHANGE_DATA, cd);
@@ -578,7 +587,7 @@
 
   private void appliedBy(SubmitRecord.Label label, Term status)
       throws UserTermExpected {
-    if (status.isStructure() && status.arity() == 1) {
+    if (status instanceof StructureTerm && status.arity() == 1) {
       Term who = status.arg(0);
       if (isUser(who)) {
         label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
@@ -589,10 +598,10 @@
   }
 
   private static boolean isUser(Term who) {
-    return who.isStructure()
+    return who instanceof StructureTerm
         && who.arity() == 1
         && who.name().equals("user")
-        && who.arg(0).isInteger();
+        && who.arg(0) instanceof IntegerTerm;
   }
 
   public Term getSubmitRule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
index 12be5d3..afbd3be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
index 0c70285..1b6b888 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import java.io.IOException;
@@ -28,24 +29,24 @@
 public class TagsCollection implements
     ChildCollection<ProjectResource, TagResource> {
   private final DynamicMap<RestView<TagResource>> views;
-  private final ListTags list;
+  private final Provider<ListTags> list;
 
   @Inject
   public TagsCollection(DynamicMap<RestView<TagResource>> views,
-     ListTags list) {
+     Provider<ListTags> list) {
     this.views = views;
     this.list = list;
   }
 
   @Override
   public RestView<ProjectResource> list() throws ResourceNotFoundException {
-    return list;
+    return list.get();
   }
 
   @Override
   public TagResource parse(ProjectResource resource, IdString id)
       throws ResourceNotFoundException, IOException {
-    return new TagResource(resource.getControl(), list.get(resource, id));
+    return new TagResource(resource.getControl(), list.get().get(resource, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
index e3750fa..d336bb5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
@@ -39,8 +39,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     if (getClass() == other.getClass()) {
       final IntPredicate<?> p = (IntPredicate<?>) other;
       return getOperator().equals(p.getOperator())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
index 6a9a877..248fb9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -74,8 +74,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     return getClass() == other.getClass()
         && getChildren().equals(((Predicate<?>) other).getChildren());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
index 899fc3b..87460d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
@@ -50,8 +50,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     if (getClass() == other.getClass()) {
       final OperatorPredicate<?> p = (OperatorPredicate<?>) other;
       return getOperator().equals(p.getOperator())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index 845c805..2432a41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -92,8 +92,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     return getClass() == other.getClass()
         && getChildren().equals(((Predicate<?>) other).getChildren());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index bd1fa0c..c8f9972 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -24,7 +24,6 @@
 import static com.google.gerrit.server.query.QueryParser.NOT;
 import static com.google.gerrit.server.query.QueryParser.OR;
 import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD;
-import static com.google.gerrit.server.query.QueryParser.VARIABLE_ASSIGN;
 
 import org.antlr.runtime.tree.Tree;
 
@@ -222,20 +221,6 @@
       case FIELD_NAME:
         return operator(r.getText(), onlyChildOf(r));
 
-      case VARIABLE_ASSIGN: {
-        final String var = r.getText();
-        final Tree opTree = onlyChildOf(r);
-        if (opTree.getType() == FIELD_NAME) {
-          final Tree val = onlyChildOf(opTree);
-          if (val.getType() == SINGLE_WORD && "*".equals(val.getText())) {
-            final String op = opTree.getText();
-            final WildPatternPredicate<T> pat = new WildPatternPredicate<>(op);
-            return new VariablePredicate<>(var, pat);
-          }
-        }
-        return new VariablePredicate<>(var, toPredicate(opTree));
-      }
-
       default:
         throw error("Unsupported operator: " + r);
     }
@@ -365,14 +350,9 @@
         throws QueryParseException {
       try {
         return (Predicate<T>) method.invoke(builder, value);
-      } catch (RuntimeException e) {
-        throw error("Error in operator " + name + ":" + value, e);
-      } catch (IllegalAccessException e) {
+      } catch (RuntimeException | IllegalAccessException e) {
         throw error("Error in operator " + name + ":" + value, e);
       } catch (InvocationTargetException e) {
-        if (e.getCause() instanceof QueryParseException) {
-          throw (QueryParseException) e.getCause();
-        }
         throw error("Error in operator " + name + ":" + value, e.getCause());
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
deleted file mode 100644
index 6173fb6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
+++ /dev/null
@@ -1,505 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query;
-
-import com.google.common.collect.Lists;
-import com.google.inject.name.Named;
-
-import java.lang.annotation.Annotation;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Rewrites a Predicate tree by applying rewrite rules.
- * <p>
- * Subclasses may document their rewrite rules by declaring public methods with
- * {@link Rewrite} annotations, such as:
- *
- * <pre>
- * &#064;Rewrite(&quot;A=(owner:*) B=(status:*)&quot;)
- * public Predicate r1_ownerStatus(@Named(&quot;A&quot;) OperatorPredicate owner,
- *     &#064;Named(&quot;B&quot;) OperatorPredicate status) {
- * }
- * </pre>
- * <p>
- * Rewrite methods are applied in order by declared name, so naming methods with
- * a numeric prefix to ensure a specific ordering (if required) is suggested.
- *
- * @param <T> type of object the predicate can evaluate in memory.
- */
-public abstract class QueryRewriter<T> {
-  /**
-   * Defines the rewrite rules known by a QueryRewriter.
-   *
-   * This class is thread-safe and may be reused or cached.
-   *
-   * @param <T> type of object the predicates can evaluate in memory.
-   * @param <R> type of the rewriter subclass.
-   */
-  public static class Definition<T, R extends QueryRewriter<T>> {
-    private final List<RewriteRule<T>> rewriteRules;
-
-    public Definition(Class<R> clazz, QueryBuilder<T> qb) {
-      rewriteRules = Lists.newArrayList();
-
-      Class<?> c = clazz;
-      while (c != QueryRewriter.class) {
-        Method[] declared = c.getDeclaredMethods();
-        for (Method m : declared) {
-          Rewrite rp = m.getAnnotation(Rewrite.class);
-          if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT
-              && (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC
-              && rp != null) {
-            rewriteRules.add(new MethodRewrite<>(qb, rp.value(), m));
-          }
-        }
-        c = c.getSuperclass();
-      }
-      Collections.sort(rewriteRules);
-    }
-  }
-
-  private final List<RewriteRule<T>> rewriteRules;
-
-  protected QueryRewriter(final Definition<T, ? extends QueryRewriter<T>> def) {
-    this.rewriteRules = def.rewriteRules;
-  }
-
-  /** Combine the passed predicates into a single AND node. */
-  public Predicate<T> and(Collection<? extends Predicate<T>> that) {
-    return Predicate.and(that);
-  }
-
-  /** Combine the passed predicates into a single AND node. */
-  public Predicate<T> and(@SuppressWarnings("unchecked") Predicate<T>... that) {
-    return and(Arrays.asList(that));
-  }
-
-  /** Combine the passed predicates into a single OR node. */
-  public Predicate<T> or(Collection<? extends Predicate<T>> that) {
-    return Predicate.or(that);
-  }
-
-  /** Combine the passed predicates into a single OR node. */
-  public Predicate<T> or(@SuppressWarnings("unchecked") Predicate<T>... that) {
-    return or(Arrays.asList(that));
-  }
-
-  /** Invert the passed node. */
-  public Predicate<T> not(Predicate<T> that) {
-    return Predicate.not(that);
-  }
-
-  protected Predicate<T> preRewrite(Predicate<T> in) {
-    return in;
-  }
-
-  /**
-   * Apply rewrites to a graph until it stops changing.
-   *
-   * @param in the graph to rewrite.
-   * @return the rewritten graph.
-   */
-  public final Predicate<T> rewrite(Predicate<T> in) {
-    in = preRewrite(in);
-    return rewriteImpl(in);
-  }
-
-  private Predicate<T> rewriteImpl(Predicate<T> in) {
-    Predicate<T> old;
-    do {
-      old = in;
-      in = rewriteOne(in);
-
-      if (old.equals(in) && in.getChildCount() > 0) {
-        List<Predicate<T>> n = new ArrayList<>(in.getChildCount());
-        for (Predicate<T> p : in.getChildren()) {
-          n.add(rewriteImpl(p));
-        }
-        n = removeDuplicates(n);
-        if (n.size() == 1 && (isAND(in) || isOR(in))) {
-          in = n.get(0);
-        } else {
-          in = in.copy(n);
-        }
-      }
-
-    } while (!old.equals(in));
-    return replaceGenericNodes(in);
-  }
-
-  protected Predicate<T> replaceGenericNodes(final Predicate<T> in) {
-    if (in instanceof NotPredicate) {
-      return not(replaceGenericNodes(in.getChild(0)));
-
-    } else if (in instanceof AndPredicate) {
-      List<Predicate<T>> n = new ArrayList<>(in.getChildCount());
-      for (Predicate<T> c : in.getChildren()) {
-        n.add(replaceGenericNodes(c));
-      }
-      return and(n);
-
-    } else if (in instanceof OrPredicate) {
-      List<Predicate<T>> n = new ArrayList<>(in.getChildCount());
-      for (Predicate<T> c : in.getChildren()) {
-        n.add(replaceGenericNodes(c));
-      }
-      return or(n);
-
-    } else {
-      return in;
-    }
-  }
-
-  private Predicate<T> rewriteOne(Predicate<T> input) {
-    Predicate<T> best = null;
-    for (RewriteRule<T> r : rewriteRules) {
-      Predicate<T> n = r.rewrite(this, input);
-      if (n == null) {
-        continue;
-      }
-
-      if (!r.useBestCost()) {
-        return n;
-      }
-
-      if (best == null || n.getCost() < best.getCost()) {
-        best = n;
-        continue;
-      }
-    }
-    return best != null ? best : input;
-  }
-
-  private static class MatchResult<T> {
-    private static final MatchResult<?> FAIL = new MatchResult<>(null);
-    private static final MatchResult<?> OK = new MatchResult<>(null);
-
-    @SuppressWarnings("unchecked")
-    static <T> MatchResult<T> fail() {
-      return (MatchResult<T>) FAIL;
-    }
-
-    @SuppressWarnings("unchecked")
-    static <T> MatchResult<T> ok() {
-      return (MatchResult<T>) OK;
-    }
-
-    final Predicate<T> extra;
-
-    MatchResult(Predicate<T> extra) {
-      this.extra = extra;
-    }
-
-    boolean success() {
-      return this != FAIL;
-    }
-  }
-
-  private MatchResult<T> match(final Map<String, Predicate<T>> outVars,
-      final Predicate<T> pattern, final Predicate<T> actual) {
-    if (pattern instanceof VariablePredicate) {
-      final VariablePredicate<T> v = (VariablePredicate<T>) pattern;
-      final MatchResult<T> r = match(outVars, v.getChild(0), actual);
-      if (r.success()) {
-        Predicate<T> old = outVars.get(v.getName());
-        if (old == null) {
-          outVars.put(v.getName(), actual);
-          return r;
-        } else if (old.equals(actual)) {
-          return r;
-        } else {
-          return MatchResult.fail();
-        }
-      } else {
-        return MatchResult.fail();
-      }
-    }
-
-    if ((isAND(pattern) && isAND(actual)) //
-        || (isOR(pattern) && isOR(actual)) //
-        || (isNOT(pattern) && isNOT(actual)) //
-    ) {
-      // Order doesn't actually matter here. That does make our logic quite
-      // a bit more complex as we need to consult each child at most once,
-      // but in any order.
-      //
-      final LinkedList<Predicate<T>> have = dup(actual);
-      final LinkedList<Predicate<T>> extra = new LinkedList<>();
-      for (final Predicate<T> pat : pattern.getChildren()) {
-        boolean found = false;
-        for (final Iterator<Predicate<T>> i = have.iterator(); i.hasNext();) {
-          final MatchResult<T> r = match(outVars, pat, i.next());
-          if (r.success()) {
-            found = true;
-            i.remove();
-            if (r.extra != null) {
-              extra.add(r.extra);
-            }
-            break;
-          }
-        }
-        if (!found) {
-          return MatchResult.fail();
-        }
-      }
-      have.addAll(extra);
-      switch (have.size()) {
-        case 0:
-          return MatchResult.ok();
-        case 1:
-          if (isNOT(actual)) {
-            return new MatchResult<>(actual.copy(have));
-          }
-          return new MatchResult<>(have.get(0));
-        default:
-          return new MatchResult<>(actual.copy(have));
-      }
-
-    } else if (pattern.equals(actual)) {
-      return MatchResult.ok();
-
-    } else if (pattern instanceof WildPatternPredicate
-        && actual instanceof OperatorPredicate
-        && ((OperatorPredicate<T>) pattern).getOperator().equals(
-            ((OperatorPredicate<T>) actual).getOperator())) {
-      return MatchResult.ok();
-
-    } else {
-      return MatchResult.fail();
-    }
-  }
-
-  private static <T> LinkedList<Predicate<T>> dup(final Predicate<T> actual) {
-    return new LinkedList<>(actual.getChildren());
-  }
-
-  /**
-   * Denotes a method which wants to replace a predicate expression.
-   * <p>
-   * This annotation must be applied to a public method which returns
-   * {@link Predicate}. The arguments of the method should {@link Predicate}, or
-   * any subclass of it. The annotation value is a query language string which
-   * describes the subtree this rewrite applies to. Method arguments should be
-   * named with a {@link Named} annotation, and the same names should be used in
-   * the query.
-   * <p>
-   * For example:
-   *
-   * <pre>
-   * &#064;Rewrite(&quot;A=(owner:*) B=(status:*)&quot;)
-   * public Predicate ownerStatus(@Named(&quot;A&quot;) OperatorPredicate owner,
-   *     &#064;Named(&quot;B&quot;) OperatorPredicate status) {
-   * }
-   * </pre>
-   *
-   * matches an AND Predicate with at least two children, one being an operator
-   * predicate called "owner" and the other being an operator predicate called
-   * "status". The variables in the query are matched by name against the
-   * parameters.
-   */
-  @Retention(RetentionPolicy.RUNTIME)
-  @Target(ElementType.METHOD)
-  protected @interface Rewrite {
-    String value();
-  }
-
-  @Retention(RetentionPolicy.RUNTIME)
-  @Target(ElementType.METHOD)
-  protected @interface NoCostComputation {
-  }
-
-  /** Applies a rewrite rule to a Predicate. */
-  protected interface RewriteRule<T> extends Comparable<RewriteRule<T>> {
-    /**
-     * Apply a rewrite rule to the Predicate.
-     *
-     * @param input the input predicate to be tested, and possibly rewritten.
-     * @return a rewritten form of the predicate if this rule matches with the
-     *         tree {@code input} and has a rewrite for it; {@code null} if this
-     *         rule does not want this predicate.
-     */
-    Predicate<T> rewrite(QueryRewriter<T> rewriter, Predicate<T> input);
-
-    /** @return true if the best cost should be selected. */
-    boolean useBestCost();
-  }
-
-  /** Implements the magic behind {@link Rewrite} annotations. */
-  private static class MethodRewrite<T> implements RewriteRule<T> {
-    private final Method method;
-    private final Predicate<T> pattern;
-    private final String[] argNames;
-    private final Class<? extends Predicate<T>>[] argTypes;
-    private final boolean useBestCost;
-
-    @SuppressWarnings("unchecked")
-    MethodRewrite(QueryBuilder<T> queryBuilder, String patternText, Method m) {
-      method = m;
-      useBestCost = m.getAnnotation(NoCostComputation.class) == null;
-
-      Predicate<T> p;
-      try {
-        p = queryBuilder.parse(patternText);
-      } catch (QueryParseException e) {
-        throw new RuntimeException("Bad @Rewrite(\"" + patternText + "\")"
-            + " on " + m.toGenericString() + " in " + m.getDeclaringClass()
-            + ": " + e.getMessage(), e);
-      }
-      if (!Predicate.class.isAssignableFrom(m.getReturnType())) {
-        throw new RuntimeException(m.toGenericString() + " in "
-            + m.getDeclaringClass() + " must return " + Predicate.class);
-      }
-
-      pattern = p;
-      argNames = new String[method.getParameterTypes().length];
-      argTypes = new Class[argNames.length];
-      for (int i = 0; i < argNames.length; i++) {
-        Named name = null;
-        for (Annotation a : method.getParameterAnnotations()[i]) {
-          if (a instanceof Named) {
-            name = (Named) a;
-            break;
-          }
-        }
-        if (name == null) {
-          throw new RuntimeException("Argument " + (i + 1) + " of "
-              + m.toGenericString() + " in " + m.getDeclaringClass()
-              + " has no @Named annotation");
-        }
-        if (!Predicate.class.isAssignableFrom(method.getParameterTypes()[i])) {
-          throw new RuntimeException("Argument " + (i + 1) + " of "
-              + m.toGenericString() + " in " + m.getDeclaringClass()
-              + " must be of type " + Predicate.class);
-        }
-        argNames[i] = name.value();
-        argTypes[i] = (Class<Predicate<T>>) method.getParameterTypes()[i];
-      }
-    }
-
-    @Override
-    public boolean useBestCost() {
-      return useBestCost;
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public Predicate<T> rewrite(QueryRewriter<T> rewriter,
-        final Predicate<T> input) {
-      final HashMap<String, Predicate<T>> args = new HashMap<>();
-      final MatchResult<T> res = rewriter.match(args, pattern, input);
-      if (!res.success()) {
-        return null;
-      }
-
-      final Predicate<T>[] argList = new Predicate[argNames.length];
-      for (int i = 0; i < argList.length; i++) {
-        argList[i] = args.get(argNames[i]);
-        if (argList[i] == null) {
-          final String a = "@Named(\"" + argNames[i] + "\")";
-          throw error(new IllegalStateException("No value bound for " + a));
-        }
-        if (!argTypes[i].isInstance(argList[i])) {
-          return null;
-        }
-      }
-
-      final Predicate<T> rep;
-      try {
-        rep = (Predicate<T>) method.invoke(rewriter, (Object[]) argList);
-      } catch (IllegalArgumentException e) {
-        throw error(e);
-      } catch (IllegalAccessException e) {
-        throw error(e);
-      } catch (InvocationTargetException e) {
-        throw error(e.getCause());
-      }
-
-      if (rep instanceof RewritePredicate) {
-        ((RewritePredicate<T>) rep).init(method.getName(), argList);
-      }
-
-      if (res.extra == null) {
-        return rep;
-      }
-
-      Predicate<T> extra = removeDuplicates(res.extra);
-      Predicate<T>[] newArgs = new Predicate[] {extra, rep};
-      return input.copy(Arrays.asList(newArgs));
-    }
-
-    private IllegalArgumentException error(Throwable e) {
-      final String msg = "Cannot apply " + method.getName();
-      return new IllegalArgumentException(msg, e);
-    }
-
-    @Override
-    public int compareTo(RewriteRule<T> in) {
-      if (in instanceof MethodRewrite) {
-        return method.getName().compareTo(
-            ((MethodRewrite<T>) in).method.getName());
-      }
-      return 1;
-    }
-  }
-
-  private static <T> Predicate<T> removeDuplicates(Predicate<T> in) {
-    if (in.getChildCount() > 0) {
-      List<Predicate<T>> n = removeDuplicates(in.getChildren());
-      if (n.size() == 1 && (isAND(in) || isOR(in))) {
-        in = n.get(0);
-      } else {
-        in = in.copy(n);
-      }
-    }
-    return in;
-  }
-
-  private static <T> List<Predicate<T>> removeDuplicates(List<Predicate<T>> n) {
-    List<Predicate<T>> r = new ArrayList<>();
-    for (Predicate<T> p : n) {
-      if (!r.contains(p)) {
-        r.add(p);
-      }
-    }
-    return r;
-  }
-
-  private static <T> boolean isAND(final Predicate<T> p) {
-    return p instanceof AndPredicate;
-  }
-
-  private static <T> boolean isOR(final Predicate<T> p) {
-    return p instanceof OrPredicate;
-  }
-
-  private static <T> boolean isNOT(final Predicate<T> p) {
-    return p instanceof NotPredicate;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java
deleted file mode 100644
index f4a2111..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2010 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.server.query;
-
-import com.google.common.collect.ImmutableList;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-public abstract class RewritePredicate<T> extends Predicate<T> {
-  private boolean init;
-  private String name = getClass().getSimpleName();
-  private List<Predicate<T>> children = Collections.emptyList();
-
-  protected void init(String name, @SuppressWarnings("unchecked") Predicate<T>... args) {
-    this.init = true;
-    this.name = name;
-    this.children = ImmutableList.copyOf(args);
-  }
-
-  @Override
-  public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
-    return this;
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
-  public boolean equals(Object other) {
-    if (other instanceof RewritePredicate) {
-      RewritePredicate<T> that = (RewritePredicate<T>) other;
-      if (this.init && that.init) {
-        return this.getClass() == that.getClass()
-            && this.name.equals(that.name)
-            && this.children.equals(that.children);
-      }
-    }
-    return this == other;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = getClass().hashCode();
-    if (!children.isEmpty()) {
-      h *= 31;
-      h += children.get(0).hashCode();
-    }
-    return h;
-  }
-
-  @Override
-  public final String toString() {
-    final StringBuilder r = new StringBuilder();
-    r.append(name);
-    if (!children.isEmpty()) {
-      r.append("(");
-      for (int i = 0; i < children.size(); i++) {
-        if (i != 0) {
-          r.append(" ");
-        }
-        r.append(children.get(i));
-      }
-      r.append(")");
-    }
-    return r.toString();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
deleted file mode 100644
index d6d0f9c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query;
-
-import com.google.gwtorm.server.OrmException;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Holds another predicate in a named variable.
- *
- * @see QueryRewriter
- */
-public class VariablePredicate<T> extends Predicate<T> {
-  private final String name;
-  private final Predicate<T> that;
-
-  protected VariablePredicate(final String name, final Predicate<T> that) {
-    this.name = name;
-    this.that = that;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  @Override
-  public final List<Predicate<T>> getChildren() {
-    return Collections.singletonList(that);
-  }
-
-  @Override
-  public final int getChildCount() {
-    return 1;
-  }
-
-  @Override
-  public final Predicate<T> getChild(final int i) {
-    if (i != 0) {
-      throw new ArrayIndexOutOfBoundsException(i);
-    }
-    return that;
-  }
-
-  @Override
-  public Predicate<T> copy(final Collection<? extends Predicate<T>> children) {
-    if (children.size() != 1) {
-      throw new IllegalArgumentException("Expected exactly one child");
-    }
-    return new VariablePredicate<>(getName(), children.iterator().next());
-  }
-
-  @Override
-  public boolean match(final T object) throws OrmException {
-    return that.match(object);
-  }
-
-  @Override
-  public int getCost() {
-    return that.getCost();
-  }
-
-  @Override
-  public int hashCode() {
-    return getName().hashCode() * 31 + that.hashCode();
-  }
-
-  @Override
-  public boolean equals(final Object other) {
-    if (other == null)
-      return false;
-    if (getClass() == other.getClass()) {
-      final VariablePredicate<?> v = (VariablePredicate<?>) other;
-      return getName().equals(v.getName())
-          && getChildren().equals(v.getChildren());
-    }
-    return false;
-  }
-
-  @Override
-  public final String toString() {
-    return getName() + "=(" + that.toString() + ")";
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java
deleted file mode 100644
index 48f3898..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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.
-
-package com.google.gerrit.server.query;
-
-/**
- * Predicate only for use in rewrite rule patterns.
- * <p>
- * May <b>only</b> be used when nested immediately within a
- * {@link VariablePredicate}. Within the QueryRewriter this predicate matches
- * any other operator whose name matches this predicate's operator name.
- *
- * @see QueryRewriter
- */
-public final class WildPatternPredicate<T> extends OperatorPredicate<T> {
-  public WildPatternPredicate(final String name) {
-    super(name, "*");
-  }
-
-  @Override
-  public boolean match(final T object) {
-    throw new UnsupportedOperationException("Cannot match " + toString());
-  }
-
-  @Override
-  public int getCost() {
-    return 0;
-  }
-
-  @Override
-  public int hashCode() {
-    return getOperator().hashCode() * 31;
-  }
-
-  @Override
-  public boolean equals(final Object other) {
-    if (other == null)
-      return false;
-    if (getClass() == other.getClass()) {
-      final WildPatternPredicate<?> p = (WildPatternPredicate<?>) other;
-      return getOperator().equals(p.getOperator());
-    }
-    return false;
-  }
-
-  @Override
-  public String toString() {
-    return getOperator() + ":" + getValue();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index f48cfd8..9ed0447 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -128,7 +128,7 @@
       // limit the caller wants.  Restart the source and continue.
       //
       Paginated p = (Paginated) source;
-      while (skipped && r.size() < p.limit() + start) {
+      while (skipped && r.size() < p.getOptions().limit() + start) {
         skipped = false;
         ResultSet<ChangeData> next = p.restart(nextStart);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
new file mode 100644
index 0000000..6264f3a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import static com.google.gerrit.server.index.ChangeField.AUTHOR;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+public class AuthorPredicate extends IndexPredicate<ChangeData>  {
+  AuthorPredicate(String value) {
+    super(AUTHOR, FIELD_AUTHOR, value.toLowerCase());
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    return ChangeField.getAuthorParts(object).contains(
+        getValue().toLowerCase());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
deleted file mode 100644
index a9ef595..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2013 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.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryRewriter;
-import com.google.inject.Inject;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-
-public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
-  private static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
-      new ChangeQueryBuilder.Arguments(
-          new InvalidProvider<ReviewDb>(),
-          new InvalidProvider<InternalChangeQuery>(),
-          new InvalidProvider<ChangeQueryRewriter>(),
-          null, null, null, null, null, null, null, null, null, null, null,
-          null, null, null, null, null, null, null, null, null, null));
-
-  private static final QueryRewriter.Definition<ChangeData, BasicChangeRewrites> mydef =
-      new QueryRewriter.Definition<>(BasicChangeRewrites.class, BUILDER);
-
-  @Inject
-  public BasicChangeRewrites() {
-    super(mydef);
-  }
-
-  @Rewrite("-status:open")
-  @NoCostComputation
-  public Predicate<ChangeData> r00_notOpen() {
-    return ChangeStatusPredicate.closed();
-  }
-
-  @Rewrite("-status:closed")
-  @NoCostComputation
-  public Predicate<ChangeData> r00_notClosed() {
-    return ChangeStatusPredicate.open();
-  }
-
-  @SuppressWarnings("unchecked")
-  @NoCostComputation
-  @Rewrite("-status:merged")
-  public Predicate<ChangeData> r00_notMerged() {
-    return or(ChangeStatusPredicate.open(),
-        new ChangeStatusPredicate(Change.Status.ABANDONED));
-  }
-
-  @SuppressWarnings("unchecked")
-  @NoCostComputation
-  @Rewrite("-status:abandoned")
-  public Predicate<ChangeData> r00_notAbandoned() {
-    return or(ChangeStatusPredicate.open(),
-        new ChangeStatusPredicate(Change.Status.MERGED));
-  }
-
-  private static final class InvalidProvider<T> implements Provider<T> {
-    @Override
-    public T get() {
-      throw new OutOfScopeException("Not available at init");
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index c565ab2..0618db9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -33,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -62,6 +65,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -69,14 +73,20 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class ChangeData {
+  private static final int BATCH_SIZE = 50;
+
   public static List<Change> asChanges(List<ChangeData> changeDatas)
       throws OrmException {
     List<Change> result = new ArrayList<>(changeDatas.size());
@@ -97,71 +107,162 @@
 
   public static void ensureChangeLoaded(Iterable<ChangeData> changes)
       throws OrmException {
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    } else if (first.notesMigration.readChanges()) {
+      for (ChangeData cd : changes) {
+        cd.change();
+      }
+    }
+
     Map<Change.Id, ChangeData> missing = Maps.newHashMap();
     for (ChangeData cd : changes) {
       if (cd.change == null) {
         missing.put(cd.getId(), cd);
       }
     }
-    if (!missing.isEmpty()) {
-      ChangeData first = missing.values().iterator().next();
-      if (!first.notesMigration.readChanges()) {
-        ReviewDb db = missing.values().iterator().next().db;
-        for (Change change : db.changes().get(missing.keySet())) {
-          missing.get(change.getId()).change = change;
-        }
-      } else {
-        for (ChangeData cd : missing.values()) {
-          cd.change();
-        }
-      }
+    if (missing.isEmpty()) {
+      return;
+    }
+    for (Change change : first.db.changes().get(missing.keySet())) {
+      missing.get(change.getId()).change = change;
     }
   }
 
   public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes)
       throws OrmException {
-    for (ChangeData cd : changes) {
-      cd.patches();
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    } else if (first.notesMigration.readChanges()) {
+      for (ChangeData cd : changes) {
+        cd.patchSets();
+      }
+    }
+
+    List<ResultSet<PatchSet>> results = new ArrayList<>(BATCH_SIZE);
+    for (List<ChangeData> batch : Iterables.partition(changes, BATCH_SIZE)) {
+      results.clear();
+      for (ChangeData cd : batch) {
+        if (cd.patchSets == null) {
+          results.add(cd.db.patchSets().byChange(cd.getId()));
+        } else {
+          results.add(null);
+        }
+      }
+      for (int i = 0; i < batch.size(); i++) {
+        ResultSet<PatchSet> result = results.get(i);
+        if (result != null) {
+          batch.get(i).patchSets = result.toList();
+        }
+      }
     }
   }
 
   public static void ensureCurrentPatchSetLoaded(Iterable<ChangeData> changes)
       throws OrmException {
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    } else if (first.notesMigration.readChanges()) {
+      for (ChangeData cd : changes) {
+        cd.currentPatchSet();
+      }
+    }
+
     Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
     for (ChangeData cd : changes) {
-      if (cd.currentPatchSet == null && cd.patches == null) {
+      if (cd.currentPatchSet == null && cd.patchSets == null) {
         missing.put(cd.change().currentPatchSetId(), cd);
       }
     }
-    if (!missing.isEmpty()) {
-      ReviewDb db = missing.values().iterator().next().db;
-      for (PatchSet ps : db.patchSets().get(missing.keySet())) {
-        ChangeData cd = missing.get(ps.getId());
-        cd.currentPatchSet = ps;
-      }
+    if (missing.isEmpty()) {
+      return;
+    }
+    for (PatchSet ps : first.db.patchSets().get(missing.keySet())) {
+      missing.get(ps.getId()).currentPatchSet = ps;
     }
   }
 
   public static void ensureCurrentApprovalsLoaded(Iterable<ChangeData> changes)
       throws OrmException {
-    List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
-    for (ChangeData cd : changes) {
-      if (!cd.notesMigration.readChanges()) {
-        if (cd.currentApprovals == null) {
-          pending.add(cd.db.patchSetApprovals()
-              .byPatchSet(cd.change().currentPatchSetId()));
-        }
-      } else {
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    } else if (first.notesMigration.readChanges()) {
+      for (ChangeData cd : changes) {
         cd.currentApprovals();
       }
     }
-    if (!pending.isEmpty()) {
-      int idx = 0;
-      for (ChangeData cd : changes) {
+
+    List<ResultSet<PatchSetApproval>> results = new ArrayList<>(BATCH_SIZE);
+    for (List<ChangeData> batch : Iterables.partition(changes, BATCH_SIZE)) {
+      results.clear();
+      for (ChangeData cd : batch) {
         if (cd.currentApprovals == null) {
-          cd.currentApprovals = sortApprovals(pending.get(idx++));
+          PatchSet.Id psId = cd.change().currentPatchSetId();
+          results.add(cd.db.patchSetApprovals().byPatchSet(psId));
+        } else {
+          results.add(null);
         }
       }
+      for (int i = 0; i < batch.size(); i++) {
+        ResultSet<PatchSetApproval> result = results.get(i);
+        if (result != null) {
+          batch.get(i).currentApprovals = sortApprovals(result);
+        }
+      }
+    }
+  }
+
+  public static void ensureMessagesLoaded(Iterable<ChangeData> changes)
+      throws OrmException {
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    } else if (first.notesMigration.readChanges()) {
+      for (ChangeData cd : changes) {
+        cd.messages();
+      }
+      return;
+    }
+
+    List<ResultSet<ChangeMessage>> results = new ArrayList<>(BATCH_SIZE);
+    for (List<ChangeData> batch : Iterables.partition(changes, BATCH_SIZE)) {
+      results.clear();
+      for (ChangeData cd : batch) {
+        if (cd.messages == null) {
+          PatchSet.Id psId = cd.change().currentPatchSetId();
+          results.add(cd.db.changeMessages().byPatchSet(psId));
+        } else {
+          results.add(null);
+        }
+      }
+      for (int i = 0; i < batch.size(); i++) {
+        ResultSet<ChangeMessage> result = results.get(i);
+        if (result != null) {
+          batch.get(i).messages = result.toList();
+        }
+      }
+    }
+  }
+
+  public static void ensureReviewedByLoadedForOpenChanges(
+      Iterable<ChangeData> changes) throws OrmException {
+    List<ChangeData> pending = new ArrayList<>();
+    for (ChangeData cd : changes) {
+      if (cd.reviewedBy == null && cd.change().getStatus().isOpen()) {
+        pending.add(cd);
+      }
+    }
+
+    if (!pending.isEmpty()) {
+      ensureAllPatchSetsLoaded(pending);
+      ensureMessagesLoaded(pending);
+      for (ChangeData cd : pending) {
+        cd.reviewedBy();
+      }
     }
   }
 
@@ -174,12 +275,13 @@
   /**
    * Create an instance for testing only.
    * <p>
-   * Attempting to lazy load data will fail with NPEs.
+   * Attempting to lazy load data will fail with NPEs. Callers may consider
+   * manually setting fields that can be set.
    *
    * @param id change ID
    * @return instance for testing.
    */
-  static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
+  public static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
     ChangeData cd = new ChangeData(null, null, null, null, null, null, null,
         null, null, null, null, null, null, id);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
@@ -206,7 +308,7 @@
   private String commitMessage;
   private List<FooterLine> commitFooters;
   private PatchSet currentPatchSet;
-  private Collection<PatchSet> patches;
+  private Collection<PatchSet> patchSets;
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
   private List<PatchSetApproval> currentApprovals;
   private Map<Integer, List<String>> files = new HashMap<>();
@@ -217,6 +319,10 @@
   private List<SubmitRecord> submitRecords;
   private ChangedLines changedLines;
   private Boolean mergeable;
+  private Set<Account.Id> editsByUser;
+  private Set<Account.Id> reviewedBy;
+  private PersonIdent author;
+  private PersonIdent committer;
 
   @AssistedInject
   private ChangeData(
@@ -441,7 +547,7 @@
   }
 
   void cacheVisibleTo(ChangeControl ctl) {
-    visibleTo = ctl.getCurrentUser();
+    visibleTo = ctl.getUser();
     changeControl = ctl;
   }
 
@@ -452,8 +558,15 @@
     return change;
   }
 
+  public void setChange(Change c) {
+    change = c;
+  }
+
   public Change reloadChange() throws OrmException {
     change = db.changes().get(legacyId);
+    if (change == null) {
+      throw new OrmException("Unable to load change " + legacyId);
+    }
     return change;
   }
 
@@ -470,7 +583,7 @@
       if (c == null) {
         return null;
       }
-      for (PatchSet p : patches()) {
+      for (PatchSet p : patchSets()) {
         if (p.getId().equals(c.currentPatchSetId())) {
           currentPatchSet = p;
           return p;
@@ -516,11 +629,28 @@
     return commitFooters;
   }
 
+  public PersonIdent getAuthor() throws IOException, OrmException {
+    if (author == null) {
+      if (!loadCommitData()) {
+        return null;
+      }
+    }
+    return author;
+  }
+
+  public PersonIdent getCommitter() throws IOException, OrmException {
+    if (committer == null) {
+      if (!loadCommitData()) {
+        return null;
+      }
+    }
+    return committer;
+  }
+
   private boolean loadCommitData() throws OrmException,
       RepositoryNotFoundException, IOException, MissingObjectException,
       IncorrectObjectTypeException {
-    PatchSet.Id psId = change().currentPatchSetId();
-    PatchSet ps = db.patchSets().get(psId);
+    PatchSet ps = currentPatchSet();
     if (ps == null) {
       return false;
     }
@@ -530,6 +660,8 @@
       RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
       commitMessage = c.getFullMessage();
       commitFooters = c.getFooterLines();
+      author = c.getAuthorIdent();
+      committer = c.getCommitterIdent();
     }
     return true;
   }
@@ -538,20 +670,20 @@
    * @return patches for the change.
    * @throws OrmException an error occurred reading the database.
    */
-  public Collection<PatchSet> patches()
+  public Collection<PatchSet> patchSets()
       throws OrmException {
-    if (patches == null) {
-      patches = db.patchSets().byChange(legacyId).toList();
+    if (patchSets == null) {
+      patchSets = db.patchSets().byChange(legacyId).toList();
     }
-    return patches;
+    return patchSets;
   }
 
   /**
    * @return patches for the change visible to the current user.
    * @throws OrmException an error occurred reading the database.
    */
-  public Collection<PatchSet> visiblePatches() throws OrmException {
-    return FluentIterable.from(patches()).filter(new Predicate<PatchSet>() {
+  public Collection<PatchSet> visiblePatchSets() throws OrmException {
+    return FluentIterable.from(patchSets()).filter(new Predicate<PatchSet>() {
       @Override
       public boolean apply(PatchSet input) {
         try {
@@ -562,15 +694,20 @@
       }}).toList();
   }
 
+  public void setPatchSets(Collection<PatchSet> patchSets) {
+    this.currentPatchSet = null;
+    this.patchSets = patchSets;
+  }
+
   /**
    * @return patch with the given ID, or null if it does not exist.
    * @throws OrmException an error occurred reading the database.
    */
-  public PatchSet patch(PatchSet.Id psId) throws OrmException {
+  public PatchSet patchSet(PatchSet.Id psId) throws OrmException {
     if (currentPatchSet != null && currentPatchSet.getId().equals(psId)) {
       return currentPatchSet;
     }
-    for (PatchSet ps : patches()) {
+    for (PatchSet ps : patchSets()) {
       if (ps.getId().equals(psId)) {
         return ps;
       }
@@ -620,7 +757,7 @@
     return submitRecords;
   }
 
-  public void setMergeable(boolean mergeable) {
+  public void setMergeable(Boolean mergeable) {
     this.mergeable = mergeable;
   }
 
@@ -637,10 +774,8 @@
         if (ps == null || !changeControl().isPatchVisible(ps, db)) {
           return null;
         }
-        Repository repo = null;
-        try {
-          repo = repoManager.openRepository(c.getProject());
-          Ref ref = repo.getRef(c.getDest().get());
+        try (Repository repo = repoManager.openRepository(c.getProject())) {
+          Ref ref = repo.getRefDatabase().exactRef(c.getDest().get());
           SubmitTypeRecord rec = new SubmitRuleEvaluator(this)
               .getSubmitType();
           if (rec.status != SubmitTypeRecord.Status.OK) {
@@ -655,16 +790,87 @@
               ref, rec.type, mergeStrategy, c.getDest(), repo, db);
         } catch (IOException e) {
           throw new OrmException(e);
-        } finally {
-          if (repo != null) {
-            repo.close();
-          }
         }
       }
     }
     return mergeable;
   }
 
+  public Set<Account.Id> editsByUser() throws OrmException {
+    if (editsByUser == null) {
+      Change c = change();
+      if (c == null) {
+        return Collections.emptySet();
+      }
+      editsByUser = new HashSet<>();
+      Change.Id id = checkNotNull(change.getId());
+      try (Repository repo = repoManager.openRepository(change.getProject())) {
+        for (String ref
+            : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).keySet()) {
+          if (id.equals(Change.Id.fromEditRefPart(ref))) {
+            editsByUser.add(Account.Id.fromRefPart(ref));
+          }
+        }
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+    }
+    return editsByUser;
+  }
+
+  public Set<Account.Id> reviewedBy() throws OrmException {
+    if (reviewedBy == null) {
+      Change c = change();
+      if (c == null) {
+        return Collections.emptySet();
+      }
+      List<ReviewedByEvent> events = new ArrayList<>();
+      for (ChangeMessage msg : messages()) {
+        if (msg.getAuthor() != null) {
+          events.add(ReviewedByEvent.create(msg));
+        }
+      }
+      for (PatchSet ps : patchSets()) {
+        events.add(ReviewedByEvent.create(ps));
+      }
+      Collections.sort(events, Collections.reverseOrder());
+      reviewedBy = new LinkedHashSet<>();
+      Account.Id owner = c.getOwner();
+      for (ReviewedByEvent event : events) {
+        if (owner.equals(event.author())) {
+          break;
+        }
+        reviewedBy.add(event.author());
+      }
+    }
+    return reviewedBy;
+  }
+
+  public void setReviewedBy(Set<Account.Id> reviewedBy) {
+    this.reviewedBy = reviewedBy;
+  }
+
+  @AutoValue
+  abstract static class ReviewedByEvent implements Comparable<ReviewedByEvent> {
+    private static ReviewedByEvent create(PatchSet ps) {
+      return new AutoValue_ChangeData_ReviewedByEvent(
+          ps.getUploader(), ps.getCreatedOn());
+    }
+
+    private static ReviewedByEvent create(ChangeMessage msg) {
+      return new AutoValue_ChangeData_ReviewedByEvent(
+          msg.getAuthor(), msg.getWrittenOn());
+    }
+
+    public abstract Account.Id author();
+    public abstract Timestamp ts();
+
+    @Override
+    public int compareTo(ReviewedByEvent other) {
+      return ts().compareTo(other.ts());
+    }
+  }
+
   @Override
   public String toString() {
     MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 34fdf5c..a4cff07 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -30,6 +29,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -38,8 +38,12 @@
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.VersionedAccountDestinations;
+import com.google.gerrit.server.account.VersionedAccountQueries;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -49,6 +53,7 @@
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.IndexRewriter;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
@@ -63,9 +68,12 @@
 import com.google.inject.ProvisionException;
 import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -90,15 +98,20 @@
   public static final String FIELD_ADDED = "added";
   public static final String FIELD_AFTER = "after";
   public static final String FIELD_AGE = "age";
+  public static final String FIELD_AUTHOR = "author";
   public static final String FIELD_BEFORE = "before";
   public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
   public static final String FIELD_COMMENT = "comment";
+  public static final String FIELD_COMMENTBY = "commentby";
   public static final String FIELD_COMMIT = "commit";
+  public static final String FIELD_COMMITTER = "committer";
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
+  public static final String FIELD_DESTINATION = "destination";
   public static final String FIELD_DRAFTBY = "draftby";
+  public static final String FIELD_EDITBY = "editby";
   public static final String FIELD_FILE = "file";
   public static final String FIELD_IS = "is";
   public static final String FIELD_HAS = "has";
@@ -114,7 +127,9 @@
   public static final String FIELD_PATH = "path";
   public static final String FIELD_PROJECT = "project";
   public static final String FIELD_PROJECTS = "projects";
+  public static final String FIELD_QUERY = "query";
   public static final String FIELD_REF = "ref";
+  public static final String FIELD_REVIEWEDBY = "reviewedby";
   public static final String FIELD_REVIEWER = "reviewer";
   public static final String FIELD_REVIEWERIN = "reviewerin";
   public static final String FIELD_STARREDBY = "starredby";
@@ -135,7 +150,7 @@
   public static class Arguments {
     final Provider<ReviewDb> db;
     final Provider<InternalChangeQuery> queryProvider;
-    final Provider<ChangeQueryRewriter> rewriter;
+    final IndexRewriter rewriter;
     final IdentifiedUser.GenericFactory userFactory;
     final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
@@ -145,14 +160,15 @@
     final AccountResolver accountResolver;
     final GroupBackend groupBackend;
     final AllProjectsName allProjectsName;
+    final AllUsersNameProvider allUsersName;
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
     final ProjectCache projectCache;
     final Provider<ListChildProjects> listChildProjects;
-    final IndexCollection indexes;
     final SubmitStrategyFactory submitStrategyFactory;
     final ConflictsCache conflictsCache;
     final TrackingFooters trackingFooters;
+    final ChangeIndex index;
     final IndexConfig indexConfig;
     final Provider<ListMembers> listMembers;
     final boolean allowsDrafts;
@@ -163,7 +179,7 @@
     @VisibleForTesting
     public Arguments(Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
-        Provider<ChangeQueryRewriter> rewriter,
+        IndexRewriter rewriter,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         CapabilityControl.Factory capabilityControlFactory,
@@ -174,6 +190,7 @@
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
+        AllUsersNameProvider allUsersName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
@@ -188,16 +205,18 @@
       this(db, queryProvider, rewriter, userFactory, self,
           capabilityControlFactory, changeControlGenericFactory,
           changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
-          allProjectsName, patchListCache, repoManager, projectCache,
-          listChildProjects, indexes, submitStrategyFactory,
-          conflictsCache, trackingFooters, indexConfig, listMembers,
+          allProjectsName, allUsersName, patchListCache, repoManager,
+          projectCache, listChildProjects, submitStrategyFactory,
+          conflictsCache, trackingFooters,
+          indexes != null ? indexes.getSearchIndex() : null,
+          indexConfig, listMembers,
           cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
     }
 
     private Arguments(
         Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
-        Provider<ChangeQueryRewriter> rewriter,
+        IndexRewriter rewriter,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         CapabilityControl.Factory capabilityControlFactory,
@@ -208,14 +227,15 @@
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
+        AllUsersNameProvider allUsersName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
         Provider<ListChildProjects> listChildProjects,
-        IndexCollection indexes,
         SubmitStrategyFactory submitStrategyFactory,
         ConflictsCache conflictsCache,
         TrackingFooters trackingFooters,
+        ChangeIndex index,
         IndexConfig indexConfig,
         Provider<ListMembers> listMembers,
         boolean allowsDrafts) {
@@ -232,14 +252,15 @@
      this.accountResolver = accountResolver;
      this.groupBackend = groupBackend;
      this.allProjectsName = allProjectsName;
+     this.allUsersName = allUsersName;
      this.patchListCache = patchListCache;
      this.repoManager = repoManager;
      this.projectCache = projectCache;
      this.listChildProjects = listChildProjects;
-     this.indexes = indexes;
      this.submitStrategyFactory = submitStrategyFactory;
      this.conflictsCache = conflictsCache;
      this.trackingFooters = trackingFooters;
+     this.index = index;
      this.indexConfig = indexConfig;
      this.listMembers = listMembers;
      this.allowsDrafts = allowsDrafts;
@@ -250,16 +271,16 @@
           Providers.of(otherUser),
           capabilityControlFactory, changeControlGenericFactory,
           changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
-          allProjectsName, patchListCache, repoManager, projectCache,
-          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
-          trackingFooters, indexConfig, listMembers, allowsDrafts);
+          allProjectsName, allUsersName, patchListCache, repoManager,
+          projectCache, listChildProjects, submitStrategyFactory,
+          conflictsCache, trackingFooters, index, indexConfig, listMembers,
+          allowsDrafts);
     }
 
     Arguments asUser(Account.Id otherId) {
       try {
         CurrentUser u = self.get();
-        if (u.isIdentifiedUser()
-            && otherId.equals(((IdentifiedUser) u).getAccountId())) {
+        if (u.isIdentifiedUser() && otherId.equals(u.getAccountId())) {
           return this;
         }
       } catch (ProvisionException e) {
@@ -270,9 +291,9 @@
 
     IdentifiedUser getIdentifiedUser() throws QueryParseException {
       try {
-        CurrentUser u = getCurrentUser();
+        CurrentUser u = getUser();
         if (u.isIdentifiedUser()) {
-          return (IdentifiedUser) u;
+          return u.asIdentifiedUser();
         }
         throw new QueryParseException(NotSignedInException.MESSAGE);
       } catch (ProvisionException e) {
@@ -280,13 +301,17 @@
       }
     }
 
-    CurrentUser getCurrentUser() throws QueryParseException {
+    CurrentUser getUser() throws QueryParseException {
       try {
         return self.get();
       } catch (ProvisionException e) {
         throw new QueryParseException(NotSignedInException.MESSAGE, e);
       }
     }
+
+    Schema<ChangeData> getSchema() {
+      return index != null ? index.getSchema() : null;
+    }
   }
 
   private final Arguments args;
@@ -337,7 +362,8 @@
   @Operator
   public Predicate<ChangeData> change(String query) throws QueryParseException {
     if (PAT_LEGACY_ID.matcher(query).matches()) {
-      return new LegacyChangeIdPredicate(Change.Id.parse(query));
+      return new LegacyChangeIdPredicate(
+          args.getSchema(), Change.Id.parse(query));
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
       return new ChangeIdPredicate(parseChangeId(query));
     }
@@ -354,14 +380,13 @@
 
   @Operator
   public Predicate<ChangeData> comment(String value) {
-    ChangeIndex index = args.indexes.getSearchIndex();
-    return new CommentPredicate(index, value);
+    return new CommentPredicate(args.index, value);
   }
 
   @Operator
   public Predicate<ChangeData> status(String statusName) {
     if ("reviewed".equalsIgnoreCase(statusName)) {
-      return new IsReviewedPredicate();
+      return IsReviewedPredicate.create(args.getSchema());
     } else {
       return ChangeStatusPredicate.parse(statusName);
     }
@@ -381,6 +406,9 @@
       return new HasDraftByPredicate(args, self());
     }
 
+    if ("edit".equalsIgnoreCase(value)) {
+      return new EditByPredicate(self());
+    }
     throw new IllegalArgumentException();
   }
 
@@ -399,7 +427,7 @@
     }
 
     if ("reviewed".equalsIgnoreCase(value)) {
-      return new IsReviewedPredicate();
+      return IsReviewedPredicate.create(args.getSchema());
     }
 
     if ("owner".equalsIgnoreCase(value)) {
@@ -411,7 +439,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
+      return new IsMergeablePredicate(args.fillArgs);
     }
 
     try {
@@ -425,7 +453,7 @@
 
   @Operator
   public Predicate<ChangeData> commit(String id) {
-    return new CommitPredicate(AbbreviatedObjectId.fromString(id));
+    return new CommitPredicate(args.getSchema(), id);
   }
 
   @Operator
@@ -441,8 +469,9 @@
 
   @Operator
   public Predicate<ChangeData> project(String name) {
-    if (name.startsWith("^"))
+    if (name.startsWith("^")) {
       return new RegexProjectPredicate(name);
+    }
     return new ProjectPredicate(name);
   }
 
@@ -459,15 +488,10 @@
 
   @Operator
   public Predicate<ChangeData> branch(String name) {
-    if (name.startsWith("^"))
-      return ref("^" + branchToRef(name.substring(1)));
-    return ref(branchToRef(name));
-  }
-
-  private static String branchToRef(String name) {
-    if (!name.startsWith(Branch.R_HEADS))
-      return Branch.R_HEADS + name;
-    return name;
+    if (name.startsWith("^")) {
+      return ref("^" + RefNames.fullName(name.substring(1)));
+    }
+    return ref(RefNames.fullName(name));
   }
 
   @Operator
@@ -477,15 +501,25 @@
 
   @Operator
   public Predicate<ChangeData> topic(String name) {
-    if (name.startsWith("^"))
-      return new RegexTopicPredicate(name);
-    return new TopicPredicate(name);
+    return new ExactTopicPredicate(args.getSchema(), name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> intopic(String name) {
+    if (name.startsWith("^")) {
+      return new RegexTopicPredicate(args.getSchema(), name);
+    }
+    if (name.isEmpty()) {
+      return new ExactTopicPredicate(args.getSchema(), name);
+    }
+    return new FuzzyTopicPredicate(args.getSchema(), name, args.index);
   }
 
   @Operator
   public Predicate<ChangeData> ref(String ref) {
-    if (ref.startsWith("^"))
+    if (ref.startsWith("^")) {
       return new RegexRefPredicate(ref);
+    }
     return new RefPredicate(ref);
   }
 
@@ -588,8 +622,7 @@
 
   @Operator
   public Predicate<ChangeData> message(String text) {
-    ChangeIndex index = args.indexes.getSearchIndex();
-    return new MessagePredicate(index, text);
+    return new MessagePredicate(args.index, text);
   }
 
   @Operator
@@ -615,11 +648,7 @@
     Account.Id callerId;
     try {
       CurrentUser caller = args.self.get();
-      if (caller.isIdentifiedUser()) {
-        callerId = ((IdentifiedUser) caller).getAccountId();
-      } else {
-        callerId = null;
-      }
+      callerId = caller.isIdentifiedUser() ? caller.getAccountId() : null;
     } catch (ProvisionException e) {
       callerId = null;
     }
@@ -682,7 +711,7 @@
   }
 
   public Predicate<ChangeData> is_visible() throws QueryParseException {
-    return visibleto(args.getCurrentUser());
+    return visibleto(args.getUser());
   }
 
   @Operator
@@ -694,9 +723,12 @@
   @Operator
   public Predicate<ChangeData> owner(String who) throws QueryParseException,
       OrmException {
-    Set<Account.Id> m = parseAccount(who);
-    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(m.size());
-    for (Account.Id id : m) {
+    return owner(parseAccount(who));
+  }
+
+  private Predicate<ChangeData> owner(Set<Account.Id> who) {
+    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(who.size());
+    for (Account.Id id : who) {
       p.add(new OwnerPredicate(id));
     }
     return Predicate.or(p);
@@ -778,6 +810,84 @@
     return new DeltaPredicate(value);
   }
 
+  @Operator
+  public Predicate<ChangeData> commentby(String who)
+      throws QueryParseException, OrmException {
+    return commentby(parseAccount(who));
+  }
+
+  private Predicate<ChangeData> commentby(Set<Account.Id> who) {
+    List<CommentByPredicate> p = Lists.newArrayListWithCapacity(who.size());
+    for (Account.Id id : who) {
+      p.add(new CommentByPredicate(id));
+    }
+    return Predicate.or(p);
+  }
+
+  @Operator
+  public Predicate<ChangeData> from(String who)
+      throws QueryParseException, OrmException {
+    Set<Account.Id> ownerIds = parseAccount(who);
+    return Predicate.or(owner(ownerIds), commentby(ownerIds));
+  }
+
+  @Operator
+  public Predicate<ChangeData> query(String name) throws QueryParseException {
+    AllUsersName allUsers = args.allUsersName.get();
+    try (Repository git = args.repoManager.openRepository(allUsers)) {
+      VersionedAccountQueries q = VersionedAccountQueries.forUser(self());
+      q.load(git);
+      String query = q.getQueryList().getQuery(name);
+      if (query != null) {
+        return parse(query);
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw new QueryParseException("Unknown named query (no " +
+          allUsers.get() +" repo): " + name, e);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new QueryParseException("Error parsing named query: " + name, e);
+    }
+    throw new QueryParseException("Unknown named query: " + name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> reviewedby(String who)
+      throws QueryParseException, OrmException {
+    return IsReviewedPredicate.create(args.getSchema(), parseAccount(who));
+  }
+
+  @Operator
+  public Predicate<ChangeData> destination(String name)
+      throws QueryParseException {
+    AllUsersName allUsers = args.allUsersName.get();
+    try (Repository git = args.repoManager.openRepository(allUsers)) {
+      VersionedAccountDestinations d =
+          VersionedAccountDestinations.forUser(self());
+      d.load(git);
+      Set<Branch.NameKey> destinations =
+          d.getDestinationList().getDestinations(name);
+      if (destinations != null) {
+        return new DestinationPredicate(destinations, name);
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw new QueryParseException("Unknown named destination (no " +
+          allUsers.get() +" repo): " + name, e);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new QueryParseException("Error parsing named destination: " + name, e);
+    }
+    throw new QueryParseException("Unknown named destination: " + name);
+  }
+
+  @Operator
+  public Predicate<ChangeData> author(String who) {
+    return new AuthorPredicate(who);
+  }
+
+  @Operator
+  public Predicate<ChangeData> committer(String who) {
+    return new CommitterPredicate(who);
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
@@ -869,9 +979,4 @@
   private Account.Id self() throws QueryParseException {
     return args.getIdentifiedUser().getAccountId();
   }
-
-  private static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
-    ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
-    return index != null ? index.getSchema() : null;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
deleted file mode 100644
index 83492d2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2013 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.server.query.change;
-
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-
-public interface ChangeQueryRewriter {
-  Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start, int limit)
-      throws QueryParseException;
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
new file mode 100644
index 0000000..dee7086
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Objects;
+
+class CommentByPredicate extends IndexPredicate<ChangeData> {
+  private final Account.Id id;
+
+  CommentByPredicate(Account.Id id) {
+    super(ChangeField.COMMENTBY, id.toString());
+    this.id = id;
+  }
+
+  Account.Id getAccountId() {
+    return id;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    for (ChangeMessage m : cd.messages()) {
+      if (Objects.equals(m.getAuthor(), id)) {
+        return true;
+      }
+    }
+    for (PatchLineComment c : cd.publishedComments()) {
+      if (Objects.equals(c.getAuthor(), id)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 3983f62..dd3c3b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -32,9 +32,11 @@
   @Override
   public boolean match(ChangeData object) throws OrmException {
     try {
-      for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(object.getId()), this), 0, 1)
-          .read()) {
+      Predicate<ChangeData> p = Predicate.and(
+          new LegacyChangeIdPredicate(index.getSchema(), object.getId()),
+          this);
+      for (ChangeData cData
+          : index.getSource(p, QueryOptions.oneResult()).read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 14daa4d..5184c53 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2015 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,35 +14,48 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.server.index.ChangeField.COMMIT;
+import static com.google.gerrit.server.index.ChangeField.EXACT_COMMIT;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
+
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-
 class CommitPredicate extends IndexPredicate<ChangeData> {
-  private final AbbreviatedObjectId abbrevId;
+  static FieldDef<ChangeData, ?> commitField(Schema<ChangeData> schema,
+      String id) {
+    if (id.length() == OBJECT_ID_STRING_LENGTH
+        && schema != null && schema.hasField(EXACT_COMMIT)) {
+      return EXACT_COMMIT;
+    }
+    return COMMIT;
+  }
 
-  CommitPredicate(AbbreviatedObjectId id) {
-    super(ChangeField.COMMIT, id.name());
-    this.abbrevId = id;
+  CommitPredicate(Schema<ChangeData> schema, String id) {
+    super(commitField(schema, id), id);
   }
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
-    for (PatchSet p : object.patches()) {
-      if (p.getRevision() != null && p.getRevision().get() != null) {
-        final ObjectId id = ObjectId.fromString(p.getRevision().get());
-        if (abbrevId.prefixCompare(id) == 0) {
-          return true;
-        }
+    String id = getValue().toLowerCase();
+    for (PatchSet p : object.patchSets()) {
+      if (equals(p, id)) {
+        return true;
       }
     }
     return false;
   }
 
+  private boolean equals(PatchSet p, String id) {
+    boolean exact = getField() == EXACT_COMMIT;
+    String rev = p.getRevision() != null ? p.getRevision().get() : null;
+    return (exact && id.equals(rev))
+        || (!exact && rev != null && rev.startsWith(id));
+  }
+
   @Override
   public int getCost() {
     return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
new file mode 100644
index 0000000..3cb7f8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import static com.google.gerrit.server.index.ChangeField.COMMITTER;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+public class CommitterPredicate extends IndexPredicate<ChangeData>  {
+  CommitterPredicate(String value) {
+    super(COMMITTER, FIELD_COMMITTER, value.toLowerCase());
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    return ChangeField.getCommitterParts(object).contains(
+        getValue().toLowerCase());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index e356f64..9b47302 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -21,7 +21,8 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -79,36 +80,14 @@
       List<Predicate<ChangeData>> predicatesForOneChange =
           Lists.newArrayListWithCapacity(5);
       predicatesForOneChange.add(
-          not(new LegacyChangeIdPredicate(c.getId())));
+          not(new LegacyChangeIdPredicate(args.getSchema(), c.getId())));
       predicatesForOneChange.add(
           new ProjectPredicate(c.getProject().get()));
       predicatesForOneChange.add(
           new RefPredicate(c.getDest().get()));
 
-      OperatorPredicate<ChangeData> isMerge = new OperatorPredicate<ChangeData>(
-              ChangeQueryBuilder.FIELD_MERGE, value) {
-
-        @Override
-        public boolean match(ChangeData cd) throws OrmException {
-          ObjectId id = ObjectId.fromString(
-              cd.currentPatchSet().getRevision().get());
-          try (Repository repo =
-                args.repoManager.openRepository(cd.change().getProject());
-              RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-            RevCommit commit = rw.parseCommit(id);
-            return commit.getParentCount() > 1;
-          } catch (IOException e) {
-            throw new IllegalStateException(e);
-          }
-        }
-
-        @Override
-        public int getCost() {
-          return 2;
-        }
-      };
-
-      predicatesForOneChange.add(or(or(filePredicates), isMerge));
+      predicatesForOneChange.add(or(or(filePredicates),
+          new IsMergePredicate(args, value)));
 
       predicatesForOneChange.add(new OperatorPredicate<ChangeData>(
           ChangeQueryBuilder.FIELD_CONFLICTS, value) {
@@ -137,22 +116,21 @@
           }
           try (Repository repo =
                 args.repoManager.openRepository(otherChange.getProject());
-              RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+              CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
             RevFlag canMergeFlag = rw.newFlag("CAN_MERGE");
             CodeReviewCommit commit =
-                (CodeReviewCommit) rw.parseCommit(changeDataCache.getTestAgainst());
-            SubmitStrategy strategy =
-                args.submitStrategyFactory.create(submitType,
-                    db.get(), repo, rw, null, canMergeFlag,
-                    getAlreadyAccepted(repo, rw, commit),
-                    otherChange.getDest());
-            CodeReviewCommit otherCommit =
-                (CodeReviewCommit) rw.parseCommit(other);
+                rw.parseCommit(changeDataCache.getTestAgainst());
+            SubmitStrategy strategy = args.submitStrategyFactory.create(
+                submitType, db.get(), repo, rw, null, canMergeFlag,
+                getAlreadyAccepted(repo, rw, commit), otherChange.getDest(),
+                null);
+            CodeReviewCommit otherCommit = rw.parseCommit(other);
             otherCommit.add(canMergeFlag);
             conflicts = !strategy.dryRun(commit, otherCommit);
             args.conflictsCache.put(conflictsKey, conflicts);
             return conflicts;
-          } catch (MergeException | NoSuchProjectException | IOException e) {
+          } catch (IntegrationException | NoSuchProjectException
+              | IOException e) {
             throw new IllegalStateException(e);
           }
         }
@@ -171,7 +149,7 @@
         }
 
         private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw,
-            CodeReviewCommit tip) throws MergeException {
+            CodeReviewCommit tip) throws IntegrationException {
           Set<RevCommit> alreadyAccepted = Sets.newHashSet();
 
           if (tip != null) {
@@ -187,7 +165,7 @@
               }
             }
           } catch (IOException e) {
-            throw new MergeException(
+            throw new IntegrationException(
                 "Failed to determine already accepted commits.", e);
           }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
index 07a6714..25fa09f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2015 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,16 +14,19 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.server.index.ChangeField.TOPIC;
-
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class TopicPredicate extends IndexPredicate<ChangeData> {
-  TopicPredicate(String topic) {
-    super(ChangeField.TOPIC, topic);
+import java.util.Set;
+
+class DestinationPredicate extends OperatorPredicate<ChangeData> {
+  Set<Branch.NameKey> destinations;
+
+  DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
+    super(ChangeQueryBuilder.FIELD_DESTINATION, value);
+    this.destinations = destinations;
   }
 
   @Override
@@ -32,11 +35,7 @@
     if (change == null) {
       return false;
     }
-    String t = change.getTopic();
-    if (t == null && getField() == TOPIC) {
-      t = "";
-    }
-    return getValue().equals(t);
+    return destinations.contains(change.getDest());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java
new file mode 100644
index 0000000..b544da6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+class EditByPredicate extends IndexPredicate<ChangeData> {
+  private final Account.Id id;
+
+  EditByPredicate(Account.Id id) {
+    super(ChangeField.EDITBY, id.toString());
+    this.id = id;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.editsByUser().contains(id);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index e5fc51d..a21c590 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -24,8 +24,7 @@
   static Predicate<ChangeData> create(Arguments args, String value) {
     Predicate<ChangeData> eqPath =
         new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value);
-    if (!args.indexes.getSearchIndex().getSchema().getFields().containsKey(
-        ChangeField.FILE_PART.getName())) {
+    if (!args.getSchema().hasField(ChangeField.FILE_PART)) {
       return eqPath;
     }
     return Predicate.or(eqPath, new EqualsFilePredicate(value));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
new file mode 100644
index 0000000..6a9d86b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import static com.google.gerrit.server.index.ChangeField.EXACT_TOPIC;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
+import com.google.gwtorm.server.OrmException;
+
+class ExactTopicPredicate extends IndexPredicate<ChangeData> {
+  @SuppressWarnings("deprecation")
+  static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_TOPIC2;
+    }
+    if (schema.hasField(EXACT_TOPIC)) {
+      return schema.getFields().get(EXACT_TOPIC.getName());
+    }
+    if (schema.hasField(ChangeField.LEGACY_TOPIC2)) {
+      return schema.getFields().get(ChangeField.LEGACY_TOPIC2.getName());
+    }
+    // Not exact, but we cannot do any better.
+    return schema.getFields().get(ChangeField.LEGACY_TOPIC3.getName());
+  }
+
+  ExactTopicPredicate(Schema<ChangeData> schema, String topic) {
+    super(topicField(schema), topic);
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    Change change = object.change();
+    if (change == null) {
+      return false;
+    }
+    String t = change.getTopic();
+    if (t == null && getField() == ChangeField.LEGACY_TOPIC2) {
+      t = "";
+    }
+    return getValue().equals(t);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
new file mode 100644
index 0000000..5b9b94c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2010 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.server.query.change;
+
+import static com.google.gerrit.server.index.ChangeField.FUZZY_TOPIC;
+import static com.google.gerrit.server.index.ChangeField.LEGACY_TOPIC2;
+import static com.google.gerrit.server.index.ChangeField.LEGACY_TOPIC3;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+class FuzzyTopicPredicate extends IndexPredicate<ChangeData> {
+  private final ChangeIndex index;
+
+  @SuppressWarnings("deprecation")
+  static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
+    return schema.getField(FUZZY_TOPIC, LEGACY_TOPIC3, LEGACY_TOPIC2).get();
+  }
+
+  FuzzyTopicPredicate(Schema<ChangeData> schema, String topic,
+      ChangeIndex index) {
+    super(topicField(schema), topic);
+    this.index = index;
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public boolean match(final ChangeData cd) throws OrmException {
+    Change change = cd.change();
+    if (change == null) {
+      return false;
+    }
+    String t = change.getTopic();
+    if (t == null) {
+      return false;
+    }
+    if (getField() == FUZZY_TOPIC || getField() == LEGACY_TOPIC3) {
+      try {
+        Predicate<ChangeData> thisId =
+            new LegacyChangeIdPredicate(index.getSchema(), cd.getId());
+        Iterable<ChangeData> results =
+            index.getSource(and(thisId, this), QueryOptions.oneResult()).read();
+        return !Iterables.isEmpty(results);
+      } catch (QueryParseException e) {
+        throw new OrmException(e);
+      }
+    }
+    if (getField() == LEGACY_TOPIC2) {
+      return t.equals(getValue());
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
new file mode 100644
index 0000000..235d64e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.List;
+
+class GroupPredicate extends IndexPredicate<ChangeData> {
+  GroupPredicate(String group) {
+    super(ChangeField.GROUP, group);
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    for (PatchSet ps : cd.patchSets()) {
+      List<String> groups = ps.getGroups();
+      if (groups != null && groups.contains(getValue())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 008cd0f..34a2bdf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -14,18 +14,41 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.index.ChangeField.SUBMISSIONID;
 import static com.google.gerrit.server.query.Predicate.and;
+import static com.google.gerrit.server.query.Predicate.not;
+import static com.google.gerrit.server.query.Predicate.or;
 import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Execute a single query over changes, for use by Gerrit internals.
@@ -52,15 +75,25 @@
     return new ChangeStatusPredicate(status);
   }
 
-  private static Predicate<ChangeData> topic(String topic) {
-    return new TopicPredicate(topic);
+  private static Predicate<ChangeData> commit(Schema<ChangeData> schema,
+      String id) {
+    return new CommitPredicate(schema, id);
   }
 
+  private final IndexConfig indexConfig;
   private final QueryProcessor qp;
+  private final IndexCollection indexes;
+  private final ChangeData.Factory changeDataFactory;
 
   @Inject
-  InternalChangeQuery(QueryProcessor queryProcessor) {
+  InternalChangeQuery(IndexConfig indexConfig,
+      QueryProcessor queryProcessor,
+      IndexCollection indexes,
+      ChangeData.Factory changeDataFactory) {
+    this.indexConfig = indexConfig;
     qp = queryProcessor.enforceVisibility(false);
+    this.indexes = indexes;
+    this.changeDataFactory = changeDataFactory;
   }
 
   public InternalChangeQuery setLimit(int n) {
@@ -94,17 +127,6 @@
     return query(project(project));
   }
 
-  public List<ChangeData> submitted(Branch.NameKey branch) throws OrmException {
-    return query(and(
-        ref(branch),
-        project(branch.getParentKey()),
-        status(Change.Status.SUBMITTED)));
-  }
-
-  public List<ChangeData> allSubmitted() throws OrmException {
-    return query(status(Change.Status.SUBMITTED));
-  }
-
   public List<ChangeData> byBranchOpen(Branch.NameKey branch)
       throws OrmException {
     return query(and(
@@ -113,6 +135,73 @@
         open()));
   }
 
+  public Iterable<ChangeData> byCommitsOnBranchNotMerged(Repository repo,
+      ReviewDb db, Branch.NameKey branch, List<String> hashes)
+      throws OrmException, IOException {
+    return byCommitsOnBranchNotMerged(repo, db, branch, hashes,
+        // Account for all commit predicates plus ref, project, status.
+        indexConfig.maxTerms() - 3);
+  }
+
+  @VisibleForTesting
+  Iterable<ChangeData> byCommitsOnBranchNotMerged(Repository repo, ReviewDb db,
+      Branch.NameKey branch, List<String> hashes, int indexLimit)
+      throws OrmException, IOException {
+    if (hashes.size() > indexLimit) {
+      return byCommitsOnBranchNotMergedFromDatabase(repo, db, branch, hashes);
+    } else {
+      return byCommitsOnBranchNotMergedFromIndex(branch, hashes);
+    }
+  }
+
+  private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
+      Repository repo, ReviewDb db, Branch.NameKey branch, List<String> hashes)
+      throws OrmException, IOException {
+    Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size());
+    String lastPrefix = null;
+    for (Ref ref :
+        repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES).values()) {
+      String r = ref.getName();
+      if ((lastPrefix != null && r.startsWith(lastPrefix))
+          || !hashes.contains(ref.getObjectId().name())) {
+        continue;
+      }
+      Change.Id id = Change.Id.fromRef(r);
+      if (id == null) {
+        continue;
+      }
+      if (changeIds.add(id)) {
+        lastPrefix = r.substring(0, r.lastIndexOf('/'));
+      }
+    }
+
+    List<ChangeData> cds = new ArrayList<>(hashes.size());
+    for (Change c : db.changes().get(changeIds)) {
+      if (c.getDest().equals(branch) && c.getStatus() != Change.Status.MERGED) {
+        cds.add(changeDataFactory.create(db, c));
+      }
+    }
+    return cds;
+  }
+
+  private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex(
+      Branch.NameKey branch, List<String> hashes) throws OrmException {
+    return query(and(
+        ref(branch),
+        project(branch.getParentKey()),
+        not(status(Change.Status.MERGED)),
+        or(commits(schema(indexes), hashes))));
+  }
+
+  private static List<Predicate<ChangeData>> commits(Schema<ChangeData> schema,
+      List<String> hashes) {
+    List<Predicate<ChangeData>> commits = new ArrayList<>(hashes.size());
+    for (String s : hashes) {
+      commits.add(commit(schema, s));
+    }
+    return commits;
+  }
+
   public List<ChangeData> byProjectOpen(Project.NameKey project)
       throws OrmException {
     return query(and(project(project), open()));
@@ -120,7 +209,35 @@
 
   public List<ChangeData> byTopicOpen(String topic)
       throws OrmException {
-    return query(and(topic(topic), open()));
+    return query(and(new ExactTopicPredicate(schema(indexes), topic), open()));
+  }
+
+  public List<ChangeData> byCommit(ObjectId id) throws OrmException {
+    return query(commit(schema(indexes), id.name()));
+  }
+
+  public List<ChangeData> byProjectCommits(Project.NameKey project,
+      List<String> hashes) throws OrmException {
+    int n = indexConfig.maxTerms() - 1;
+    checkArgument(hashes.size() <= n, "cannot exceed %s commits", n);
+    return query(and(project(project), or(commits(schema(indexes), hashes))));
+  }
+
+  public List<ChangeData> bySubmissionId(String cs) throws OrmException {
+    if (Strings.isNullOrEmpty(cs) || !schema(indexes).hasField(SUBMISSIONID)) {
+      return Collections.emptyList();
+    } else {
+      return query(new SubmissionIdPredicate(cs));
+    }
+  }
+
+  public List<ChangeData> byProjectGroups(Project.NameKey project,
+      Collection<String> groups) throws OrmException {
+    List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size());
+    for (String g : groups) {
+      groupPredicates.add(new GroupPredicate(g));
+    }
+    return query(and(project(project), or(groupPredicates)));
   }
 
   public List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
@@ -130,4 +247,9 @@
       throw new OrmException(e);
     }
   }
+
+  private static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
+    ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
+    return index != null ? index.getSchema() : null;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
new file mode 100644
index 0000000..3c02bab
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+public class IsMergePredicate extends OperatorPredicate<ChangeData> {
+  private final Arguments args;
+
+  public IsMergePredicate(Arguments args, String value) {
+    super(ChangeQueryBuilder.FIELD_MERGE, value);
+    this.args = args;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    ObjectId id = ObjectId.fromString(
+        cd.currentPatchSet().getRevision().get());
+    try (Repository repo =
+          args.repoManager.openRepository(cd.change().getProject());
+        RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(id);
+      return commit.getParentCount() > 1;
+    } catch (IOException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 2;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
index 6ef4ab6..8e300a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
@@ -14,33 +14,16 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.server.index.ChangeField.MERGEABLE;
-
 import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 class IsMergeablePredicate extends IndexPredicate<ChangeData> {
-  @SuppressWarnings("deprecation")
-  static FieldDef<ChangeData, ?> mergeableField(Schema<ChangeData> schema) {
-    if (schema == null) {
-      return ChangeField.LEGACY_MERGEABLE;
-    }
-    FieldDef<ChangeData, ?> f = schema.getFields().get(MERGEABLE.getName());
-    if (f != null) {
-      return f;
-    }
-    return schema.getFields().get(ChangeField.LEGACY_MERGEABLE.getName());
-  }
-
   private final FillArgs args;
 
-  IsMergeablePredicate(Schema<ChangeData> schema,
-      FillArgs args) {
-    super(mergeableField(schema), "1");
+  IsMergeablePredicate(FillArgs args) {
+    super(ChangeField.MERGEABLE, "1");
     this.args = args;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 2bf41bb..645fa4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2015 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,42 +14,74 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.index.ChangeField.LEGACY_REVIEWED;
+import static com.google.gerrit.server.index.ChangeField.REVIEWEDBY;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
 class IsReviewedPredicate extends IndexPredicate<ChangeData> {
-  IsReviewedPredicate() {
-    super(ChangeField.REVIEWED, "1");
+  private static final Account.Id NOT_REVIEWED =
+      new Account.Id(ChangeField.NOT_REVIEWED);
+
+  @SuppressWarnings("deprecation")
+  static Predicate<ChangeData> create(Schema<ChangeData> schema) {
+    if (getField(schema) == LEGACY_REVIEWED) {
+      return new LegacyIsReviewedPredicate();
+    }
+    return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED));
+  }
+
+  @SuppressWarnings("deprecation")
+  static Predicate<ChangeData> create(Schema<ChangeData> schema,
+      Collection<Account.Id> ids) throws QueryParseException {
+    if (getField(schema) == LEGACY_REVIEWED) {
+      throw new QueryParseException("Only is:reviewed is supported");
+    }
+    List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
+    for (Account.Id id : ids) {
+      predicates.add(new IsReviewedPredicate(id));
+    }
+    return Predicate.or(predicates);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static FieldDef<ChangeData, ?> getField(Schema<ChangeData> schema) {
+    Optional<FieldDef<ChangeData, ?>> f =
+        schema.getField(REVIEWEDBY, LEGACY_REVIEWED);
+    checkState(f.isPresent(), "Schema %s missing field %s",
+        schema.getVersion(), REVIEWEDBY.getName());
+    return f.get();
+  }
+
+  private final Account.Id id;
+
+  private IsReviewedPredicate(Account.Id id) {
+    super(REVIEWEDBY, Integer.toString(id.get()));
+    this.id = id;
   }
 
   @Override
-  public boolean match(final ChangeData object) throws OrmException {
-    Change c = object.change();
-    if (c == null) {
-      return false;
-    }
-
-    PatchSet.Id current = c.currentPatchSetId();
-    for (PatchSetApproval p : object.approvals().get(current)) {
-      if (p.getValue() != 0) {
-        return true;
-      }
-    }
-
-    return false;
+  public boolean match(ChangeData cd) throws OrmException {
+    Set<Account.Id> reviewedBy = cd.reviewedBy();
+    return !reviewedBy.isEmpty() ? reviewedBy.contains(id) : id == NOT_REVIEWED;
   }
 
   @Override
   public int getCost() {
-    return 2;
-  }
-
-  @Override
-  public String toString() {
-    return "is:reviewed";
+    return 1;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index b5bef07..1ac2729 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
@@ -32,15 +33,16 @@
     ChangeDataSource {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
-      return ((IdentifiedUser) user).getAccountId().toString();
+      return user.getAccountId().toString();
     }
     return user.toString();
   }
 
-  private static List<Predicate<ChangeData>> predicates(Set<Change.Id> ids) {
+  private static List<Predicate<ChangeData>> predicates(
+      Schema<ChangeData> schema, Set<Change.Id> ids) {
     List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
     for (Change.Id id : ids) {
-      r.add(new LegacyChangeIdPredicate(id));
+      r.add(new LegacyChangeIdPredicate(schema, id));
     }
     return r;
   }
@@ -53,7 +55,7 @@
   }
 
   private IsStarredByPredicate(Arguments args, IdentifiedUser user) {
-    super(predicates(user.getStarredChanges()));
+    super(predicates(args.getSchema(), user.getStarredChanges()));
     this.args = args;
     this.user = user;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index 7f91699..856a559 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.OperatorPredicate;
@@ -27,7 +26,7 @@
 class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
-      return ((IdentifiedUser) user).getAccountId().toString();
+      return user.getAccountId().toString();
     }
     if (user instanceof SingleGroupUser) {
       return "group:" + user.getEffectiveGroups().getKnownGroups() //
@@ -65,6 +64,7 @@
         return true;
       }
     } catch (NoSuchChangeException e) {
+      // Ignored
     }
     return false;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 606f577..a027869 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -18,7 +18,6 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
@@ -29,7 +28,7 @@
 class IsWatchedByPredicate extends AndPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
-      return ((IdentifiedUser) user).getAccountId().toString();
+      return user.getAccountId().toString();
     }
     return user.toString();
   }
@@ -39,13 +38,13 @@
   IsWatchedByPredicate(ChangeQueryBuilder.Arguments args,
       boolean checkIsVisible) throws QueryParseException {
     super(filters(args, checkIsVisible));
-    this.user = args.getCurrentUser();
+    this.user = args.getUser();
   }
 
   private static List<Predicate<ChangeData>> filters(
       ChangeQueryBuilder.Arguments args,
       boolean checkIsVisible) throws QueryParseException {
-    CurrentUser user = args.getCurrentUser();
+    CurrentUser user = args.getIdentifiedUser();
     List<Predicate<ChangeData>> r = Lists.newArrayList();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
     for (AccountProjectWatch w : user.getNotificationFilters()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 83364c3..2e2454d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -141,10 +141,10 @@
     List<Predicate<ChangeData>> r =
         Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
     for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
-      r.add(not(equalsLabelPredicate(args, label, i)));
-      r.add(not(equalsLabelPredicate(args, label, -i)));
+      r.add(equalsLabelPredicate(args, label, i));
+      r.add(equalsLabelPredicate(args, label, -i));
     }
-    return and(r);
+    return not(or(r));
   }
 
   private static Predicate<ChangeData> equalsLabelPredicate(Args args,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 9ecc7b2..bf59553 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -14,16 +14,32 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.server.index.ChangeField.LEGACY_ID;
+import static com.google.gerrit.server.index.ChangeField.LEGACY_ID2;
+
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 
 /** Predicate over change number (aka legacy ID or Change.Id). */
-class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> {
+public class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> {
   private final Change.Id id;
 
-  LegacyChangeIdPredicate(Change.Id id) {
-    super(ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+  @SuppressWarnings("deprecation")
+  public static FieldDef<ChangeData, ?> idField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_ID2;
+    } else if (schema.hasField(LEGACY_ID2)) {
+      return schema.getFields().get(LEGACY_ID2.getName());
+    } else {
+      return schema.getFields().get(LEGACY_ID.getName());
+    }
+  }
+
+  LegacyChangeIdPredicate(Schema<ChangeData> schema, Change.Id id) {
+    super(idField(schema), ChangeQueryBuilder.FIELD_CHANGE, id.toString());
     this.id = id;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyIsReviewedPredicate.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyIsReviewedPredicate.java
index 07a6714..e12e6e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyIsReviewedPredicate.java
@@ -14,33 +14,44 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.server.index.ChangeField.TOPIC;
-
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class TopicPredicate extends IndexPredicate<ChangeData> {
-  TopicPredicate(String topic) {
-    super(ChangeField.TOPIC, topic);
+@Deprecated
+class LegacyIsReviewedPredicate extends IndexPredicate<ChangeData> {
+  @Deprecated
+  LegacyIsReviewedPredicate() {
+    super(ChangeField.LEGACY_REVIEWED, "1");
   }
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
-    Change change = object.change();
-    if (change == null) {
+    Change c = object.change();
+    if (c == null) {
       return false;
     }
-    String t = change.getTopic();
-    if (t == null && getField() == TOPIC) {
-      t = "";
+
+    PatchSet.Id current = c.currentPatchSetId();
+    for (PatchSetApproval p : object.approvals().get(current)) {
+      if (p.getValue() != 0) {
+        return true;
+      }
     }
-    return getValue().equals(t);
+
+    return false;
   }
 
   @Override
   public int getCost() {
-    return 1;
+    return 2;
+  }
+
+  @Override
+  public String toString() {
+    return "is:reviewed";
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 0a16d02..cf3140a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -36,9 +36,11 @@
   @Override
   public boolean match(ChangeData object) throws OrmException {
     try {
-      for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(object.getId()), this), 0, 1)
-          .read()) {
+      Predicate<ChangeData> p = Predicate.and(
+          new LegacyChangeIdPredicate(index.getSchema(), object.getId()),
+          this);
+      for (ChangeData cData
+          : index.getSource(p, QueryOptions.oneResult()).read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 3012735..280be50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -14,22 +14,31 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.data.QueryStatsAttribute;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
@@ -45,7 +54,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Change query implementation that outputs to a stream in the style of an SSH
@@ -62,6 +73,8 @@
     TEXT, JSON
   }
 
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final QueryProcessor queryProcessor;
   private final EventFactory eventFactory;
@@ -84,11 +97,15 @@
 
   @Inject
   OutputStreamQuery(
+      Provider<ReviewDb> db,
+      GitRepositoryManager repoManager,
       ChangeQueryBuilder queryBuilder,
       QueryProcessor queryProcessor,
       EventFactory eventFactory,
       TrackingFooters trackingFooters,
       CurrentUser user) {
+    this.db = db;
+    this.repoManager = repoManager;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
     this.eventFactory = eventFactory;
@@ -164,7 +181,7 @@
   public void query(String queryString) throws IOException {
     out = new PrintWriter( //
         new BufferedWriter( //
-            new OutputStreamWriter(outputStream, "UTF-8")));
+            new OutputStreamWriter(outputStream, UTF_8)));
     try {
       if (queryProcessor.isDisabled()) {
         ErrorMessage m = new ErrorMessage();
@@ -177,83 +194,16 @@
         final QueryStatsAttribute stats = new QueryStatsAttribute();
         stats.runTimeMilliseconds = TimeUtil.nowMs();
 
+        Map<Project.NameKey, Repository> repos = new HashMap<>();
+        Map<Project.NameKey, RevWalk> revWalks = new HashMap<>();
         QueryResult results =
             queryProcessor.queryChanges(queryBuilder.parse(queryString));
-        ChangeAttribute c = null;
-        for (ChangeData d : results.changes()) {
-          ChangeControl cc = d.changeControl().forUser(user);
-
-          LabelTypes labelTypes = cc.getLabelTypes();
-          c = eventFactory.asChangeAttribute(d.change());
-          eventFactory.extend(c, d.change());
-
-          if (!trackingFooters.isEmpty()) {
-            eventFactory.addTrackingIds(c,
-                trackingFooters.extract(d.commitFooters()));
+        try {
+          for (ChangeData d : results.changes()) {
+            show(buildChangeAttribute(d, repos, revWalks));
           }
-
-          if (includeAllReviewers) {
-            eventFactory.addAllReviewers(c, d.notes());
-          }
-
-          if (includeSubmitRecords) {
-            eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
-                .setAllowClosed(true)
-                .setAllowDraft(true)
-                .evaluate());
-          }
-
-          if (includeCommitMessage) {
-            eventFactory.addCommitMessage(c, d.commitMessage());
-          }
-
-          if (includePatchSets) {
-            if (includeFiles) {
-              eventFactory.addPatchSets(c, d.visiblePatches(),
-                  includeApprovals ? d.approvals().asMap() : null,
-                  includeFiles, d.change(), labelTypes);
-            } else {
-              eventFactory.addPatchSets(c, d.visiblePatches(),
-                  includeApprovals ? d.approvals().asMap() : null,
-                  labelTypes);
-            }
-          }
-
-          if (includeCurrentPatchSet) {
-            PatchSet current = d.currentPatchSet();
-            if (current != null && cc.isPatchVisible(current, d.db())) {
-              c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
-              eventFactory.addApprovals(c.currentPatchSet,
-                  d.currentApprovals(), labelTypes);
-
-              if (includeFiles) {
-                eventFactory.addPatchSetFileNames(c.currentPatchSet,
-                    d.change(), d.currentPatchSet());
-              }
-              if (includeComments) {
-                eventFactory.addPatchSetComments(c.currentPatchSet,
-                    d.publishedComments());
-              }
-            }
-          }
-
-          if (includeComments) {
-            eventFactory.addComments(c, d.messages());
-            if (includePatchSets) {
-              eventFactory.addPatchSets(c, d.visiblePatches(),
-                  includeApprovals ? d.approvals().asMap() : null,
-                  includeFiles, d.change(), labelTypes);
-              for (PatchSetAttribute attribute : c.patchSets) {
-                eventFactory.addPatchSetComments(attribute,  d.publishedComments());
-              }
-            }
-          }
-
-          if (includeDependencies) {
-            eventFactory.addDependencies(c, d.change());
-          }
-
-          show(c);
+        } finally {
+          closeAll(revWalks.values(), repos.values());
         }
 
         stats.rowCount = results.changes().size();
@@ -282,6 +232,108 @@
     }
   }
 
+  private ChangeAttribute buildChangeAttribute(ChangeData d,
+      Map<Project.NameKey, Repository> repos,
+      Map<Project.NameKey, RevWalk> revWalks)
+      throws OrmException, IOException {
+    ChangeControl cc = d.changeControl().forUser(user);
+
+    LabelTypes labelTypes = cc.getLabelTypes();
+    ChangeAttribute c = eventFactory.asChangeAttribute(db.get(), d.change());
+    eventFactory.extend(c, d.change());
+
+    if (!trackingFooters.isEmpty()) {
+      eventFactory.addTrackingIds(c,
+          trackingFooters.extract(d.commitFooters()));
+    }
+
+    if (includeAllReviewers) {
+      eventFactory.addAllReviewers(db.get(), c, d.notes());
+    }
+
+    if (includeSubmitRecords) {
+      eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
+          .setAllowClosed(true)
+          .setAllowDraft(true)
+          .evaluate());
+    }
+
+    if (includeCommitMessage) {
+      eventFactory.addCommitMessage(c, d.commitMessage());
+    }
+
+    RevWalk rw = null;
+    if (includePatchSets || includeCurrentPatchSet || includeDependencies) {
+      Project.NameKey p = d.change().getProject();
+      rw = revWalks.get(p);
+      // Cache and reuse repos and revwalks.
+      if (rw == null) {
+        Repository repo = repoManager.openRepository(p);
+        checkState(repos.put(p, repo) == null);
+        rw = new RevWalk(repo);
+        revWalks.put(p, rw);
+      }
+    }
+
+    if (includePatchSets) {
+      eventFactory.addPatchSets(db.get(), rw, c, d.visiblePatchSets(),
+          includeApprovals ? d.approvals().asMap() : null,
+          includeFiles, d.change(), labelTypes);
+    }
+
+    if (includeCurrentPatchSet) {
+      PatchSet current = d.currentPatchSet();
+      if (current != null && cc.isPatchVisible(current, d.db())) {
+        c.currentPatchSet =
+            eventFactory.asPatchSetAttribute(db.get(), rw, current);
+        eventFactory.addApprovals(c.currentPatchSet,
+            d.currentApprovals(), labelTypes);
+
+        if (includeFiles) {
+          eventFactory.addPatchSetFileNames(c.currentPatchSet,
+              d.change(), d.currentPatchSet());
+        }
+        if (includeComments) {
+          eventFactory.addPatchSetComments(c.currentPatchSet,
+              d.publishedComments());
+        }
+      }
+    }
+
+    if (includeComments) {
+      eventFactory.addComments(c, d.messages());
+      if (includePatchSets) {
+        eventFactory.addPatchSets(db.get(), rw, c, d.visiblePatchSets(),
+            includeApprovals ? d.approvals().asMap() : null,
+            includeFiles, d.change(), labelTypes);
+        for (PatchSetAttribute attribute : c.patchSets) {
+          eventFactory.addPatchSetComments(
+              attribute, d.publishedComments());
+        }
+      }
+    }
+
+    if (includeDependencies) {
+      eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
+    }
+
+    return c;
+  }
+
+  private static void closeAll(Iterable<RevWalk> revWalks,
+      Iterable<Repository> repos) {
+    if (repos != null) {
+      for (Repository repo : repos) {
+        repo.close();
+      }
+    }
+    if (revWalks != null) {
+      for (RevWalk revWalk : revWalks) {
+        revWalk.close();
+      }
+    }
+  }
+
   private void show(Object data) {
     switch (outputFormat) {
       default:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
index 7afd934..3278b7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
@@ -18,7 +18,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 public interface Paginated {
-  int limit();
+  QueryOptions getOptions();
 
   ResultSet<ChangeData> restart(int start) throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
index 3e794b7..23350d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -38,7 +38,7 @@
    * Parses query arguments into {@link #keyValue} and/or {@link #positional}..
    * <p>
    * Labels for these arguments should be kept in ChangeQueryBuilder
-   * as {@code ARG_ID_{argument name}}.
+   * as {@code ARG_ID_[argument name]}.
    *
    * @param args arguments to be parsed
    * @throws QueryParseException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 153329a..dfc0f75e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -38,7 +38,7 @@
 import java.util.regex.Pattern;
 
 public class QueryChanges implements RestReadView<TopLevelResource> {
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
   private final ChangeQueryBuilder qb;
   private final QueryProcessor imp;
   private final Provider<CurrentUser> user;
@@ -68,7 +68,7 @@
   }
 
   @Inject
-  QueryChanges(ChangeJson json,
+  QueryChanges(ChangeJson.Factory json,
       ChangeQueryBuilder qb,
       QueryProcessor qp,
       Provider<CurrentUser> user) {
@@ -127,7 +127,7 @@
     IdentifiedUser self = null;
     try {
       if (user.get().isIdentifiedUser()) {
-        self = (IdentifiedUser) user.get();
+        self = user.get().asIdentifiedUser();
         self.asyncStarredChanges();
       }
       return query0();
@@ -142,7 +142,7 @@
       QueryParseException {
     int cnt = queries.size();
     List<QueryResult> results = imp.queryChanges(qb.parse(queries));
-    List<List<ChangeInfo>> res = json.addOptions(options)
+    List<List<ChangeInfo>> res = json.create(options)
         .formatQueryResults(results);
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
new file mode 100644
index 0000000..1964fa5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.server.index.IndexConfig;
+
+@AutoValue
+public abstract class QueryOptions {
+  public static QueryOptions create(IndexConfig config, int start, int limit) {
+    checkArgument(start >= 0, "start must be nonnegative: %s", start);
+    checkArgument(limit > 0, "limit must be positive: %s", limit);
+    return new AutoValue_QueryOptions(config, start, limit);
+  }
+
+  public static QueryOptions oneResult() {
+    return create(IndexConfig.createDefault(), 0, 1);
+  }
+
+  public abstract IndexConfig config();
+  public abstract int start();
+  public abstract int limit();
+
+  public QueryOptions withLimit(int newLimit) {
+    return create(config(), start(), newLimit);
+  }
+
+  public QueryOptions withStart(int newStart) {
+    return create(config(), newStart, limit());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 51d971d..870ca040 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -24,10 +24,12 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.IndexRewriter;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -39,7 +41,7 @@
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> userProvider;
   private final ChangeControl.GenericFactory changeControlFactory;
-  private final ChangeQueryRewriter queryRewriter;
+  private final IndexRewriter rewriter;
   private final IndexConfig indexConfig;
 
   private int limitFromCaller;
@@ -50,12 +52,12 @@
   QueryProcessor(Provider<ReviewDb> db,
       Provider<CurrentUser> userProvider,
       ChangeControl.GenericFactory changeControlFactory,
-      ChangeQueryRewriter queryRewriter,
+      IndexRewriter rewriter,
       IndexConfig indexConfig) {
     this.db = db;
     this.userProvider = userProvider;
     this.changeControlFactory = changeControlFactory;
-    this.queryRewriter = queryRewriter;
+    this.rewriter = rewriter;
     this.indexConfig = indexConfig;
   }
 
@@ -99,7 +101,12 @@
    */
   public List<QueryResult> queryChanges(List<Predicate<ChangeData>> queries)
       throws OrmException, QueryParseException {
-    return queryChanges(null, queries);
+    try {
+      return queryChanges(null, queries);
+    } catch (OrmRuntimeException e) {
+      throw new OrmException(e.getMessage(), e);
+    }
+
   }
 
   static {
@@ -132,10 +139,18 @@
       if (limit == getBackendSupportedLimit()) {
         limit--;
       }
-      Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
+
+      int page = (start / limit) + 1;
+      if (page > indexConfig.maxPages()) {
+        throw new QueryParseException(
+            "Cannot go beyond page " + indexConfig.maxPages() + "of results");
+      }
+
+      QueryOptions opts = QueryOptions.create(indexConfig, start, limit + 1);
+      Predicate<ChangeData> s = rewriter.rewrite(q, opts);
       if (!(s instanceof ChangeDataSource)) {
         q = Predicate.and(open(), q);
-        s = queryRewriter.rewrite(q, start, limit);
+        s = rewriter.rewrite(q, opts);
       }
       if (!(s instanceof ChangeDataSource)) {
         throw new QueryParseException("invalid query: " + s);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 3a9604f..fecc72e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 import dk.brics.automaton.RegExp;
@@ -25,8 +25,8 @@
 class RegexTopicPredicate extends RegexPredicate<ChangeData> {
   private final RunAutomaton pattern;
 
-  RegexTopicPredicate(String re) {
-    super(ChangeField.TOPIC, re);
+  RegexTopicPredicate(Schema<ChangeData> schema, String re) {
+    super(ExactTopicPredicate.topicField(schema), re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
deleted file mode 100644
index 0947fae..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2013 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.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-/**
- * Predicate which creates Repository, RevWalk objects and properly
- * closes them. Git based operators should extend this predicate.
- *
- */
-public abstract class RevWalkPredicate extends OperatorPredicate<ChangeData> {
-  private static final Logger log =
-      LoggerFactory.getLogger(RevWalkPredicate.class);
-
-  public static class Arguments {
-    public final PatchSet patchSet;
-    public final RevId revision;
-    public final AnyObjectId objectId;
-    public final Change change;
-    public final Project.NameKey projectName;
-
-    public Arguments(PatchSet patchSet,
-        RevId revision,
-        AnyObjectId objectId,
-        Change change,
-        Project.NameKey projectName) {
-      this.patchSet = patchSet;
-      this.revision = revision;
-      this.objectId = objectId;
-      this.change = change;
-      this.projectName = projectName;
-    }
-  }
-
-  public final Provider<ReviewDb> db;
-  public final GitRepositoryManager repoManager;
-
-  public RevWalkPredicate(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager, String operator, String ref) {
-    super(operator, ref);
-    this.db = db;
-    this.repoManager = repoManager;
-  }
-
-  @Override
-  public boolean match(ChangeData object) throws OrmException {
-    final PatchSet patchSet = object.currentPatchSet();
-    if (patchSet == null) {
-      return false;
-    }
-
-    final RevId revision = patchSet.getRevision();
-    if (revision == null) {
-      return false;
-    }
-
-    final AnyObjectId objectId = ObjectId.fromString(revision.get());
-    if (objectId == null) {
-      return false;
-    }
-
-    Change change = object.change();
-    if (change == null) {
-      return false;
-    }
-
-    final Project.NameKey projectName = change.getProject();
-    if (projectName == null) {
-      return false;
-    }
-
-    Arguments args = new Arguments(patchSet, revision, objectId, change, projectName);
-
-    try (Repository repo = repoManager.openRepository(projectName);
-        RevWalk rw = new RevWalk(repo)) {
-      return match(repo, rw, args);
-    } catch (RepositoryNotFoundException e) {
-      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
-    } catch (IOException e) {
-      log.error(projectName.get() + " cannot be read as a repository", e);
-    }
-    return false;
-  }
-
-  public abstract boolean match(Repository repo, RevWalk rw, Arguments args);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
similarity index 69%
rename from gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
index 07a6714..3b2dd94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2015 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,29 +14,27 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.server.index.ChangeField.TOPIC;
-
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class TopicPredicate extends IndexPredicate<ChangeData> {
-  TopicPredicate(String topic) {
-    super(ChangeField.TOPIC, topic);
+class SubmissionIdPredicate extends IndexPredicate<ChangeData> {
+
+  SubmissionIdPredicate(String changeSet) {
+    super(ChangeField.SUBMISSIONID, changeSet);
   }
 
   @Override
-  public boolean match(final ChangeData object) throws OrmException {
+  public boolean match(ChangeData object) throws OrmException {
     Change change = object.change();
     if (change == null) {
       return false;
     }
-    String t = change.getTopic();
-    if (t == null && getField() == TOPIC) {
-      t = "";
+    if (change.getSubmissionId() == null) {
+      return false;
     }
-    return getValue().equals(t);
+    return getValue().equals(change.getSubmissionId());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AclUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AclUtil.java
new file mode 100644
index 0000000..ced8cd0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AclUtil.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2015 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.server.schema;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.server.git.ProjectConfig;
+
+public class AclUtil {
+  public static void grant(ProjectConfig config, AccessSection section,
+      String permission, GroupReference... groupList) {
+    grant(config, section, permission, false, groupList);
+  }
+
+  public static void grant(ProjectConfig config, AccessSection section,
+      String permission, boolean force, GroupReference... groupList) {
+    Permission p = section.getPermission(permission, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setForce(force);
+        p.add(r);
+      }
+    }
+  }
+
+  public static void grant(ProjectConfig config,
+      AccessSection section, LabelType type,
+      int min, int max, GroupReference... groupList) {
+    String name = Permission.LABEL + type.getName();
+    Permission p = section.getPermission(name, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setRange(min, max);
+        p.add(r);
+      }
+    }
+  }
+
+  public static PermissionRule rule(ProjectConfig config, GroupReference group) {
+    return new PermissionRule(config.resolve(group));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 1eefbf9..9b6e36a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -17,6 +17,8 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.schema.AclUtil.grant;
+import static com.google.gerrit.server.schema.AclUtil.rule;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
@@ -94,27 +96,19 @@
   }
 
   public void create() throws IOException, ConfigInvalidException {
-    Repository git = null;
-    try {
-      git = mgr.openRepository(allProjectsName);
+    try (Repository git = mgr.openRepository(allProjectsName)) {
       initAllProjects(git);
     } catch (RepositoryNotFoundException notFound) {
       // A repository may be missing if this project existed only to store
       // inheritable permissions. For example 'All-Projects'.
-      try {
-        git = mgr.createRepository(allProjectsName);
+      try (Repository git = mgr.createRepository(allProjectsName)) {
         initAllProjects(git);
-
         RefUpdate u = git.updateRef(Constants.HEAD);
         u.link(RefNames.REFS_CONFIG);
       } catch (RepositoryNotFoundException err) {
         String name = allProjectsName.get();
         throw new IOException("Cannot create repository " + name, err);
       }
-    } finally {
-      if (git != null) {
-        git.close();
-      }
     }
   }
 
@@ -137,6 +131,7 @@
     p.setUseContentMerge(InheritableBoolean.TRUE);
     p.setUseContributorAgreements(InheritableBoolean.FALSE);
     p.setUseSignedOffBy(InheritableBoolean.FALSE);
+    p.setEnableSignedPush(InheritableBoolean.FALSE);
 
     AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
     AccessSection all = config.getAccessSection(AccessSection.ALL, true);
@@ -183,41 +178,6 @@
     config.commitToNewRef(md, RefNames.REFS_CONFIG);
   }
 
-  private void grant(ProjectConfig config, AccessSection section,
-      String permission, GroupReference... groupList) {
-    grant(config, section, permission, false, groupList);
-  }
-
-  private void grant(ProjectConfig config, AccessSection section,
-      String permission, boolean force, GroupReference... groupList) {
-    Permission p = section.getPermission(permission, true);
-    for (GroupReference group : groupList) {
-      if (group != null) {
-        PermissionRule r = rule(config, group);
-        r.setForce(force);
-        p.add(r);
-      }
-    }
-  }
-
-  private void grant(ProjectConfig config,
-      AccessSection section, LabelType type,
-      int min, int max, GroupReference... groupList) {
-    String name = Permission.LABEL + type.getName();
-    Permission p = section.getPermission(name, true);
-    for (GroupReference group : groupList) {
-      if (group != null) {
-        PermissionRule r = rule(config, group);
-        r.setRange(min, max);
-        p.add(r);
-      }
-    }
-  }
-
-  private PermissionRule rule(ProjectConfig config, GroupReference group) {
-    return new PermissionRule(config.resolve(group));
-  }
-
   public static LabelType initCodeReviewLabel(ProjectConfig c) {
     LabelType type = new LabelType("Code-Review", ImmutableList.of(
         new LabelValue((short) 2, "Looks good to me, approved"),
@@ -226,6 +186,7 @@
         new LabelValue((short) -1, "I would prefer this is not merged as is"),
         new LabelValue((short) -2, "This shall not be merged")));
     type.setCopyMinScore(true);
+    type.setCopyAllScoresOnTrivialRebase(true);
     c.getLabelSections().put(type.getName(), type);
     return type;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
index fda5306..22a345a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
+import static com.google.gerrit.server.schema.AclUtil.grant;
+
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -40,6 +43,8 @@
   private final AllUsersName allUsersName;
   private final PersonIdent serverUser;
 
+  private GroupReference admin;
+
   @Inject
   AllUsersCreator(
       GitRepositoryManager mgr,
@@ -50,23 +55,21 @@
     this.serverUser = serverUser;
   }
 
+  public AllUsersCreator setAdministrators(GroupReference admin) {
+    this.admin = admin;
+    return this;
+  }
+
   public void create() throws IOException, ConfigInvalidException {
-    Repository git = null;
-    try {
-      git = mgr.openRepository(allUsersName);
+    try (Repository git = mgr.openRepository(allUsersName)) {
       initAllUsers(git);
     } catch (RepositoryNotFoundException notFound) {
-      try {
-        git = mgr.createRepository(allUsersName);
+      try (Repository git = mgr.createRepository(allUsersName)) {
         initAllUsers(git);
       } catch (RepositoryNotFoundException err) {
         String name = allUsersName.get();
         throw new IOException("Cannot create repository " + name, err);
       }
-    } finally {
-      if (git != null) {
-        git.close();
-      }
     }
   }
 
@@ -84,8 +87,17 @@
     Project project = config.getProject();
     project.setDescription("Individual user settings and preferences.");
 
-    AccessSection all = config.getAccessSection(RefNames.REFS_USER + "*", true);
+    AccessSection all = config.getAccessSection(RefNames.REFS_USERS + "*", true);
     all.getPermission(Permission.READ, true).setExclusiveGroup(true);
+
+    AccessSection defaults = config.getAccessSection(RefNames.REFS_USERS_DEFAULT, true);
+    defaults.getPermission(Permission.READ, true).setExclusiveGroup(true);
+    grant(config, defaults, Permission.READ, admin);
+    defaults.getPermission(Permission.PUSH, true).setExclusiveGroup(true);
+    grant(config, defaults, Permission.PUSH, admin);
+    defaults.getPermission(Permission.CREATE, true).setExclusiveGroup(true);
+    grant(config, defaults, Permission.CREATE, admin);
+
     config.commit(md);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
index e72c3e9..bf87ee0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
@@ -21,6 +21,7 @@
 
 public abstract class BaseDataSourceType implements DataSourceType {
 
+  private static final String DEFAULT_VALIDATION_QUERY = "select 1";
   private final String driver;
 
   protected BaseDataSourceType(String driver) {
@@ -38,6 +39,11 @@
   }
 
   @Override
+  public String getValidationQuery() {
+    return DEFAULT_VALIDATION_QUERY;
+  }
+
+  @Override
   public ScriptRunner getIndexScript() throws IOException {
     return getScriptRunner("index_generic.sql");
   }
@@ -46,15 +52,12 @@
     if (path == null) {
       return ScriptRunner.NOOP;
     }
-    InputStream in =  ReviewDb.class.getResourceAsStream(path);
-    if (in == null) {
-      throw new IllegalStateException("SQL script " + path + " not found");
-    }
     ScriptRunner runner;
-    try {
+    try (InputStream in = ReviewDb.class.getResourceAsStream(path)) {
+      if (in == null) {
+        throw new IllegalStateException("SQL script " + path + " not found");
+      }
       runner = new ScriptRunner(path, in);
-    } finally {
-      in.close();
     }
     return runner;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java
new file mode 100644
index 0000000..4f0b63f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 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.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+public class DB2 extends BaseDataSourceType {
+  private Config cfg;
+
+  @Inject
+  public DB2(@GerritServerConfig final Config cfg) {
+    super("com.ibm.db2.jcc.DB2Driver");
+    this.cfg = cfg;
+  }
+
+  @Override
+  public String getUrl() {
+    final StringBuilder b = new StringBuilder();
+    final ConfigSection dbc = new ConfigSection(cfg, "database");
+    b.append("jdbc:db2://");
+    b.append(hostname(dbc.optional("hostname")));
+    b.append(port(dbc.optional("port")));
+    b.append("/");
+    b.append(dbc.required("database"));
+    return b.toString();
+  }
+
+  @Override
+  public String getValidationQuery() {
+    return "SELECT 1 FROM SYSIBM.SYSDUMMY1";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
index f500444..777b5b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
@@ -21,6 +21,8 @@
 
   @Override
   protected void configure() {
+    bind(DataSourceType.class).annotatedWith(Names.named("db2")).to(DB2.class);
+    bind(DataSourceType.class).annotatedWith(Names.named("derby")).to(Derby.class);
     bind(DataSourceType.class).annotatedWith(Names.named("h2")).to(H2.class);
     bind(DataSourceType.class).annotatedWith(Names.named("jdbc")).to(JDBC.class);
     bind(DataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 30ebaa7..ad6744f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -126,6 +126,9 @@
       ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
           "poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
       ds.setInitialSize(ds.getMinIdle());
+      ds.setValidationQuery(dst.getValidationQuery());
+      ds.setValidationQueryTimeout(5);
+
       return intercept(interceptor, ds);
 
     } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
index ecaaf5e..2651ee2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
@@ -24,6 +24,8 @@
 
   public String getUrl();
 
+  public String getValidationQuery();
+
   public boolean usePool();
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
index c7832d4..199b3bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
@@ -16,8 +16,8 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.TypeLiteral;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Derby.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Derby.java
new file mode 100644
index 0000000..f98e83b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Derby.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 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.server.schema;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+class Derby extends BaseDataSourceType {
+
+  protected final Config cfg;
+  private final SitePaths site;
+
+  @Inject
+  Derby(@GerritServerConfig Config cfg,
+      SitePaths site) {
+    super("org.apache.derby.jdbc.EmbeddedDriver");
+    this.cfg = cfg;
+    this.site = site;
+  }
+
+  @Override
+  public String getUrl() {
+    String database = cfg.getString("database", null, "database");
+    if (database == null || database.isEmpty()) {
+      database = "db/ReviewDB";
+    }
+    return "jdbc:derby:" + site.resolve(database).toString() + ";create=true";
+  }
+
+  @Override
+  public String getValidationQuery() {
+    return "values 1";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
index f43530f..66f2f1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
@@ -20,9 +20,6 @@
 
 import org.eclipse.jgit.lib.Config;
 
-import java.io.File;
-import java.io.IOException;
-
 class H2 extends BaseDataSourceType {
 
   protected final Config cfg;
@@ -41,12 +38,6 @@
     if (database == null || database.isEmpty()) {
       database = "db/ReviewDB";
     }
-    File db = site.resolve(database);
-    try {
-      db = db.getCanonicalFile();
-    } catch (IOException e) {
-      db = db.getAbsoluteFile();
-    }
-    return "jdbc:h2:" + db.toURI().toString();
+    return "jdbc:h2:" + site.resolve(database).toUri().toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
index bb4c477..e7d3390 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
@@ -43,4 +43,9 @@
     b.append(dbc.required("instance"));
     return b.toString();
   }
+
+  @Override
+  public String getValidationQuery() {
+    return "select 1 from dual";
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index daf1d4d..2581d56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -32,14 +32,15 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.PersonIdent;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Collections;
 
 /** Creates the current database schema and populates initial code rows. */
 public class SchemaCreator {
-  private final @SitePath
-  File site_path;
+  @SitePath
+  private final
+  Path site_path;
 
   private final AllProjectsCreator allProjectsCreator;
   private final AllUsersCreator allUsersCreator;
@@ -58,7 +59,7 @@
     this(site.site_path, ap, auc, au, dst);
   }
 
-  public SchemaCreator(@SitePath File site,
+  public SchemaCreator(@SitePath Path site,
       AllProjectsCreator ap,
       AllUsersCreator auc,
       @GerritPersonIdent PersonIdent au,
@@ -86,7 +87,9 @@
       .setAdministrators(GroupReference.forGroup(admin))
       .setBatchUsers(GroupReference.forGroup(batch))
       .create();
-    allUsersCreator.create();
+    allUsersCreator
+      .setAdministrators(GroupReference.forGroup(admin))
+      .create();
     dataSourceType.getIndexScript().run(db);
   }
 
@@ -117,9 +120,9 @@
 
     final SystemConfig s = SystemConfig.create();
     try {
-      s.sitePath = site_path.getCanonicalPath();
+      s.sitePath = site_path.toRealPath().normalize().toString();
     } catch (IOException e) {
-      s.sitePath = site_path.getAbsolutePath();
+      s.sitePath = site_path.toAbsolutePath().normalize().toString();
     }
     c.systemConfig().insert(Collections.singleton(s));
     return s;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
index 6faf148..3770b82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -16,6 +16,7 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.FactoryModule;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 2b9d4b4..148d1df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -76,6 +77,7 @@
         for (Class<?> c : new Class<?>[] {
             AllProjectsName.class,
             AllUsersCreator.class,
+            AllUsersName.class,
             GitRepositoryManager.class,
             SitePaths.class,
             }) {
@@ -90,16 +92,13 @@
   }
 
   public void update(final UpdateUI ui) throws OrmException {
-    final ReviewDb db = schema.open();
-    try {
+    try (ReviewDb db = schema.open()) {
       final SchemaVersion u = updater.get();
       final CurrentSchemaVersion version = getSchemaVersion(db);
       if (version == null) {
         try {
           creator.create(db);
-        } catch (IOException e) {
-          throw new OrmException("Cannot initialize schema", e);
-        } catch (ConfigInvalidException e) {
+        } catch (IOException | ConfigInvalidException e) {
           throw new OrmException("Cannot initialize schema", e);
         }
 
@@ -112,8 +111,6 @@
 
         updateSystemConfig(db);
       }
-    } finally {
-      db.close();
     }
   }
 
@@ -131,9 +128,9 @@
       throw new OrmException("No record in system_config table");
     }
     try {
-      sc.sitePath = site.site_path.getCanonicalPath();
+      sc.sitePath = site.site_path.toRealPath().normalize().toString();
     } catch (IOException e) {
-      sc.sitePath = site.site_path.getAbsolutePath();
+      sc.sitePath = site.site_path.toAbsolutePath().normalize().toString();
     }
     db.systemConfig().update(Collections.singleton(sc));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 945baa8..27df9a5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_107> C = Schema_107.class;
+  public static final Class<Schema_115> C = Schema_115.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
@@ -49,8 +49,9 @@
   public static int guessVersion(Class<?> c) {
     String n = c.getName();
     n = n.substring(n.lastIndexOf('_') + 1);
-    while (n.startsWith("0"))
+    while (n.startsWith("0")) {
       n = n.substring(1);
+    }
     return Integer.parseInt(n);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 591601d..245e94d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -48,31 +48,26 @@
 
   @Override
   public void start() {
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        final CurrentSchemaVersion currentVer = getSchemaVersion(db);
-        final int expectedVer = SchemaVersion.getBinaryVersion();
+    try (ReviewDb db = schema.open()) {
+      final CurrentSchemaVersion currentVer = getSchemaVersion(db);
+      final int expectedVer = SchemaVersion.getBinaryVersion();
 
-        if (currentVer == null) {
-          throw new ProvisionException("Schema not yet initialized."
-              + "  Run init to initialize the schema:\n"
-              + "$ java -jar gerrit.war init -d "
-              + site.site_path.getAbsolutePath());
-        }
-        if (currentVer.versionNbr < expectedVer) {
-          throw new ProvisionException("Unsupported schema version "
-              + currentVer.versionNbr + "; expected schema version " + expectedVer
-              + ".  Run init to upgrade:\n"
-              + "$ java -jar " + site.gerrit_war.getAbsolutePath() + " init -d "
-              + site.site_path.getAbsolutePath());
-        } else if (currentVer.versionNbr > expectedVer) {
-          throw new ProvisionException("Unsupported schema version "
-              + currentVer.versionNbr + "; expected schema version " + expectedVer
-              + ". Downgrade is not supported.");
-        }
-      } finally {
-        db.close();
+      if (currentVer == null) {
+        throw new ProvisionException("Schema not yet initialized."
+            + "  Run init to initialize the schema:\n"
+            + "$ java -jar gerrit.war init -d "
+            + site.site_path.toAbsolutePath());
+      }
+      if (currentVer.versionNbr < expectedVer) {
+        throw new ProvisionException("Unsupported schema version "
+            + currentVer.versionNbr + "; expected schema version " + expectedVer
+            + ".  Run init to upgrade:\n"
+            + "$ java -jar " + site.gerrit_war.toAbsolutePath() + " init -d "
+            + site.site_path.toAbsolutePath());
+      } else if (currentVer.versionNbr > expectedVer) {
+        throw new ProvisionException("Unsupported schema version "
+            + currentVer.versionNbr + "; expected schema version " + expectedVer
+            + ". Downgrade is not supported.");
       }
     } catch (OrmException e) {
       throw new ProvisionException("Cannot read schema_version", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
index ef7e291..ecdf28f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
@@ -61,38 +61,33 @@
     ui.message(String.format("creating reflog files for %s branches ...",
         RefNames.REFS_CONFIG));
     for (Project.NameKey project : repoList) {
-      try {
-        Repository repo = repoManager.openRepository(project);
-        try {
-          File metaConfigLog =
-              new File(repo.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
-          if (metaConfigLog.exists()) {
-            continue;
-          }
+      try (Repository repo = repoManager.openRepository(project)) {
+        File metaConfigLog =
+            new File(repo.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
+        if (metaConfigLog.exists()) {
+          continue;
+        }
 
-          if (!metaConfigLog.getParentFile().mkdirs()
-              || !metaConfigLog.createNewFile()) {
-            throw new IOException(String.format(
-                "Failed to create reflog for %s in repository %s",
-                RefNames.REFS_CONFIG, project));
-          }
+        if (!metaConfigLog.getParentFile().mkdirs()
+            || !metaConfigLog.createNewFile()) {
+          throw new IOException(String.format(
+              "Failed to create reflog for %s in repository %s",
+              RefNames.REFS_CONFIG, project));
+        }
 
-          ObjectId metaConfigId = repo.resolve(RefNames.REFS_CONFIG);
-          if (metaConfigId != null) {
-            try (PrintWriter writer =
-                new PrintWriter(metaConfigLog, UTF_8.name())) {
-              writer.print(ObjectId.zeroId().name());
-              writer.print(" ");
-              writer.print(metaConfigId.name());
-              writer.print(" ");
-              writer.print(serverUser.toExternalString());
-              writer.print("\t");
-              writer.print("create reflog");
-              writer.println();
-            }
+        ObjectId metaConfigId = repo.resolve(RefNames.REFS_CONFIG);
+        if (metaConfigId != null) {
+          try (PrintWriter writer =
+              new PrintWriter(metaConfigLog, UTF_8.name())) {
+            writer.print(ObjectId.zeroId().name());
+            writer.print(" ");
+            writer.print(metaConfigId.name());
+            writer.print(" ");
+            writer.print(serverUser.toExternalString());
+            writer.print("\t");
+            writer.print("create reflog");
+            writer.println();
           }
-        } finally {
-          repo.close();
         }
       } catch (IOException e) {
         ui.message(String.format("ERROR: Failed to create reflog file for the"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java
index 13ab09a..c2c2305 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java
@@ -31,11 +31,8 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
       stmt.executeUpdate("UPDATE accounts set mute_common_path_prefixes = 'Y'");
-    } finally {
-      stmt.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
new file mode 100644
index 0000000..12a70eb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
@@ -0,0 +1,192 @@
+// Copyright (C) 2015 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.server.schema;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.GroupCollector;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+
+public class Schema_108 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  Schema_108(Provider<Schema_107> prior,
+      GitRepositoryManager repoManager) {
+    super(prior);
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    ui.message("Listing all changes ...");
+    SetMultimap<Project.NameKey, Change.Id> openByProject =
+        getOpenChangesByProject(db, ui);
+    ui.message("done");
+
+    ui.message("Updating groups for open changes ...");
+    int i = 0;
+    for (Map.Entry<Project.NameKey, Collection<Change.Id>> e
+        : openByProject.asMap().entrySet()) {
+      try (Repository repo = repoManager.openRepository(e.getKey());
+          RevWalk rw = new RevWalk(repo)) {
+        updateProjectGroups(db, repo, rw, (Set<Change.Id>) e.getValue(), ui);
+      } catch (IOException err) {
+        throw new OrmException(err);
+      }
+      if (++i % 100 == 0) {
+        ui.message("  done " + i + " projects ...");
+      }
+    }
+    ui.message("done");
+  }
+
+  private static void updateProjectGroups(ReviewDb db, Repository repo,
+      RevWalk rw, Set<Change.Id> changes, UpdateUI ui)
+          throws OrmException, IOException {
+    // Match sorting in ReceiveCommits.
+    rw.reset();
+    rw.sort(RevSort.TOPO);
+    rw.sort(RevSort.REVERSE, true);
+
+    RefDatabase refdb = repo.getRefDatabase();
+    for (Ref ref : refdb.getRefs(Constants.R_HEADS).values()) {
+      RevCommit c = maybeParseCommit(rw, ref.getObjectId(), ui);
+      if (c != null) {
+        rw.markUninteresting(c);
+      }
+    }
+
+    Multimap<ObjectId, Ref> changeRefsBySha = ArrayListMultimap.create();
+    Multimap<ObjectId, PatchSet.Id> patchSetsBySha = ArrayListMultimap.create();
+    for (Ref ref : refdb.getRefs(RefNames.REFS_CHANGES).values()) {
+      ObjectId id = ref.getObjectId();
+      if (ref.getObjectId() == null) {
+        continue;
+      }
+      id = id.copy();
+      changeRefsBySha.put(id, ref);
+      PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+      if (psId != null && changes.contains(psId.getParentKey())) {
+        patchSetsBySha.put(id, psId);
+        RevCommit c = maybeParseCommit(rw, id, ui);
+        if (c != null) {
+          rw.markStart(c);
+        }
+      }
+    }
+
+    GroupCollector collector = new GroupCollector(changeRefsBySha, db);
+    RevCommit c;
+    while ((c = rw.next()) != null) {
+      collector.visit(c);
+    }
+
+    updateGroups(db, collector, patchSetsBySha);
+  }
+
+  private static void updateGroups(ReviewDb db, GroupCollector collector,
+      Multimap<ObjectId, PatchSet.Id> patchSetsBySha) throws OrmException {
+    Map<PatchSet.Id, PatchSet> patchSets =
+        db.patchSets().toMap(db.patchSets().get(patchSetsBySha.values()));
+    for (Map.Entry<ObjectId, Collection<String>> e
+        : collector.getGroups().asMap().entrySet()) {
+      for (PatchSet.Id psId : patchSetsBySha.get(e.getKey())) {
+        PatchSet ps = patchSets.get(psId);
+        if (ps != null) {
+          ps.setGroups(e.getValue());
+        }
+      }
+    }
+
+    db.patchSets().update(patchSets.values());
+  }
+
+  private SetMultimap<Project.NameKey, Change.Id> getOpenChangesByProject(
+      ReviewDb db, UpdateUI ui) throws OrmException {
+    SortedSet<NameKey> projects = repoManager.list();
+    SortedSet<NameKey> nonExistentProjects = Sets.newTreeSet();
+    SetMultimap<Project.NameKey, Change.Id> openByProject =
+        HashMultimap.create();
+    for (Change c : db.changes().all()) {
+      Status status = c.getStatus();
+      if (status != null && status.isClosed()) {
+        continue;
+      }
+
+      NameKey projectKey = c.getProject();
+      if (!projects.contains(projectKey)) {
+        nonExistentProjects.add(projectKey);
+      } else {
+        // The old "submitted" state is not supported anymore
+        // (thus status is null) but it was an opened state and needs
+        // to be migrated as such
+        openByProject.put(projectKey, c.getId());
+      }
+    }
+
+    if (!nonExistentProjects.isEmpty()) {
+      ui.message("Detected open changes referring to the following non-existent projects:");
+      ui.message(Joiner.on(", ").join(nonExistentProjects));
+      ui.message("It is highly recommended to remove\n"
+          + "the obsolete open changes, comments and patch-sets from your DB.\n");
+    }
+    return openByProject;
+  }
+
+  private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id, UpdateUI ui)
+      throws IOException {
+    if (id != null) {
+      try {
+        RevObject obj = rw.parseAny(id);
+        return (obj instanceof RevCommit) ? (RevCommit) obj : null;
+      } catch (MissingObjectException moe) {
+        ui.message("Missing object: " + id.getName() + "\n");
+      }
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_109.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_109.java
new file mode 100644
index 0000000..5e77c12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_109.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2015 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.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_109 extends SchemaVersion {
+  @Inject
+  Schema_109(Provider<Schema_108> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    try (StatementExecutor e = newExecutor(db)) {
+      e.execute("UPDATE changes SET status = 'n' WHERE status = 's'");
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_110.java
similarity index 66%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_110.java
index 4413603..9e0f112 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_110.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.server.schema;
 
-import com.google.gwt.core.client.JsArray;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+public class Schema_110 extends SchemaVersion {
+  @Inject
+  Schema_110(Provider<Schema_109> prior) {
+    super(prior);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_111.java
similarity index 66%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_111.java
index 4413603..223fdb6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_111.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.server.schema;
 
-import com.google.gwt.core.client.JsArray;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+public class Schema_111 extends SchemaVersion {
+  @Inject
+  Schema_111(Provider<Schema_110> prior) {
+    super(prior);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
similarity index 66%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
index 4413603..3e879bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.server.schema;
 
-import com.google.gwt.core.client.JsArray;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+public class Schema_112 extends SchemaVersion {
+  @Inject
+  Schema_112(Provider<Schema_111> prior) {
+    super(prior);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_113.java
similarity index 66%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_113.java
index 4413603..32d655e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_113.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.server.schema;
 
-import com.google.gwt.core.client.JsArray;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+public class Schema_113 extends SchemaVersion {
+  @Inject
+  Schema_113(Provider<Schema_112> prior) {
+    super(prior);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_114.java
similarity index 66%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_114.java
index 4413603..85c93d2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_114.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.server.schema;
 
-import com.google.gwt.core.client.JsArray;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
+public class Schema_114 extends SchemaVersion {
+  @Inject
+  Schema_114(Provider<Schema_113> prior) {
+    super(prior);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
new file mode 100644
index 0000000..3401a86
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
@@ -0,0 +1,215 @@
+// Copyright (C) 2015 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.server.schema;
+
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class Schema_115 extends SchemaVersion {
+  private final GitRepositoryManager mgr;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_115(Provider<Schema_114> prior,
+      GitRepositoryManager mgr,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.mgr = mgr;
+    this.allUsersName = allUsersName;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    Map<Account.Id, DiffPreferencesInfo> imports = new HashMap<>();
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+        ResultSet rs = stmt.executeQuery(
+          "SELECT * FROM account_diff_preferences")) {
+        Set<String> availableColumns = getColumns(rs);
+        while (rs.next()) {
+          Account.Id accountId = new Account.Id(rs.getInt("id"));
+          DiffPreferencesInfo prefs = new DiffPreferencesInfo();
+          if (availableColumns.contains("context")) {
+            prefs.context = (int)rs.getShort("context");
+          }
+          if (availableColumns.contains("expand_all_comments")) {
+            prefs.expandAllComments = toBoolean(rs.getString("expand_all_comments"));
+          }
+          if (availableColumns.contains("hide_line_numbers")) {
+            prefs.hideLineNumbers = toBoolean(rs.getString("hide_line_numbers"));
+          }
+          if (availableColumns.contains("hide_top_menu")) {
+            prefs.hideTopMenu = toBoolean(rs.getString("hide_top_menu"));
+          }
+          if (availableColumns.contains("ignore_whitespace")) {
+            // Enum with char as value
+            prefs.ignoreWhitespace = toWhitespace(rs.getString("ignore_whitespace"));
+          }
+          if (availableColumns.contains("intraline_difference")) {
+            prefs.intralineDifference = toBoolean(rs.getString("intraline_difference"));
+          }
+          if (availableColumns.contains("line_length")) {
+            prefs.lineLength = rs.getInt("line_length");
+          }
+          if (availableColumns.contains("manual_review")) {
+            prefs.manualReview = toBoolean(rs.getString("manual_review"));
+          }
+          if (availableColumns.contains("render_entire_file")) {
+            prefs.renderEntireFile = toBoolean(rs.getString("render_entire_file"));
+          }
+          if (availableColumns.contains("retain_header")) {
+            prefs.retainHeader = toBoolean(rs.getString("retain_header"));
+          }
+          if (availableColumns.contains("show_line_endings")) {
+            prefs.showLineEndings = toBoolean(rs.getString("show_line_endings"));
+          }
+          if (availableColumns.contains("show_tabs")) {
+            prefs.showTabs = toBoolean(rs.getString("show_tabs"));
+          }
+          if (availableColumns.contains("show_whitespace_errors")) {
+            prefs.showWhitespaceErrors = toBoolean(rs.getString("show_whitespace_errors"));
+          }
+          if (availableColumns.contains("skip_deleted")) {
+            prefs.skipDeleted = toBoolean(rs.getString("skip_deleted"));
+          }
+          if (availableColumns.contains("skip_uncommented")) {
+            prefs.skipUncommented = toBoolean(rs.getString("skip_uncommented"));
+          }
+          if (availableColumns.contains("syntax_highlighting")) {
+            prefs.syntaxHighlighting = toBoolean(rs.getString("syntax_highlighting"));
+          }
+          if (availableColumns.contains("tab_size")) {
+            prefs.tabSize = rs.getInt("tab_size");
+          }
+          if (availableColumns.contains("theme")) {
+            // Enum with name as values; can be null
+            prefs.theme = toTheme(rs.getString("theme"));
+          }
+          if (availableColumns.contains("hide_empty_pane")) {
+            prefs.hideEmptyPane = toBoolean(rs.getString("hide_empty_pane"));
+          }
+          if (availableColumns.contains("auto_hide_diff_table_header")) {
+            prefs.autoHideDiffTableHeader = toBoolean(rs.getString("auto_hide_diff_table_header"));
+          }
+          imports.put(accountId, prefs);
+        }
+    }
+
+    if (imports.isEmpty()) {
+      return;
+    }
+
+    try (Repository git = mgr.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(git)) {
+      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+
+      for (Map.Entry<Account.Id, DiffPreferencesInfo> e : imports.entrySet()) {
+        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+            allUsersName, git, bru);
+        try {
+          md.getCommitBuilder().setAuthor(serverUser);
+          md.getCommitBuilder().setCommitter(serverUser);
+
+          VersionedAccountPreferences p =
+              VersionedAccountPreferences.forUser(e.getKey());
+          p.load(md);
+          storeSection(p.getConfig(), UserConfigSections.DIFF, null,
+              e.getValue(), DiffPreferencesInfo.defaults());
+          p.commit(md);
+        } finally {
+          md.close();
+        }
+      }
+
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    } catch (ConfigInvalidException | IOException ex) {
+      throw new OrmException(ex);
+    }
+  }
+
+  private Set<String> getColumns(ResultSet rs) throws SQLException {
+    ResultSetMetaData metaData = rs.getMetaData();
+    int columnCount = metaData.getColumnCount();
+    Set<String> columns = new HashSet<>(columnCount);
+    for (int i = 1; i <= columnCount; i++) {
+      columns.add(metaData.getColumnLabel(i).toLowerCase());
+    }
+    return columns;
+  }
+
+  private static Theme toTheme(String v) {
+    if (v == null) {
+      return Theme.DEFAULT;
+    }
+    return Theme.valueOf(v);
+  }
+
+  private static Whitespace toWhitespace(String v) {
+    Preconditions.checkNotNull(v);
+    if (v.isEmpty()) {
+      return Whitespace.IGNORE_NONE;
+    }
+    Whitespace r = PatchListKey.WHITESPACE_TYPES.inverse().get(v.charAt(0));
+    if (r == null) {
+      throw new IllegalArgumentException("Cannot find Whitespace type for: "
+          + v);
+    }
+    return r;
+  }
+
+  private static boolean toBoolean(String v) {
+    Preconditions.checkState(!Strings.isNullOrEmpty(v));
+    return v.equals("Y");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
index 2347f87..684a72e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.schema;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -73,8 +75,7 @@
   }
 
   private List<String> parse(final InputStream in) throws IOException {
-    BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
-    try {
+    try (BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8))) {
       String delimiter = ";";
       List<String> commands = new ArrayList<>();
       StringBuilder buffer = new StringBuilder();
@@ -107,8 +108,6 @@
         commands.add(buffer.toString());
       }
       return commands;
-    } finally {
-      br.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
index b852217..b481944 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -35,8 +35,7 @@
 
   @Inject
   DefaultSecureStore(SitePaths site) {
-    File secureConfig = new File(site.etc_dir, "secure.config");
-    sec = new FileBasedConfig(secureConfig, FS.DETECTED);
+    sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
     try {
       sec.load();
     } catch (Exception e) {
@@ -94,7 +93,7 @@
     if (FileUtil.modified(sec)) {
       final byte[] out = Constants.encode(sec.toText());
       final File path = sec.getFile();
-      final LockFile lf = new LockFile(path, FS.DETECTED);
+      final LockFile lf = new LockFile(path);
       if (!lf.lock()) {
         throw new IOException("Cannot lock " + path);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
index e830590..99127d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
@@ -26,14 +26,14 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
+import java.nio.file.Path;
 
 @Singleton
 public class SecureStoreProvider implements Provider<SecureStore> {
   private static final Logger log = LoggerFactory
       .getLogger(SecureStoreProvider.class);
 
-  private final File libdir;
+  private final Path libdir;
   private final Injector injector;
   private final String className;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
index 3e41858..4810b3d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.tools;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -58,7 +62,11 @@
    * @param name path of the item, relative to the root of the catalog.
    * @return the entry; null if the item is not part of the catalog.
    */
-  public Entry get(String name) {
+  @Nullable
+  public Entry get(@Nullable String name) {
+    if (Strings.isNullOrEmpty(name)) {
+      return null;
+    }
     if (name.startsWith("/")) {
       name = name.substring(1);
     }
@@ -72,7 +80,7 @@
     SortedMap<String, Entry> toc = new TreeMap<>();
     final BufferedReader br =
         new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
-            read("TOC")), "UTF-8"));
+            read("TOC")), UTF_8));
     String line;
     while ((line = br.readLine()) != null) {
       if (line.length() > 0 && !line.startsWith("#")) {
@@ -107,23 +115,18 @@
     return Collections.unmodifiableSortedMap(toc);
   }
 
+  @Nullable
   private static byte[] read(String path) {
     String name = "root/" + path;
-    InputStream in = ToolsCatalog.class.getResourceAsStream(name);
-    if (in == null) {
-      return null;
-    }
-
-    try {
+    try (InputStream in = ToolsCatalog.class.getResourceAsStream(name)) {
+      if (in == null) {
+        return null;
+      }
       final ByteArrayOutputStream out = new ByteArrayOutputStream();
-      try {
-        final byte[] buf = new byte[8192];
-        int n;
-        while ((n = in.read(buf, 0, buf.length)) > 0) {
-          out.write(buf, 0, n);
-        }
-      } finally {
-        in.close();
+      final byte[] buf = new byte[8192];
+      int n;
+      while ((n = in.read(buf, 0, buf.length)) > 0) {
+        out.write(buf, 0, n);
       }
       return out.toByteArray();
     } catch (Exception e) {
@@ -132,6 +135,7 @@
     }
   }
 
+  @Nullable
   private static String dirOf(String path) {
     final int s = path.lastIndexOf('/');
     return s < 0 ? null : path.substring(0, s);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
index 5514ef5..55c2992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -37,7 +37,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
index 0ef4a18..900bb42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
@@ -31,7 +31,7 @@
   private final ThreadLocalRequestContext requestContext;
   private final RequestContext old;
 
-  ManualRequestContext(CurrentUser user, SchemaFactory<ReviewDb> schemaFactory,
+  public ManualRequestContext(CurrentUser user, SchemaFactory<ReviewDb> schemaFactory,
       ThreadLocalRequestContext requestContext) throws OrmException {
     this.user = user;
     this.db = Providers.of(schemaFactory.open());
@@ -40,7 +40,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginLogFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginLogFile.java
new file mode 100644
index 0000000..17f6535
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginLogFile.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2015 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.server.util;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.Inject;
+
+import org.apache.log4j.AsyncAppender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public abstract class PluginLogFile implements LifecycleListener {
+
+  private final SystemLog systemLog;
+  private final ServerInformation serverInfo;
+  private final String logName;
+  private final Layout layout;
+
+  @Inject
+  public PluginLogFile(SystemLog systemLog,
+      ServerInformation serverInfo,
+      String logName,
+      Layout layout) {
+    this.systemLog = systemLog;
+    this.serverInfo = serverInfo;
+    this.logName = logName;
+    this.layout = layout;
+  }
+
+  @Override
+  public void start() {
+    AsyncAppender asyncAppender =
+        systemLog.createAsyncAppender(logName, layout);
+    Logger logger = LogManager.getLogger(logName);
+    logger.removeAppender(logName);
+    logger.addAppender(asyncAppender);
+    logger.setAdditivity(false);
+  }
+
+  @Override
+  public void stop() {
+    // stop is called when plugin is unloaded or when the server shutdown.
+    // Only clean up when the server is shutting down to prevent issue when a
+    // plugin is reloaded. The issue is that gerrit load the new plugin and then
+    // unload the old one so because loggers are static, the unload of the old
+    // plugin would remove the appenders just created by the new plugin.
+    if (serverInfo.getState() == ServerInformation.State.SHUTDOWN) {
+      LogManager.getLogger(logName).removeAllAppenders();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
index a836fd7..943e518 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
@@ -29,7 +29,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java
index 5c5e2f9..92873d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RangeUtil.java
@@ -46,7 +46,7 @@
   /**
    * Determine the range of values being requested in the given query.
    *
-   * @param rangeQuery the raw query, e.g. "added:>12345"
+   * @param rangeQuery the raw query, e.g. "{@code added:>12345}"
    * @param minValue the minimum possible value for the field, inclusive
    * @param maxValue the maximum possible value for the field, inclusive
    * @return the calculated {@link Range}, or null if the query is invalid
@@ -83,7 +83,8 @@
    */
   public static Range getRange(
       String prefix, String test, int queryInt, int minValue, int maxValue) {
-    int min, max;
+    int min;
+    int max;
     switch (test) {
       case "=":
       default:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
index 4b0fd35..0a99a8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
@@ -75,7 +75,8 @@
 
   public Iterable<T> search(List<T> list) {
     checkNotNull(list);
-    int begin, end;
+    int begin;
+    int end;
 
     if (0 < prefixLen) {
       // Assumes many consecutive elements may have the same prefix, so the cost
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
index 86c74e0..506a1c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
@@ -23,6 +23,6 @@
  * by the GerritGlobalModule scope.
  */
 public interface RequestContext {
-  CurrentUser getCurrentUser();
+  CurrentUser getUser();
   Provider<ReviewDb> getReviewDbProvider();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index f63da5439..d1cf47c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -188,8 +188,8 @@
       public T call() throws Exception {
         RequestContext old = local.setContext(new RequestContext() {
           @Override
-          public CurrentUser getCurrentUser() {
-            return context.getCurrentUser();
+          public CurrentUser getUser() {
+            return context.getUser();
           }
 
           @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
index 2b6b86e..ede3365 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
@@ -31,7 +31,7 @@
   }
 
   @Override
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return user;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
index c6a67db..ad2ab90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
@@ -14,18 +14,20 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.Constants;
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Set;
 
 /**
  * It parses from a configuration file submodule sections.
@@ -45,22 +47,30 @@
  * </pre>
  */
 public class SubmoduleSectionParser {
+
+  public interface Factory {
+    SubmoduleSectionParser create(BlobBasedConfig bbc, String thisServer,
+        Branch.NameKey superProjectBranch);
+  }
+
+  private final ProjectCache projectCache;
   private final BlobBasedConfig bbc;
   private final String thisServer;
   private final Branch.NameKey superProjectBranch;
-  private final GitRepositoryManager repoManager;
 
-  public SubmoduleSectionParser(final BlobBasedConfig bbc,
-      final String thisServer, final Branch.NameKey superProjectBranch,
-      final GitRepositoryManager repoManager) {
+  @Inject
+  public SubmoduleSectionParser(ProjectCache projectCache,
+      @Assisted BlobBasedConfig bbc,
+      @Assisted String thisServer,
+      @Assisted Branch.NameKey superProjectBranch) {
+    this.projectCache = projectCache;
     this.bbc = bbc;
     this.thisServer = thisServer;
     this.superProjectBranch = superProjectBranch;
-    this.repoManager = repoManager;
   }
 
-  public List<SubmoduleSubscription> parseAllSections() {
-    List<SubmoduleSubscription> parsedSubscriptions = new ArrayList<>();
+  public Set<SubmoduleSubscription> parseAllSections() {
+    Set<SubmoduleSubscription> parsedSubscriptions = Sets.newHashSet();
     for (final String id : bbc.getSubsections("submodule")) {
       final SubmoduleSubscription subscription = parse(id);
       if (subscription != null) {
@@ -92,8 +102,6 @@
           // Subscription really related to this running server.
           if (branch.equals(".")) {
             branch = superProjectBranch.get();
-          } else if (!branch.startsWith(Constants.R_REFS)) {
-            branch = Constants.R_HEADS + branch;
           }
 
           final String urlExtractedPath = new URI(url).getPath();
@@ -107,8 +115,8 @@
               projectName = projectName.substring(0, //
                   projectName.length() - Constants.DOT_GIT_EXT.length());
             }
-
-            if (repoManager.list().contains(new Project.NameKey(projectName))) {
+            Project.NameKey projectKey = new Project.NameKey(projectName);
+            if (projectCache.get(projectKey) != null) {
               ss = new SubmoduleSubscription(
                   superProjectBranch,
                   new Branch.NameKey(new Project.NameKey(projectName), branch),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
index f885e78..c857c40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.util;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -33,8 +35,8 @@
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 @Singleton
 public class SystemLog {
@@ -56,12 +58,12 @@
     return Strings.isNullOrEmpty(System.getProperty(LOG4J_CONFIGURATION));
   }
 
-  public static Appender createAppender(File logdir, String name, Layout layout) {
+  public static Appender createAppender(Path logdir, String name, Layout layout) {
     final DailyRollingFileAppender dst = new DailyRollingFileAppender();
     dst.setName(name);
     dst.setLayout(layout);
-    dst.setEncoding("UTF-8");
-    dst.setFile(new File(resolve(logdir), name).getPath());
+    dst.setEncoding(UTF_8.name());
+    dst.setFile(resolve(logdir).resolve(name).toString());
     dst.setImmediateFlush(true);
     dst.setAppend(true);
     dst.setErrorHandler(new DieErrorHandler());
@@ -92,11 +94,11 @@
     return async;
   }
 
-  private static File resolve(final File logs_dir) {
+  private static Path resolve(Path p) {
     try {
-      return logs_dir.getCanonicalFile();
+      return p.toRealPath().normalize();
     } catch (IOException e) {
-      return logs_dir.getAbsoluteFile();
+      return p.toAbsolutePath().normalize();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index d1b1da4..3e405a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -53,13 +53,13 @@
 
       @Provides
       CurrentUser provideCurrentUser(RequestContext ctx) {
-        return ctx.getCurrentUser();
+        return ctx.getUser();
       }
 
       @Provides
       IdentifiedUser provideCurrentUser(CurrentUser user) {
         if (user.isIdentifiedUser()) {
-          return (IdentifiedUser) user;
+          return user.asIdentifiedUser();
         }
         throw new ProvisionException(NotSignedInException.MESSAGE,
             new NotSignedInException());
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
index 606e883..32713d1 100644
--- a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index f0806a5..b9b6c5a 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -9,13 +9,13 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 
+import com.googlecode.prolog_cafe.exceptions.JavaException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.JavaException;
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
diff --git a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
index a955307..8efc2f1 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
@@ -20,15 +20,17 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.ChangeControl;
 
-import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
-import com.googlecode.prolog_cafe.lang.PInstantiationException;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
 
 /**
  * Resolves the valid range for a label on a CurrentUser.
@@ -54,18 +56,18 @@
     Term a3 = arg3.dereference();
     Term a4 = arg4.dereference();
 
-    if (a1.isVariable()) {
+    if (a1 instanceof VariableTerm) {
       throw new PInstantiationException(this, 1);
     }
-    if (!a1.isSymbol()) {
+    if (!(a1 instanceof SymbolTerm)) {
       throw new IllegalTypeException(this, 1, "atom", a1);
     }
     String label = a1.name();
 
-    if (a2.isVariable()) {
+    if (a2 instanceof VariableTerm) {
       throw new PInstantiationException(this, 2);
     }
-    if (!a2.isJavaObject() || !a2.convertible(CurrentUser.class)) {
+    if (!(a2 instanceof JavaObjectTerm) || !a2.convertible(CurrentUser.class)) {
       throw new IllegalTypeException(this, 2, "CurrentUser)", a2);
     }
     CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object();
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
index b835b34..ee5bdc9 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
index 51502f8..b56b036 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
index 29c6704..e131605 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
index 7ba648f..d1a91d9 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
index 3700909..59c4c18 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_commit_author_3 extends AbstractCommitUserIdentityPredicate {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
index 64823df..77a668b 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
@@ -18,9 +18,9 @@
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_commit_committer_3 extends AbstractCommitUserIdentityPredicate {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
index 51a871c..893c5bc 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
@@ -19,15 +19,16 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 
-import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
-import com.googlecode.prolog_cafe.lang.PInstantiationException;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
 
 import java.util.Iterator;
 import java.util.regex.Pattern;
@@ -66,22 +67,22 @@
     engine.setB0();
 
     Term a1 = arg1.dereference();
-    if (a1.isVariable()) {
+    if (a1 instanceof VariableTerm) {
       throw new PInstantiationException(this, 1);
     }
-    if (!a1.isSymbol()) {
+    if (!(a1 instanceof SymbolTerm)) {
       throw new IllegalTypeException(this, 1, "symbol", a1);
     }
     Pattern regex = Pattern.compile(a1.name());
-    engine.areg1 = new JavaObjectTerm(regex);
-    engine.areg2 = arg2;
-    engine.areg3 = arg3;
-    engine.areg4 = arg4;
+    engine.r1 = new JavaObjectTerm(regex);
+    engine.r2 = arg2;
+    engine.r3 = arg3;
+    engine.r4 = arg4;
 
     PatchList pl = StoredValues.PATCH_LIST.get(engine);
     Iterator<PatchListEntry> iter = pl.getPatches().iterator();
 
-    engine.areg5 = new JavaObjectTerm(iter);
+    engine.r5 = new JavaObjectTerm(iter);
 
     return engine.jtry5(commit_delta_check, commit_delta_next);
   }
@@ -89,11 +90,11 @@
   private static final class PRED_commit_delta_check extends Operation {
     @Override
     public Operation exec(Prolog engine) {
-      Term a1 = engine.areg1;
-      Term a2 = engine.areg2;
-      Term a3 = engine.areg3;
-      Term a4 = engine.areg4;
-      Term a5 = engine.areg5;
+      Term a1 = engine.r1;
+      Term a2 = engine.r2;
+      Term a3 = engine.r3;
+      Term a4 = engine.r4;
+      Term a5 = engine.r5;
 
       Pattern regex = (Pattern)((JavaObjectTerm)a1).object();
       @SuppressWarnings("unchecked")
@@ -144,7 +145,7 @@
   private static final class PRED_commit_delta_empty extends Operation {
     @Override
     public Operation exec(Prolog engine) {
-      Term a5 = engine.areg5;
+      Term a5 = engine.r5;
 
       @SuppressWarnings("unchecked")
       Iterator<PatchListEntry> iter =
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
index 509faf0..c97a964 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -19,14 +19,16 @@
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.Text;
 
-import com.googlecode.prolog_cafe.lang.IllegalTypeException;
-import com.googlecode.prolog_cafe.lang.JavaException;
+import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.JavaException;
+import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
-import com.googlecode.prolog_cafe.lang.PInstantiationException;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -134,10 +136,10 @@
   }
 
   private Pattern getRegexParameter(Term term) {
-    if (term.isVariable()) {
+    if (term instanceof VariableTerm) {
       throw new PInstantiationException(this, 1);
     }
-    if (!term.isSymbol()) {
+    if (!(term instanceof SymbolTerm)) {
       throw new IllegalTypeException(this, 1, "symbol", term);
     }
     return Pattern.compile(term.name(), Pattern.MULTILINE);
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
index e2eb6b1..6e1dc91 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
index 83878be..4f665ee 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.patch.PatchList;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.Term;
 
 /**
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 6d0dd0f..a3e1a96 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -18,15 +18,14 @@
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 
-import com.googlecode.prolog_cafe.lang.EvaluationException;
+import com.googlecode.prolog_cafe.exceptions.EvaluationException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
@@ -54,7 +53,7 @@
     Term resultTerm;
 
     if (curUser.isIdentifiedUser()) {
-      Account.Id id = ((IdentifiedUser)curUser).getAccountId();
+      Account.Id id = curUser.getAccountId();
       resultTerm = new IntegerTerm(id.get());
     } else if (curUser instanceof AnonymousUser) {
       resultTerm = anonymous;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 62fe075..3ee8d82 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -24,17 +24,18 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.util.Providers;
 
-import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
-import com.googlecode.prolog_cafe.lang.PInstantiationException;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
 
 import java.util.Map;
 
@@ -64,7 +65,7 @@
     Term a1 = arg1.dereference();
     Term a2 = arg2.dereference();
 
-    if (a1.isVariable()) {
+    if (a1 instanceof VariableTerm) {
       throw new PInstantiationException(this, 1);
     }
 
@@ -76,7 +77,7 @@
   }
 
   public Term createUser(Prolog engine, Term key) {
-    if (!key.isStructure()
+    if (!(key instanceof StructureTerm)
         || key.arity() != 1
         || !((StructureTerm) key).functor().equals(user)) {
       throw new IllegalTypeException(this, 1, "user(int)", key);
@@ -84,7 +85,7 @@
 
     Term idTerm = key.arg(0);
     CurrentUser user;
-    if (idTerm.isInteger()) {
+    if (idTerm instanceof IntegerTerm) {
       Map<Account.Id, IdentifiedUser> cache = StoredValues.USERS.get(engine);
       Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
       user = cache.get(accountId);
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
index 698c11c..f93e424 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -18,12 +18,12 @@
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.rules.StoredValues;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
index 824c6ef..b1a8a74 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.project.ChangeControl;
 
+import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 9c48292..1de7eea 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -7,6 +7,7 @@
 emailReviewers = Email Reviewers
 flushCaches = Flush Caches
 killTask = Kill Task
+maintainServer = Maintain Server
 modifyAccount = Modify Account
 priority = Priority
 queryLimit = Query Limit
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
new file mode 100644
index 0000000..c60ce8b
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
@@ -0,0 +1,61 @@
+## Copyright (C) 2015 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example".  If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used.  If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The AddKey.vm template will determine the contents of the email
+## related to adding a new SSH or GPG key to an account.
+##
+One or more new ${email.keyType} keys have been added to Gerrit Code Review at ${email.gerritHost}:
+
+#if($email.sshKey)
+$email.sshKey
+#elseif($email.gpgKeys)
+$email.gpgKeys
+#end
+
+If this is not expected, please contact your Gerrit Administrators
+immediately.
+
+You can also manage your ${email.keyType} keys by visiting
+#if($email.sshKey)
+$email.gerritUrl#/settings/ssh-keys
+#elseif($email.gpgKeys)
+$email.gerritUrl#/settings/gpg-keys
+#end
+#if($email.userNameEmail)
+(while signed in as $email.userNameEmail)
+#else
+(while signed in as $email.email)
+#end
+
+If clicking the link above does not work, copy and paste the URL in a
+new browser window instead.
+
+This is a send-only email address.  Replies to this message will not
+be read or answered.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index 3d8d271..ea99846 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -33,6 +33,7 @@
 pig = text/x-pig
 pl = text/x-perl
 pm = text/x-perl
+pp = text/x-puppet
 project.config = text/x-ini
 properties = text/x-ini
 py = text/x-python
@@ -44,8 +45,10 @@
 soy = text/x-soy
 st = text/x-stsrc
 stex = text/x-stex
+tcl = text/x-tcl
 v = text/x-verilog
 vert = x-shader/x-vertex
 vh = text/x-verilog
+vhdl = text/x-vhdl
 vm = text/velocity
 yaml = text/x-yaml
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/bin/gerrit-cherry-pick b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/bin/gerrit-cherry-pick
index 007faa4..7a414c8 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/bin/gerrit-cherry-pick
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/bin/gerrit-cherry-pick
@@ -1,6 +1,6 @@
 #!/bin/sh
 #
-# Part of Gerrit Code Review (http://code.google.com/p/gerrit/)
+# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
 #
 # Copyright (C) 2009 The Android Open Source Project
 #
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index d8f009b..cca2ac1 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -1,6 +1,6 @@
 #!/bin/sh
 #
-# Part of Gerrit Code Review (http://code.google.com/p/gerrit/)
+# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
 #
 # Copyright (C) 2009 The Android Open Source Project
 #
@@ -19,7 +19,7 @@
 
 unset GREP_OPTIONS
 
-CHANGE_ID_AFTER="Bug|Issue"
+CHANGE_ID_AFTER="Bug|Depends-On|Issue|Test"
 MSG="$1"
 
 # Check for, and add if missing, a unique Change-Id
@@ -38,6 +38,12 @@
 		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
@@ -57,6 +63,10 @@
 		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
@@ -75,8 +85,8 @@
 		blankLines = 0
 	}
 
-	# Skip lines starting with "#" without any spaces before it.
-	/^#/ { next }
+	# 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.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index f5dca09..d6a5c67 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -14,12 +14,10 @@
 
 package com.google.gerrit.rules;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
-import static org.junit.Assert.fail;
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
@@ -33,7 +31,8 @@
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.inject.AbstractModule;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
+import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
@@ -41,7 +40,9 @@
 
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.io.PushbackReader;
 import java.io.StringReader;
@@ -61,6 +62,9 @@
   private ProjectConfig local;
   private Util util;
 
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Before
   public void setUp() throws Exception {
     util = new Util();
@@ -112,7 +116,6 @@
   public void testReductionLimit() throws CompileException {
     PrologEnvironment env = envFactory.create(machine);
     setUpEnvironment(env);
-    env.setEnabled(Prolog.Feature.IO, true);
 
     String script = "loopy :- b(5).\n"
         + "b(N) :- N > 0, !, S = N - 1, b(S).\n"
@@ -125,12 +128,9 @@
       throw new CompileException("Cannot consult " + nameTerm);
     }
 
-    try {
-      env.once(Prolog.BUILTIN, "call", new StructureTerm(":",
-          SymbolTerm.create("user"), SymbolTerm.create("loopy")));
-      fail("long running loop did not abort with ReductionLimitException");
-    } catch (ReductionLimitException e) {
-      assertThat(e.getMessage()).isEqualTo("exceeded reduction limit of 1300");
-    }
+    exception.expect(ReductionLimitException.class);
+    exception.expectMessage("exceeded reduction limit of 1300");
+    env.once(Prolog.BUILTIN, "call", new StructureTerm(":",
+        SymbolTerm.create("user"), SymbolTerm.create("loopy")));
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index aaab173..8956e8f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.rules;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.inject.Guice;
 import com.google.inject.Module;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
 import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
@@ -78,7 +78,7 @@
     for (Term[] pair : env.all(Prolog.BUILTIN, "clause", head, new VariableTerm())) {
       tests.add(pair[0]);
     }
-    assertTrue("has tests", tests.size() > 0);
+    assertThat(tests).isNotEmpty();
     machine = PrologMachineCopy.save(env);
   }
 
@@ -100,20 +100,17 @@
   protected void consult(BufferingPrologControl env,
       Class<?> clazz,
       String prologResource) throws CompileException, IOException {
-    InputStream in = clazz.getResourceAsStream(prologResource);
-    if (in == null) {
-      throw new FileNotFoundException(prologResource);
-    }
-    try {
+    try (InputStream in = clazz.getResourceAsStream(prologResource)) {
+      if (in == null) {
+        throw new FileNotFoundException(prologResource);
+      }
       SymbolTerm pathTerm = SymbolTerm.create(prologResource);
       JavaObjectTerm inTerm =
           new JavaObjectTerm(new PushbackReader(new BufferedReader(
-              new InputStreamReader(in, "UTF-8")), Prolog.PUSHBACK_SIZE));
+              new InputStreamReader(in, UTF_8)), Prolog.PUSHBACK_SIZE));
       if (!env.execute(Prolog.BUILTIN, "consult_stream", pathTerm, inTerm)) {
         throw new CompileException("Cannot consult " + prologResource);
       }
-    } finally {
-      in.close();
     }
   }
 
@@ -173,20 +170,21 @@
         tests.size(), errors, (end - start) / 1000.0);
     System.out.println();
 
-    assertEquals("No Errors", 0, errors);
+    assertThat(errors).isEqualTo(0);
   }
 
   private void call(BufferingPrologControl env, String name) {
     StructureTerm head = SymbolTerm.create(pkg, name, 0);
-    if (!env.execute(Prolog.BUILTIN, "call", head)) {
-      fail("Cannot invoke " + pkg + ":" + name);
-    }
+    assert_()
+      .withFailureMessage("Cannot invoke " + pkg + ":" + name)
+      .that(env.execute(Prolog.BUILTIN, "call", head))
+      .isTrue();
   }
 
   private Term removePackage(Term test) {
     Term name = test;
-    if (name.isStructure() && ":".equals(((StructureTerm) name).name())) {
-      name = ((StructureTerm) name).args()[1];
+    if (name instanceof StructureTerm && ":".equals(name.name())) {
+      name = name.arg(1);
     }
     return name;
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
new file mode 100644
index 0000000..039871e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2015 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.server;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.FakeRealm;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeAccountCache;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(ConfigSuite.class)
+public class IdentifiedUserTest {
+  @ConfigSuite.Parameter
+  public Config config;
+
+  private IdentifiedUser identifiedUser;
+
+  @Inject
+  private IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  private static final String[] TEST_CASES = {
+    "",
+    "FirstName.LastName@Corporation.com",
+    "!#$%&'+-/=.?^`{|}~@[IPv6:0123:4567:89AB:CDEF:0123:4567:89AB:CDEF]"
+  };
+
+  @Before
+  public void setUp() throws Exception {
+    final FakeAccountCache accountCache = new FakeAccountCache();
+    final Realm mockRealm = new FakeRealm() {
+      HashSet<String> emails = new HashSet<>(Arrays.asList(TEST_CASES));
+
+      @Override
+      public boolean hasEmailAddress(IdentifiedUser who, String email) {
+        return emails.contains(email);
+      }
+
+      @Override
+      public Set<String> getEmailAddresses(IdentifiedUser who) {
+        return emails;
+      }
+    };
+
+    AbstractModule mod = new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+          .toInstance(Boolean.FALSE);
+        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(config);
+        bind(String.class).annotatedWith(AnonymousCowardName.class)
+          .toProvider(AnonymousCowardNameProvider.class);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+          .toInstance("http://localhost:8080/");
+        bind(AccountCache.class).toInstance(accountCache);
+        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+        bind(CapabilityControl.Factory.class)
+          .toProvider(Providers.<CapabilityControl.Factory>of(null));
+        bind(Realm.class).toInstance(mockRealm);
+
+      }
+    };
+
+    Injector injector = Guice.createInjector(mod);
+    injector.injectMembers(this);
+
+    Account account = new Account(new Account.Id(1), TimeUtil.nowTs());
+    Account.Id ownerId = account.getId();
+
+    identifiedUser = identifiedUserFactory.create(ownerId);
+
+    /* Trigger identifiedUser to load the email addresses from mockRealm */
+    identifiedUser.getEmailAddresses();
+  }
+
+  @Test
+  public void testEmailsExistence() {
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[0])).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toLowerCase())).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toUpperCase())).isTrue();
+    /* assert again to test cached email address by IdentifiedUser.validEmails */
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
+
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2])).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2].toLowerCase())).isTrue();
+
+
+    assertThat(identifiedUser.hasEmailAddress("non-exist@email.com")).isFalse();
+    /* assert again to test cached email address by IdentifiedUser.invalidEmails */
+    assertThat(identifiedUser.hasEmailAddress("non-exist@email.com")).isFalse();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index 8bedd17..f5e6d74 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -20,7 +20,6 @@
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
-import static org.junit.Assert.fail;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
@@ -88,7 +87,9 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 
 import java.sql.Timestamp;
@@ -106,10 +107,14 @@
   public Config config;
 
   @ConfigSuite.Config
-  public static @GerritServerConfig Config noteDbEnabled() {
+  @GerritServerConfig
+  public static Config noteDbEnabled() {
     return NotesMigration.allEnabledConfig();
   }
 
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   private Injector injector;
   private ReviewDb db;
   private Project.NameKey project;
@@ -199,7 +204,7 @@
 
       @Provides
       @Singleton
-      CurrentUser getCurrentUser(IdentifiedUser.GenericFactory userFactory) {
+      CurrentUser getUser(IdentifiedUser.GenericFactory userFactory) {
         return userFactory.create(ownerId);
       }
     };
@@ -240,24 +245,23 @@
     plc1 = newPatchLineComment(psId1, "Comment1", null,
         "FileOne.txt", Side.REVISION, 3, ownerId, timeBase,
         "First Comment", new CommentRange(1, 2, 3, 4));
-    plc1.setRevId(new RevId("ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD"));
+    plc1.setRevId(new RevId("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"));
     plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
         "FileOne.txt", Side.REVISION, 3, otherUserId, timeBase + 1000,
         "Reply to First Comment",  new CommentRange(1, 2, 3, 4));
-    plc2.setRevId(new RevId("ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD"));
+    plc2.setRevId(new RevId("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"));
     plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
         "FileOne.txt", Side.PARENT, 3, ownerId, timeBase + 2000,
         "First Parent Comment",  new CommentRange(1, 2, 3, 4));
-    plc3.setRevId(new RevId("CDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEF"));
+    plc3.setRevId(new RevId("cdefcdefcdefcdefcdefcdefcdefcdefcdefcdef"));
     plc4 = newPatchLineComment(psId2, "Comment4", null, "FileOne.txt",
         Side.REVISION, 3, ownerId, timeBase + 3000, "Second Comment",
         new CommentRange(1, 2, 3, 4), Status.DRAFT);
-    plc4.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
+    plc4.setRevId(new RevId("bcdebcdebcdebcdebcdebcdebcdebcdebcdebcde"));
     plc5 = newPatchLineComment(psId2, "Comment5", null, "FileOne.txt",
         Side.REVISION, 5, ownerId, timeBase + 4000, "Third Comment",
         new CommentRange(3, 4, 5, 6), Status.DRAFT);
-    plc5.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
-    plc5.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
+    plc5.setRevId(new RevId("bcdebcdebcdebcdebcdebcdebcdebcdebcdebcde"));
     plc6 = newPatchLineComment(psId3, "Comment6", null, "FileOne.txt",
         Side.REVISION, 5, ownerId, timeBase + 5000, "Sixth Comment",
         new CommentRange(3, 4, 5, 6), Status.DRAFT);
@@ -318,9 +322,9 @@
     update.commit();
 
     ChangeControl ctl = stubChangeControl(change1);
-    revRes1 = new RevisionResource(new ChangeResource(ctl, null), ps1);
-    revRes2 = new RevisionResource(new ChangeResource(ctl, null), ps2);
-    revRes3 = new RevisionResource(new ChangeResource(stubChangeControl(change2), null), ps3);
+    revRes1 = new RevisionResource(new ChangeResource(ctl), ps1);
+    revRes2 = new RevisionResource(new ChangeResource(ctl), ps2);
+    revRes3 = new RevisionResource(new ChangeResource(stubChangeControl(change2)), ps3);
   }
 
   private ChangeControl stubChangeControl(Change c) throws OrmException {
@@ -349,12 +353,19 @@
   }
 
   @Test
-  public void testGetComment() throws Exception {
+  public void testGetCommentExisting() throws Exception {
     // test GetComment for existing comment
-    assertGetComment(revRes1, plc1, plc1.getKey().get());
+    String uuid = plc1.getKey().get();
+    CommentResource commentRes = comments.parse(revRes1, IdString.fromUrl(uuid));
+    CommentInfo actual = getComment.apply(commentRes);
+    assertComment(plc1, actual, true);
+  }
 
+  @Test
+  public void testGetCommentNotExisting() throws Exception {
     // test GetComment for non-existent comment
-    assertGetComment(revRes1, null, "BadComment");
+    exception.expect(ResourceNotFoundException.class);
+    comments.parse(revRes1, IdString.fromUrl("BadComment"));
   }
 
   @Test
@@ -371,7 +382,7 @@
   @Test
   public void testPatchLineCommentsUtilByCommentStatus() throws OrmException {
     assertThat(plcUtil.publishedByChange(db, revRes2.getNotes()))
-        .containsExactly(plc1, plc2, plc3).inOrder();
+        .containsExactly(plc3, plc1, plc2).inOrder();
     assertThat(plcUtil.draftByChange(db, revRes2.getNotes()))
         .containsExactly(plc4, plc5).inOrder();
   }
@@ -390,23 +401,8 @@
       @Override
       public ResultSet<PatchLineComment> answer() throws Throwable {
         return new ListResultSet<>(Lists.newArrayList(comments));
-      }};
-  }
-
-  private void assertGetComment(RevisionResource res, PatchLineComment expected,
-      String uuid) throws Exception {
-    try {
-      CommentResource commentRes = comments.parse(res, IdString.fromUrl(uuid));
-      if (expected == null) {
-        fail("Expected no comment");
       }
-      CommentInfo actual = getComment.apply(commentRes);
-      assertComment(expected, actual, true);
-    } catch (ResourceNotFoundException e) {
-      if (expected != null) {
-        fail("Expected to find comment");
-      }
-    }
+    };
   }
 
   private void assertListComments(RevisionResource res,
@@ -422,7 +418,7 @@
   private void assertCommentMap(Map<String, List<CommentInfo>> actual,
       Map<String, ? extends List<PatchLineComment>> expected,
       boolean isPublished) {
-    assertThat((Iterable<?>)actual.keySet()).containsExactlyElementsIn(expected.keySet());
+    assertThat(actual.keySet()).containsExactlyElementsIn(expected.keySet());
     for (Map.Entry<String, List<CommentInfo>> entry : actual.entrySet()) {
       List<CommentInfo> actualList = entry.getValue();
       List<PatchLineComment> expectedList = expected.get(entry.getKey());
@@ -443,7 +439,7 @@
       assertThat(new Account.Id(ci.author._accountId))
           .isEqualTo(plc.getAuthor());
     }
-    assertThat((int) ci.line).isEqualTo(plc.getLine());
+    assertThat(ci.line).isEqualTo(plc.getLine());
     assertThat(MoreObjects.firstNonNull(ci.side, Side.REVISION))
         .isEqualTo(plc.getSide() == 0 ? Side.PARENT : Side.REVISION);
     assertThat(TimeUtil.roundToSecond(ci.updated))
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
deleted file mode 100644
index 358620f..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
+++ /dev/null
@@ -1,449 +0,0 @@
-// Copyright (C) 2014 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.server.change;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testutil.TestChanges.newChange;
-import static com.google.gerrit.testutil.TestChanges.newPatchSet;
-import static java.util.Collections.singleton;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.common.ProblemInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.testutil.FakeAccountByEmailCache;
-import com.google.gerrit.testutil.InMemoryDatabase;
-import com.google.gerrit.testutil.InMemoryRepositoryManager;
-import com.google.gerrit.testutil.TestChanges;
-import com.google.inject.util.Providers;
-
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.List;
-
-public class ConsistencyCheckerTest {
-  private InMemoryDatabase schemaFactory;
-  private ReviewDb db;
-  private InMemoryRepositoryManager repoManager;
-  private ConsistencyChecker checker;
-
-  private TestRepository<InMemoryRepository> repo;
-  private Project.NameKey project;
-  private Account.Id userId;
-  private RevCommit tip;
-
-  @Before
-  public void setUp() throws Exception {
-    FakeAccountByEmailCache accountCache = new FakeAccountByEmailCache();
-    schemaFactory = InMemoryDatabase.newDatabase();
-    schemaFactory.create();
-    db = schemaFactory.open();
-    repoManager = new InMemoryRepositoryManager();
-    checker = new ConsistencyChecker(
-        Providers.<ReviewDb> of(db),
-        repoManager,
-        Providers.<CurrentUser> of(new InternalUser(null)),
-        Providers.of(new PersonIdent("server", "noreply@example.com")),
-        new PatchSetInfoFactory(repoManager, accountCache));
-    project = new Project.NameKey("repo");
-    repo = new TestRepository<>(repoManager.createRepository(project));
-    userId = new Account.Id(1);
-    accountCache.putAny(userId);
-    db.accounts().insert(singleton(new Account(userId, TimeUtil.nowTs())));
-    tip = repo.branch("master").commit().create();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    if (db != null) {
-      db.close();
-    }
-    if (schemaFactory != null) {
-      InMemoryDatabase.drop(schemaFactory);
-    }
-  }
-
-  @Test
-  public void validNewChange() throws Exception {
-    Change c = insertChange();
-    insertPatchSet(c);
-    incrementPatchSet(c);
-    insertPatchSet(c);
-    assertProblems(c);
-  }
-
-  @Test
-  public void validMergedChange() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    insertPatchSet(c);
-    incrementPatchSet(c);
-
-    incrementPatchSet(c);
-    RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
-    db.patchSets().insert(singleton(ps2));
-
-    repo.branch(c.getDest().get()).update(commit2);
-    assertProblems(c);
-  }
-
-  @Test
-  public void missingOwner() throws Exception {
-    Change c = newChange(project, new Account.Id(2));
-    db.changes().insert(singleton(c));
-    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
-    db.patchSets().insert(singleton(ps));
-
-    assertProblems(c, "Missing change owner: 2");
-  }
-
-  @Test
-  public void missingRepo() throws Exception {
-    Change c = newChange(new Project.NameKey("otherproject"), userId);
-    db.changes().insert(singleton(c));
-    insertMissingPatchSet(c, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertProblems(c, "Destination repository not found: otherproject");
-  }
-
-  @Test
-  public void invalidRevision() throws Exception {
-    Change c = insertChange();
-
-    db.patchSets().insert(singleton(newPatchSet(c.currentPatchSetId(),
-            "fooooooooooooooooooooooooooooooooooooooo", userId)));
-    incrementPatchSet(c);
-    insertPatchSet(c);
-
-    assertProblems(c,
-        "Invalid revision on patch set 1:"
-        + " fooooooooooooooooooooooooooooooooooooooo");
-  }
-
-  // No test for ref existing but object missing; InMemoryRepository won't let
-  // us do such a thing.
-
-  @Test
-  public void patchSetObjectAndRefMissing() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
-    db.patchSets().insert(singleton(ps));
-
-    assertProblems(c,
-        "Ref missing: " + ps.getId().toRefName(),
-        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-  }
-
-  @Test
-  public void patchSetObjectAndRefMissingWithFix() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
-    db.patchSets().insert(singleton(ps));
-
-    String refName = ps.getId().toRefName();
-    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + refName);
-    assertThat(p.status).isNull();
-  }
-
-  @Test
-  public void patchSetRefMissing() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = insertPatchSet(c);
-    String refName = ps.getId().toRefName();
-    repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
-    deleteRef(refName);
-
-    assertProblems(c, "Ref missing: " + refName);
-  }
-
-  @Test
-  public void patchSetRefMissingWithFix() throws Exception {
-    Change c = insertChange();
-    PatchSet ps = insertPatchSet(c);
-    String refName = ps.getId().toRefName();
-    repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
-    deleteRef(refName);
-
-    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + refName);
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Repaired patch set ref");
-
-    assertThat(repo.getRepository().getRef(refName).getObjectId().name())
-        .isEqualTo(ps.getRevision().get());
-  }
-
-  @Test
-  public void patchSetObjectAndRefMissingWithDeletingPatchSet()
-      throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertPatchSet(c);
-    incrementPatchSet(c);
-    PatchSet ps2 = insertMissingPatchSet(c,
-        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-
-    FixInput fix = new FixInput();
-    fix.deletePatchSetIfCommitMissing = true;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(2);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
-    assertThat(p.status).isNull();
-    p = problems.get(1);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Deleted patch set");
-
-    c = db.changes().get(c.getId());
-    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
-    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
-    assertThat(db.patchSets().get(ps2.getId())).isNull();
-  }
-
-  @Test
-  public void patchSetMultipleObjectsMissingWithDeletingPatchSets()
-      throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertPatchSet(c);
-
-    incrementPatchSet(c);
-    PatchSet ps2 = insertMissingPatchSet(c,
-        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-
-    incrementPatchSet(c);
-    PatchSet ps3 = insertPatchSet(c);
-
-    incrementPatchSet(c);
-    PatchSet ps4 = insertMissingPatchSet(c,
-        "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
-
-    FixInput fix = new FixInput();
-    fix.deletePatchSetIfCommitMissing = true;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(4);
-
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps4.getId().toRefName());
-    assertThat(p.status).isNull();
-
-    p = problems.get(1);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 4: c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Deleted patch set");
-
-    p = problems.get(2);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
-    assertThat(p.status).isNull();
-
-    p = problems.get(3);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Deleted patch set");
-
-    c = db.changes().get(c.getId());
-    assertThat(c.currentPatchSetId().get()).isEqualTo(3);
-    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
-    assertThat(db.patchSets().get(ps2.getId())).isNull();
-    assertThat(db.patchSets().get(ps3.getId())).isNotNull();
-    assertThat(db.patchSets().get(ps4.getId())).isNull();
-  }
-
-  @Test
-  public void onlyPatchSetObjectMissingWithFix() throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertMissingPatchSet(c,
-        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-
-    FixInput fix = new FixInput();
-    fix.deletePatchSetIfCommitMissing = true;
-    List<ProblemInfo> problems = checker.check(c, fix).problems();
-    assertThat(problems).hasSize(2);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo("Ref missing: " + ps1.getId().toRefName());
-    assertThat(p.status).isNull();
-    p = problems.get(1);
-    assertThat(p.message).isEqualTo(
-        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIX_FAILED);
-    assertThat(p.outcome)
-        .isEqualTo("Cannot delete patch set; no patch sets would remain");
-
-    c = db.changes().get(c.getId());
-    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
-    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
-  }
-
-  @Test
-  public void currentPatchSetMissing() throws Exception {
-    Change c = insertChange();
-    assertProblems(c, "Current patch set 1 not found");
-  }
-
-  @Test
-  public void duplicatePatchSetRevisions() throws Exception {
-    Change c = insertChange();
-    PatchSet ps1 = insertPatchSet(c);
-    String rev = ps1.getRevision().get();
-    incrementPatchSet(c);
-    PatchSet ps2 = insertMissingPatchSet(c, rev);
-    updatePatchSetRef(ps2);
-
-    assertProblems(c,
-        "Multiple patch sets pointing to " + rev + ": [1, 2]");
-  }
-
-  @Test
-  public void missingDestRef() throws Exception {
-    RefUpdate ru = repo.getRepository().updateRef("refs/heads/master");
-    ru.setForceUpdate(true);
-    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    Change c = insertChange();
-    RevCommit commit = repo.commit().create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
-    updatePatchSetRef(ps);
-    db.patchSets().insert(singleton(ps));
-
-    assertProblems(c, "Destination ref not found (may be new branch): master");
-  }
-
-  @Test
-  public void mergedChangeIsNotMerged() throws Exception {
-    Change c = insertChange();
-    c.setStatus(Change.Status.MERGED);
-    PatchSet ps = insertPatchSet(c);
-    String rev = ps.getRevision().get();
-
-    assertProblems(c,
-        "Patch set 1 (" + rev + ") is not merged into destination ref"
-        + " master (" + tip.name() + "), but change status is MERGED");
-  }
-
-  @Test
-  public void newChangeIsMerged() throws Exception {
-    Change c = insertChange();
-    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
-    db.patchSets().insert(singleton(ps));
-    repo.branch(c.getDest().get()).update(commit);
-
-    assertProblems(c,
-        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
-        + " master (" + commit.name() + "), but change status is NEW");
-  }
-
-  @Test
-  public void newChangeIsMergedWithFix() throws Exception {
-    Change c = insertChange();
-    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
-    db.patchSets().insert(singleton(ps));
-    repo.branch(c.getDest().get()).update(commit);
-
-    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
-    assertThat(problems).hasSize(1);
-    ProblemInfo p = problems.get(0);
-    assertThat(p.message).isEqualTo(
-        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
-        + " master (" + commit.name() + "), but change status is NEW");
-    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
-    assertThat(p.outcome).isEqualTo("Marked change as merged");
-
-    c = db.changes().get(c.getId());
-    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
-    assertProblems(c);
-  }
-
-  private Change insertChange() throws Exception {
-    Change c = newChange(project, userId);
-    db.changes().insert(singleton(c));
-    return c;
-  }
-
-  private void incrementPatchSet(Change c) throws Exception {
-    TestChanges.incrementPatchSet(c);
-    db.changes().upsert(singleton(c));
-  }
-
-  private PatchSet insertPatchSet(Change c) throws Exception {
-    db.changes().upsert(singleton(c));
-    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
-        .parent(tip).create();
-    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
-    updatePatchSetRef(ps);
-    db.patchSets().insert(singleton(ps));
-    return ps;
-  }
-
-  private PatchSet insertMissingPatchSet(Change c, String id) throws Exception {
-    PatchSet ps = newPatchSet(c.currentPatchSetId(),
-        ObjectId.fromString(id), userId);
-    db.patchSets().insert(singleton(ps));
-    return ps;
-  }
-
-  private void updatePatchSetRef(PatchSet ps) throws Exception {
-    repo.update(ps.getId().toRefName(),
-        ObjectId.fromString(ps.getRevision().get()));
-  }
-
-  private void deleteRef(String refName) throws Exception {
-    RefUpdate ru = repo.getRepository().updateRef(refName, true);
-    ru.setForceUpdate(true);
-    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
-  }
-
-  private void assertProblems(Change c, String... expected) {
-    assertThat(Lists.transform(checker.check(c).problems(),
-          new Function<ProblemInfo, String>() {
-            @Override
-            public String apply(ProblemInfo in) {
-              checkArgument(in.status == null,
-                  "Status is not null: " + in.message);
-              return in.message;
-            }
-          })).containsExactly((Object[]) expected);
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
index d5b722c..50a6c11 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
@@ -23,45 +23,45 @@
 public class HashtagsTest {
   @Test
   public void emptyCommitMessage() throws Exception {
-    assertThat((Iterable<?>)HashtagsUtil.extractTags("")).isEmpty();
+    assertThat(HashtagsUtil.extractTags("")).isEmpty();
   }
 
   @Test
   public void nullCommitMessage() throws Exception {
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(null)).isEmpty();
+    assertThat(HashtagsUtil.extractTags((String) null)).isEmpty();
   }
 
   @Test
   public void noHashtags() throws Exception {
     String commitMessage = "Subject\n\nLine 1\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage)).isEmpty();
+    assertThat(HashtagsUtil.extractTags(commitMessage)).isEmpty();
   }
 
   @Test
   public void singleHashtag() throws Exception {
     String commitMessage = "#Subject\n\nLine 1\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Subject"));
   }
 
   @Test
   public void singleHashtagNumeric() throws Exception {
     String commitMessage = "Subject\n\n#123\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("123"));
   }
 
   @Test
   public void multipleHashtags() throws Exception {
     String commitMessage = "#Subject\n\n#Hashtag\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Subject", "Hashtag"));
   }
 
   @Test
   public void repeatedHashtag() throws Exception {
     String commitMessage = "#Subject\n\n#Hashtag1\n\n#Hashtag2\n\n#Hashtag1";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(
           Sets.newHashSet("Subject", "Hashtag1", "Hashtag2"));
   }
@@ -69,21 +69,21 @@
   @Test
   public void multipleHashtagsNoSpaces() throws Exception {
     String commitMessage = "Subject\n\n#Hashtag1#Hashtag2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Hashtag1"));
   }
 
   @Test
   public void hyphenatedHashtag() throws Exception {
     String commitMessage = "Subject\n\n#Hyphenated-Hashtag";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Hyphenated-Hashtag"));
   }
 
   @Test
   public void underscoredHashtag() throws Exception {
     String commitMessage = "Subject\n\n#Underscored_Hashtag";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Underscored_Hashtag"));
   }
 
@@ -91,7 +91,7 @@
   public void hashtagsWithAccentedCharacters() throws Exception {
     String commitMessage = "Jag #måste #öva på min #Svenska!\n\n"
         + "Jag behöver en #läkare.";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(
           Sets.newHashSet("måste", "öva", "Svenska", "läkare"));
   }
@@ -99,6 +99,6 @@
   @Test
   public void hashWithoutHashtag() throws Exception {
     String commitMessage = "Subject\n\n# Text";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage)).isEmpty();
+    assertThat(HashtagsUtil.extractTags(commitMessage)).isEmpty();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java
new file mode 100644
index 0000000..cb2d5a12
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java
@@ -0,0 +1,399 @@
+// Copyright (C) 2015 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.server.change;
+
+import static com.google.common.collect.Collections2.permutations;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.change.WalkSorter.PatchSetData;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testutil.TestChanges;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class WalkSorterTest {
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  private Account.Id userId;
+  private InMemoryRepositoryManager repoManager;
+
+  @Before
+  public void setUp() {
+    userId = new Account.Id(1);
+    repoManager = new InMemoryRepositoryManager();
+  }
+
+  @Test
+  public void seriesOfChanges() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c1_1 = p.commit().create();
+    RevCommit c2_1 = p.commit().parent(c1_1).create();
+    RevCommit c3_1 = p.commit().parent(c2_1).create();
+
+    ChangeData cd1 = newChange(p, c1_1);
+    ChangeData cd2 = newChange(p, c2_1);
+    ChangeData cd3 = newChange(p, c3_1);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd3, c3_1),
+        patchSetData(cd2, c2_1),
+        patchSetData(cd1, c1_1)));
+
+    // Add new patch sets whose commits are in reverse order, so output is in
+    // reverse order.
+    RevCommit c3_2 = p.commit().create();
+    RevCommit c2_2 = p.commit().parent(c3_2).create();
+    RevCommit c1_2 = p.commit().parent(c2_2).create();
+
+    addPatchSet(cd1, c1_2);
+    addPatchSet(cd2, c2_2);
+    addPatchSet(cd3, c3_2);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd1, c1_2),
+        patchSetData(cd2, c2_2),
+        patchSetData(cd3, c3_2)));
+  }
+
+  @Test
+  public void subsetOfSeriesOfChanges() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c1_1 = p.commit().create();
+    RevCommit c2_1 = p.commit().parent(c1_1).create();
+    RevCommit c3_1 = p.commit().parent(c2_1).create();
+
+    ChangeData cd1 = newChange(p, c1_1);
+    ChangeData cd3 = newChange(p, c3_1);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd3);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd3, c3_1),
+        patchSetData(cd1, c1_1)));
+  }
+
+  @Test
+  public void seriesOfChangesAtSameTimestamp() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c0 = p.commit().tick(0).create();
+    RevCommit c1 = p.commit().tick(0).parent(c0).create();
+    RevCommit c2 = p.commit().tick(0).parent(c1).create();
+    RevCommit c3 = p.commit().tick(0).parent(c2).create();
+    RevCommit c4 = p.commit().tick(0).parent(c3).create();
+
+    RevWalk rw = p.getRevWalk();
+    rw.parseCommit(c1);
+    assertThat(rw.parseCommit(c2).getCommitTime())
+        .isEqualTo(c1.getCommitTime());
+    assertThat(rw.parseCommit(c3).getCommitTime())
+        .isEqualTo(c1.getCommitTime());
+    assertThat(rw.parseCommit(c4).getCommitTime())
+        .isEqualTo(c1.getCommitTime());
+
+    ChangeData cd1 = newChange(p, c1);
+    ChangeData cd2 = newChange(p, c2);
+    ChangeData cd3 = newChange(p, c3);
+    ChangeData cd4 = newChange(p, c4);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd4, c4),
+        patchSetData(cd3, c3),
+        patchSetData(cd2, c2),
+        patchSetData(cd1, c1)));
+  }
+
+  @Test
+  public void seriesOfChangesWithReverseTimestamps() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c0 = p.commit().tick(-1).create();
+    RevCommit c1 = p.commit().tick(-1).parent(c0).create();
+    RevCommit c2 = p.commit().tick(-1).parent(c1).create();
+    RevCommit c3 = p.commit().tick(-1).parent(c2).create();
+    RevCommit c4 = p.commit().tick(-1).parent(c3).create();
+
+    RevWalk rw = p.getRevWalk();
+    rw.parseCommit(c1);
+    assertThat(rw.parseCommit(c2).getCommitTime())
+        .isLessThan(c1.getCommitTime());
+    assertThat(rw.parseCommit(c3).getCommitTime())
+        .isLessThan(c2.getCommitTime());
+    assertThat(rw.parseCommit(c4).getCommitTime())
+        .isLessThan(c3.getCommitTime());
+
+    ChangeData cd1 = newChange(p, c1);
+    ChangeData cd2 = newChange(p, c2);
+    ChangeData cd3 = newChange(p, c3);
+    ChangeData cd4 = newChange(p, c4);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd4, c4),
+        patchSetData(cd3, c3),
+        patchSetData(cd2, c2),
+        patchSetData(cd1, c1)));
+  }
+
+  @Test
+  public void subsetOfSeriesOfChangesWithReverseTimestamps() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c0 = p.commit().tick(-1).create();
+    RevCommit c1 = p.commit().tick(-1).parent(c0).create();
+    RevCommit c2 = p.commit().tick(-1).parent(c1).create();
+    RevCommit c3 = p.commit().tick(-1).parent(c2).create();
+    RevCommit c4 = p.commit().tick(-1).parent(c3).create();
+
+    RevWalk rw = p.getRevWalk();
+    rw.parseCommit(c1);
+    assertThat(rw.parseCommit(c2).getCommitTime())
+        .isLessThan(c1.getCommitTime());
+    assertThat(rw.parseCommit(c3).getCommitTime())
+        .isLessThan(c2.getCommitTime());
+    assertThat(rw.parseCommit(c4).getCommitTime())
+        .isLessThan(c3.getCommitTime());
+
+    ChangeData cd1 = newChange(p, c1);
+    ChangeData cd2 = newChange(p, c2);
+    ChangeData cd4 = newChange(p, c4);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd4);
+    WalkSorter sorter = new WalkSorter(repoManager);
+    List<PatchSetData> expected = ImmutableList.of(
+        patchSetData(cd4, c4),
+        patchSetData(cd2, c2),
+        patchSetData(cd1, c1));
+
+    for (List<ChangeData> list : permutations(changes)) {
+      // Not inOrder(); since child of c2 is missing, partial topo sort isn't
+      // guaranteed to work.
+      assertThat(sorter.sort(list)).containsExactlyElementsIn(expected);
+    }
+  }
+
+  @Test
+  public void seriesOfChangesAtSameTimestampWithRootCommit() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c1 = p.commit().tick(0).create();
+    RevCommit c2 = p.commit().tick(0).parent(c1).create();
+    RevCommit c3 = p.commit().tick(0).parent(c2).create();
+    RevCommit c4 = p.commit().tick(0).parent(c3).create();
+
+    RevWalk rw = p.getRevWalk();
+    rw.parseCommit(c1);
+    assertThat(rw.parseCommit(c2).getCommitTime())
+        .isEqualTo(c1.getCommitTime());
+    assertThat(rw.parseCommit(c3).getCommitTime())
+        .isEqualTo(c1.getCommitTime());
+    assertThat(rw.parseCommit(c4).getCommitTime())
+        .isEqualTo(c1.getCommitTime());
+
+    ChangeData cd1 = newChange(p, c1);
+    ChangeData cd2 = newChange(p, c2);
+    ChangeData cd3 = newChange(p, c3);
+    ChangeData cd4 = newChange(p, c4);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd4, c4),
+        patchSetData(cd3, c3),
+        patchSetData(cd2, c2),
+        patchSetData(cd1, c1)));
+  }
+
+  @Test
+  public void projectsSortedByName() throws Exception {
+    TestRepository<Repo> pa = newRepo("a");
+    TestRepository<Repo> pb = newRepo("b");
+    RevCommit c1 = pa.commit().create();
+    RevCommit c2 = pb.commit().create();
+    RevCommit c3 = pa.commit().parent(c1).create();
+    RevCommit c4 = pb.commit().parent(c2).create();
+
+    ChangeData cd1 = newChange(pa, c1);
+    ChangeData cd2 = newChange(pb, c2);
+    ChangeData cd3 = newChange(pa, c3);
+    ChangeData cd4 = newChange(pb, c4);
+
+    assertSorted(
+        new WalkSorter(repoManager),
+        ImmutableList.of(cd1, cd2, cd3, cd4),
+        ImmutableList.of(
+            patchSetData(cd3, c3),
+            patchSetData(cd1, c1),
+            patchSetData(cd4, c4),
+            patchSetData(cd2, c2)));
+  }
+
+  @Test
+  public void restrictToPatchSets() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c1_1 = p.commit().create();
+    RevCommit c2_1 = p.commit().parent(c1_1).create();
+
+    ChangeData cd1 = newChange(p, c1_1);
+    ChangeData cd2 = newChange(p, c2_1);
+
+    // Add new patch sets whose commits are in reverse order.
+    RevCommit c2_2 = p.commit().create();
+    RevCommit c1_2 = p.commit().parent(c2_2).create();
+
+    addPatchSet(cd1, c1_2);
+    addPatchSet(cd2, c2_2);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd1, c1_2),
+        patchSetData(cd2, c2_2)));
+
+    // If we restrict to PS1 of each change, the sorter uses that commit.
+    sorter.includePatchSets(ImmutableSet.of(
+        new PatchSet.Id(cd1.getId(), 1),
+        new PatchSet.Id(cd2.getId(), 1)));
+    assertSorted(sorter, changes, ImmutableList.of(
+        patchSetData(cd2, 1, c2_1),
+        patchSetData(cd1, 1, c1_1)));
+  }
+
+  @Test
+  public void restrictToPatchSetsOmittingWholeProject() throws Exception {
+    TestRepository<Repo> pa = newRepo("a");
+    TestRepository<Repo> pb = newRepo("b");
+    RevCommit c1 = pa.commit().create();
+    RevCommit c2 = pa.commit().create();
+
+    ChangeData cd1 = newChange(pa, c1);
+    ChangeData cd2 = newChange(pb, c2);
+
+    List<ChangeData> changes = ImmutableList.of(cd1, cd2);
+    WalkSorter sorter = new WalkSorter(repoManager)
+        .includePatchSets(ImmutableSet.of(cd1.currentPatchSet().getId()));
+
+    assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd1, c1)));
+  }
+
+  @Test
+  public void retainBody() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c = p.commit().message("message").create();
+    ChangeData cd = newChange(p, c);
+
+    List<ChangeData> changes = ImmutableList.of(cd);
+    RevCommit actual = new WalkSorter(repoManager)
+        .setRetainBody(true)
+        .sort(changes)
+        .iterator().next()
+        .commit();
+    assertThat(actual.getRawBuffer()).isNotNull();
+    assertThat(actual.getShortMessage()).isEqualTo("message");
+
+    actual = new WalkSorter(repoManager)
+        .setRetainBody(false)
+        .sort(changes)
+        .iterator().next()
+        .commit();
+    assertThat(actual.getRawBuffer()).isNull();
+  }
+
+  @Test
+  public void oneChange() throws Exception {
+    TestRepository<Repo> p = newRepo("p");
+    RevCommit c = p.commit().create();
+    ChangeData cd = newChange(p, c);
+
+    List<ChangeData> changes = ImmutableList.of(cd);
+    WalkSorter sorter = new WalkSorter(repoManager);
+
+    assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd, c)));
+  }
+
+  private ChangeData newChange(TestRepository<Repo> tr, ObjectId id)
+      throws Exception {
+    Project.NameKey project = tr.getRepository().getDescription().getProject();
+    Change c = TestChanges.newChange(project, userId);
+    ChangeData cd = ChangeData.createForTest(c.getId(), 1);
+    cd.setChange(c);
+    cd.currentPatchSet().setRevision(new RevId(id.name()));
+    cd.setPatchSets(ImmutableList.of(cd.currentPatchSet()));
+    return cd;
+  }
+
+  private PatchSet addPatchSet(ChangeData cd, ObjectId id) throws Exception {
+    TestChanges.incrementPatchSet(cd.change());
+    PatchSet ps = new PatchSet(cd.change().currentPatchSetId());
+    ps.setRevision(new RevId(id.name()));
+    List<PatchSet> patchSets = new ArrayList<>(cd.patchSets());
+    patchSets.add(ps);
+    cd.setPatchSets(patchSets);
+    return ps;
+  }
+
+  private TestRepository<Repo> newRepo(String name)
+      throws Exception {
+    return new TestRepository<>(
+        repoManager.createRepository(new Project.NameKey(name)));
+  }
+
+  private static PatchSetData patchSetData(ChangeData cd, RevCommit commit)
+      throws Exception {
+    return PatchSetData.create(cd, cd.currentPatchSet(), commit);
+  }
+
+  private static PatchSetData patchSetData(ChangeData cd, int psId,
+      RevCommit commit) throws Exception {
+    return PatchSetData.create(
+        cd, cd.patchSet(new PatchSet.Id(cd.getId(), psId)), commit);
+  }
+
+  private static void assertSorted(WalkSorter sorter, List<ChangeData> changes,
+      List<PatchSetData> expected) throws Exception {
+    for (List<ChangeData> list : permutations(changes)) {
+      assertThat(sorter.sort(list))
+          .containsExactlyElementsIn(expected).inOrder();
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index 480efb4..4f409d1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.common.truth.Truth.assertThat;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -21,11 +22,117 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
 
+import com.google.gerrit.extensions.client.Theme;
+
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
 import java.util.concurrent.TimeUnit;
 
 public class ConfigUtilTest {
+  private static final String SECT = "foo";
+  private static final String SUB = "bar";
+
+  static class SectionInfo {
+    public static final String CONSTANT = "42";
+    public transient String missing;
+    public int i;
+    public Integer ii;
+    public Integer id;
+    public long l;
+    public Long ll;
+    public Long ld;
+    public boolean b;
+    public Boolean bb;
+    public Boolean bd;
+    public String s;
+    public String sd;
+    public Theme t;
+    public Theme td;
+    static SectionInfo defaults() {
+      SectionInfo i = new SectionInfo();
+      i.i = 1;
+      i.ii = 2;
+      i.id = 3;
+      i.l = 4L;
+      i.ll = 5L;
+      i.ld = 6L;
+      i.b = true;
+      i.bb = false;
+      i.bd = true;
+      i.s = "foo";
+      i.sd = "bar";
+      i.t = Theme.DEFAULT;
+      i.td = Theme.DEFAULT;
+      return i;
+    }
+  }
+
+  @Test
+  public void testStoreLoadSection() throws Exception {
+    SectionInfo d = SectionInfo.defaults();
+    SectionInfo in = new SectionInfo();
+    in.missing = "42";
+    in.i = 1;
+    in.ii = 43;
+    in.l = 4L;
+    in.ll = -43L;
+    in.b = false;
+    in.bb = true;
+    in.bd = false;
+    in.s = "baz";
+    in.t = Theme.MIDNIGHT;
+
+    Config cfg = new Config();
+    ConfigUtil.storeSection(cfg, SECT, SUB, in, d);
+
+    assertThat(cfg.getString(SECT, SUB, "CONSTANT")).isNull();
+    assertThat(cfg.getString(SECT, SUB, "missing")).isNull();
+    assertThat(cfg.getBoolean(SECT, SUB, "b", false)).isEqualTo(in.b);
+    assertThat(cfg.getBoolean(SECT, SUB, "bb", false)).isEqualTo(in.bb);
+    assertThat(cfg.getInt(SECT, SUB, "i", 0)).isEqualTo(0);
+    assertThat(cfg.getInt(SECT, SUB, "ii", 0)).isEqualTo(in.ii);
+    assertThat(cfg.getLong(SECT, SUB, "l", 0L)).isEqualTo(0L);
+    assertThat(cfg.getLong(SECT, SUB, "ll", 0L)).isEqualTo(in.ll);
+    assertThat(cfg.getString(SECT, SUB, "s")).isEqualTo(in.s);
+    assertThat(cfg.getString(SECT, SUB, "sd")).isNull();
+
+    SectionInfo out = new SectionInfo();
+    ConfigUtil.loadSection(cfg, SECT, SUB, out, d, null);
+    assertThat(out.i).isEqualTo(in.i);
+    assertThat(out.ii).isEqualTo(in.ii);
+    assertThat(out.id).isEqualTo(d.id);
+    assertThat(out.l).isEqualTo(in.l);
+    assertThat(out.ll).isEqualTo(in.ll);
+    assertThat(out.ld).isEqualTo(d.ld);
+    assertThat(out.b).isEqualTo(in.b);
+    assertThat(out.bb).isEqualTo(in.bb);
+    assertThat(out.bd).isNull();
+    assertThat(out.s).isEqualTo(in.s);
+    assertThat(out.sd).isEqualTo(d.sd);
+    assertThat(out.t).isEqualTo(in.t);
+    assertThat(out.td).isEqualTo(d.td);
+  }
+
+  @Test
+  public void mergeSection() throws Exception {
+    SectionInfo d = SectionInfo.defaults();
+    Config cfg = new Config();
+    ConfigUtil.storeSection(cfg, SECT, SUB, d, d);
+
+    SectionInfo in = new SectionInfo();
+    in.i = 42;
+
+    SectionInfo out = new SectionInfo();
+    ConfigUtil.loadSection(cfg, SECT, SUB, out, d, in);
+    // Check original values preserved
+    assertThat(out.id).isEqualTo(d.id);
+    // Check merged values
+    assertThat(out.i).isEqualTo(in.i);
+    // Check that boolean attribute not nullified
+    assertThat(out.bb).isFalse();
+  }
+
   @Test
   public void testTimeUnit() {
     assertEquals(ms(0, MILLISECONDS), parse("0"));
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
similarity index 84%
rename from gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
rename to gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
index 57b089a..b03a381 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.server.config;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
 
-public class GitWebConfigTest {
+public class GitwebConfigTest {
 
   private static final String VALID_CHARACTERS = "*()";
   private static final String SOME_INVALID_CHARACTERS = "09AZaz$-_.+!',";
@@ -27,14 +27,14 @@
   @Test
   public void testValidPathSeparator() {
     for(char c : VALID_CHARACTERS.toCharArray()) {
-      assertTrue("valid character rejected: " + c, GitWebConfig.isValidPathSeparator(c));
+      assertTrue("valid character rejected: " + c, GitwebConfig.isValidPathSeparator(c));
     }
   }
 
   @Test
   public void testInalidPathSeparator() {
     for(char c : SOME_INVALID_CHARACTERS.toCharArray()) {
-      assertFalse("invalid character accepted: " + c, GitWebConfig.isValidPathSeparator(c));
+      assertFalse("invalid character accepted: " + c, GitwebConfig.isValidPathSeparator(c));
     }
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
new file mode 100644
index 0000000..1fb6d81
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2014 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.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class RepositoryConfigTest {
+
+  private Config cfg;
+  private RepositoryConfig repoCfg;
+
+  @Before
+  public void setUp() throws Exception {
+    cfg = new Config();
+    repoCfg = new RepositoryConfig(cfg);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeWhenNotConfigured() {
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeForStarFilter() {
+    configureDefaultSubmitType("*", SubmitType.CHERRY_PICK);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.CHERRY_PICK);
+
+    configureDefaultSubmitType("*", SubmitType.FAST_FORWARD_ONLY);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.FAST_FORWARD_ONLY);
+
+    configureDefaultSubmitType("*", SubmitType.REBASE_IF_NECESSARY);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.REBASE_IF_NECESSARY);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeForSpecificFilter() {
+    configureDefaultSubmitType("someProject", SubmitType.CHERRY_PICK);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someOtherProject")))
+        .isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.CHERRY_PICK);
+  }
+
+  @Test
+  public void testDefaultSubmitTypeForStartWithFilter() {
+    configureDefaultSubmitType("somePath/somePath/*",
+        SubmitType.REBASE_IF_NECESSARY);
+    configureDefaultSubmitType("somePath/*", SubmitType.CHERRY_PICK);
+    configureDefaultSubmitType("*", SubmitType.MERGE_ALWAYS);
+
+    assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
+        .isEqualTo(SubmitType.MERGE_ALWAYS);
+
+    assertThat(
+        repoCfg.getDefaultSubmitType(new NameKey("somePath/someProject")))
+        .isEqualTo(SubmitType.CHERRY_PICK);
+
+    assertThat(
+        repoCfg.getDefaultSubmitType(new NameKey(
+            "somePath/somePath/someProject"))).isEqualTo(
+        SubmitType.REBASE_IF_NECESSARY);
+  }
+
+  private void configureDefaultSubmitType(String projectFilter,
+      SubmitType submitType) {
+    cfg.setString(RepositoryConfig.SECTION_NAME, projectFilter,
+        RepositoryConfig.DEFAULT_SUBMIT_TYPE_NAME, submitType.toString());
+  }
+
+  @Test
+  public void testOwnerGroupsWhenNotConfigured() {
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        new String[] {});
+  }
+
+  @Test
+  public void testOwnerGroupsForStarFilter() {
+    String[] ownerGroups = new String[] {"group1", "group2"};
+    configureOwnerGroups("*", Lists.newArrayList(ownerGroups));
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        ownerGroups);
+  }
+
+  @Test
+  public void testOwnerGroupsForSpecificFilter() {
+    String[] ownerGroups = new String[] {"group1", "group2"};
+    configureOwnerGroups("someProject", Lists.newArrayList(ownerGroups));
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someOtherProject")))
+        .isEqualTo(new String[] {});
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        ownerGroups);
+  }
+
+  @Test
+  public void testOwnerGroupsForStartWithFilter() {
+    String[] ownerGroups1 = new String[] {"group1"};
+    String[] ownerGroups2 = new String[] {"group2"};
+    String[] ownerGroups3 = new String[] {"group3"};
+
+    configureOwnerGroups("*", Lists.newArrayList(ownerGroups1));
+    configureOwnerGroups("somePath/*", Lists.newArrayList(ownerGroups2));
+    configureOwnerGroups("somePath/somePath/*",
+        Lists.newArrayList(ownerGroups3));
+
+    assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEqualTo(
+        ownerGroups1);
+
+    assertThat(repoCfg.getOwnerGroups(new NameKey("somePath/someProject")))
+        .isEqualTo(ownerGroups2);
+
+    assertThat(
+        repoCfg.getOwnerGroups(new NameKey("somePath/somePath/someProject")))
+        .isEqualTo(ownerGroups3);
+  }
+
+  private void configureOwnerGroups(String projectFilter,
+      List<String> ownerGroups) {
+    cfg.setStringList(RepositoryConfig.SECTION_NAME, projectFilter,
+        RepositoryConfig.OWNER_GROUP_NAME, ownerGroups);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
index 5fdecf0..743e43d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
@@ -19,94 +19,95 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import com.google.gerrit.server.util.HostPlatform;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
-import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 public class SitePathsTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Test
   public void testCreate_NotExisting() throws IOException {
-    final File root = random();
+    final Path root = random();
     final SitePaths site = new SitePaths(root);
     assertTrue(site.isNew);
     assertEquals(root, site.site_path);
-    assertEquals(new File(root, "etc"), site.etc_dir);
+    assertEquals(root.resolve("etc"), site.etc_dir);
   }
 
   @Test
   public void testCreate_Empty() throws IOException {
-    final File root = random();
+    final Path root = random();
     try {
-      assertTrue(root.mkdir());
+      Files.createDirectory(root);
 
       final SitePaths site = new SitePaths(root);
       assertTrue(site.isNew);
       assertEquals(root, site.site_path);
     } finally {
-      root.delete();
+      Files.delete(root);
     }
   }
 
   @Test
   public void testCreate_NonEmpty() throws IOException {
-    final File root = random();
-    final File txt = new File(root, "test.txt");
+    final Path root = random();
+    final Path txt = root.resolve("test.txt");
     try {
-      assertTrue(root.mkdir());
-      assertTrue(txt.createNewFile());
+      Files.createDirectory(root);
+      Files.createFile(txt);
 
       final SitePaths site = new SitePaths(root);
       assertFalse(site.isNew);
       assertEquals(root, site.site_path);
     } finally {
-      txt.delete();
-      root.delete();
+      Files.delete(txt);
+      Files.delete(root);
     }
   }
 
   @Test
   public void testCreate_NotDirectory() throws IOException {
-    final File root = random();
+    final Path root = random();
     try {
-      assertTrue(root.createNewFile());
-      try {
-        new SitePaths(root);
-        fail("Did not throw exception");
-      } catch (FileNotFoundException e) {
-        assertEquals("Not a directory: " + root.getPath(), e.getMessage());
-      }
+      Files.createFile(root);
+      exception.expect(NotDirectoryException.class);
+      new SitePaths(root);
     } finally {
-      root.delete();
+      Files.delete(root);
     }
   }
 
   @Test
   public void testResolve() throws IOException {
-    final File root = random();
+    final Path root = random();
     final SitePaths site = new SitePaths(root);
 
     assertNull(site.resolve(null));
     assertNull(site.resolve(""));
 
     assertNotNull(site.resolve("a"));
-    assertEquals(new File(root, "a").getCanonicalFile(), site.resolve("a"));
+    assertEquals(root.resolve("a").toAbsolutePath().normalize(),
+        site.resolve("a"));
 
     final String pfx = HostPlatform.isWin32() ? "C:/" : "/";
     assertNotNull(site.resolve(pfx + "a"));
-    assertEquals(new File(pfx + "a").getCanonicalFile(), site.resolve(pfx + "a"));
+    assertEquals(Paths.get(pfx + "a"), site.resolve(pfx + "a"));
   }
 
-  private static File random() throws IOException {
-    File tmp = File.createTempFile("gerrit_test_", "_site");
-    if (!tmp.delete()) {
-      throw new IOException("Cannot create " + tmp.getPath());
-    }
+  private static Path random() throws IOException {
+    Path tmp = Files.createTempFile("gerrit_test_", "_site");
+    Files.deleteIfExists(tmp);
     return tmp;
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
new file mode 100644
index 0000000..2304ece
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/DestinationListTest.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2015 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.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Set;
+
+public class DestinationListTest extends TestCase {
+  public static final String R_FOO = "refs/heads/foo";
+  public static final String R_BAR = "refs/heads/bar";
+
+  public static final String P_MY = "myproject";
+  public static final String P_SLASH = "my/project/with/slashes";
+  public static final String P_COMPLEX = " a/project/with spaces and \ttabs ";
+
+  public static final String L_FOO = R_FOO + "\t" + P_MY + "\n";
+  public static final String L_BAR = R_BAR + "\t" + P_SLASH + "\n";
+  public static final String L_FOO_PAD_F = " " + R_FOO + "\t" + P_MY + "\n";
+  public static final String L_FOO_PAD_E = R_FOO + " \t" + P_MY + "\n";
+  public static final String L_COMPLEX = R_FOO + "\t" + P_COMPLEX + "\n";
+  public static final String L_BAD = R_FOO + "\n";
+
+  public static final String HEADER = "# Ref\tProject\n";
+  public static final String HEADER_PROPER = "# Ref         \tProject\n";
+  public static final String C1 = "# A Simple Comment\n";
+  public static final String C2 = "# Comment with a tab\t and multi # # #\n";
+
+  public static final String F_SIMPLE = L_FOO + L_BAR;
+  public static final String F_PROPER = L_BAR + L_FOO; // alpha order
+  public static final String F_PAD_F = L_FOO_PAD_F + L_BAR;
+  public static final String F_PAD_E = L_FOO_PAD_E + L_BAR;
+
+  public static final String LABEL = "label";
+  public static final String LABEL2 = "another";
+
+  public static final Branch.NameKey B_FOO = dest(P_MY, R_FOO);
+  public static final Branch.NameKey B_BAR = dest(P_SLASH, R_BAR);
+  public static final Branch.NameKey B_COMPLEX = dest(P_COMPLEX, R_FOO);
+
+  public static final Set<Branch.NameKey> D_SIMPLE = Sets.newHashSet();
+  static {
+    D_SIMPLE.clear();
+    D_SIMPLE.add(B_FOO);
+    D_SIMPLE.add(B_BAR);
+  }
+
+  private static Branch.NameKey dest(String project, String ref) {
+    return new Branch.NameKey(new Project.NameKey(project), ref);
+  }
+
+  @Test
+  public void testParseSimple() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_SIMPLE, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseWHeader() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, HEADER + F_SIMPLE, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseWComments() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, C1 + F_SIMPLE + C2, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseFooComment() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, "#" + L_FOO + L_BAR, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).doesNotContain(B_FOO);
+    assertThat(branches).contains(B_BAR);
+  }
+
+  @Test
+  public void testParsePaddedFronts() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_PAD_F, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParsePaddedEnds() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_PAD_E, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+  }
+
+  @Test
+  public void testParseComplex() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, L_COMPLEX, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).contains(B_COMPLEX);
+  }
+
+  @Test(expected = IOException.class)
+  public void testParseBad() throws IOException {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    new DestinationList().parseLabel(LABEL, L_BAD, sink);
+  }
+
+  @Test
+  public void testParse2Labels() throws Exception {
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_SIMPLE, null);
+    Set<Branch.NameKey> branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+
+    dl.parseLabel(LABEL2, L_COMPLEX, null);
+    branches = dl.getDestinations(LABEL);
+    assertThat(branches).containsExactlyElementsIn(D_SIMPLE);
+    branches = dl.getDestinations(LABEL2);
+    assertThat(branches).contains(B_COMPLEX);
+  }
+
+  @Test
+  public void testAsText() throws Exception {
+    String text = HEADER_PROPER + "#\n" + F_PROPER;
+    DestinationList dl = new DestinationList();
+    dl.parseLabel(LABEL, F_SIMPLE, null);
+    String asText = dl.asText(LABEL);
+    assertThat(text).isEqualTo(asText);
+
+    dl.parseLabel(LABEL2, asText, null);
+    assertThat(text).isEqualTo(dl.asText(LABEL2));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java
new file mode 100644
index 0000000..ba0599d
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -0,0 +1,365 @@
+// Copyright (C) 2015 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.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GroupCollectorTest {
+  private TestRepository<?> tr;
+
+  @Before
+  public void setUp() throws Exception {
+    tr = new TestRepository<>(
+        new InMemoryRepository(new DfsRepositoryDescription("repo")));
+  }
+
+  @Test
+  public void commitWhoseParentIsUninterestingGetsNewGroup() throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(a, branchTip),
+        patchSets(),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+  }
+
+  @Test
+  public void commitWhoseParentIsNewPatchSetGetsParentsGroup()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(a).create();
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(b, branchTip),
+        patchSets(),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(b, a.name());
+  }
+
+  @Test
+  public void commitWhoseParentIsExistingPatchSetGetsParentsGroup()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(a).create();
+
+    String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(b, branchTip),
+        patchSets().put(a, psId(1, 1)),
+        groups().put(psId(1, 1), group));
+
+    assertThat(groups).containsEntry(a, group);
+    assertThat(groups).containsEntry(b, group);
+  }
+
+  @Test
+  public void commitWhoseParentIsExistingPatchSetWithNoGroup()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(a).create();
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(b, branchTip),
+        patchSets().put(a, psId(1, 1)),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(b, a.name());
+  }
+
+  @Test
+  public void mergeCommitAndNewParentsAllGetSameGroup() throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(a).parent(b).create();
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets(),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(b, a.name());
+    assertThat(groups).containsEntry(m, a.name());
+  }
+
+  @Test
+  public void mergeCommitWhereOneParentHasExistingGroup() throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(a).parent(b).create();
+
+    String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets().put(b, psId(1, 1)),
+        groups().put(psId(1, 1), group));
+
+    // Merge commit and other parent get the existing group.
+    assertThat(groups).containsEntry(a, group);
+    assertThat(groups).containsEntry(b, group);
+    assertThat(groups).containsEntry(m, group);
+  }
+
+  @Test
+  public void mergeCommitWhereBothParentsHaveDifferentGroups()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(a).parent(b).create();
+
+    String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    String group2 = "1234567812345678123456781234567812345678";
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets()
+            .put(a, psId(1, 1))
+            .put(b, psId(2, 1)),
+        groups()
+            .put(psId(1, 1), group1)
+            .put(psId(2, 1), group2));
+
+    assertThat(groups).containsEntry(a, group1);
+    assertThat(groups).containsEntry(b, group2);
+    // Merge commit gets joined group of parents.
+    assertThat(groups.asMap())
+        .containsEntry(m, ImmutableSet.of(group1, group2));
+  }
+
+  @Test
+  public void mergeCommitMergesGroupsFromParent() throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(a).parent(b).create();
+
+    String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    String group2a = "1234567812345678123456781234567812345678";
+    String group2b = "ef123456ef123456ef123456ef123456ef123456";
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets()
+            .put(a, psId(1, 1))
+            .put(b, psId(2, 1)),
+        groups()
+            .put(psId(1, 1), group1)
+            .put(psId(2, 1), group2a)
+            .put(psId(2, 1), group2b));
+
+    assertThat(groups).containsEntry(a, group1);
+    assertThat(groups.asMap())
+        .containsEntry(b, ImmutableSet.of(group2a, group2b));
+    // Joined parent groups are split and resorted.
+    assertThat(groups.asMap())
+        .containsEntry(m, ImmutableSet.of(group1, group2a, group2b));
+  }
+
+  @Test
+  public void mergeCommitWithOneUninterestingParentAndOtherParentIsExisting()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(branchTip).parent(a).create();
+
+    String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets().put(a, psId(1, 1)),
+        groups().put(psId(1, 1), group));
+
+    assertThat(groups).containsEntry(a, group);
+    assertThat(groups).containsEntry(m, group);
+  }
+
+  @Test
+  public void mergeCommitWithOneUninterestingParentAndOtherParentIsNew()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(branchTip).parent(a).create();
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets(),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(m, a.name());
+  }
+
+  @Test
+  public void multipleMergeCommitsInHistoryAllResolveToSameGroup()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(branchTip).create();
+    RevCommit c = tr.commit().parent(branchTip).create();
+    RevCommit m1 = tr.commit().parent(b).parent(c).create();
+    RevCommit m2 = tr.commit().parent(a).parent(m1).create();
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m2, branchTip),
+        patchSets(),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(b, a.name());
+    assertThat(groups).containsEntry(c, a.name());
+    assertThat(groups).containsEntry(m1, a.name());
+    assertThat(groups).containsEntry(m2, a.name());
+  }
+
+  @Test
+  public void mergeCommitWithDuplicatedParentGetsParentsGroup()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit m = tr.commit().parent(a).parent(a).create();
+    tr.getRevWalk().parseBody(m);
+    assertThat(m.getParentCount()).isEqualTo(2);
+    assertThat(m.getParent(0)).isEqualTo(m.getParent(1));
+
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets(),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(m, a.name());
+  }
+
+  @Test
+  public void mergeCommitWithOneNewParentAndTwoExistingPatchSets()
+      throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(branchTip).create();
+    RevCommit c = tr.commit().parent(b).create();
+    RevCommit m = tr.commit().parent(a).parent(c).create();
+
+    String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    String group2 = "1234567812345678123456781234567812345678";
+    Multimap<ObjectId, String> groups = collectGroups(
+        newWalk(m, branchTip),
+        patchSets()
+            .put(a, psId(1, 1))
+            .put(b, psId(2, 1)),
+        groups()
+            .put(psId(1, 1), group1)
+            .put(psId(2, 1), group2));
+
+    assertThat(groups).containsEntry(a, group1);
+    assertThat(groups).containsEntry(b, group2);
+    assertThat(groups).containsEntry(c, group2);
+    assertThat(groups.asMap())
+        .containsEntry(m, ImmutableSet.of(group1, group2));
+  }
+
+  @Test
+  public void collectGroupsForMultipleTipsInParallel() throws Exception {
+    RevCommit branchTip = tr.commit().create();
+    RevCommit a = tr.commit().parent(branchTip).create();
+    RevCommit b = tr.commit().parent(a).create();
+    RevCommit c = tr.commit().parent(branchTip).create();
+    RevCommit d = tr.commit().parent(c).create();
+
+    RevWalk rw = newWalk(b, branchTip);
+    rw.markStart(rw.parseCommit(d));
+    // Schema upgrade case: all commits are existing patch sets, but none have
+    // groups assigned yet.
+    Multimap<ObjectId, String> groups = collectGroups(
+        rw,
+        patchSets()
+            .put(branchTip, psId(1, 1))
+            .put(a, psId(2, 1))
+            .put(b, psId(3, 1))
+            .put(c, psId(4, 1))
+            .put(d, psId(5, 1)),
+        groups());
+
+    assertThat(groups).containsEntry(a, a.name());
+    assertThat(groups).containsEntry(b, a.name());
+    assertThat(groups).containsEntry(c, c.name());
+    assertThat(groups).containsEntry(d, c.name());
+  }
+
+  // TODO(dborowitz): Tests for octopus merges.
+
+  private static PatchSet.Id psId(int c, int p) {
+    return new PatchSet.Id(new Change.Id(c), p);
+  }
+
+  private RevWalk newWalk(ObjectId start, ObjectId branchTip) throws Exception {
+    // Match RevWalk conditions from ReceiveCommits.
+    RevWalk rw = new RevWalk(tr.getRepository());
+    rw.sort(RevSort.TOPO);
+    rw.sort(RevSort.REVERSE, true);
+    rw.markStart(rw.parseCommit(start));
+    rw.markUninteresting(rw.parseCommit(branchTip));
+    return rw;
+  }
+
+  private static Multimap<ObjectId, String> collectGroups(
+      RevWalk rw,
+      ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha,
+      ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup)
+      throws Exception {
+    GroupCollector gc =
+        new GroupCollector(patchSetsBySha.build(), groupLookup.build());
+    RevCommit c;
+    while ((c = rw.next()) != null) {
+      gc.visit(c);
+    }
+    return gc.getGroups();
+  }
+
+  // Helper methods for constructing various map arguments, to avoid lots of
+  // type specifications.
+  private static ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSets() {
+    return ImmutableMultimap.builder();
+  }
+
+  private static ImmutableListMultimap.Builder<PatchSet.Id, String> groups() {
+    return ImmutableListMultimap.builder();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
index e741a91c..fde86a5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
@@ -118,4 +118,8 @@
         "ebe31c01aec2c9ac3b3c03e87a47450829ff4310")));
   }
 
+  @Test
+  public void testAsText() throws Exception {
+    assertTrue(TEXT.equals(groupList.asText()));
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
index 78a4c5c..b3f7894 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
@@ -46,6 +46,7 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.Repository;
 import org.junit.After;
@@ -59,7 +60,7 @@
   @Inject private AccountManager accountManager;
   @Inject private AllProjectsName allProjects;
   @Inject private GitRepositoryManager repoManager;
-  @Inject private IdentifiedUser.RequestFactory userFactory;
+  @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private LabelNormalizer norm;
   @Inject private MetaDataUpdate.User metaDataUpdateFactory;
@@ -84,7 +85,7 @@
     schemaCreator.create(db);
     userId = accountManager.authenticate(AuthRequest.forUser("user"))
         .getAccountId();
-    user = userFactory.create(userId);
+    user = userFactory.create(Providers.of(db), userId);
 
     configureProject();
     setUpChange();
@@ -187,13 +188,10 @@
   }
 
   private ProjectConfig loadAllProjects() throws Exception {
-    Repository repo = repoManager.openRepository(allProjects);
-    try {
+    try (Repository repo = repoManager.openRepository(allProjects)) {
       ProjectConfig pc = new ProjectConfig(allProjects);
       pc.load(repo);
       return pc;
-    } finally {
-      repo.close();
     }
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
new file mode 100644
index 0000000..ca154b1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2015 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.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.testutil.TempFileUtil;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.easymock.EasyMockSupport;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public class LocalDiskRepositoryManagerTest extends EasyMockSupport {
+
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  private Config cfg;
+  private SitePaths site;
+  private LocalDiskRepositoryManager repoManager;
+
+  @Before
+  public void setUp() throws Exception {
+    site = new SitePaths(TempFileUtil.createTempDirectory().toPath());
+    site.resolve("git").toFile().mkdir();
+    cfg = new Config();
+    cfg.setString("gerrit", null, "basePath", "git");
+    repoManager =
+        new LocalDiskRepositoryManager(site, cfg,
+            createNiceMock(NotesMigration.class));
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testThatNullBasePathThrowsAnException() {
+    new LocalDiskRepositoryManager(site, new Config(),
+        createNiceMock(NotesMigration.class));
+  }
+
+  @Test
+  public void testProjectCreation() throws Exception {
+    Project.NameKey projectA = new Project.NameKey("projectA");
+    try (Repository repo = repoManager.createRepository(projectA)) {
+      assertThat(repo).isNotNull();
+    }
+    try (Repository repo = repoManager.openRepository(projectA)) {
+      assertThat(repo).isNotNull();
+    }
+    assertThat(repoManager.list()).containsExactly(projectA);
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithEmptyName() throws Exception {
+    repoManager.createRepository(new Project.NameKey(""));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithTrailingSlash() throws Exception {
+    repoManager.createRepository(new Project.NameKey("projectA/"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithBackSlash() throws Exception {
+    repoManager.createRepository(new Project.NameKey("a\\projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationAbsolutePath() throws Exception {
+    repoManager.createRepository(new Project.NameKey("/projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationStartingWithDotDot() throws Exception {
+    repoManager.createRepository(new Project.NameKey("../projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationContainsDotDot() throws Exception {
+    repoManager.createRepository(new Project.NameKey("a/../projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationDotPathSegment() throws Exception {
+    repoManager.createRepository(new Project.NameKey("a/./projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithTwoSlashes() throws Exception {
+    repoManager.createRepository(new Project.NameKey("a//projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithPathSegmentEndingByDotGit()
+      throws Exception {
+    repoManager.createRepository(new Project.NameKey("a/b.git/projectA"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithQuestionMark() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project?A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithPercentageSign() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project%A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithWidlcard() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project*A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithColon() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project:A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithLessThatSign() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project<A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithGreaterThatSign() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project>A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithPipe() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project|A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithDollarSign() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project$A"));
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testProjectCreationWithCarriageReturn() throws Exception {
+    repoManager.createRepository(new Project.NameKey("project\\rA"));
+  }
+
+  @Test
+  public void testOpenRepositoryCreatedDirectlyOnDisk() throws Exception {
+    createRepository(repoManager.getBasePath(), "projectA");
+    Project.NameKey projectA = new Project.NameKey("projectA");
+    try (Repository repo = repoManager.openRepository(projectA)) {
+      assertThat(repo).isNotNull();
+    }
+    assertThat(repoManager.list()).containsExactly(projectA);
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testOpenRepositoryInvalidName() throws Exception {
+    repoManager.openRepository(new Project.NameKey("project%?|<>A"));
+  }
+
+  @Test
+  public void testList() throws Exception {
+    Project.NameKey projectA = new Project.NameKey("projectA");
+    createRepository(repoManager.getBasePath(), projectA.get());
+
+    Project.NameKey projectB = new Project.NameKey("path/projectB");
+    createRepository(repoManager.getBasePath(), projectB.get());
+
+    Project.NameKey projectC = new Project.NameKey("anotherPath/path/projectC");
+    createRepository(repoManager.getBasePath(), projectC.get());
+    // create an invalid git repo named only .git
+    repoManager.getBasePath().resolve(".git").toFile().mkdir();
+    // create an invalid repo name
+    createRepository(repoManager.getBasePath(), "project?A");
+    assertThat(repoManager.list())
+        .containsExactly(projectA, projectB, projectC);
+  }
+
+  @Test
+  public void testGetSetProjectDescription() throws Exception {
+    Project.NameKey projectA = new Project.NameKey("projectA");
+    try (Repository repo = repoManager.createRepository(projectA)) {
+      assertThat(repo).isNotNull();
+    }
+
+    assertThat(repoManager.getProjectDescription(projectA)).isNull();
+    repoManager.setProjectDescription(projectA, "projectA description");
+    assertThat(repoManager.getProjectDescription(projectA)).isEqualTo(
+        "projectA description");
+
+    repoManager.setProjectDescription(projectA, "");
+    assertThat(repoManager.getProjectDescription(projectA)).isNull();
+  }
+
+  @Test(expected = RepositoryNotFoundException.class)
+  public void testGetProjectDescriptionFromUnexistingRepository()
+      throws Exception {
+    repoManager.getProjectDescription(new Project.NameKey("projectA"));
+  }
+
+  private void createRepository(Path directory, String projectName)
+      throws IOException {
+    String n = projectName + Constants.DOT_GIT_EXT;
+    FileKey loc = FileKey.exact(directory.resolve(n).toFile(), FS.DETECTED);
+    try (Repository db = RepositoryCache.open(loc, false)) {
+      db.create(true /* bare */);
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index cc4a9e3..7914fa8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -14,12 +14,8 @@
 
 package com.google.gerrit.server.git;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
@@ -40,6 +36,7 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -85,36 +82,34 @@
             + "  description = A simple description\n" //
             + "  accepted = group Developers\n" //
             + "  accepted = group Staff\n" //
-            + "  requireContactInformation = true\n" //
             + "  autoVerify = group Developers\n" //
             + "  agreementUrl = http://www.example.com/agree\n")) //
         ));
 
     ProjectConfig cfg = read(rev);
-    assertEquals(2, cfg.getAccountsSection().getSameGroupVisibility().size());
+    assertThat(cfg.getAccountsSection().getSameGroupVisibility()).hasSize(2);
     ContributorAgreement ca = cfg.getContributorAgreement("Individual");
-    assertEquals("Individual", ca.getName());
-    assertEquals("A simple description", ca.getDescription());
-    assertEquals("http://www.example.com/agree", ca.getAgreementUrl());
-    assertEquals(2, ca.getAccepted().size());
-    assertEquals(developers, ca.getAccepted().get(0).getGroup());
-    assertEquals("Staff", ca.getAccepted().get(1).getGroup().getName());
-    assertEquals("Developers", ca.getAutoVerify().getName());
-    assertTrue(ca.isRequireContactInformation());
+    assertThat(ca.getName()).isEqualTo("Individual");
+    assertThat(ca.getDescription()).isEqualTo("A simple description");
+    assertThat(ca.getAgreementUrl()).isEqualTo("http://www.example.com/agree");
+    assertThat(ca.getAccepted()).hasSize(2);
+    assertThat(ca.getAccepted().get(0).getGroup()).isEqualTo(developers);
+    assertThat(ca.getAccepted().get(1).getGroup().getName()).isEqualTo("Staff");
+    assertThat(ca.getAutoVerify().getName()).isEqualTo("Developers");
 
     AccessSection section = cfg.getAccessSection("refs/heads/*");
-    assertNotNull("has refs/heads/*", section);
-    assertNull("no refs/*", cfg.getAccessSection("refs/*"));
+    assertThat(section).isNotNull();
+    assertThat(cfg.getAccessSection("refs/*")).isNull();
 
     Permission create = section.getPermission(Permission.CREATE);
     Permission submit = section.getPermission(Permission.SUBMIT);
     Permission read = section.getPermission(Permission.READ);
     Permission push = section.getPermission(Permission.PUSH);
 
-    assertTrue(create.getExclusiveGroup());
-    assertTrue(submit.getExclusiveGroup());
-    assertTrue(read.getExclusiveGroup());
-    assertFalse(push.getExclusiveGroup());
+    assertThat(create.getExclusiveGroup()).isTrue();
+    assertThat(submit.getExclusiveGroup()).isTrue();
+    assertThat(read.getExclusiveGroup()).isTrue();
+    assertThat(push.getExclusiveGroup()).isFalse();
   }
 
   @Test
@@ -131,7 +126,7 @@
     ProjectConfig cfg = read(rev);
     Map<String, LabelType> labels = cfg.getLabelSections();
     Short dv = labels.entrySet().iterator().next().getValue().getDefaultValue();
-    assertEquals(0, (int) dv);
+    assertThat((int)dv).isEqualTo(0);
   }
 
   @Test
@@ -149,7 +144,7 @@
     ProjectConfig cfg = read(rev);
     Map<String, LabelType> labels = cfg.getLabelSections();
     Short dv = labels.entrySet().iterator().next().getValue().getDefaultValue();
-    assertEquals(-1, (int) dv);
+    assertThat((int)dv).isEqualTo(-1);
   }
 
   @Test
@@ -165,10 +160,10 @@
         ));
 
     ProjectConfig cfg = read(rev);
-    assertEquals(1, cfg.getValidationErrors().size());
-    assertEquals("project.config: Invalid defaultValue \"-2\" "
-        + "for label \"CustomLabel\"",
-        Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage());
+    assertThat(cfg.getValidationErrors()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
+      .isEqualTo("project.config: Invalid defaultValue \"-2\" "
+        + "for label \"CustomLabel\"");
   }
 
   @Test
@@ -187,7 +182,6 @@
             + "[contributor-agreement \"Individual\"]\n" //
             + "  description = A simple description\n" //
             + "  accepted = group Developers\n" //
-            + "  requireContactInformation = true\n" //
             + "  autoVerify = group Developers\n" //
             + "  agreementUrl = http://www.example.com/agree\n")) //
         ));
@@ -200,12 +194,11 @@
     Permission submit = section.getPermission(Permission.SUBMIT);
     submit.add(new PermissionRule(cfg.resolve(staff)));
     ContributorAgreement ca = cfg.getContributorAgreement("Individual");
-    ca.setRequireContactInformation(false);
     ca.setAccepted(Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
     ca.setAutoVerify(null);
     ca.setDescription("A new description");
     rev = commit(cfg);
-    assertEquals(""//
+    assertThat(text(rev, "project.config")).isEqualTo(""//
         + "[access \"refs/heads/*\"]\n" //
         + "  exclusiveGroupPermissions = read submit\n" //
         + "  submit = group Developers\n" //
@@ -217,8 +210,7 @@
         + "[contributor-agreement \"Individual\"]\n" //
         + "  description = A new description\n" //
         + "  accepted = group Staff\n" //
-        + "  agreementUrl = http://www.example.com/agree\n",
-        text(rev, "project.config"));
+        + "  agreementUrl = http://www.example.com/agree\n");
   }
 
   @Test
@@ -239,13 +231,13 @@
     Permission submit = section.getPermission(Permission.SUBMIT);
     submit.add(new PermissionRule(cfg.resolve(staff)));
     rev = commit(cfg);
-    assertEquals(""//
+    assertThat(text(rev, "project.config")).isEqualTo(""//
         + "[access \"refs/heads/*\"]\n" //
         + "  exclusiveGroupPermissions = read submit\n" //
         + "  submit = group People Who Can Submit\n" //
         + "\tsubmit = group Staff\n" //
         + "  upload = group Developers\n" //
-        + "  read = group Developers\n", text(rev, "project.config"));
+        + "  read = group Developers\n");
   }
 
   private ProjectConfig read(RevCommit rev) throws IOException,
@@ -266,7 +258,7 @@
     md.setMessage("Edit\n");
     cfg.commit(md);
 
-    Ref ref = db.getRef(RefNames.REFS_CONFIG);
+    Ref ref = db.exactRef(RefNames.REFS_CONFIG);
     return util.getRevWalk().parseCommit(ref.getObjectId());
   }
 
@@ -274,15 +266,11 @@
     RefUpdate u = db.updateRef(RefNames.REFS_CONFIG);
     u.disableRefLog();
     u.setNewObjectId(rev);
-    switch (u.forceUpdate()) {
-      case FAST_FORWARD:
-      case FORCED:
-      case NEW:
-      case NO_CHANGE:
-        break;
-      default:
-        fail("Cannot update ref for test: " + u.getResult());
-    }
+    Result result = u.forceUpdate();
+    assert_()
+      .withFailureMessage("Cannot update ref for test: " + result)
+      .that(result)
+      .isAnyOf(Result.FAST_FORWARD, Result.FORCED, Result.NEW, Result.NO_CHANGE);
   }
 
   private String text(RevCommit rev, String path) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java
new file mode 100644
index 0000000..d022d3e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2015 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.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class QueryListTest extends TestCase {
+  public static final String Q_P = "project:foo";
+  public static final String Q_B = "branch:bar";
+  public static final String Q_COMPLEX = "branch:bar AND peers:'is:open\t'";
+
+  public static final String N_FOO = "foo";
+  public static final String N_BAR = "bar";
+
+  public static final String L_FOO = N_FOO + "\t" + Q_P + "\n";
+  public static final String L_BAR = N_BAR + "\t" + Q_B + "\n";
+  public static final String L_FOO_PROP = N_FOO + "   \t" + Q_P + "\n";
+  public static final String L_BAR_PROP = N_BAR + "   \t" + Q_B + "\n";
+  public static final String L_FOO_PAD_F = " " + N_FOO + "\t" + Q_P + "\n";
+  public static final String L_FOO_PAD_E = N_FOO + " \t" + Q_P + "\n";
+  public static final String L_BAR_PAD_F = N_BAR + "\t " + Q_B + "\n";
+  public static final String L_BAR_PAD_E = N_BAR + "\t" + Q_B + " \n";
+  public static final String L_COMPLEX = N_FOO + "\t" + Q_COMPLEX + "\t \n";
+  public static final String L_BAD = N_FOO + "\n";
+
+  public static final String HEADER = "# Name\tQuery\n";
+  public static final String C1 = "# A Simple Comment\n";
+  public static final String C2 = "# Comment with a tab\t and multi # # #\n";
+
+  public static final String F_SIMPLE = L_FOO + L_BAR;
+  public static final String F_PROPER = L_BAR_PROP + L_FOO_PROP; // alpha order
+  public static final String F_PAD_F = L_FOO_PAD_F + L_BAR_PAD_F;
+  public static final String F_PAD_E = L_FOO_PAD_E + L_BAR_PAD_E;
+
+  @Test
+  public void testParseSimple() throws Exception {
+    QueryList ql = QueryList.parse(F_SIMPLE, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseWHeader() throws Exception {
+    QueryList ql = QueryList.parse(HEADER + F_SIMPLE, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseWComments() throws Exception {
+    QueryList ql = QueryList.parse(C1 + F_SIMPLE + C2, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseFooComment() throws Exception {
+    QueryList ql = QueryList.parse("#" + L_FOO + L_BAR, null);
+    assertThat(ql.getQuery(N_FOO)).isNull();
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParsePaddedFronts() throws Exception {
+    QueryList ql = QueryList.parse(F_PAD_F, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParsePaddedEnds() throws Exception {
+    QueryList ql = QueryList.parse(F_PAD_E, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseComplex() throws Exception {
+    QueryList ql = QueryList.parse(L_COMPLEX, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_COMPLEX);
+  }
+
+  @Test(expected = IOException.class)
+  public void testParseBad() throws Exception {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    QueryList.parse(L_BAD, sink);
+  }
+
+  @Test
+  public void testAsText() throws Exception {
+    String expectedText = HEADER + "#\n" + F_PROPER;
+    QueryList ql = QueryList.parse(F_SIMPLE, null);
+    String asText = ql.asText();
+    assertThat(asText).isEqualTo(expectedText);
+
+    ql = QueryList.parse(asText, null);
+    asText = ql.asText();
+    assertThat(asText).isEqualTo(expectedText);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
deleted file mode 100644
index c2f3f6f..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ /dev/null
@@ -1,1017 +0,0 @@
-// Copyright (C) 2011 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.server.git;
-
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.capture;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.junit.Assert.assertEquals;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.ResultSet;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StandardKeyEncoder;
-import com.google.inject.Provider;
-
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.easymock.IMocksControl;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.dircache.DirCacheBuilder;
-import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
-  private static final String newLine = System.getProperty("line.separator");
-
-  private IMocksControl mockMaker;
-  private SchemaFactory<ReviewDb> schemaFactory;
-  private SubmoduleSubscriptionAccess subscriptions;
-  private ReviewDb schema;
-  private Provider<String> urlProvider;
-  private GitRepositoryManager repoManager;
-  private GitReferenceUpdated gitRefUpdated;
-  private ChangeHooks changeHooks;
-
-  @SuppressWarnings("unchecked")
-  @Override
-  @Before
-  public void setUp() throws Exception {
-    super.setUp();
-
-    mockMaker = EasyMock.createStrictControl();
-    schemaFactory = mockMaker.createMock(SchemaFactory.class);
-    schema = mockMaker.createMock(ReviewDb.class);
-    subscriptions = mockMaker.createMock(SubmoduleSubscriptionAccess.class);
-    urlProvider = mockMaker.createMock(Provider.class);
-    repoManager = mockMaker.createMock(GitRepositoryManager.class);
-    gitRefUpdated = mockMaker.createMock(GitReferenceUpdated.class);
-    changeHooks = mockMaker.createMock(ChangeHooks.class);
-  }
-
-  private void doReplay() {
-    mockMaker.replay();
-  }
-
-  private void doVerify() {
-    mockMaker.verify();
-  }
-
-  /**
-   * It tests Submodule.update in the scenario a merged commit is an empty one
-   * (it does not have a .gitmodules file) and the project the commit was merged
-   * is not a submodule of other project.
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testEmptyCommit() throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository realDb = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git git = new Git(realDb);
-
-      final RevCommit mergeTip = git.commit().setMessage("test").call();
-
-      final Branch.NameKey branchNameKey =
-          new Branch.NameKey(new Project.NameKey("test-project"), "test-branch");
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080");
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> emptySubscriptions =
-          new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
-      expect(subscriptions.bySubmodule(branchNameKey)).andReturn(
-          emptySubscriptions);
-
-      schema.close();
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(branchNameKey, mergeTip, new RevWalk(realDb), urlProvider,
-              schemaFactory, realDb, null, new ArrayList<Change>(), null, null,
-              null, null, null, null);
-
-      submoduleOp.update();
-
-      doVerify();
-    }
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project"</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = .
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. The row inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "a" on branch "refs/heads/master"</li>
-   * <li>path "a"</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionToDotBranchValue() throws Exception {
-    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", ".").toString(), "refs/heads/master");
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project"</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = refs/heads/master
-   * </pre>
-   *
-   * <p>
-   * It expects to insert a new row in subscriptions table. The row inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source" on branch "refs/heads/master"</li>
-   * <li>path "source"</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionToSameBranch() throws Exception {
-    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", "refs/heads/master").toString(),
-        "refs/heads/master");
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project"</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = refs/heads/test
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. The row inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source" on branch "refs/heads/test"</li>
-   * <li>path "source"</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionToDifferentBranch() throws Exception {
-    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", "refs/heads/test").toString(),
-        "refs/heads/test");
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source-a"]
-   *       path = source-a
-   *       url = http://localhost:8080/source-a
-   *       branch = .
-   *
-   *     [submodule "source-b"]
-   *       path = source-b
-   *       url = http://localhost:8080/source-b
-   *       branch = .
-   * </pre>
-   * <p>
-   * It expects to insert new rows in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
-   * <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionsWithDotBranchValue() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("source-a", "source-a",
-            "http://localhost:8080/source-a", ".");
-    sb.append(buildSubmoduleSection("source-b", "source-b",
-        "http://localhost:8080/source-b", "."));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
-
-    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
-        subscriptionsToInsert);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source-a"]
-   *       path = source-a
-   *       url = http://localhost:8080/source-a
-   *       branch = .
-   *
-   *     [submodule "source-b"]
-   *       path = source-b
-   *       url = http://localhost:8080/source-b
-   *       branch = refs/heads/master
-   * </pre>
-   * <p>
-   * It expects to insert new rows in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
-   * <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionsDotAndSameBranchValues() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("source-a", "source-a",
-            "http://localhost:8080/source-a", ".");
-    sb.append(buildSubmoduleSection("source-b", "source-b",
-        "http://localhost:8080/source-b", "refs/heads/master"));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
-
-    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
-        subscriptionsToInsert);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   *
-   * <pre>
-   *     [submodule "source-a"]
-   *       path = source-a
-   *       url = http://localhost:8080/source-a
-   *       branch = refs/heads/test-a
-   *
-   *     [submodule "source-b"]
-   *       path = source-b
-   *       url = http://localhost:8080/source-b
-   *       branch = refs/heads/test-b
-   * </pre>
-   *
-   * <p>
-   * It expects to insert new rows in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source-a" on branch "refs/heads/test-a" with "source-a" path</li>
-   * <li>source "source-b" on branch "refs/heads/test-b" with "source-b" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionsSpecificBranchValues() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("source-a", "source-a",
-            "http://localhost:8080/source-a", "refs/heads/test-a");
-    sb.append(buildSubmoduleSection("source-b", "source-b",
-        "http://localhost:8080/source-b", "refs/heads/test-b"));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/test-a"), "source-a"));
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/test-b"), "source-b"));
-
-    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
-        subscriptionsToInsert);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>one subscription existing to destination project/branch</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = refs/heads/master
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source" on branch "refs/heads/master" with "source" path</li>
-   * </ul>
-   * </p>
-   * <p>
-   * It also expects to remove the row in subscriptions table specifying another
-   * project/branch subscribed to merged branch. This one to be removed is:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "old-source" on branch "refs/heads/master" with "old-source"
-   * path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testSubscriptionsInsertOneRemoveOne() throws Exception {
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("source"), "refs/heads/master"),
-        "source"));
-
-    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
-    oldOnesToMergedBranch.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("old-source"),
-            "refs/heads/master"), "old-source"));
-
-    doOnlySubscriptionTableOperations(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", "refs/heads/master").toString(),
-        mergedBranch, subscriptionsToInsert, oldOnesToMergedBranch);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>one subscription existing to destination project/branch with a source
-   * called old on refs/heads/master branch</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>
-   * commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "new"]
-   *       path = new
-   *       url = http://localhost:8080/new
-   *       branch = refs/heads/master
-   *
-   *     [submodule "old"]
-   *       path = old
-   *       url = http://localhost:8080/old
-   *       branch = refs/heads/master
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. It should not remove
-   * any row. The rows inserted specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "new" on branch "refs/heads/master" with "new" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testSubscriptionAddedAndMantainPreviousOne() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("new", "new", "http://localhost:8080/new",
-            "refs/heads/master");
-    sb.append(buildSubmoduleSection("old", "old", "http://localhost:8080/old",
-        "refs/heads/master"));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    final SubmoduleSubscription old =
-        new SubmoduleSubscription(mergedBranch, new Branch.NameKey(new Project.NameKey(
-            "old"), "refs/heads/master"), "old");
-
-    List<SubmoduleSubscription> extractedsubscriptions = new ArrayList<>();
-    extractedsubscriptions.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("new"), "refs/heads/master"),
-        "new"));
-    extractedsubscriptions.add(old);
-
-    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
-    oldOnesToMergedBranch.add(old);
-
-    doOnlySubscriptionTableOperations(sb.toString(), mergedBranch,
-        extractedsubscriptions, oldOnesToMergedBranch);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering an empty .gitmodules
-   * file is part of a commit to a destination project/branch having two sources
-   * subscribed.
-   * <p>
-   * It expects to remove the subscriptions to destination project/branch.
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testRemoveSubscriptions() throws Exception {
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> extractedsubscriptions = new ArrayList<>();
-    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
-    oldOnesToMergedBranch
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
-    oldOnesToMergedBranch
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
-
-    doOnlySubscriptionTableOperations("", mergedBranch, extractedsubscriptions,
-        oldOnesToMergedBranch);
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering no .gitmodules file
-   * in a merged commit to a destination project/branch that is a source one to
-   * one called "target-project".
-   * <p>
-   * It expects to update the git link called "source-project" to be in target
-   * repository.
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testOneSubscriberToUpdate() throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository sourceRepository = createWorkRepository();
-        Repository targetRepository = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git sourceGit = new Git(sourceRepository);
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git targetGit = new Git(targetRepository);
-
-      addRegularFileToIndex("file.txt", "test content", sourceRepository);
-
-      final RevCommit sourceMergeTip =
-          sourceGit.commit().setMessage("test").call();
-
-      final Branch.NameKey sourceBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("source-project"),
-              "refs/heads/master");
-
-      final CodeReviewCommit codeReviewCommit =
-          new CodeReviewCommit(sourceMergeTip.toObjectId());
-      final Change submittedChange = new Change(
-          new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
-          new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
-
-      final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
-      mergedCommits.put(submittedChange.getId(), codeReviewCommit);
-
-      final List<Change> submitted = new ArrayList<>();
-      submitted.add(submittedChange);
-
-      addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
-
-      targetGit.commit().setMessage("test").call();
-
-      final Branch.NameKey targetBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("target-project"),
-              sourceBranchNameKey.get());
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080");
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> subscribers =
-          new ListResultSet<>(Collections
-              .singletonList(new SubmoduleSubscription(targetBranchNameKey,
-                  sourceBranchNameKey, "source-project")));
-      expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
-          subscribers);
-
-      expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
-          .andReturn(targetRepository).anyTimes();
-
-      Capture<RefUpdate> ruCapture = new Capture<>();
-      gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
-          capture(ruCapture));
-      changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
-          anyObject(RefUpdate.class), EasyMock.<Account>isNull());
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> emptySubscriptions =
-          new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
-      expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
-          emptySubscriptions);
-
-      schema.close();
-
-      final PersonIdent myIdent =
-          new PersonIdent("test-user", "test-user@email.com");
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
-              sourceRepository), urlProvider, schemaFactory, sourceRepository,
-              new Project(sourceBranchNameKey.getParentKey()), submitted,
-              mergedCommits, myIdent, repoManager, gitRefUpdated, null,
-              changeHooks);
-
-      submoduleOp.update();
-
-      doVerify();
-      RefUpdate ru = ruCapture.getValue();
-      assertEquals(ru.getName(), targetBranchNameKey.get());
-    }
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering established circular
-   * reference in submodule_subscriptions table.
-   * <p>
-   * In the tested scenario there is no .gitmodules file in a merged commit to a
-   * destination project/branch that is a source one to one called
-   * "target-project".
-   * <p>
-   * submodule_subscriptions table will be incorrect due source appearing as a
-   * subscriber or target-project: according to database target-project has as
-   * source the source-project, and source-project has as source the
-   * target-project.
-   * <p>
-   * It expects to update the git link called "source-project" to be in target
-   * repository and ignoring the incorrect row in database establishing the
-   * circular reference.
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testAvoidingCircularReference() throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository sourceRepository = createWorkRepository();
-        Repository targetRepository = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git sourceGit = new Git(sourceRepository);
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git targetGit = new Git(targetRepository);
-
-      addRegularFileToIndex("file.txt", "test content", sourceRepository);
-
-      final RevCommit sourceMergeTip =
-          sourceGit.commit().setMessage("test").call();
-
-      final Branch.NameKey sourceBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("source-project"),
-              "refs/heads/master");
-
-      final CodeReviewCommit codeReviewCommit =
-          new CodeReviewCommit(sourceMergeTip.toObjectId());
-      final Change submittedChange = new Change(
-          new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
-          new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
-
-      final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
-      mergedCommits.put(submittedChange.getId(), codeReviewCommit);
-
-      final List<Change> submitted = new ArrayList<>();
-      submitted.add(submittedChange);
-
-      addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
-
-      targetGit.commit().setMessage("test").call();
-
-      final Branch.NameKey targetBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("target-project"),
-              sourceBranchNameKey.get());
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080");
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> subscribers =
-          new ListResultSet<>(Collections
-              .singletonList(new SubmoduleSubscription(targetBranchNameKey,
-                  sourceBranchNameKey, "source-project")));
-      expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
-          subscribers);
-
-      expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
-          .andReturn(targetRepository).anyTimes();
-
-      Capture<RefUpdate> ruCapture = new Capture<>();
-      gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
-          capture(ruCapture));
-      changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
-            anyObject(RefUpdate.class), EasyMock.<Account>isNull());
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
-          new ListResultSet<>(Collections
-              .singletonList(new SubmoduleSubscription(sourceBranchNameKey,
-                  targetBranchNameKey, "target-project")));
-      expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
-          incorrectSubscriptions);
-
-      schema.close();
-
-      final PersonIdent myIdent =
-          new PersonIdent("test-user", "test-user@email.com");
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
-              sourceRepository), urlProvider, schemaFactory, sourceRepository,
-              new Project(sourceBranchNameKey.getParentKey()), submitted,
-              mergedCommits, myIdent, repoManager, gitRefUpdated, null, changeHooks);
-
-      submoduleOp.update();
-
-      doVerify();
-      RefUpdate ru = ruCapture.getValue();
-      assertEquals(ru.getName(), targetBranchNameKey.get());
-    }
-  }
-
-  /**
-   * It calls SubmoduleOp.update considering only one insert on Subscriptions
-   * table.
-   * <p>
-   * It considers a commit containing a .gitmodules file was merged in
-   * refs/heads/master of a dest-project.
-   * </p>
-   * <p>
-   * The .gitmodules file content should indicate a source project called
-   * "source".
-   * </p>
-   *
-   * @param gitModulesFileContent The .gitmodules file content. During the test
-   *        this file is created, so the commit containing it.
-   * @param sourceBranchName The branch name of source project "pointed by"
-   *        .gitmodules file.
-   * @throws Exception If an exception occurs.
-   */
-  private void doOneSubscriptionInsert(final String gitModulesFileContent,
-      final String sourceBranchName) throws Exception {
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("source"), sourceBranchName),
-        "source"));
-
-    doOnlySubscriptionInserts(gitModulesFileContent, mergedBranch,
-        subscriptionsToInsert);
-  }
-
-  /**
-   * It calls SubmoduleOp.update method considering scenario only inserting new
-   * subscriptions.
-   * <p>
-   * In this test a commit is created and considered merged to
-   * {@code mergedBranch} branch.
-   * </p>
-   * <p>
-   * The destination project the commit was merged is not considered to be a
-   * source of another project (no subscribers found to this project).
-   * </p>
-   *
-   * @param gitModulesFileContent The .gitmodules file content.
-   * @param mergedBranch The {@code Branch.NameKey} instance representing the
-   *        project/branch the commit was merged.
-   * @param extractedSubscriptions The subscription rows extracted from
-   *        gitmodules file.
-   * @throws Exception If an exception occurs.
-   */
-  private void doOnlySubscriptionInserts(final String gitModulesFileContent,
-      final Branch.NameKey mergedBranch,
-      final List<SubmoduleSubscription> extractedSubscriptions) throws Exception {
-    doOnlySubscriptionTableOperations(gitModulesFileContent, mergedBranch,
-        extractedSubscriptions, new ArrayList<SubmoduleSubscription>());
-  }
-
-  /**
-   * It calls SubmoduleOp.update method considering scenario only updating
-   * Subscriptions table.
-   * <p>
-   * In this test a commit is created and considered merged to
-   * {@code mergedBranch} branch.
-   * </p>
-   * <p>
-   * The destination project the commit was merged is not considered to be a
-   * source of another project (no subscribers found to this project).
-   * </p>
-   *
-   * @param gitModulesFileContent The .gitmodules file content.
-   * @param mergedBranch The {@code Branch.NameKey} instance representing the
-   *        project/branch the commit was merged.
-   * @param extractedSubscriptions The subscription rows extracted from
-   *        gitmodules file.
-   * @param previousSubscriptions The subscription rows to be considering as
-   *        existing and pointing as target to the {@code mergedBranch}
-   *        before updating the table.
-   * @throws Exception If an exception occurs.
-   */
-  private void doOnlySubscriptionTableOperations(
-      final String gitModulesFileContent, final Branch.NameKey mergedBranch,
-      final List<SubmoduleSubscription> extractedSubscriptions,
-      final List<SubmoduleSubscription> previousSubscriptions) throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository realDb = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git git = new Git(realDb);
-
-      addRegularFileToIndex(".gitmodules", gitModulesFileContent, realDb);
-
-      final RevCommit mergeTip = git.commit().setMessage("test").call();
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080").times(2);
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      expect(subscriptions.bySuperProject(mergedBranch)).andReturn(
-          new ListResultSet<>(previousSubscriptions));
-
-      SortedSet<Project.NameKey> existingProjects = new TreeSet<>();
-
-      for (SubmoduleSubscription extracted : extractedSubscriptions) {
-        existingProjects.add(extracted.getSubmodule().getParentKey());
-      }
-
-      for (int index = 0; index < extractedSubscriptions.size(); index++) {
-        expect(repoManager.list()).andReturn(existingProjects);
-      }
-
-      final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-      for (SubmoduleSubscription s : extractedSubscriptions) {
-        if (previousSubscriptions.contains(s)) {
-          alreadySubscribeds.add(s);
-        }
-      }
-
-      final Set<SubmoduleSubscription> subscriptionsToRemove =
-          new HashSet<>(previousSubscriptions);
-      final List<SubmoduleSubscription> subscriptionsToInsert =
-          new ArrayList<>(extractedSubscriptions);
-
-      subscriptionsToRemove.removeAll(subscriptionsToInsert);
-      subscriptionsToInsert.removeAll(alreadySubscribeds);
-
-      if (!subscriptionsToRemove.isEmpty()) {
-        expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-        subscriptions.delete(subscriptionsToRemove);
-      }
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      subscriptions.insert(subscriptionsToInsert);
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      expect(subscriptions.bySubmodule(mergedBranch)).andReturn(
-          new ListResultSet<>(new ArrayList<SubmoduleSubscription>()));
-
-      schema.close();
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb),
-              urlProvider, schemaFactory, realDb, new Project(mergedBranch
-                  .getParentKey()), new ArrayList<Change>(), null, null,
-              repoManager, null, null, null);
-
-      submoduleOp.update();
-    }
-  }
-
-  /**
-   * It creates and adds a regular file to git index of a repository.
-   *
-   * @param fileName The file name.
-   * @param content File content.
-   * @param repository The Repository instance.
-   * @throws IOException If an I/O exception occurs.
-   */
-  private void addRegularFileToIndex(final String fileName,
-      final String content, final Repository repository) throws IOException {
-    final ObjectInserter oi = repository.newObjectInserter();
-    AnyObjectId objectId =
-        oi.insert(Constants.OBJ_BLOB, Constants.encode(content));
-    oi.flush();
-    addEntryToIndex(fileName, FileMode.REGULAR_FILE, objectId, repository);
-  }
-
-  /**
-   * It creates and adds a git link to git index of a repository.
-   *
-   * @param fileName The file name.
-   * @param objectId The sha-1 value of git link.
-   * @param repository The Repository instance.
-   * @throws IOException If an I/O exception occurs.
-   */
-  private void addGitLinkToIndex(final String fileName,
-      final AnyObjectId objectId, final Repository repository)
-      throws IOException {
-    addEntryToIndex(fileName, FileMode.GITLINK, objectId, repository);
-  }
-
-  /**
-   * It adds an entry to index.
-   *
-   * @param path The entry path.
-   * @param fileMode The entry file mode.
-   * @param objectId The ObjectId value of the entry.
-   * @param repository The repository instance.
-   * @throws IOException If an I/O exception occurs.
-   */
-  private void addEntryToIndex(final String path, final FileMode fileMode,
-      final AnyObjectId objectId, final Repository repository)
-      throws IOException {
-    final DirCacheEntry e = new DirCacheEntry(path);
-    e.setFileMode(fileMode);
-    e.setObjectId(objectId);
-
-    final DirCacheBuilder dirCacheBuilder = repository.lockDirCache().builder();
-    dirCacheBuilder.add(e);
-    dirCacheBuilder.commit();
-  }
-
-  private static StringBuilder buildSubmoduleSection(final String name,
-      final String path, final String url, final String branch) {
-    final StringBuilder sb = new StringBuilder();
-
-    sb.append("[submodule \"");
-    sb.append(name);
-    sb.append("\"]");
-    sb.append(newLine);
-
-    sb.append("\tpath = ");
-    sb.append(path);
-    sb.append(newLine);
-
-    sb.append("\turl = ");
-    sb.append(url);
-    sb.append(newLine);
-
-    sb.append("\tbranch = ");
-    sb.append(branch);
-    sb.append(newLine);
-
-    return sb;
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
index 9b3d5ed..d38da43 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
@@ -84,8 +85,8 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException {
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException {
     return new FakeIndex.Source(p);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index 2430815..63ae818 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -25,10 +25,9 @@
     super(
         new FakeQueryBuilder.Definition<>(
           FakeQueryBuilder.class),
-        new ChangeQueryBuilder.Arguments(null, null, null, null,
-          null, null, null, null, null, null, null, null, null,
-          null, null, null, null, indexes, null, null, null, null,
-          null, null));
+        new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
+            null, null, null, null, null, null, null, null, null, null, null,
+            null, indexes, null, null, null, null, null, null));
   }
 
   @Operator
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
deleted file mode 100644
index 042459b..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
+++ /dev/null
@@ -1,246 +0,0 @@
-// Copyright (C) 2013 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.server.index;
-
-import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
-import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
-import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
-import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
-import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
-import static com.google.gerrit.reviewdb.client.Change.Status.SUBMITTED;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.query.AndPredicate;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.AndSource;
-import com.google.gerrit.server.query.change.BasicChangeRewrites;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.OrSource;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.util.Set;
-
-public class IndexRewriteTest {
-  private FakeIndex index;
-  private IndexCollection indexes;
-  private ChangeQueryBuilder queryBuilder;
-  private IndexRewriteImpl rewrite;
-
-  @Before
-  public void setUp() throws Exception {
-    index = new FakeIndex(FakeIndex.V2);
-    indexes = new IndexCollection();
-    indexes.setSearchIndex(index);
-    queryBuilder = new FakeQueryBuilder(indexes);
-    rewrite = new IndexRewriteImpl(indexes, new BasicChangeRewrites());
-  }
-
-  @Test
-  public void testIndexPredicate() throws Exception {
-    Predicate<ChangeData> in = parse("file:a");
-    assertEquals(query(in), rewrite(in));
-  }
-
-  @Test
-  public void testNonIndexPredicate() throws Exception {
-    Predicate<ChangeData> in = parse("foo:a");
-    assertSame(in, rewrite(in));
-  }
-
-  @Test
-  public void testIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("file:a file:b");
-    assertEquals(query(in), rewrite(in));
-  }
-
-  @Test
-  public void testNonIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("foo:a OR foo:b");
-    assertEquals(in, rewrite(in));
-  }
-
-  @Test
-  public void testOneIndexPredicate() throws Exception {
-    Predicate<ChangeData> in = parse("foo:a file:b");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(
-        ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("-status:abandoned (status:open OR status:merged)");
-    assertEquals(
-        query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
-        rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT));
-  }
-
-  @Test
-  public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
-    Predicate<ChangeData> out = rewrite(in);
-    assertEquals(AndSource.class, out.getClass());
-    assertEquals(
-        ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testMultipleIndexPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("file:a OR foo:b OR file:c OR foo:d");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(OrSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.or(in.getChild(0), in.getChild(2))),
-          in.getChild(1), in.getChild(3)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testIndexAndNonIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("status:new bar:p file:a");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.and(in.getChild(0), in.getChild(2))),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("(status:new OR status:draft) bar:p file:a");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.and(in.getChild(0), in.getChild(2))),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("(status:new OR file:a) bar:p file:b");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.and(in.getChild(0), in.getChild(2))),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testLimitArgumentOverridesAllLimitPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
-    Predicate<ChangeData> out = rewrite(in, 5);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(in.getChild(1), 5),
-          parse("limit:5"),
-          parse("limit:5")),
-        out.getChildren());
-  }
-
-  @Test
-  public void testStartIncreasesLimit() throws Exception {
-    int n = 3;
-    Predicate<ChangeData> f = parse("file:a");
-    Predicate<ChangeData> l = parse("limit:" + n);
-    Predicate<ChangeData> in = and(f, l);
-    assertEquals(and(query(f, 3), parse("limit:3")), rewrite.rewrite(in, 0, n));
-    assertEquals(and(query(f, 4), parse("limit:4")), rewrite.rewrite(in, 1, n));
-    assertEquals(and(query(f, 5), parse("limit:5")), rewrite.rewrite(in, 2, n));
-  }
-
-  @Test
-  public void testGetPossibleStatus() throws Exception {
-    assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
-    assertEquals(EnumSet.of(NEW), status("is:new"));
-    assertEquals(EnumSet.of(SUBMITTED, DRAFT, MERGED, ABANDONED),
-        status("-is:new"));
-    assertEquals(EnumSet.of(NEW, MERGED), status("is:new OR is:merged"));
-
-    EnumSet<Change.Status> none = EnumSet.noneOf(Change.Status.class);
-    assertEquals(none, status("is:new is:merged"));
-    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
-    assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
-
-    assertEquals(EnumSet.of(MERGED, SUBMITTED),
-        status("(is:new is:draft) OR (is:merged OR is:submitted)"));
-  }
-
-  @Test
-  public void testUnsupportedIndexOperator() throws Exception {
-    Predicate<ChangeData> in = parse("status:merged file:a");
-    assertEquals(query(in), rewrite(in));
-
-    indexes.setSearchIndex(new FakeIndex(FakeIndex.V1));
-    Predicate<ChangeData> out = rewrite(in);
-    assertTrue(out instanceof AndPredicate);
-    assertEquals(ImmutableList.of(
-          query(in.getChild(0)),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  private Predicate<ChangeData> parse(String query) throws QueryParseException {
-    return queryBuilder.parse(query);
-  }
-
-  @SafeVarargs
-  private static AndSource and(Predicate<ChangeData>... preds) {
-    return new AndSource(Arrays.asList(preds));
-  }
-
-  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
-      throws QueryParseException {
-    return rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT);
-  }
-
-  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int limit)
-      throws QueryParseException {
-    return rewrite.rewrite(in, 0, limit);
-  }
-
-  private IndexedChangeQuery query(Predicate<ChangeData> p)
-      throws QueryParseException {
-    return query(p, DEFAULT_MAX_QUERY_LIMIT);
-  }
-
-  private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
-      throws QueryParseException {
-    return new IndexedChangeQuery(index, p, limit);
-  }
-
-  private Set<Change.Status> status(String query) throws QueryParseException {
-    return IndexRewriteImpl.getPossibleStatus(parse(query));
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
new file mode 100644
index 0000000..9ac83d5
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
@@ -0,0 +1,298 @@
+// Copyright (C) 2013 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.server.index;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
+import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
+import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
+import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
+import static com.google.gerrit.server.index.IndexedChangeQuery.convertOptions;
+import static com.google.gerrit.server.query.Predicate.and;
+import static com.google.gerrit.server.query.Predicate.or;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.QueryOptions;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Set;
+
+public class IndexRewriterTest {
+  private static final IndexConfig CONFIG = IndexConfig.createDefault();
+
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
+  private FakeIndex index;
+  private IndexCollection indexes;
+  private ChangeQueryBuilder queryBuilder;
+  private IndexRewriter rewrite;
+
+  @Before
+  public void setUp() throws Exception {
+    index = new FakeIndex(FakeIndex.V2);
+    indexes = new IndexCollection();
+    indexes.setSearchIndex(index);
+    queryBuilder = new FakeQueryBuilder(indexes);
+    rewrite = new IndexRewriter(indexes,
+        IndexConfig.create(0, 0, 3, 100));
+  }
+
+  @Test
+  public void testIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("file:a");
+    assertThat(rewrite(in)).isEqualTo(query(in));
+  }
+
+  @Test
+  public void testNonIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a");
+    assertThat(in).isSameAs(rewrite(in));
+  }
+
+  @Test
+  public void testIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("file:a file:b");
+    assertThat(rewrite(in)).isEqualTo(query(in));
+  }
+
+  @Test
+  public void testNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a OR foo:b");
+    assertThat(in).isEqualTo(rewrite(in));
+  }
+
+  @Test
+  public void testOneIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(AndSource.class).isSameAs(out.getClass());
+    assertThat(out.getChildren())
+        .containsExactly(
+            query(in.getChild(1)),
+            in.getChild(0))
+        .inOrder();
+  }
+
+  @Test
+  public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("-status:abandoned (file:a OR file:b)");
+    assertThat(rewrite.rewrite(in, options(0, DEFAULT_MAX_QUERY_LIMIT)))
+        .isEqualTo(query(in));
+  }
+
+  @Test
+  public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isSameAs(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(in.getChild(1)),
+          in.getChild(0))
+        .inOrder();
+  }
+
+  @Test
+  public void testMultipleIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("file:a OR foo:b OR file:c OR foo:d");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isSameAs(OrSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(or(in.getChild(0), in.getChild(2))),
+          in.getChild(1),
+          in.getChild(3))
+        .inOrder();
+  }
+
+  @Test
+  public void testIndexAndNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("status:new bar:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(AndSource.class).isSameAs(out.getClass());
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(and(in.getChild(0), in.getChild(2))),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR status:draft) bar:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isEqualTo(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(and(in.getChild(0), in.getChild(2))),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR file:a) bar:p file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isEqualTo(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(and(in.getChild(0), in.getChild(2))),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testOptionsArgumentOverridesAllLimitPredicates()
+      throws Exception {
+    Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
+    Predicate<ChangeData> out = rewrite(in, options(0, 5));
+    assertThat(out.getClass()).isEqualTo(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(in.getChild(1), 5),
+          parse("limit:5"),
+          parse("limit:5"))
+        .inOrder();
+  }
+
+  @Test
+  public void testStartIncreasesLimitInQueryButNotPredicate() throws Exception {
+    int n = 3;
+    Predicate<ChangeData> f = parse("file:a");
+    Predicate<ChangeData> l = parse("limit:" + n);
+    Predicate<ChangeData> in = andSource(f, l);
+    assertThat(rewrite.rewrite(in, options(0, n)))
+        .isEqualTo(andSource(query(f, 3), l));
+    assertThat(rewrite.rewrite(in, options(1, n)))
+        .isEqualTo(andSource(query(f, 4), l));
+    assertThat(rewrite.rewrite(in, options(2, n)))
+        .isEqualTo(andSource(query(f, 5), l));
+  }
+
+  @Test
+  public void testGetPossibleStatus() throws Exception {
+    assertThat(status("file:a")).isEqualTo(EnumSet.allOf(Change.Status.class));
+    assertThat(status("is:new")).containsExactly(NEW);
+    assertThat(status("-is:new"))
+        .containsExactly(DRAFT, MERGED, ABANDONED);
+    assertThat(status("is:new OR is:merged")).containsExactly(NEW, MERGED);
+
+    assertThat(status("is:new is:merged")).isEmpty();
+    assertThat(status("(is:new is:draft) (is:merged)")).isEmpty();
+    assertThat(status("(is:new is:draft) (is:merged)")).isEmpty();
+
+    assertThat(status("(is:new is:draft) OR (is:merged)"))
+        .containsExactly(MERGED);
+  }
+
+  @Test
+  public void testUnsupportedIndexOperator() throws Exception {
+    Predicate<ChangeData> in = parse("status:merged file:a");
+    assertThat(rewrite(in)).isEqualTo(query(in));
+
+    indexes.setSearchIndex(new FakeIndex(FakeIndex.V1));
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out).isInstanceOf(AndPredicate.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(in.getChild(0)),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testTooManyTerms() throws Exception {
+    String q = "file:a OR file:b OR file:c";
+    Predicate<ChangeData> in = parse(q);
+    assertEquals(query(in), rewrite(in));
+
+    exception.expect(QueryParseException.class);
+    exception.expectMessage("too many terms in query");
+    rewrite(parse(q + " OR file:d"));
+  }
+
+  @Test
+  public void testConvertOptions() throws Exception {
+    assertEquals(options(0, 3), convertOptions(options(0, 3)));
+    assertEquals(options(0, 4), convertOptions(options(1, 3)));
+    assertEquals(options(0, 5), convertOptions(options(2, 3)));
+  }
+
+  @Test
+  public void testAddingStartToLimitDoesNotExceedBackendLimit() throws Exception {
+    int max = CONFIG.maxLimit();
+    assertEquals(options(0, max), convertOptions(options(0, max)));
+    assertEquals(options(0, max), convertOptions(options(1, max)));
+    assertEquals(options(0, max), convertOptions(options(1, max - 1)));
+    assertEquals(options(0, max), convertOptions(options(2, max - 1)));
+  }
+
+  private Predicate<ChangeData> parse(String query) throws QueryParseException {
+    return queryBuilder.parse(query);
+  }
+
+  @SafeVarargs
+  private static AndSource andSource(Predicate<ChangeData>... preds) {
+    return new AndSource(Arrays.asList(preds));
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+      throws QueryParseException {
+    return rewrite.rewrite(in, options(0, DEFAULT_MAX_QUERY_LIMIT));
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in,
+      QueryOptions opts) throws QueryParseException {
+    return rewrite.rewrite(in, opts);
+  }
+
+  private IndexedChangeQuery query(Predicate<ChangeData> p)
+      throws QueryParseException {
+    return query(p, DEFAULT_MAX_QUERY_LIMIT);
+  }
+
+  private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    return new IndexedChangeQuery(index, p, options(0, limit));
+  }
+
+  private static QueryOptions options(int start, int limit) {
+    return QueryOptions.create(CONFIG, start, limit);
+  }
+
+  private Set<Change.Status> status(String query) throws QueryParseException {
+    return IndexRewriter.getPossibleStatus(parse(query));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
index 02d0582..fa4ac0e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -25,7 +25,7 @@
    * Holds an in-memory {@link java.io.PrintWriter} object and allows
    * comparisons of its contents to a supplied string via an assert statement.
    */
-  class PrintWriterComparator {
+  static class PrintWriterComparator {
     private PrintWriter printWriter;
     private StringWriter stringWriter;
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
index c77e0f6..145042c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -14,62 +14,70 @@
 
 package com.google.gerrit.server.mail;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
+import static com.google.common.truth.Truth.assertThat;
 
+import org.junit.Rule;
 import org.junit.Test;
-
-import java.io.UnsupportedEncodingException;
+import org.junit.rules.ExpectedException;
 
 public class AddressTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Test
   public void testParse_NameEmail1() {
     final Address a = Address.parse("A U Thor <author@example.com>");
-    assertEquals("A U Thor", a.name);
-    assertEquals("author@example.com", a.email);
+    assertThat(a.name).isEqualTo("A U Thor");
+    assertThat(a.email).isEqualTo("author@example.com");
   }
 
   @Test
   public void testParse_NameEmail2() {
     final Address a = Address.parse("A <a@b>");
-    assertEquals("A", a.name);
-    assertEquals("a@b", a.email);
+    assertThat(a.name).isEqualTo("A");
+    assertThat(a.email).isEqualTo("a@b");
   }
 
   @Test
   public void testParse_NameEmail3() {
     final Address a = Address.parse("<a@b>");
-    assertNull(a.name);
-    assertEquals("a@b", a.email);
+    assertThat(a.name).isNull();
+    assertThat(a.email).isEqualTo("a@b");
   }
 
   @Test
   public void testParse_NameEmail4() {
     final Address a = Address.parse("A U Thor<author@example.com>");
-    assertEquals("A U Thor", a.name);
-    assertEquals("author@example.com", a.email);
+    assertThat(a.name).isEqualTo("A U Thor");
+    assertThat(a.email).isEqualTo("author@example.com");
   }
 
   @Test
   public void testParse_NameEmail5() {
     final Address a = Address.parse("A U Thor  <author@example.com>");
-    assertEquals("A U Thor", a.name);
-    assertEquals("author@example.com", a.email);
+    assertThat(a.name).isEqualTo("A U Thor");
+    assertThat(a.email).isEqualTo("author@example.com");
   }
 
   @Test
   public void testParse_Email1() {
     final Address a = Address.parse("author@example.com");
-    assertNull(a.name);
-    assertEquals("author@example.com", a.email);
+    assertThat(a.name).isNull();
+    assertThat(a.email).isEqualTo("author@example.com");
   }
 
   @Test
   public void testParse_Email2() {
     final Address a = Address.parse("a@b");
-    assertNull(a.name);
-    assertEquals("a@b", a.email);
+    assertThat(a.name).isNull();
+    assertThat(a.email).isEqualTo("a@b");
+  }
+
+  @Test
+  public void testParse_NewTLD() {
+    Address a = Address.parse("A U Thor <author@example.systems>");
+    assertThat(a.name).isEqualTo("A U Thor");
+    assertThat(a.email).isEqualTo("author@example.systems");
   }
 
   @Test
@@ -91,66 +99,60 @@
     assertInvalid("a <@a>");
   }
 
-  private static void assertInvalid(final String in) {
-    try {
-      Address.parse(in);
-      fail("Incorrectly accepted " + in);
-    } catch (IllegalArgumentException e) {
-      assertEquals("Invalid email address: " + in, e.getMessage());
-    }
+  private void assertInvalid(final String in) {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage("Invalid email address: " + in);
+    Address.parse(in);
   }
 
   @Test
   public void testToHeaderString_NameEmail1() {
-    assertEquals("A <a@a>", format("A", "a@a"));
+    assertThat(format("A", "a@a")).isEqualTo("A <a@a>");
   }
 
   @Test
   public void testToHeaderString_NameEmail2() {
-    assertEquals("A B <a@a>", format("A B", "a@a"));
+    assertThat(format("A B", "a@a")).isEqualTo("A B <a@a>");
   }
 
   @Test
   public void testToHeaderString_NameEmail3() {
-    assertEquals("\"A B. C\" <a@a>", format("A B. C", "a@a"));
+    assertThat(format("A B. C", "a@a")).isEqualTo("\"A B. C\" <a@a>");
   }
 
   @Test
   public void testToHeaderString_NameEmail4() {
-    assertEquals("\"A B, C\" <a@a>", format("A B, C", "a@a"));
+    assertThat(format("A B, C", "a@a")).isEqualTo("\"A B, C\" <a@a>");
   }
 
   @Test
   public void testToHeaderString_NameEmail5() {
-    assertEquals("\"A \\\" C\" <a@a>", format("A \" C", "a@a"));
+    assertThat(format("A \" C", "a@a")).isEqualTo("\"A \\\" C\" <a@a>");
   }
 
   @Test
   public void testToHeaderString_NameEmail6() {
-    assertEquals("=?UTF-8?Q?A_=E2=82=AC_B?= <a@a>", format("A \u20ac B", "a@a"));
+    assertThat(format("A \u20ac B", "a@a"))
+      .isEqualTo("=?UTF-8?Q?A_=E2=82=AC_B?= <a@a>");
   }
 
   @Test
   public void testToHeaderString_NameEmail7() {
-    assertEquals("=?UTF-8?Q?A_=E2=82=AC_B_=28Code_Review=29?= <a@a>",
-        format("A \u20ac B (Code Review)", "a@a"));
+    assertThat(format("A \u20ac B (Code Review)", "a@a"))
+      .isEqualTo("=?UTF-8?Q?A_=E2=82=AC_B_=28Code_Review=29?= <a@a>");
   }
 
   @Test
   public void testToHeaderString_Email1() {
-    assertEquals("a@a", format(null, "a@a"));
+    assertThat(format(null, "a@a")).isEqualTo("a@a");
   }
 
   @Test
   public void testToHeaderString_Email2() {
-    assertEquals("<a,b@a>", format(null, "a,b@a"));
+    assertThat(format(null, "a,b@a")).isEqualTo("<a,b@a>");
   }
 
   private static String format(final String name, final String email) {
-    try {
-      return new Address(name, email).toHeaderString();
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("Cannot encode address", e);
-    }
+    return new Address(name, email).toHeaderString();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index f33f720..b96b780 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -14,14 +14,12 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
@@ -60,19 +58,19 @@
 
   @Test
   public void testDefaultIsMIXED() {
-    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
   }
 
   @Test
   public void testSelectUSER() {
     setFrom("USER");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.UserGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class);
 
     setFrom("user");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.UserGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class);
 
     setFrom("uSeR");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.UserGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class);
   }
 
   @Test
@@ -85,9 +83,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals(name, r.name);
-    assertEquals(email, r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
     verify(accountCache);
   }
 
@@ -100,9 +98,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals(null, r.name);
-    assertEquals(email, r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isNull();
+    assertThat(r.email).isEqualTo(email);
     verify(accountCache);
   }
 
@@ -115,9 +113,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals(name, r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
@@ -126,22 +124,22 @@
     setFrom("USER");
     replay(accountCache);
     final Address r = create().from(null);
-    assertNotNull(r);
-    assertEquals(ident.getName(), r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(ident.getName());
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
   @Test
   public void testSelectSERVER() {
     setFrom("SERVER");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.ServerGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
 
     setFrom("server");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.ServerGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
 
     setFrom("sErVeR");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.ServerGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
   }
 
   @Test
@@ -154,9 +152,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals(ident.getName(), r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(ident.getName());
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
@@ -165,22 +163,22 @@
     setFrom("SERVER");
     replay(accountCache);
     final Address r = create().from(null);
-    assertNotNull(r);
-    assertEquals(ident.getName(), r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(ident.getName());
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
   @Test
   public void testSelectMIXED() {
     setFrom("MIXED");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
 
     setFrom("mixed");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
 
     setFrom("mIxEd");
-    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+    assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
   }
 
   @Test
@@ -193,9 +191,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals(name + " (Code Review)", r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
@@ -208,9 +206,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals("Anonymous Coward (Code Review)", r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo("Anonymous Coward (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
@@ -223,9 +221,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals(name + " (Code Review)", r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
@@ -234,9 +232,9 @@
     setFrom("MIXED");
     replay(accountCache);
     final Address r = create().from(null);
-    assertNotNull(r);
-    assertEquals(ident.getName(), r.name);
-    assertEquals(ident.getEmailAddress(), r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(ident.getName());
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
 
@@ -250,9 +248,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals("A " + name + " B", r.name);
-    assertEquals("my.server@email.address", r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo("A " + name + " B");
+    assertThat(r.email).isEqualTo("my.server@email.address");
     verify(accountCache);
   }
 
@@ -265,9 +263,9 @@
 
     replay(accountCache);
     final Address r = create().from(user);
-    assertNotNull(r);
-    assertEquals("A Anonymous Coward B", r.name);
-    assertEquals("my.server@email.address", r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo("A Anonymous Coward B");
+    assertThat(r.email).isEqualTo("my.server@email.address");
     verify(accountCache);
   }
 
@@ -277,9 +275,9 @@
 
     replay(accountCache);
     final Address r = create().from(null);
-    assertNotNull(r);
-    assertEquals(ident.getName(), r.name);
-    assertEquals("my.server@email.address", r.email);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(ident.getName());
+    assertThat(r.email).isEqualTo("my.server@email.address");
     verify(accountCache);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
new file mode 100644
index 0000000..05dc24b
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2016 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.server.mail;
+
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class ValidatorTest {
+  private static final String UNSUPPORTED_PREFIX = "#! ";
+
+  @Test
+  public void validateLocalDomain() throws Exception {
+    assertThat(OutgoingEmailValidator.isValid("foo@bar.local")).isTrue();
+  }
+
+  @Test
+  public void validateTopLevelDomains() throws Exception {
+    try (InputStream in =
+        this.getClass().getResourceAsStream("tlds-alpha-by-domain.txt")) {
+      if (in == null) {
+        throw new Exception("TLD list not found");
+      }
+      BufferedReader r = new BufferedReader(new InputStreamReader(in));
+      String tld;
+      while ((tld = r.readLine()) != null) {
+        if (tld.startsWith("# ") || tld.startsWith("XN--")) {
+          // Ignore comments and non-latin domains
+          continue;
+        }
+        if (tld.startsWith(UNSUPPORTED_PREFIX)) {
+          String test = "test@example."
+              + tld.toLowerCase().substring(UNSUPPORTED_PREFIX.length());
+          assert_()
+            .withFailureMessage("expected invalid TLD \"" + test + "\"")
+            .that(OutgoingEmailValidator.isValid(test))
+            .isFalse();
+        } else {
+          String test = "test@example." + tld.toLowerCase();
+          assert_()
+            .withFailureMessage("failed to validate TLD \"" + test + "\"")
+            .that(OutgoingEmailValidator.isValid(test))
+            .isTrue();
+        }
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 1308723..cbd8ff5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.inject.Scopes.SINGLETON;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.CommentRange;
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitModule;
@@ -51,6 +50,7 @@
 import com.google.gerrit.testutil.FakeAccountCache;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.gerrit.testutil.TestChanges;
+import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.StandardKeyEncoder;
@@ -62,15 +62,11 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.After;
 import org.junit.Before;
 
 import java.sql.Timestamp;
 import java.util.TimeZone;
-import java.util.concurrent.atomic.AtomicLong;
 
 public class AbstractChangeNotesTest {
   private static final TimeZone TZ =
@@ -91,7 +87,6 @@
 
   private Injector injector;
   private String systemTimeZone;
-  private volatile long clockStepMs;
 
   @Inject private AllUsersNameProvider allUsers;
 
@@ -151,21 +146,12 @@
 
   private void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    clockStepMs = MILLISECONDS.convert(1, SECONDS);
-    final AtomicLong clockMs = new AtomicLong(
-        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
-    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
-      @Override
-      public long getMillis() {
-        return clockMs.getAndAdd(clockStepMs);
-      }
-    });
+    TestTimeUtil.resetWithClockStep(1, SECONDS);
   }
 
   @After
   public void resetTime() {
-    DateTimeUtils.setCurrentMillisSystem();
+    TestTimeUtil.useSystemTime();
     System.setProperty("user.timezone", systemTimeZone);
   }
 
@@ -203,16 +189,16 @@
     return label;
   }
 
-  protected PatchLineComment newPublishedPatchLineComment(PatchSet.Id psId,
+  protected PatchLineComment newPublishedComment(PatchSet.Id psId,
       String filename, String UUID, CommentRange range, int line,
       IdentifiedUser commenter, String parentUUID, Timestamp t,
       String message, short side, String commitSHA1) {
-    return newPatchLineComment(psId, filename, UUID, range, line, commenter,
+    return newComment(psId, filename, UUID, range, line, commenter,
         parentUUID, t, message, side, commitSHA1,
         PatchLineComment.Status.PUBLISHED);
   }
 
-  protected PatchLineComment newPatchLineComment(PatchSet.Id psId,
+  protected PatchLineComment newComment(PatchSet.Id psId,
       String filename, String UUID, CommentRange range, int line,
       IdentifiedUser commenter, String parentUUID, Timestamp t,
       String message, short side, String commitSHA1,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index c41c4ec..bac72f0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import static org.junit.Assert.fail;
-
 import com.google.gerrit.common.TimeUtil;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -29,12 +27,17 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 public class ChangeNotesParserTest extends AbstractChangeNotesTest {
   private TestRepository<InMemoryRepository> testRepo;
   private RevWalk walk;
 
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Before
   public void setUpTestRepo() throws Exception {
     testRepo = new TestRepository<>(repo);
@@ -202,10 +205,8 @@
 
   private void assertParseFails(RevCommit commit) throws Exception {
     try (ChangeNotesParser parser = newParser(commit)) {
+      exception.expect(ConfigInvalidException.class);
       parser.parseAll();
-      fail("Expected parse to fail:\n" + commit.getFullMessage());
-    } catch (ConfigInvalidException e) {
-      // Expected.
     }
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index aea966a..bd51cee 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -14,23 +14,19 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.notedb.ReviewerState.CC;
 import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
 import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Multimap;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -41,8 +37,8 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -50,12 +46,12 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
@@ -71,22 +67,23 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(1, notes.getApprovals().keySet().size());
+    assertThat(notes.getApprovals().keySet())
+        .containsExactly(c.currentPatchSetId());
     List<PatchSetApproval> psas =
       notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
+    assertThat(psas).hasSize(2);
 
-    assertEquals(c.currentPatchSetId(), psas.get(0).getPatchSetId());
-    assertEquals(1, psas.get(0).getAccountId().get());
-    assertEquals("Code-Review", psas.get(0).getLabel());
-    assertEquals((short) -1, psas.get(0).getValue());
-    assertEquals(truncate(after(c, 1000)), psas.get(0).getGranted());
+    assertThat(psas.get(0).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(0).getAccountId().get()).isEqualTo(1);
+    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(0).getValue()).isEqualTo((short) -1);
+    assertThat(psas.get(0).getGranted()).isEqualTo(truncate(after(c, 1000)));
 
-    assertEquals(c.currentPatchSetId(), psas.get(1).getPatchSetId());
-    assertEquals(1, psas.get(1).getAccountId().get());
-    assertEquals("Verified", psas.get(1).getLabel());
-    assertEquals((short) 1, psas.get(1).getValue());
-    assertEquals(psas.get(0).getGranted(), psas.get(1).getGranted());
+    assertThat(psas.get(1).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(1).getAccountId().get()).isEqualTo(1);
+    assertThat(psas.get(1).getLabel()).isEqualTo("Verified");
+    assertThat(psas.get(1).getValue()).isEqualTo((short) 1);
+    assertThat(psas.get(1).getGranted()).isEqualTo(psas.get(0).getGranted());
   }
 
   @Test
@@ -105,21 +102,21 @@
 
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, PatchSetApproval> psas = notes.getApprovals();
-    assertEquals(2, notes.getApprovals().keySet().size());
+    assertThat(psas).hasSize(2);
 
     PatchSetApproval psa1 = Iterables.getOnlyElement(psas.get(ps1));
-    assertEquals(ps1, psa1.getPatchSetId());
-    assertEquals(1, psa1.getAccountId().get());
-    assertEquals("Code-Review", psa1.getLabel());
-    assertEquals((short) -1, psa1.getValue());
-    assertEquals(truncate(after(c, 1000)), psa1.getGranted());
+    assertThat(psa1.getPatchSetId()).isEqualTo(ps1);
+    assertThat(psa1.getAccountId().get()).isEqualTo(1);
+    assertThat(psa1.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa1.getValue()).isEqualTo((short) -1);
+    assertThat(psa1.getGranted()).isEqualTo(truncate(after(c, 1000)));
 
     PatchSetApproval psa2 = Iterables.getOnlyElement(psas.get(ps2));
-    assertEquals(ps2, psa2.getPatchSetId());
-    assertEquals(1, psa2.getAccountId().get());
-    assertEquals("Code-Review", psa2.getLabel());
-    assertEquals((short) +1, psa2.getValue());
-    assertEquals(truncate(after(c, 2000)), psa2.getGranted());
+    assertThat(psa2.getPatchSetId()).isEqualTo(ps2);
+    assertThat(psa2.getAccountId().get()).isEqualTo(1);
+    assertThat(psa2.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa2.getValue()).isEqualTo((short) +1);
+    assertThat(psa2.getGranted()).isEqualTo(truncate(after(c, 2000)));
   }
 
   @Test
@@ -132,8 +129,8 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa = Iterables.getOnlyElement(
         notes.getApprovals().get(c.currentPatchSetId()));
-    assertEquals("Code-Review", psa.getLabel());
-    assertEquals((short) -1, psa.getValue());
+    assertThat(psa.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa.getValue()).isEqualTo((short) -1);
 
     update = newUpdate(c, changeOwner);
     update.putApproval("Code-Review", (short) 1);
@@ -142,8 +139,8 @@
     notes = newNotes(c);
     psa = Iterables.getOnlyElement(
         notes.getApprovals().get(c.currentPatchSetId()));
-    assertEquals("Code-Review", psa.getLabel());
-    assertEquals((short) 1, psa.getValue());
+    assertThat(psa.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa.getValue()).isEqualTo((short) 1);
   }
 
   @Test
@@ -158,22 +155,23 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(1, notes.getApprovals().keySet().size());
+    assertThat(notes.getApprovals().keySet())
+        .containsExactly(c.currentPatchSetId());
     List<PatchSetApproval> psas =
       notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
+    assertThat(psas).hasSize(2);
 
-    assertEquals(c.currentPatchSetId(), psas.get(0).getPatchSetId());
-    assertEquals(1, psas.get(0).getAccountId().get());
-    assertEquals("Code-Review", psas.get(0).getLabel());
-    assertEquals((short) -1, psas.get(0).getValue());
-    assertEquals(truncate(after(c, 1000)), psas.get(0).getGranted());
+    assertThat(psas.get(0).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(0).getAccountId().get()).isEqualTo(1);
+    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(0).getValue()).isEqualTo((short) -1);
+    assertThat(psas.get(0).getGranted()).isEqualTo(truncate(after(c, 1000)));
 
-    assertEquals(c.currentPatchSetId(), psas.get(1).getPatchSetId());
-    assertEquals(2, psas.get(1).getAccountId().get());
-    assertEquals("Code-Review", psas.get(1).getLabel());
-    assertEquals((short) 1, psas.get(1).getValue());
-    assertEquals(truncate(after(c, 2000)), psas.get(1).getGranted());
+    assertThat(psas.get(1).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(1).getAccountId().get()).isEqualTo(2);
+    assertThat(psas.get(1).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(1).getValue()).isEqualTo((short) 1);
+    assertThat(psas.get(1).getGranted()).isEqualTo(truncate(after(c, 2000)));
   }
 
   @Test
@@ -186,16 +184,16 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa = Iterables.getOnlyElement(
         notes.getApprovals().get(c.currentPatchSetId()));
-    assertEquals(1, psa.getAccountId().get());
-    assertEquals("Not-For-Long", psa.getLabel());
-    assertEquals((short) 1, psa.getValue());
+    assertThat(psa.getAccountId().get()).isEqualTo(1);
+    assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
+    assertThat(psa.getValue()).isEqualTo((short) 1);
 
     update = newUpdate(c, changeOwner);
     update.removeApproval("Not-For-Long");
     update.commit();
 
     notes = newNotes(c);
-    assertTrue(notes.getApprovals().isEmpty());
+    assertThat(notes.getApprovals()).isEmpty();
   }
 
   @Test
@@ -207,10 +205,10 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(
           REVIEWER, new Account.Id(1),
-          REVIEWER, new Account.Id(2)),
-        notes.getReviewers());
+          REVIEWER, new Account.Id(2)));
   }
 
   @Test
@@ -222,10 +220,10 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
-          REVIEWER, new Account.Id(1),
-          CC, new Account.Id(2)),
-        notes.getReviewers());
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(
+            REVIEWER, new Account.Id(1),
+            CC, new Account.Id(2)));
   }
 
   @Test
@@ -236,18 +234,16 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
-          REVIEWER, new Account.Id(2)),
-        notes.getReviewers());
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(REVIEWER, new Account.Id(2)));
 
     update = newUpdate(c, otherUser);
     update.putReviewer(otherUser.getAccount().getId(), CC);
     update.commit();
 
     notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
-          CC, new Account.Id(2)),
-        notes.getReviewers());
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(CC, new Account.Id(2)));
   }
 
   @Test
@@ -268,9 +264,11 @@
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> psas =
         notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
-    assertEquals(changeOwner.getAccount().getId(), psas.get(0).getAccountId());
-    assertEquals(otherUser.getAccount().getId(), psas.get(1).getAccountId());
+    assertThat(psas).hasSize(2);
+    assertThat(psas.get(0).getAccountId())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(psas.get(1).getAccountId())
+        .isEqualTo(otherUser.getAccount().getId());
 
     update = newUpdate(c, changeOwner);
     update.removeReviewer(otherUser.getAccount().getId());
@@ -278,8 +276,9 @@
 
     notes = newNotes(c);
     psas = notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(1, psas.size());
-    assertEquals(changeOwner.getAccount().getId(), psas.get(0).getAccountId());
+    assertThat(psas).hasSize(1);
+    assertThat(psas.get(0).getAccountId())
+        .isEqualTo(changeOwner.getAccount().getId());
   }
 
   @Test
@@ -288,7 +287,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
 
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Code-Review", "NEED", null)),
@@ -299,13 +298,15 @@
 
     ChangeNotes notes = newNotes(c);
     List<SubmitRecord> recs = notes.getSubmitRecords();
-    assertEquals(2, recs.size());
-    assertEquals(submitRecord("NOT_READY", null,
-        submitLabel("Verified", "OK", changeOwner.getAccountId()),
-        submitLabel("Code-Review", "NEED", null)), recs.get(0));
-    assertEquals(submitRecord("NOT_READY", null,
-        submitLabel("Verified", "OK", changeOwner.getAccountId()),
-        submitLabel("Alternative-Code-Review", "NEED", null)), recs.get(1));
+    assertThat(recs).hasSize(2);
+    assertThat(recs.get(0)).isEqualTo(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Code-Review", "NEED", null)));
+    assertThat(recs.get(1)).isEqualTo(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Alternative-Code-Review", "NEED", null)));
   }
 
   @Test
@@ -313,7 +314,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", otherUser.getAccountId()))));
     update.commit();
@@ -321,22 +322,22 @@
     incrementPatchSet(c);
     update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 2");
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("OK", null,
           submitLabel("Code-Review", "OK", changeOwner.getAccountId()))));
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(submitRecord("OK", null,
-          submitLabel("Code-Review", "OK", changeOwner.getAccountId())),
-        Iterables.getOnlyElement(notes.getSubmitRecords()));
+    assertThat(notes.getSubmitRecords()).containsExactly(
+        submitRecord("OK", null,
+          submitLabel("Code-Review", "OK", changeOwner.getAccountId())));
   }
 
   @Test
   public void emptyChangeUpdate() throws Exception {
     ChangeUpdate update = newUpdate(newChange(), changeOwner);
     update.commit();
-    assertNull(update.getRevision());
+    assertThat(update.getRevision()).isNull();
   }
 
   @Test
@@ -351,7 +352,7 @@
     try (RevWalk walk = new RevWalk(repo)) {
       RevCommit commit = walk.parseCommit(update.getRevision());
       walk.parseBody(commit);
-      assertTrue(commit.getFullMessage().endsWith("Hashtags: tag1,tag2\n"));
+      assertThat(commit.getFullMessage()).endsWith("Hashtags: tag1,tag2\n");
     }
   }
 
@@ -366,7 +367,7 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(hashtags, notes.getHashtags());
+    assertThat(notes.getHashtags()).isEqualTo(hashtags);
   }
 
   @Test
@@ -374,7 +375,7 @@
     ChangeUpdate update = newUpdate(newChange(), changeOwner);
     update.setSubject("Create change");
     update.commit();
-    assertNotNull(update.getRevision());
+    assertThat(update.getRevision()).isNotNull();
   }
 
   @Test
@@ -398,15 +399,17 @@
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> psas =
         notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
+    assertThat(psas).hasSize(2);
 
-    assertEquals(changeOwner.getAccount().getId(), psas.get(0).getAccountId());
-    assertEquals("Verified", psas.get(0).getLabel());
-    assertEquals((short) 1, psas.get(0).getValue());
+    assertThat(psas.get(0).getAccountId())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(psas.get(0).getLabel()).isEqualTo("Verified");
+    assertThat(psas.get(0).getValue()).isEqualTo((short) 1);
 
-    assertEquals(otherUser.getAccount().getId(), psas.get(1).getAccountId());
-    assertEquals("Code-Review", psas.get(1).getLabel());
-    assertEquals((short) 2, psas.get(1).getValue());
+    assertThat(psas.get(1).getAccountId())
+        .isEqualTo(otherUser.getAccount().getId());
+    assertThat(psas.get(1).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(1).getValue()).isEqualTo((short) 2);
   }
 
   @Test
@@ -420,7 +423,7 @@
     PatchSet.Id psId = c.currentPatchSetId();
     BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
     BatchMetaDataUpdate batch = update1.openUpdateInBatch(bru);
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment1 = newPublishedComment(psId, "file1",
         uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update1.setPatchSetId(psId);
@@ -437,27 +440,27 @@
       ChangeNotes notes = newNotes(c);
       ObjectId tip = notes.getRevision();
       RevCommit commitWithApprovals = rw.parseCommit(tip);
-      assertNotNull(commitWithApprovals);
+      assertThat(commitWithApprovals).isNotNull();
       RevCommit commitWithComments = commitWithApprovals.getParent(0);
-      assertNotNull(commitWithComments);
+      assertThat(commitWithComments).isNotNull();
 
-      ChangeNotesParser notesWithComments =
-          new ChangeNotesParser(c, commitWithComments.copy(), rw, repoManager);
-      notesWithComments.parseAll();
-      ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
-          notesWithComments.buildApprovals();
-      assertEquals(0, approvals1.size());
-      assertEquals(1, notesWithComments.commentsForBase.size());
-      notesWithComments.close();
+      try (ChangeNotesParser notesWithComments =
+          new ChangeNotesParser(c, commitWithComments.copy(), rw, repoManager)) {
+        notesWithComments.parseAll();
+        ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
+            notesWithComments.buildApprovals();
+        assertThat(approvals1).isEmpty();
+        assertThat(notesWithComments.comments).hasSize(1);
+      }
 
-      ChangeNotesParser notesWithApprovals =
-          new ChangeNotesParser(c, commitWithApprovals.copy(), rw, repoManager);
-      notesWithApprovals.parseAll();
-      ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
-          notesWithApprovals.buildApprovals();
-      assertEquals(1, approvals2.size());
-      assertEquals(1, notesWithApprovals.commentsForBase.size());
-      notesWithApprovals.close();
+      try (ChangeNotesParser notesWithApprovals =
+          new ChangeNotesParser(c, commitWithApprovals.copy(), rw, repoManager)) {
+        notesWithApprovals.parseAll();
+        ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
+            notesWithApprovals.buildApprovals();
+        assertThat(approvals2).hasSize(1);
+        assertThat(notesWithApprovals.comments).hasSize(1);
+      }
     } finally {
       batch.close();
     }
@@ -481,12 +484,12 @@
       batch1 = update1.openUpdateInBatch(bru);
       batch1.write(update1, new CommitBuilder());
       batch1.commit();
-      assertNull(repo.getRef(update1.getRefName()));
+      assertThat(repo.exactRef(update1.getRefName())).isNull();
 
       batch2 = update2.openUpdateInBatch(bru);
       batch2.write(update2, new CommitBuilder());
       batch2.commit();
-      assertNull(repo.getRef(update2.getRefName()));
+      assertThat(repo.exactRef(update2.getRefName())).isNull();
     } finally {
       if (batch1 != null) {
         batch1.close();
@@ -497,19 +500,19 @@
     }
 
     List<ReceiveCommand> cmds = bru.getCommands();
-    assertEquals(2, cmds.size());
-    assertEquals(update1.getRefName(), cmds.get(0).getRefName());
-    assertEquals(update2.getRefName(), cmds.get(1).getRefName());
+    assertThat(cmds).hasSize(2);
+    assertThat(cmds.get(0).getRefName()).isEqualTo(update1.getRefName());
+    assertThat(cmds.get(1).getRefName()).isEqualTo(update2.getRefName());
 
     try (RevWalk rw = new RevWalk(repo)) {
       bru.execute(rw, NullProgressMonitor.INSTANCE);
     }
 
-    assertEquals(ReceiveCommand.Result.OK, cmds.get(0).getResult());
-    assertEquals(ReceiveCommand.Result.OK, cmds.get(1).getResult());
+    assertThat(cmds.get(0).getResult()).isEqualTo(ReceiveCommand.Result.OK);
+    assertThat(cmds.get(1).getResult()).isEqualTo(ReceiveCommand.Result.OK);
 
-    assertNotNull(repo.getRef(update1.getRefName()));
-    assertNotNull(repo.getRef(update2.getRefName()));
+    assertThat(repo.exactRef(update1.getRefName())).isNotNull();
+    assertThat(repo.exactRef(update2.getRefName())).isNotNull();
   }
 
   @Test
@@ -524,14 +527,12 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages.keySet()).containsExactly(ps1);
 
     ChangeMessage cm = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Just a little code change.\n",
-        cm.getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm.getAuthor());
-    assertEquals(ps1, cm.getPatchSetId());
+    assertThat(cm.getMessage()).isEqualTo("Just a little code change.\n");
+    assertThat(cm.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.getPatchSetId()).isEqualTo(ps1);
   }
 
   @Test
@@ -542,9 +543,7 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
-        notes.getChangeMessages();
-    assertEquals(0, changeMessages.keySet().size());
+    assertThat(notes.getChangeMessages()).isEmpty();
   }
 
   @Test
@@ -559,11 +558,11 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages).hasSize(1);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Testing trailing double newline\n" + "\n", cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+    assertThat(cm1.getMessage()).isEqualTo("Testing trailing double newline\n" + "\n");
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
   }
 
   @Test
@@ -581,15 +580,15 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages).hasSize(1);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Testing paragraph 1\n"
+    assertThat(cm1.getMessage()).isEqualTo("Testing paragraph 1\n"
         + "\n"
         + "Testing paragraph 2\n"
         + "\n"
-        + "Testing paragraph 3", cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+        + "Testing paragraph 3");
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
   }
 
   @Test
@@ -611,20 +610,19 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(2, changeMessages.keySet().size());
+    assertThat(changeMessages).hasSize(2);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("This is the change message for the first PS.",
-        cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm1.getAuthor());
+    assertThat(cm1.getMessage())
+        .isEqualTo("This is the change message for the first PS.");
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
 
     ChangeMessage cm2 = Iterables.getOnlyElement(changeMessages.get(ps2));
-    assertEquals(ps1, cm1.getPatchSetId());
-    assertEquals("This is the change message for the second PS.",
-        cm2.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm2.getAuthor());
-    assertEquals(ps2, cm2.getPatchSetId());
+    assertThat(cm1.getPatchSetId()).isEqualTo(ps1);
+    assertThat(cm2.getMessage())
+        .isEqualTo("This is the change message for the second PS.");
+    assertThat(cm2.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm2.getPatchSetId()).isEqualTo(ps2);
   }
 
   @Test
@@ -645,20 +643,18 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages.keySet()).hasSize(1);
 
     List<ChangeMessage> cm = changeMessages.get(ps1);
-    assertEquals(2, cm.size());
-    assertEquals("First change message.\n",
-        cm.get(0).getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm.get(0).getAuthor());
-    assertEquals(ps1, cm.get(0).getPatchSetId());
-    assertEquals("Second change message.\n",
-        cm.get(1).getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm.get(1).getAuthor());
-    assertEquals(ps1, cm.get(1).getPatchSetId());
+    assertThat(cm).hasSize(2);
+    assertThat(cm.get(0).getMessage()).isEqualTo("First change message.\n");
+    assertThat(cm.get(0).getAuthor())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.get(0).getPatchSetId()).isEqualTo(ps1);
+    assertThat(cm.get(1).getMessage()).isEqualTo("Second change message.\n");
+    assertThat(cm.get(1).getAuthor())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.get(1).getPatchSetId()).isEqualTo(ps1);
   }
 
   @Test
@@ -677,7 +673,7 @@
     Timestamp time3 = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment1 = newPublishedComment(psId, "file1",
         uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -686,7 +682,7 @@
 
     update = newUpdate(c, otherUser);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment2 = newPublishedComment(psId, "file1",
         uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -695,7 +691,7 @@
 
     update = newUpdate(c, otherUser);
     CommentRange range3 = new CommentRange(3, 1, 4, 1);
-    PatchLineComment comment3 = newPublishedPatchLineComment(psId, "file2",
+    PatchLineComment comment3 = newPublishedComment(psId, "file2",
         uuid3, range3, range3.getEndLine(), otherUser, null, time3, message3,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -713,7 +709,7 @@
           walk.getObjectReader().open(
               note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      assertEquals("Patch-set: 1\n"
+      assertThat(noteString).isEqualTo("Patch-set: 1\n"
           + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
           + "File: file1\n"
           + "\n"
@@ -739,8 +735,7 @@
           + "UUID: uuid3\n"
           + "Bytes: 9\n"
           + "comment 3\n"
-          + "\n",
-          noteString);
+          + "\n");
     }
   }
 
@@ -757,7 +752,7 @@
     Timestamp time2 = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment1 = newPublishedComment(psId, "file1",
         uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -766,7 +761,7 @@
 
     update = newUpdate(c, otherUser);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment2 = newPublishedComment(psId, "file1",
         uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -784,7 +779,7 @@
           walk.getObjectReader().open(
               note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      assertEquals("Base-for-patch-set: 1\n"
+      assertThat(noteString).isEqualTo("Base-for-patch-set: 1\n"
           + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
           + "File: file1\n"
           + "\n"
@@ -801,8 +796,7 @@
           + "UUID: uuid2\n"
           + "Bytes: 9\n"
           + "comment 2\n"
-          + "\n",
-          noteString);
+          + "\n");
     }
   }
 
@@ -813,6 +807,8 @@
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     String messageForBase = "comment for base";
     String messageForPS = "comment for ps";
     CommentRange range = new CommentRange(1, 1, 2, 1);
@@ -820,34 +816,26 @@
     PatchSet.Id psId = c.currentPatchSetId();
 
     PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid1,
+        newPublishedComment(psId, "filename", uuid1,
         range, range.getEndLine(), otherUser, null, now, messageForBase,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        (short) 0, rev1);
     update.setPatchSetId(psId);
     update.upsertComment(commentForBase);
     update.commit();
 
     update = newUpdate(c, otherUser);
     PatchLineComment commentForPS =
-        newPublishedPatchLineComment(psId, "filename", uuid2,
+        newPublishedComment(psId, "filename", uuid2,
         range, range.getEndLine(), otherUser, null, now, messageForPS,
-        (short) 1, "abcd4567abcd4567abcd4567abcd4567abcd4567");
+        (short) 1, rev2);
     update.setPatchSetId(psId);
     update.upsertComment(commentForPS);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        notes.getPatchSetComments();
-    assertEquals(commentsForBase.size(), 1);
-    assertEquals(commentsForPS.size(), 1);
-
-    assertEquals(commentForBase,
-        Iterables.getOnlyElement(commentsForBase.get(psId)));
-    assertEquals(commentForPS,
-        Iterables.getOnlyElement(commentsForPS.get(psId)));
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+            new RevId(rev1), commentForBase,
+            new RevId(rev2), commentForPS));
   }
 
   @Test
@@ -855,6 +843,7 @@
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename = "filename";
@@ -863,37 +852,25 @@
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp timeForComment1 = TimeUtil.nowTs();
     Timestamp timeForComment2 = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename,
+    PatchLineComment comment1 = newPublishedComment(psId, filename,
         uuid1, range, range.getEndLine(), otherUser, null, timeForComment1,
-        "comment 1", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        "comment 1", side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename,
+    PatchLineComment comment2 = newPublishedComment(psId, filename,
         uuid2, range, range.getEndLine(), otherUser, null, timeForComment2,
-        "comment 2", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        "comment 2", side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment2);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        notes.getPatchSetComments();
-    assertEquals(commentsForBase.size(), 0);
-    assertEquals(commentsForPS.size(), 2);
-
-    ImmutableList<PatchLineComment> commentsForThisPS =
-        (ImmutableList<PatchLineComment>) commentsForPS.get(psId);
-    assertEquals(commentsForThisPS.size(), 2);
-    PatchLineComment commentFromNotes1 = commentsForThisPS.get(0);
-    PatchLineComment commentFromNotes2 = commentsForThisPS.get(1);
-
-    assertEquals(comment1, commentFromNotes1);
-    assertEquals(comment2, commentFromNotes2);
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev), comment1,
+          new RevId(rev), comment2)).inOrder();
   }
 
   @Test
@@ -901,6 +878,7 @@
       throws Exception {
     Change c = newChange();
     String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename1 = "filename1";
@@ -909,43 +887,33 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp now = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename1,
+    PatchLineComment comment1 = newPublishedComment(psId, filename1,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment 1",
-        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename2,
+    PatchLineComment comment2 = newPublishedComment(psId, filename2,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment 2",
-        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment2);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        notes.getPatchSetComments();
-    assertEquals(commentsForBase.size(), 0);
-    assertEquals(commentsForPS.size(), 2);
-
-    ImmutableList<PatchLineComment> commentsForThisPS =
-        (ImmutableList<PatchLineComment>) commentsForPS.get(psId);
-    assertEquals(commentsForThisPS.size(), 2);
-    PatchLineComment commentFromNotes1 = commentsForThisPS.get(0);
-    PatchLineComment commentFromNotes2 = commentsForThisPS.get(1);
-
-    assertEquals(comment1, commentFromNotes1);
-    assertEquals(comment2, commentFromNotes2);
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev), comment1,
+          new RevId(rev), comment2)).inOrder();
   }
 
   @Test
   public void patchLineCommentMultiplePatchsets() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -953,9 +921,9 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp now = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPublishedPatchLineComment(ps1, filename,
+    PatchLineComment comment1 = newPublishedComment(ps1, filename,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
-        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        side, rev1);
     update.setPatchSetId(ps1);
     update.upsertComment(comment1);
     update.commit();
@@ -965,37 +933,24 @@
 
     update = newUpdate(c, otherUser);
     now = TimeUtil.nowTs();
-    PatchLineComment comment2 = newPublishedPatchLineComment(ps2, filename,
+    PatchLineComment comment2 = newPublishedComment(ps2, filename,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
-        side, "abcd4567abcd4567abcd4567abcd4567abcd4567");
+        side, rev2);
     update.setPatchSetId(ps2);
     update.upsertComment(comment2);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    LinkedListMultimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        LinkedListMultimap.create(notes.getBaseComments());
-    LinkedListMultimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        LinkedListMultimap.create(notes.getPatchSetComments());
-    assertEquals(commentsForBase.keys().size(), 0);
-    assertEquals(commentsForPS.values().size(), 2);
-
-    List<PatchLineComment> commentsForPS1 = commentsForPS.get(ps1);
-    assertEquals(commentsForPS1.size(), 1);
-    PatchLineComment commentFromPs1 = commentsForPS1.get(0);
-
-    List<PatchLineComment> commentsForPS2 = commentsForPS.get(ps2);
-    assertEquals(commentsForPS2.size(), 1);
-    PatchLineComment commentFromPs2 = commentsForPS2.get(0);
-
-    assertEquals(comment1, commentFromPs1);
-    assertEquals(comment2, commentFromPs2);
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev1), comment1,
+          new RevId(rev2), comment2));
   }
 
   @Test
   public void patchLineCommentSingleDraftToPublished() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
+    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -1003,16 +958,17 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp now = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPatchLineComment(ps1, filename, uuid,
-        range, range.getEndLine(), otherUser, null, now, "comment on ps1", side,
-        "abcd4567abcd4567abcd4567abcd4567abcd4567", Status.DRAFT);
+    PatchLineComment comment1 = newComment(ps1, filename, uuid, range,
+        range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        rev, Status.DRAFT);
     update.setPatchSetId(ps1);
     update.insertComment(comment1);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(1, notes.getDraftPsComments(otherUserId).values().size());
-    assertEquals(0, notes.getDraftBaseComments(otherUserId).values().size());
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment1));
+    assertThat(notes.getComments()).isEmpty();
 
     comment1.setStatus(Status.PUBLISHED);
     update = newUpdate(c, otherUser);
@@ -1021,49 +977,44 @@
     update.commit();
 
     notes = newNotes(c);
-
-    assertTrue(notes.getDraftPsComments(otherUserId).values().isEmpty());
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
-
-    assertTrue(notes.getBaseComments().values().isEmpty());
-    PatchLineComment commentFromNotes =
-        Iterables.getOnlyElement(notes.getPatchSetComments().values());
-    assertEquals(comment1, commentFromNotes);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment1));
   }
 
   @Test
   public void patchLineCommentMultipleDraftsSameSidePublishOne()
-      throws OrmException, IOException {
+      throws Exception {
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
     short side = (short) 1;
     Timestamp now = TimeUtil.nowTs();
-    String commitSHA1 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     PatchSet.Id psId = c.currentPatchSetId();
 
     // Write two drafts on the same side of one patch set.
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
-    PatchLineComment comment1 = newPatchLineComment(psId, filename, uuid1,
+    PatchLineComment comment1 = newComment(psId, filename, uuid1,
         range1, range1.getEndLine(), otherUser, null, now, "comment on ps1",
-        side, commitSHA1, Status.DRAFT);
-    PatchLineComment comment2 = newPatchLineComment(psId, filename, uuid2,
+        side, rev, Status.DRAFT);
+    PatchLineComment comment2 = newComment(psId, filename, uuid2,
         range2, range2.getEndLine(), otherUser, null, now, "other on ps1",
-        side, commitSHA1, Status.DRAFT);
+        side, rev, Status.DRAFT);
     update.insertComment(comment1);
     update.insertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
-    assertEquals(2, notes.getDraftPsComments(otherUserId).values().size());
-
-    assertTrue(notes.getDraftPsComments(otherUserId).containsValue(comment1));
-    assertTrue(notes.getDraftPsComments(otherUserId).containsValue(comment2));
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev), comment1,
+          new RevId(rev), comment2)).inOrder();
+    assertThat(notes.getComments()).isEmpty();
 
     // Publish first draft.
     update = newUpdate(c, otherUser);
@@ -1073,54 +1024,46 @@
     update.commit();
 
     notes = newNotes(c);
-    assertEquals(comment1,
-        Iterables.getOnlyElement(notes.getPatchSetComments().get(psId)));
-    assertEquals(comment2,
-        Iterables.getOnlyElement(
-            notes.getDraftPsComments(otherUserId).values()));
-
-    assertTrue(notes.getBaseComments().values().isEmpty());
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment2));
+    assertThat(notes.getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment1));
   }
 
   @Test
   public void patchLineCommentsMultipleDraftsBothSidesPublishAll()
-      throws OrmException, IOException {
+      throws Exception {
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
     Timestamp now = TimeUtil.nowTs();
-    String commitSHA1 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
-    String baseSHA1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     PatchSet.Id psId = c.currentPatchSetId();
 
     // Write two drafts, one on each side of the patchset.
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
-    PatchLineComment baseComment = newPatchLineComment(psId, filename, uuid1,
+    PatchLineComment baseComment = newComment(psId, filename, uuid1,
         range1, range1.getEndLine(), otherUser, null, now, "comment on base",
-        (short) 0, baseSHA1, Status.DRAFT);
-    PatchLineComment psComment = newPatchLineComment(psId, filename, uuid2,
+        (short) 0, rev1, Status.DRAFT);
+    PatchLineComment psComment = newComment(psId, filename, uuid2,
         range2, range2.getEndLine(), otherUser, null, now, "comment on ps",
-        (short) 1, commitSHA1, Status.DRAFT);
+        (short) 1, rev2, Status.DRAFT);
 
     update.insertComment(baseComment);
     update.insertComment(psComment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    PatchLineComment baseDraftCommentFromNotes =
-        Iterables.getOnlyElement(
-            notes.getDraftBaseComments(otherUserId).values());
-    PatchLineComment psDraftCommentFromNotes =
-        Iterables.getOnlyElement(
-            notes.getDraftPsComments(otherUserId).values());
-
-    assertEquals(baseComment, baseDraftCommentFromNotes);
-    assertEquals(psComment, psDraftCommentFromNotes);
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(
+            new RevId(rev1), baseComment,
+            new RevId(rev2), psComment));
+    assertThat(notes.getComments()).isEmpty();
 
     // Publish both comments.
     update = newUpdate(c, otherUser);
@@ -1133,17 +1076,98 @@
     update.commit();
 
     notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getComments()).containsExactly(
+        ImmutableMultimap.of(
+            new RevId(rev1), baseComment,
+            new RevId(rev2), psComment));
+  }
 
-    PatchLineComment baseCommentFromNotes =
-        Iterables.getOnlyElement(notes.getBaseComments().values());
-    PatchLineComment psCommentFromNotes =
-        Iterables.getOnlyElement(notes.getPatchSetComments().values());
+  @Test
+  public void patchLineCommentsDeleteAllDrafts() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId objId = ObjectId.fromString(rev);
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id psId = c.currentPatchSetId();
+    String filename = "filename";
+    short side = (short) 1;
 
-    assertEquals(baseComment, baseCommentFromNotes);
-    assertEquals(psComment, psCommentFromNotes);
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment = newComment(psId, filename, uuid, range,
+        range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        rev, Status.DRAFT);
+    update.setPatchSetId(psId);
+    update.upsertComment(comment);
+    update.commit();
 
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
-    assertTrue(notes.getDraftPsComments(otherUserId).values().isEmpty());
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
+    assertThat(notes.getDraftCommentNotes().getNoteMap().contains(objId))
+      .isTrue();
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    update.setPatchSetId(psId);
+    update.deleteComment(comment);
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getDraftCommentNotes().getNoteMap()).isNull();
+  }
+
+  @Test
+  public void patchLineCommentsDeleteAllDraftsForOneRevision()
+      throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId objId1 = ObjectId.fromString(rev1);
+    ObjectId objId2 = ObjectId.fromString(rev2);
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id ps1 = c.currentPatchSetId();
+    String filename = "filename1";
+    short side = (short) 1;
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newComment(ps1, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
+        side, rev1, Status.DRAFT);
+    update.setPatchSetId(ps1);
+    update.upsertComment(comment1);
+    update.commit();
+
+    incrementPatchSet(c);
+    PatchSet.Id ps2 = c.currentPatchSetId();
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    PatchLineComment comment2 = newComment(ps2, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
+        side, rev2, Status.DRAFT);
+    update.setPatchSetId(ps2);
+    update.upsertComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(2);
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    update.setPatchSetId(ps2);
+    update.deleteComment(comment2);
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
+    NoteMap noteMap = notes.getDraftCommentNotes().getNoteMap();
+    assertThat(noteMap.contains(objId1)).isTrue();
+    assertThat(noteMap.contains(objId2)).isFalse();
   }
 
   @Test
@@ -1151,27 +1175,20 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     String messageForBase = "comment for base";
     Timestamp now = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid,
-        null, 0, otherUser, null, now, messageForBase,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    PatchLineComment comment = newPublishedComment(
+        psId, "filename", uuid, null, 0, otherUser, null, now, messageForBase,
+        (short) 0, rev);
     update.setPatchSetId(psId);
-    update.upsertComment(commentForBase);
+    update.upsertComment(comment);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
-        notes.getPatchSetComments();
-
-    assertTrue(commentsForPs.isEmpty());
-    assertEquals(commentForBase,
-        Iterables.getOnlyElement(commentsForBase.get(psId)));
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment));
   }
 
   @Test
@@ -1179,26 +1196,63 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     String messageForBase = "comment for base";
     Timestamp now = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid,
-        null, 1, otherUser, null, now, messageForBase,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    PatchLineComment comment = newPublishedComment(
+        psId, "filename", uuid, null, 1, otherUser, null, now, messageForBase,
+        (short) 0, rev);
     update.setPatchSetId(psId);
-    update.upsertComment(commentForBase);
+    update.upsertComment(comment);
+    update.commit();
+
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment));
+  }
+
+  @Test
+  public void updateCommentsForMultipleRevisions() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id ps1 = c.currentPatchSetId();
+    String filename = "filename1";
+    short side = (short) 1;
+
+    incrementPatchSet(c);
+    PatchSet.Id ps2 = c.currentPatchSetId();
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(ps2);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newComment(ps1, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
+        side, rev1, Status.DRAFT);
+    PatchLineComment comment2 = newComment(ps2, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
+        side, rev2, Status.DRAFT);
+    update.upsertComment(comment1);
+    update.upsertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
-        notes.getPatchSetComments();
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(2);
+    assertThat(notes.getComments()).isEmpty();
 
-    assertTrue(commentsForPs.isEmpty());
-    assertEquals(commentForBase,
-        Iterables.getOnlyElement(commentsForBase.get(psId)));
+    update = newUpdate(c, otherUser);
+    update.setPatchSetId(ps2);
+    comment1.setStatus(Status.PUBLISHED);
+    comment2.setStatus(Status.PUBLISHED);
+    update.upsertComment(comment1);
+    update.upsertComment(comment2);
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getComments()).hasSize(2);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 328509a..c206902 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.notedb.ReviewerState.CC;
 import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
-import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
@@ -43,7 +43,7 @@
     update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
     update.putReviewer(otherUser.getAccount().getId(), CC);
     update.commit();
-    assertEquals("refs/changes/01/1/meta", update.getRefName());
+    assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
 
     RevCommit commit = parseCommit(update.getRevision());
     assertBodyEquals("Update patch set 1\n"
@@ -56,17 +56,18 @@
         commit);
 
     PersonIdent author = commit.getAuthorIdent();
-    assertEquals("Change Owner", author.getName());
-    assertEquals("1@gerrit", author.getEmailAddress());
-    assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
-        author.getWhen());
-    assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
+    assertThat(author.getName()).isEqualTo("Change Owner");
+    assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
+    assertThat(author.getWhen())
+        .isEqualTo(new Date(c.getCreatedOn().getTime() + 1000));
+    assertThat(author.getTimeZone())
+        .isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
 
     PersonIdent committer = commit.getCommitterIdent();
-    assertEquals("Gerrit Server", committer.getName());
-    assertEquals("noreply@gerrit.com", committer.getEmailAddress());
-    assertEquals(author.getWhen(), committer.getWhen());
-    assertEquals(author.getTimeZone(), committer.getTimeZone());
+    assertThat(committer.getName()).isEqualTo("Gerrit Server");
+    assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
+    assertThat(committer.getWhen()).isEqualTo(author.getWhen());
+    assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
   }
 
   @Test
@@ -76,7 +77,7 @@
     update.setChangeMessage("Just a little code change.\n"
         + "How about a new line");
     update.commit();
-    assertEquals("refs/changes/01/1/meta", update.getRefName());
+    assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
 
     assertBodyEquals("Update patch set 1\n"
         + "\n"
@@ -107,7 +108,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
 
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("NOT_READY", null,
           submitLabel("Verified", "OK", changeOwner.getAccountId()),
           submitLabel("Code-Review", "NEED", null)),
@@ -120,7 +121,7 @@
     assertBodyEquals("Submit patch set 1\n"
         + "\n"
         + "Patch-set: 1\n"
-        + "Status: submitted\n"
+        + "Status: merged\n"
         + "Submitted-with: NOT_READY\n"
         + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
         + "Submitted-with: NEED: Code-Review\n"
@@ -130,17 +131,18 @@
         commit);
 
     PersonIdent author = commit.getAuthorIdent();
-    assertEquals("Change Owner", author.getName());
-    assertEquals("1@gerrit", author.getEmailAddress());
-    assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
-        author.getWhen());
-    assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
+    assertThat(author.getName()).isEqualTo("Change Owner");
+    assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
+    assertThat(author.getWhen())
+        .isEqualTo(new Date(c.getCreatedOn().getTime() + 1000));
+    assertThat(author.getTimeZone())
+        .isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
 
     PersonIdent committer = commit.getCommitterIdent();
-    assertEquals("Gerrit Server", committer.getName());
-    assertEquals("noreply@gerrit.com", committer.getEmailAddress());
-    assertEquals(author.getWhen(), committer.getWhen());
-    assertEquals(author.getTimeZone(), committer.getTimeZone());
+    assertThat(committer.getName()).isEqualTo("Gerrit Server");
+    assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
+    assertThat(committer.getWhen()).isEqualTo(author.getWhen());
+    assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
   }
 
   @Test
@@ -161,8 +163,8 @@
         commit);
 
     PersonIdent author = commit.getAuthorIdent();
-    assertEquals("Anonymous Coward (3)", author.getName());
-    assertEquals("3@gerrit", author.getEmailAddress());
+    assertThat(author.getName()).isEqualTo("Anonymous Coward (3)");
+    assertThat(author.getEmailAddress()).isEqualTo("3@gerrit");
   }
 
   @Test
@@ -171,14 +173,14 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubject("Submit patch set 1");
 
-    update.submit(ImmutableList.of(
+    update.merge(ImmutableList.of(
         submitRecord("RULE_ERROR", "Problem with patch set:\n1")));
     update.commit();
 
     assertBodyEquals("Submit patch set 1\n"
         + "\n"
         + "Patch-set: 1\n"
-        + "Status: submitted\n"
+        + "Status: merged\n"
         + "Submitted-with: RULE_ERROR Problem with patch set: 1\n",
         update.getRevision());
   }
@@ -252,6 +254,6 @@
   private void assertBodyEquals(String expected, ObjectId commitId)
       throws Exception {
     RevCommit commit = parseCommit(commitId);
-    assertEquals(expected, commit.getFullMessage());
+    assertThat(commit.getFullMessage()).isEqualTo(expected);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
index b5b321e..0bd4f51 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
@@ -36,6 +36,7 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -49,7 +50,7 @@
 /** Unit tests for {@link ProjectControl}. */
 public class ProjectControlTest {
   @Inject private AccountManager accountManager;
-  @Inject private IdentifiedUser.RequestFactory userFactory;
+  @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private InMemoryRepositoryManager repoManager;
   @Inject private ProjectControl.GenericFactory projectControlFactory;
@@ -73,7 +74,7 @@
     schemaCreator.create(db);
     Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user"))
         .getAccountId();
-    user = userFactory.create(userId);
+    user = userFactory.create(Providers.of(db), userId);
 
     Project.NameKey name = new Project.NameKey("project");
     InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index b892f0b..9706feb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static com.google.gerrit.common.data.Permission.OWNER;
@@ -30,8 +31,6 @@
 import static com.google.gerrit.server.project.Util.deny;
 import static com.google.gerrit.server.project.Util.doNotInherit;
 import static com.google.gerrit.testutil.InMemoryRepositoryManager.newRepository;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.PermissionRange;
@@ -45,12 +44,139 @@
 import org.junit.Test;
 
 public class RefControlTest {
-  private static void assertOwner(String ref, ProjectControl u) {
-    assertTrue("OWN " + ref, u.controlForRef(ref).isOwner());
+  private void assertAdminsAreOwnersAndDevsAreNot() {
+    ProjectControl uBlah = util.user(local, DEVS);
+    ProjectControl uAdmin = util.user(local, DEVS, ADMIN);
+
+    assertThat(uBlah.isOwner()).named("not owner").isFalse();
+    assertThat(uAdmin.isOwner()).named("is owner").isTrue();
   }
 
-  private static void assertNotOwner(String ref, ProjectControl u) {
-    assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
+  private void assertOwner(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).isOwner())
+      .named("OWN " + ref)
+      .isTrue();
+  }
+  private void assertNotOwner(ProjectControl u) {
+    assertThat(u.isOwner()).named("not owner").isFalse();
+  }
+
+  private void assertOwnerAnyRef(ProjectControl u) {
+    assertThat(u.isOwnerAnyRef()).named("owns ref").isTrue();
+  }
+
+  private void assertNotOwner(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).isOwner())
+      .named("NOT OWN " + ref)
+      .isFalse();
+  }
+
+  private void assertCanRead(ProjectControl u) {
+    assertThat(u.isVisible())
+      .named("can read")
+      .isTrue();
+  }
+
+  private void assertCannotRead(ProjectControl u) {
+    assertThat(u.isVisible())
+      .named("cannot read")
+      .isFalse();
+  }
+
+  private void assertCanRead(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).isVisible())
+      .named("can read " + ref)
+      .isTrue();
+  }
+
+  private void assertCannotRead(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).isVisible())
+      .named("cannot read " + ref)
+      .isFalse();
+  }
+
+  private void assertCanSubmit(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canSubmit())
+      .named("can submit " + ref)
+      .isTrue();
+  }
+
+  private void assertCannotSubmit(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canSubmit())
+      .named("can submit " + ref)
+      .isFalse();
+  }
+
+  private void assertCanUpload(ProjectControl u) {
+    assertThat(u.canPushToAtLeastOneRef())
+      .named("can upload")
+      .isEqualTo(Capable.OK);
+  }
+
+  private void assertCanUpload(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canUpload())
+      .named("can upload " + ref)
+      .isTrue();
+  }
+
+  private void assertCannotUpload(ProjectControl u) {
+    assertThat(u.canPushToAtLeastOneRef())
+      .named("cannot upload")
+      .isNotEqualTo(Capable.OK);
+  }
+
+  private void assertCannotUpload(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canUpload())
+      .named("cannot upload " + ref)
+      .isFalse();
+  }
+
+  private void assertBlocked(String p, String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).isBlocked(p))
+      .named(p + " is blocked for " + ref)
+      .isTrue();
+  }
+
+  private void assertNotBlocked(String p, String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).isBlocked(p))
+      .named(p + " is blocked for " + ref)
+      .isFalse();
+  }
+
+  private void assertCanUpdate(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canUpdate())
+      .named("can update " + ref)
+      .isTrue();
+  }
+
+  private void assertCannotUpdate(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canUpdate())
+      .named("cannot update " + ref)
+      .isFalse();
+  }
+
+  private void assertCanForceUpdate(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canForceUpdate())
+      .named("can force push " + ref)
+      .isTrue();
+  }
+
+  private void assertCannotForceUpdate(String ref, ProjectControl u) {
+    assertThat(u.controlForRef(ref).canForceUpdate())
+      .named("cannot force push " + ref)
+      .isFalse();
+  }
+
+  private void assertCanVote(int score, PermissionRange range) {
+    assertThat(range.contains(score))
+      .named("can vote " + score)
+      .isTrue();
+  }
+
+  private void assertCannotVote(int score, PermissionRange range) {
+    assertThat(range.contains(score))
+      .named("cannot vote " + score)
+      .isFalse();
   }
 
   private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
@@ -105,8 +231,8 @@
     allow(local, OWNER, DEVS, "refs/heads/x/*");
 
     ProjectControl uDev = util.user(local, DEVS);
-    assertFalse("not owner", uDev.isOwner());
-    assertTrue("owns ref", uDev.isOwnerAnyRef());
+    assertNotOwner(uDev);
+    assertOwnerAnyRef(uDev);
 
     assertOwner("refs/heads/x/*", uDev);
     assertOwner("refs/heads/x/y", uDev);
@@ -124,8 +250,8 @@
     doNotInherit(local, OWNER, "refs/heads/x/y/*");
 
     ProjectControl uDev = util.user(local, DEVS);
-    assertFalse("not owner", uDev.isOwner());
-    assertTrue("owns ref", uDev.isOwnerAnyRef());
+    assertNotOwner(uDev);
+    assertOwnerAnyRef(uDev);
 
     assertOwner("refs/heads/x/*", uDev);
     assertOwner("refs/heads/x/y", uDev);
@@ -134,8 +260,8 @@
     assertNotOwner("refs/heads/master", uDev);
 
     ProjectControl uFix = util.user(local, fixers);
-    assertFalse("not owner", uFix.isOwner());
-    assertTrue("owns ref", uFix.isOwnerAnyRef());
+    assertNotOwner(uFix);
+    assertOwnerAnyRef(uFix);
 
     assertOwner("refs/heads/x/y/*", uFix);
     assertOwner("refs/heads/x/y/bar", uFix);
@@ -154,13 +280,9 @@
     doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
 
     ProjectControl u = util.user(local);
-    assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
-
-    assertTrue("can upload refs/heads/master", //
-        u.controlForRef("refs/heads/master").canUpload());
-
-    assertFalse("deny refs/heads/foobar", //
-        u.controlForRef("refs/heads/foobar").canUpload());
+    assertCanUpload(u);
+    assertCanUpload("refs/heads/master", u);
+    assertCannotUpload("refs/heads/foobar", u);
   }
 
   @Test
@@ -169,10 +291,8 @@
     block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
 
     ProjectControl u = util.user(local);
-    assertTrue("can upload refs/heads/master",
-        u.controlForRef("refs/heads/master").canUpload());
-    assertTrue("push is blocked to refs/drafts/master",
-        u.controlForRef("refs/drafts/refs/heads/master").isBlocked(PUSH));
+    assertCanUpload("refs/heads/master", u);
+    assertBlocked(PUSH, "refs/drafts/refs/heads/master", u);
   }
 
   @Test
@@ -180,12 +300,10 @@
     block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
     allow(parent, PUSH, ADMIN, "refs/drafts/*");
 
-    assertTrue("push is blocked for anonymous to refs/drafts/master",
-        util.user(local).controlForRef("refs/drafts/refs/heads/master")
-            .isBlocked(PUSH));
-    assertFalse("push is blocked for admin refs/drafts/master",
-        util.user(local, "a", ADMIN).controlForRef("refs/drafts/refs/heads/master")
-            .isBlocked(PUSH));
+    ProjectControl u = util.user(local);
+    ProjectControl a = util.user(local, "a", ADMIN);
+    assertBlocked(PUSH, "refs/drafts/refs/heads/master", u);
+    assertNotBlocked(PUSH, "refs/drafts/refs/heads/master", a);
   }
 
   @Test
@@ -195,26 +313,22 @@
     allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
 
     ProjectControl u = util.user(local);
-    assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
-
-    assertTrue("can upload refs/heads/master", //
-        u.controlForRef("refs/heads/master").canUpload());
-
-    assertTrue("can upload refs/heads/foobar", //
-        u.controlForRef("refs/heads/foobar").canUpload());
+    assertCanUpload(u);
+    assertCanUpload("refs/heads/master", u);
+    assertCanUpload("refs/heads/foobar", u);
   }
 
   @Test
   public void testInheritDuplicateSections() throws Exception {
     allow(parent, READ, ADMIN, "refs/*");
     allow(local, READ, DEVS, "refs/heads/*");
-    assertTrue("a can read", util.user(local, "a", ADMIN).isVisible());
+    assertCanRead(util.user(local, "a", ADMIN));
 
     local = new ProjectConfig(localKey);
     local.load(newRepository(localKey));
     local.getProject().setParentName(parentKey);
     allow(local, READ, DEVS, "refs/*");
-    assertTrue("d can read", util.user(local, "d", DEVS).isVisible());
+    assertCanRead(util.user(local, "d", DEVS));
   }
 
   @Test
@@ -222,8 +336,7 @@
     allow(parent, READ, REGISTERED_USERS, "refs/*");
     deny(local, READ, REGISTERED_USERS, "refs/*");
 
-    ProjectControl u = util.user(local);
-    assertFalse("can't read", u.isVisible());
+    assertCannotRead(util.user(local));
   }
 
   @Test
@@ -232,10 +345,10 @@
     deny(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
-    assertTrue("can read", u.isVisible());
-    assertTrue("can read", u.controlForRef("refs/master").isVisible());
-    assertTrue("can read", u.controlForRef("refs/tags/foobar").isVisible());
-    assertTrue("no master", u.controlForRef("refs/heads/master").isVisible());
+    assertCanRead(u);
+    assertCanRead("refs/master", u);
+    assertCanRead("refs/tags/foobar", u);
+    assertCanRead("refs/heads/master", u);
   }
 
   @Test
@@ -245,10 +358,10 @@
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
-    assertTrue("can read", u.isVisible());
-    assertFalse("can't read", u.controlForRef("refs/foobar").isVisible());
-    assertFalse("can't read", u.controlForRef("refs/tags/foobar").isVisible());
-    assertTrue("can read", u.controlForRef("refs/heads/foobar").isVisible());
+    assertCanRead(u);
+    assertCannotRead("refs/foobar", u);
+    assertCannotRead("refs/tags/foobar", u);
+    assertCanRead("refs/heads/foobar", u);
   }
 
   @Test
@@ -258,9 +371,9 @@
     allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
-    assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
-    assertFalse("can't submit", u.controlForRef("refs/tags/foobar").canSubmit());
-    assertTrue("can submit", u.controlForRef("refs/heads/foobar").canSubmit());
+    assertCannotSubmit("refs/foobar", u);
+    assertCannotSubmit("refs/tags/foobar", u);
+    assertCanSubmit("refs/heads/foobar", u);
   }
 
   @Test
@@ -270,34 +383,35 @@
     allow(local, PUSH, DEVS, "refs/for/refs/heads/*");
 
     ProjectControl u = util.user(local);
-    assertFalse("cannot upload", u.canPushToAtLeastOneRef() == Capable.OK);
-    assertFalse("cannot upload refs/heads/master", //
-        u.controlForRef("refs/heads/master").canUpload());
+    assertCannotUpload(u);
+    assertCannotUpload("refs/heads/master", u);
   }
 
   @Test
   public void testUsernamePatternCanUploadToAnyRef() {
     allow(local, PUSH, REGISTERED_USERS, "refs/heads/users/${username}/*");
     ProjectControl u = util.user(local, "a-registered-user");
-    assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
+    assertCanUpload(u);
   }
 
   @Test
   public void testUsernamePatternNonRegex() {
     allow(local, READ, DEVS, "refs/sb/${username}/heads/*");
 
-    ProjectControl u = util.user(local, "u", DEVS), d = util.user(local, "d", DEVS);
-    assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
-    assertTrue("d can read", d.controlForRef("refs/sb/d/heads/foobar").isVisible());
+    ProjectControl u = util.user(local, "u", DEVS);
+    ProjectControl d = util.user(local, "d", DEVS);
+    assertCannotRead("refs/sb/d/heads/foobar", u);
+    assertCanRead("refs/sb/d/heads/foobar", d);
   }
 
   @Test
   public void testUsernamePatternWithRegex() {
     allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
 
-    ProjectControl u = util.user(local, "d.v", DEVS), d = util.user(local, "dev", DEVS);
-    assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
-    assertTrue("d can read", d.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+    ProjectControl u = util.user(local, "d.v", DEVS);
+    ProjectControl d = util.user(local, "dev", DEVS);
+    assertCannotRead("refs/sb/dev/heads/foobar", u);
+    assertCanRead("refs/sb/dev/heads/foobar", d);
   }
 
   @Test
@@ -306,10 +420,8 @@
 
     ProjectControl u = util.user(local, "d.v@ger-rit.org", DEVS);
     ProjectControl d = util.user(local, "dev@ger-rit.org", DEVS);
-    assertFalse("u can't read",
-        u.controlForRef("refs/sb/dev@ger-rit.org/heads/foobar").isVisible());
-    assertTrue("d can read",
-        d.controlForRef("refs/sb/dev@ger-rit.org/heads/foobar").isVisible());
+    assertCannotRead("refs/sb/dev@ger-rit.org/heads/foobar", u);
+    assertCanRead("refs/sb/dev@ger-rit.org/heads/foobar", d);
   }
 
   @Test
@@ -317,9 +429,10 @@
     allow(local, READ, DEVS, "^refs/heads/.*");
     allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
 
-    ProjectControl u = util.user(local, DEVS), d = util.user(local, DEVS);
-    assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
-    assertTrue("d can read", d.controlForRef("refs/heads/foo-QA-bar").isVisible());
+    ProjectControl u = util.user(local, DEVS);
+    ProjectControl d = util.user(local, DEVS);
+    assertCanRead("refs/heads/foo-QA-bar", u);
+    assertCanRead("refs/heads/foo-QA-bar", d);
   }
 
   @Test
@@ -327,7 +440,7 @@
     allow(local, PUSH, DEVS, "refs/tags/*");
     block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
     ProjectControl u = util.user(local, DEVS);
-    assertFalse("u can't update tag", u.controlForRef("refs/tags/V10").canUpdate());
+    assertCannotUpdate("refs/tags/V10", u);
   }
 
   @Test
@@ -337,7 +450,7 @@
     block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
 
     ProjectControl u = util.user(local, DEVS);
-    assertFalse("u can't update tag", u.controlForRef("refs/tags/V10").canUpdate());
+    assertCannotUpdate("refs/tags/V10", u);
   }
 
   @Test
@@ -348,10 +461,10 @@
     ProjectControl u = util.user(local, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
-    assertTrue("u can vote -1", range.contains(-1));
-    assertTrue("u can vote +1", range.contains(1));
-    assertFalse("u can't vote -2", range.contains(-2));
-    assertFalse("u can't vote 2", range.contains(2));
+    assertCanVote(-1, range);
+    assertCanVote(1, range);
+    assertCannotVote(-2, range);
+    assertCannotVote(2, range);
   }
 
   @Test
@@ -365,10 +478,10 @@
 
     PermissionRange range =
         u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
-    assertTrue("u can vote -1", range.contains(-1));
-    assertTrue("u can vote +1", range.contains(1));
-    assertFalse("u can't vote -2", range.contains(-2));
-    assertFalse("u can't vote 2", range.contains(2));
+    assertCanVote(-1, range);
+    assertCanVote(1, range);
+    assertCannotVote(-2, range);
+    assertCannotVote(2, range);
   }
 
   @Test
@@ -378,8 +491,7 @@
     allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
-    assertFalse("not blocked from submitting", u.controlForRef(
-        "refs/heads/master").isBlocked(SUBMIT));
+    assertNotBlocked(SUBMIT, "refs/heads/master", u);
   }
 
   @Test
@@ -388,7 +500,7 @@
     allow(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
-    assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
+    assertCanUpdate("refs/heads/master", u);
   }
 
   @Test
@@ -398,7 +510,7 @@
     allow(local, PUSH, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, DEVS);
-    assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
+    assertCanForceUpdate("refs/heads/master", u);
   }
 
   @Test
@@ -408,7 +520,7 @@
     allow(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
-    assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
+    assertCannotForceUpdate("refs/heads/master", u);
   }
 
   @Test
@@ -417,7 +529,7 @@
     allow(local, PUSH, DEVS, "refs/heads/master");
 
     ProjectControl u = util.user(local, DEVS);
-    assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
+    assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
@@ -426,7 +538,7 @@
     allow(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
-    assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
+    assertCannotUpdate("refs/heads/master", u);
   }
 
   @Test
@@ -435,7 +547,7 @@
     allow(local, PUSH, fixers, "refs/heads/*");
 
     ProjectControl f = util.user(local, fixers);
-    assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
+    assertCannotUpdate("refs/heads/master", f);
   }
 
   @Test
@@ -445,7 +557,7 @@
     block(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl d = util.user(local, DEVS);
-    assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
+    assertCannotUpdate("refs/heads/master", d);
   }
 
   @Test
@@ -454,7 +566,9 @@
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
-    assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
+    assertThat(u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers())
+      .named("u can read")
+      .isTrue();
   }
 
   @Test
@@ -463,7 +577,9 @@
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
-    assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
+    assertThat(u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers())
+      .named("u can't read")
+      .isFalse();
   }
 
   @Test
@@ -472,8 +588,9 @@
     allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, DEVS);
-    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master")
-        .canForceEditTopicName());
+    assertThat(u.controlForRef("refs/heads/master").canForceEditTopicName())
+      .named("u can edit topic name")
+      .isTrue();
   }
 
   @Test
@@ -482,8 +599,9 @@
     allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
-    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master")
-        .canForceEditTopicName());
+    assertThat(u.controlForRef("refs/heads/master").canForceEditTopicName())
+      .named("u can't edit topic name")
+      .isFalse();
   }
 
   @Test
@@ -493,8 +611,8 @@
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
-    assertTrue("u can vote -2", range.contains(-2));
-    assertTrue("u can vote +2", range.contains(2));
+    assertCanVote(-2, range);
+    assertCanVote(2, range);
   }
 
   @Test
@@ -504,8 +622,8 @@
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
-    assertFalse("u can't vote -2", range.contains(-2));
-    assertFalse("u can't vote +2", range.contains(-2));
+    assertCannotVote(-2, range);
+    assertCannotVote(2, range);
   }
 
   @Test
@@ -515,8 +633,8 @@
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
-    assertFalse("u can't vote -2", range.contains(-2));
-    assertFalse("u can't vote +2", range.contains(-2));
+    assertCannotVote(-2, range);
+    assertCannotVote(2, range);
   }
 
   @Test
@@ -528,36 +646,30 @@
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range =
         u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
-    assertFalse("u can't vote -2", range.contains(-2));
-    assertFalse("u can't vote 2", range.contains(2));
+    assertCannotVote(-2, range);
+    assertCannotVote(2, range);
   }
 
+  @Test
   public void testUnblockRangeForChangeOwner() {
     allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master")
         .getRange(LABEL + "Code-Review", true);
-    assertTrue("u can vote -2", range.contains(-2));
-    assertTrue("u can vote +2", range.contains(2));
+    assertCanVote(-2, range);
+    assertCanVote(2, range);
   }
 
+  @Test
   public void testUnblockRangeForNotChangeOwner() {
     allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master")
         .getRange(LABEL + "Code-Review");
-    assertFalse("u can vote -2", range.contains(-2));
-    assertFalse("u can vote +2", range.contains(2));
-  }
-
-  private void assertAdminsAreOwnersAndDevsAreNot() {
-    ProjectControl uBlah = util.user(local, DEVS);
-    ProjectControl uAdmin = util.user(local, DEVS, ADMIN);
-
-    assertFalse("not owner", uBlah.isOwner());
-    assertTrue("is owner", uAdmin.isOwner());
+    assertCannotVote(-2, range);
+    assertCannotVote(2, range);
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
index e39700c..a671523 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -21,10 +21,14 @@
 import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -49,7 +53,6 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -81,15 +84,17 @@
 import java.util.Set;
 
 public class Util {
-  public static AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
-  public static AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
+  public static final AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
+  public static final AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
 
-  public static final LabelType CR = category("Code-Review",
-      value(2, "Looks good to me, approved"),
-      value(1, "Looks good to me, but someone else must approve"),
-      value(0, "No score"),
-      value(-1, "I would prefer this is not merged as is"),
-      value(-2, "This shall not be merged"));
+  public static final LabelType codeReview() {
+    return category("Code-Review",
+        value(2, "Looks good to me, approved"),
+        value(1, "Looks good to me, but someone else must approve"),
+        value(0, "No score"),
+        value(-1, "I would prefer this is not merged as is"),
+        value(-2, "This shall not be merged"));
+  }
 
   public static LabelValue value(int value, String text) {
     return new LabelValue((short) value, text);
@@ -138,6 +143,22 @@
     project.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
         .getPermission(capabilityName, true)
         .add(rule);
+      if (GlobalCapability.hasRange(capabilityName)) {
+        PermissionRange.WithDefaults range =
+            GlobalCapability.getRange(capabilityName);
+        if (range != null) {
+          rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+        }
+      }
+    return rule;
+  }
+
+  public static PermissionRule remove(ProjectConfig project,
+      String capabilityName, AccountGroup.UUID group) {
+    PermissionRule rule = newRule(project, group);
+    project.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+        .getPermission(capabilityName, true)
+        .remove(rule);
     return rule;
   }
 
@@ -157,6 +178,16 @@
     return r;
   }
 
+  public static PermissionRule blockLabel(ProjectConfig project,
+      String labelName, AccountGroup.UUID group, String ref) {
+    PermissionRule r =
+        grant(project, Permission.LABEL + labelName, newRule(project, group),
+            ref);
+    r.setBlock();
+    r.setRange(-1, 1);
+    return r;
+  }
+
   public static PermissionRule deny(ProjectConfig project,
       String permissionName, AccountGroup.UUID group, String ref) {
     PermissionRule r = grant(project, permissionName, newRule(project, group), ref);
@@ -197,7 +228,8 @@
       Repository repo = repoManager.createRepository(allProjectsName);
       allProjects = new ProjectConfig(new Project.NameKey(allProjectsName.get()));
       allProjects.load(repo);
-      allProjects.getLabelSections().put(CR.getName(), CR);
+      LabelType cr = codeReview();
+      allProjects.getLabelSections().put(cr.getName(), cr);
       add(allProjects);
     } catch (IOException | ConfigInvalidException e) {
       throw new RuntimeException(e);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java
index 1cef4b4..382610f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java
@@ -27,7 +27,7 @@
 import java.util.Collections;
 import java.util.List;
 
-public class AndPredicateTest {
+public class AndPredicateTest extends PredicateTest {
   private static final class TestPredicate extends OperatorPredicate<String> {
     private TestPredicate(String name, String value) {
       super(name, value);
@@ -64,22 +64,16 @@
     final TestPredicate b = f("author", "bob");
     final Predicate<String> n = and(a, b);
 
-    try {
-      n.getChildren().clear();
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().clear();
     assertChildren("clear", n, of(a, b));
 
-    try {
-      n.getChildren().remove(0);
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().remove(0);
     assertChildren("remove(0)", n, of(a, b));
 
-    try {
-      n.getChildren().iterator().remove();
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().iterator().remove();
     assertChildren("remove(0)", n, of(a, b));
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
index 9df906c..45a747c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
@@ -27,7 +27,7 @@
 import java.util.Collections;
 import java.util.List;
 
-public class NotPredicateTest {
+public class NotPredicateTest extends PredicateTest {
   private static final class TestPredicate extends OperatorPredicate<String> {
     private TestPredicate(String name, String value) {
       super(name, value);
@@ -70,22 +70,16 @@
     final TestPredicate p = f("author", "bob");
     final Predicate<String> n = not(p);
 
-    try {
-      n.getChildren().clear();
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().clear();
     assertOnlyChild("clear", p, n);
 
-    try {
-      n.getChildren().remove(0);
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().remove(0);
     assertOnlyChild("remove(0)", p, n);
 
-    try {
-      n.getChildren().iterator().remove();
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().iterator().remove();
     assertOnlyChild("remove(0)", p, n);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java
index 01b8588..ee5e0b0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java
@@ -27,7 +27,7 @@
 import java.util.Collections;
 import java.util.List;
 
-public class OrPredicateTest {
+public class OrPredicateTest extends PredicateTest {
   private static final class TestPredicate extends OperatorPredicate<String> {
     private TestPredicate(String name, String value) {
       super(name, value);
@@ -64,22 +64,16 @@
     final TestPredicate b = f("author", "bob");
     final Predicate<String> n = or(a, b);
 
-    try {
-      n.getChildren().clear();
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().clear();
     assertChildren("clear", n, of(a, b));
 
-    try {
-      n.getChildren().remove(0);
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().remove(0);
     assertChildren("remove(0)", n, of(a, b));
 
-    try {
-      n.getChildren().iterator().remove();
-    } catch (RuntimeException e) {
-    }
+    exception.expect(UnsupportedOperationException.class);
+    n.getChildren().iterator().remove();
     assertChildren("remove(0)", n, of(a, b));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
similarity index 67%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
copy to gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
index 4413603..865841e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/extensions/TopMenuList.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// Copyright (C) 2015 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,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.extensions;
+package com.google.gerrit.server.query;
 
-import com.google.gwt.core.client.JsArray;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
 
-public class TopMenuList extends JsArray<TopMenu> {
-
-  protected TopMenuList() {
-  }
+public class PredicateTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index eea8e58..db6301d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -15,15 +15,16 @@
 package com.google.gerrit.server.query.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
-import static org.junit.Assert.fail;
 
+import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -32,95 +33,114 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.CreateGroupArgs;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.PerformCreateGroup;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeTriplet;
-import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.change.PostReview;
-import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.Map;
 
 @Ignore
 @RunWith(ConfigSuite.class)
 public abstract class AbstractQueryChangesTest {
-  private static final TopLevelResource TLR = TopLevelResource.INSTANCE;
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return updateConfig(new Config());
+  }
 
   @ConfigSuite.Config
   public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
+    return updateConfig(NotesMigration.allEnabledConfig());
+  }
+
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
+  private static Config updateConfig(Config cfg) {
+    cfg.setInt("index", null, "maxPages", 10);
+    return cfg;
   }
 
   @ConfigSuite.Parameter public Config config;
   @Inject protected AccountManager accountManager;
+  @Inject protected BatchUpdate.Factory updateFactory;
   @Inject protected ChangeInserter.Factory changeFactory;
-  @Inject protected ChangesCollection changes;
-  @Inject protected CreateProject.Factory projectFactory;
+  @Inject protected PatchSetInserter.Factory patchSetFactory;
+  @Inject protected ChangeControl.GenericFactory changeControlFactory;
   @Inject protected GerritApi gApi;
-  @Inject protected IdentifiedUser.RequestFactory userFactory;
+  @Inject protected IdentifiedUser.GenericFactory userFactory;
+  @Inject protected IndexCollection indexes;
   @Inject protected InMemoryDatabase schemaFactory;
   @Inject protected InMemoryRepositoryManager repoManager;
+  @Inject protected InternalChangeQuery internalChangeQuery;
   @Inject protected NotesMigration notesMigration;
-  @Inject protected PostReview postReview;
   @Inject protected ProjectControl.GenericFactory projectControlFactory;
-  @Inject protected Provider<QueryChanges> queryProvider;
+  @Inject protected ChangeQueryBuilder queryBuilder;
+  @Inject protected QueryProcessor queryProcessor;
   @Inject protected SchemaCreator schemaCreator;
   @Inject protected ThreadLocalRequestContext requestContext;
   @Inject protected GroupCache groupCache;
-  @Inject protected PerformCreateGroup.Factory performCreateGroupFactory;
+  @Inject protected AddMembers addMembers;
 
   protected LifecycleManager lifecycle;
   protected ReviewDb db;
   protected Account.Id userId;
   protected CurrentUser user;
-  protected volatile long clockStepMs;
 
   private String systemTimeZone;
 
@@ -128,10 +148,10 @@
 
   @Before
   public void setUpInjector() throws Exception {
-    Injector injector = createInjector();
-    injector.injectMembers(this);
     lifecycle = new LifecycleManager();
+    Injector injector = createInjector();
     lifecycle.add(injector);
+    injector.injectMembers(this);
     lifecycle.start();
 
     db = schemaFactory.open();
@@ -141,15 +161,16 @@
     Account userAccount = db.accounts().get(userId);
     userAccount.setPreferredEmail("user@example.com");
     db.accounts().update(ImmutableList.of(userAccount));
-    user = userFactory.create(userId);
+    user = userFactory.create(Providers.of(db), userId);
     requestContext.setContext(newRequestContext(userAccount.getId()));
   }
 
-  private RequestContext newRequestContext(Account.Id requestUserId) {
-    final CurrentUser requestUser = userFactory.create(requestUserId);
+  protected RequestContext newRequestContext(Account.Id requestUserId) {
+    final CurrentUser requestUser =
+        userFactory.create(Providers.of(db), requestUserId);
     return new RequestContext() {
       @Override
-      public CurrentUser getCurrentUser() {
+      public CurrentUser getUser() {
         return requestUser;
       }
 
@@ -175,426 +196,526 @@
   @Before
   public void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    clockStepMs = 1;
-    final AtomicLong clockMs = new AtomicLong(
-        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
-    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
-      @Override
-      public long getMillis() {
-        return clockMs.getAndAdd(clockStepMs);
-      }
-    });
+    TestTimeUtil.resetWithClockStep(1, MILLISECONDS);
   }
 
   @After
   public void resetTime() {
-    DateTimeUtils.setCurrentMillisSystem();
+    TestTimeUtil.useSystemTime();
     System.setProperty("user.timezone", systemTimeZone);
   }
 
   @Test
   public void byId() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
-    assertThat(query("12345")).isEmpty();
-    assertResultEquals(change1, queryOne(change1.getId().get()));
-    assertResultEquals(change2, queryOne(change2.getId().get()));
+    assertQuery("12345");
+    assertQuery(change1.getId().get(), change1);
+    assertQuery(change2.getId().get(), change2);
   }
 
   @Test
   public void byKey() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change = newChange(repo, null, null, null, null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(newChange(repo, null, null, null, null));
     String key = change.getKey().get();
 
-    assertThat(query("I0000000000000000000000000000000000000000")).isEmpty();
+    assertQuery("I0000000000000000000000000000000000000000");
     for (int i = 0; i <= 36; i++) {
       String q = key.substring(0, 41 - i);
-      assertResultEquals("result for " + q, change, queryOne(q));
+      assertQuery(q, change);
     }
   }
 
   @Test
   public void byTriplet() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change = newChange(repo, null, null, null, "branch").insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(newChange(repo, null, null, null, "branch"));
     String k = change.getKey().get();
 
-    assertResultEquals(change, queryOne("repo~branch~" + k));
-    assertResultEquals(change, queryOne("change:repo~branch~" + k));
-    assertResultEquals(change, queryOne("repo~refs/heads/branch~" + k));
-    assertResultEquals(change, queryOne("change:repo~refs/heads/branch~" + k));
-    assertResultEquals(change, queryOne("repo~branch~" + k.substring(0, 10)));
-    assertResultEquals(change,
-        queryOne("change:repo~branch~" + k.substring(0, 10)));
+    assertQuery("repo~branch~" + k, change);
+    assertQuery("change:repo~branch~" + k, change);
+    assertQuery("repo~refs/heads/branch~" + k, change);
+    assertQuery("change:repo~refs/heads/branch~" + k, change);
+    assertQuery("repo~branch~" + k.substring(0, 10), change);
+    assertQuery("change:repo~branch~" + k.substring(0, 10), change);
 
-    assertThat(query("foo~bar")).isEmpty();
+    assertQuery("foo~bar");
     assertBadQuery("change:foo~bar");
-    assertThat(query("otherrepo~branch~" + k)).isEmpty();
-    assertThat(query("change:otherrepo~branch~" + k)).isEmpty();
-    assertThat(query("repo~otherbranch~" + k)).isEmpty();
-    assertThat(query("change:repo~otherbranch~" + k)).isEmpty();
-    assertThat(query("repo~branch~I0000000000000000000000000000000000000000"))
-        .isEmpty();
-    assertThat(query(
-          "change:repo~branch~I0000000000000000000000000000000000000000"))
-        .isEmpty();
+    assertQuery("otherrepo~branch~" + k);
+    assertQuery("change:otherrepo~branch~" + k);
+    assertQuery("repo~otherbranch~" + k);
+    assertQuery("change:repo~otherbranch~" + k);
+    assertQuery("repo~branch~I0000000000000000000000000000000000000000");
+    assertQuery("change:repo~branch~I0000000000000000000000000000000000000000");
   }
 
   @Test
   public void byStatus() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.MERGED);
-    ins2.insert();
+    insert(ins2);
 
-    assertResultEquals(change1, queryOne("status:new"));
-    assertResultEquals(change1, queryOne("status:NEW"));
-    assertResultEquals(change1, queryOne("is:new"));
-    assertResultEquals(change2, queryOne("status:merged"));
-    assertResultEquals(change2, queryOne("is:merged"));
+    assertQuery("status:new", change1);
+    assertQuery("status:NEW", change1);
+    assertQuery("is:new", change1);
+    assertQuery("status:merged", change2);
+    assertQuery("is:merged", change2);
   }
 
   @Test
   public void byStatusOpen() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.DRAFT);
-    ins2.insert();
+    insert(ins2);
     ChangeInserter ins3 = newChange(repo, null, null, null, null);
     Change change3 = ins3.getChange();
     change3.setStatus(Change.Status.MERGED);
-    ins3.insert();
+    insert(ins3);
 
-    List<ChangeInfo> results;
-    results = query("status:open");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    Change[] expected = new Change[] {change2, change1};
+    assertQuery("status:open", expected);
+    assertQuery("status:OPEN", expected);
+    assertQuery("status:o", expected);
+    assertQuery("status:op", expected);
+    assertQuery("status:ope", expected);
+    assertQuery("status:pending", expected);
+    assertQuery("status:PENDING", expected);
+    assertQuery("status:p", expected);
+    assertQuery("status:pe", expected);
+    assertQuery("status:pen", expected);
+    assertQuery("is:open", expected);
+  }
 
-    assertThat(query("status:OPEN")).hasSize(2);
-    assertThat(query("status:o")).hasSize(2);
-    assertThat(query("status:op")).hasSize(2);
-    assertThat(query("status:ope")).hasSize(2);
-    assertThat(query("status:pending")).hasSize(2);
-    assertThat(query("status:PENDING")).hasSize(2);
-    assertThat(query("status:p")).hasSize(2);
-    assertThat(query("status:pe")).hasSize(2);
-    assertThat(query("status:pen")).hasSize(2);
+  @Test
+  public void byStatusDraft() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setStatus(Change.Status.NEW);
+    insert(ins1);
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.DRAFT);
+    insert(ins2);
 
-    results = query("is:open");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    Change[] expected = new Change[] {change2};
+    assertQuery("status:draft", expected);
+    assertQuery("status:DRAFT", expected);
+    assertQuery("status:d", expected);
+    assertQuery("status:dr", expected);
+    assertQuery("status:dra", expected);
+    assertQuery("status:draf", expected);
+    assertQuery("is:draft", expected);
   }
 
   @Test
   public void byStatusClosed() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.MERGED);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.ABANDONED);
-    ins2.insert();
+    insert(ins2);
     ChangeInserter ins3 = newChange(repo, null, null, null, null);
     Change change3 = ins3.getChange();
     change3.setStatus(Change.Status.NEW);
-    ins3.insert();
+    insert(ins3);
 
-    List<ChangeInfo> results;
-    results = query("status:closed");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-
-    assertThat(query("status:CLOSED")).hasSize(2);
-    assertThat(query("status:c")).hasSize(2);
-    assertThat(query("status:cl")).hasSize(2);
-    assertThat(query("status:clo")).hasSize(2);
-    assertThat(query("status:clos")).hasSize(2);
-    assertThat(query("status:close")).hasSize(2);
-    assertThat(query("status:closed")).hasSize(2);
-
-    results = query("is:closed");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    Change[] expected = new Change[] {change2, change1};
+    assertQuery("status:closed", expected);
+    assertQuery("status:CLOSED", expected);
+    assertQuery("status:c", expected);
+    assertQuery("status:cl", expected);
+    assertQuery("status:clo", expected);
+    assertQuery("status:clos", expected);
+    assertQuery("status:close", expected);
+    assertQuery("status:closed", expected);
+    assertQuery("is:closed", expected);
   }
 
   @Test
   public void byStatusPrefix() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
-    ins1.insert();
+    insert(ins1);
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.MERGED);
-    ins2.insert();
+    insert(ins2);
 
-    assertResultEquals(change1, queryOne("status:n"));
-    assertResultEquals(change1, queryOne("status:ne"));
-    assertResultEquals(change1, queryOne("status:new"));
-    assertResultEquals(change1, queryOne("status:N"));
-    assertResultEquals(change1, queryOne("status:nE"));
-    assertResultEquals(change1, queryOne("status:neW"));
+    assertQuery("status:n", change1);
+    assertQuery("status:ne", change1);
+    assertQuery("status:new", change1);
+    assertQuery("status:N", change1);
+    assertQuery("status:nE", change1);
+    assertQuery("status:neW", change1);
     assertBadQuery("status:nx");
     assertBadQuery("status:newx");
   }
 
   @Test
   public void byCommit() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
-    ins.insert();
+    insert(ins);
     String sha = ins.getPatchSet().getRevision().get();
 
-    assertThat(query("0000000000000000000000000000000000000000")).isEmpty();
+    assertQuery("0000000000000000000000000000000000000000");
     for (int i = 0; i <= 36; i++) {
       String q = sha.substring(0, 40 - i);
-      assertResultEquals("result for " + q, ins.getChange(), queryOne(q));
+      assertQuery(q, ins.getChange());
     }
   }
 
   @Test
   public void byOwner() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
-    Change change2 = newChange(repo, null, null, user2, null).insert();
+    Change change2 = insert(newChange(repo, null, null, user2, null));
 
-    assertResultEquals(change1, queryOne("owner:" + userId.get()));
-    assertResultEquals(change2, queryOne("owner:" + user2));
+    assertQuery("owner:" + userId.get(), change1);
+    assertQuery("owner:" + user2, change2);
+  }
+
+  @Test
+  public void byAuthor() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
+
+    // By exact email address
+    assertQuery("author:jauthor@example.com", change1);
+
+    // By email address part
+    assertQuery("author:jauthor", change1);
+    assertQuery("author:example", change1);
+    assertQuery("author:example.com", change1);
+
+    // By name part
+    assertQuery("author:Author", change1);
+
+    // Case insensitive
+    assertQuery("author:jAuThOr", change1);
+    assertQuery("author:ExAmPlE", change1);
+
+    // By non-existing email address / name / part
+    assertQuery("author:jcommitter@example.com");
+    assertQuery("author:somewhere.com");
+    assertQuery("author:jcommitter");
+    assertQuery("author:Committer");
+  }
+
+  @Test
+  public void byCommitter() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
+
+    // By exact email address
+    assertQuery("committer:jcommitter@example.com", change1);
+
+    // By email address part
+    assertQuery("committer:jcommitter", change1);
+    assertQuery("committer:example", change1);
+    assertQuery("committer:example.com", change1);
+
+    // By name part
+    assertQuery("committer:Committer", change1);
+
+    // Case insensitive
+    assertQuery("committer:jCoMmItTeR", change1);
+    assertQuery("committer:ExAmPlE", change1);
+
+    // By non-existing email address / name / part
+    assertQuery("committer:jauthor@example.com");
+    assertQuery("committer:somewhere.com");
+    assertQuery("committer:jauthor");
+    assertQuery("committer:Author");
   }
 
   @Test
   public void byOwnerIn() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
-    Change change2 = newChange(repo, null, null, user2, null).insert();
+    Change change2 = insert(newChange(repo, null, null, user2, null));
 
-    assertResultEquals(change1, queryOne("ownerin:Administrators"));
-    List<ChangeInfo> results = query("ownerin:\"Registered Users\"");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery("ownerin:Administrators", change1);
+    assertQuery("ownerin:\"Registered Users\"", change2, change1);
   }
 
   @Test
   public void byProject() throws Exception {
-    TestRepository<InMemoryRepository> repo1 = createProject("repo1");
-    TestRepository<InMemoryRepository> repo2 = createProject("repo2");
-    Change change1 = newChange(repo1, null, null, null, null).insert();
-    Change change2 = newChange(repo2, null, null, null, null).insert();
+    TestRepository<Repo> repo1 = createProject("repo1");
+    TestRepository<Repo> repo2 = createProject("repo2");
+    Change change1 = insert(newChange(repo1, null, null, null, null));
+    Change change2 = insert(newChange(repo2, null, null, null, null));
 
-    assertThat(query("project:foo")).isEmpty();
-    assertThat(query("project:repo")).isEmpty();
-    assertResultEquals(change1, queryOne("project:repo1"));
-    assertResultEquals(change2, queryOne("project:repo2"));
+    assertQuery("project:foo");
+    assertQuery("project:repo");
+    assertQuery("project:repo1", change1);
+    assertQuery("project:repo2", change2);
   }
 
   @Test
   public void byProjectPrefix() throws Exception {
-    TestRepository<InMemoryRepository> repo1 = createProject("repo1");
-    TestRepository<InMemoryRepository> repo2 = createProject("repo2");
-    Change change1 = newChange(repo1, null, null, null, null).insert();
-    Change change2 = newChange(repo2, null, null, null, null).insert();
+    TestRepository<Repo> repo1 = createProject("repo1");
+    TestRepository<Repo> repo2 = createProject("repo2");
+    Change change1 = insert(newChange(repo1, null, null, null, null));
+    Change change2 = insert(newChange(repo2, null, null, null, null));
 
-    assertThat(query("projects:foo")).isEmpty();
-    assertResultEquals(change1, queryOne("projects:repo1"));
-    assertResultEquals(change2, queryOne("projects:repo2"));
-
-    List<ChangeInfo> results;
-    results = query("projects:repo");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery("projects:foo");
+    assertQuery("projects:repo1", change1);
+    assertQuery("projects:repo2", change2);
+    assertQuery("projects:repo", change2, change1);
   }
 
   @Test
   public void byBranchAndRef() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, "master").insert();
-    Change change2 = newChange(repo, null, null, null, "branch").insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, "master"));
+    Change change2 = insert(newChange(repo, null, null, null, "branch"));
 
-    assertThat(query("branch:foo")).isEmpty();
-    assertResultEquals(change1, queryOne("branch:master"));
-    assertResultEquals(change1, queryOne("branch:refs/heads/master"));
-    assertThat(query("ref:master")).isEmpty();
-    assertResultEquals(change1, queryOne("ref:refs/heads/master"));
-    assertResultEquals(change1, queryOne("branch:refs/heads/master"));
-    assertResultEquals(change2, queryOne("branch:branch"));
-    assertResultEquals(change2, queryOne("branch:refs/heads/branch"));
-    assertThat(query("ref:branch")).isEmpty();
-    assertResultEquals(change2, queryOne("ref:refs/heads/branch"));
+    assertQuery("branch:foo");
+    assertQuery("branch:master", change1);
+    assertQuery("branch:refs/heads/master", change1);
+    assertQuery("ref:master");
+    assertQuery("ref:refs/heads/master", change1);
+    assertQuery("branch:refs/heads/master", change1);
+    assertQuery("branch:branch", change2);
+    assertQuery("branch:refs/heads/branch", change2);
+    assertQuery("ref:branch");
+    assertQuery("ref:refs/heads/branch", change2);
   }
 
   @Test
   public void byTopic() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setTopic("feature1");
-    ins1.insert();
+    insert(ins1);
 
     ChangeInserter ins2 = newChange(repo, null, null, null, null);
     Change change2 = ins2.getChange();
     change2.setTopic("feature2");
-    ins2.insert();
+    insert(ins2);
 
-    Change change3 = newChange(repo, null, null, null, null).insert();
+    ChangeInserter ins3 = newChange(repo, null, null, null, null);
+    Change change3 = ins3.getChange();
+    change3.setTopic("Cherrypick-feature2");
+    insert(ins3);
 
-    assertThat(query("topic:foo")).isEmpty();
-    assertResultEquals(change1, queryOne("topic:feature1"));
-    assertResultEquals(change2, queryOne("topic:feature2"));
-    assertResultEquals(change3, queryOne("topic:\"\""));
+    ChangeInserter ins4 = newChange(repo, null, null, null, null);
+    Change change4 = ins4.getChange();
+    change4.setTopic("feature2-fixup");
+    insert(ins4);
+
+    Change change5 = insert(newChange(repo, null, null, null, null));
+
+    assertQuery("intopic:foo");
+    assertQuery("intopic:feature1", change1);
+    assertQuery("intopic:feature2", change4, change3, change2);
+    assertQuery("topic:feature2", change2);
+    assertQuery("intopic:feature2", change4, change3, change2);
+    assertQuery("intopic:fixup", change4);
+    assertQuery("topic:\"\"", change5);
+    assertQuery("intopic:\"\"", change5);
+    assertQuery("intopic:^feature2.*", change4, change2);
+    assertQuery("intopic:{^.*feature2$}", change3, change2);
   }
 
   @Test
   public void byMessageExact() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
     RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
-    assertThat(query("message:foo")).isEmpty();
-    assertResultEquals(change1, queryOne("message:one"));
-    assertResultEquals(change2, queryOne("message:two"));
+    assertQuery("message:foo");
+    assertQuery("message:one", change1);
+    assertQuery("message:two", change2);
   }
 
   @Test
   public void fullTextWithNumbers() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 =
         repo.parseBody(repo.commit().message("12345 67890").create());
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
     RevCommit commit2 =
         repo.parseBody(repo.commit().message("12346 67891").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
-    assertThat(query("message:1234")).isEmpty();
-    assertResultEquals(change1, queryOne("message:12345"));
-    assertResultEquals(change2, queryOne("message:12346"));
+    assertQuery("message:1234");
+    assertQuery("message:12345", change1);
+    assertQuery("message:12346", change2);
   }
 
   @Test
   public void byLabel() throws Exception {
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
-    Change change = ins.insert();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    ChangeInserter ins3 = newChange(repo, null, null, null, null);
+    ChangeInserter ins4 = newChange(repo, null, null, null, null);
+    ChangeInserter ins5 = newChange(repo, null, null, null, null);
 
-    ReviewInput input = new ReviewInput();
-    input.message = "toplevel";
-    input.labels = ImmutableMap.<String, Short> of("Code-Review", (short) 1);
-    postReview.apply(new RevisionResource(
-        changes.parse(change.getId()), ins.getPatchSet()), input);
+    Change reviewMinus2Change = insert(ins);
+    gApi.changes().id(reviewMinus2Change.getId().get()).current()
+        .review(ReviewInput.reject());
 
-    assertThat(query("label:Code-Review=-2")).isEmpty();
-    assertThat(query("label:Code-Review-2")).isEmpty();
-    assertThat(query("label:Code-Review=-1")).isEmpty();
-    assertThat(query("label:Code-Review-1")).isEmpty();
-    assertThat(query("label:Code-Review=0")).isEmpty();
-    assertResultEquals(change, queryOne("label:Code-Review=+1"));
-    assertResultEquals(change, queryOne("label:Code-Review=1"));
-    assertResultEquals(change, queryOne("label:Code-Review+1"));
-    assertThat(query("label:Code-Review=+2")).isEmpty();
-    assertThat(query("label:Code-Review=2")).isEmpty();
-    assertThat(query("label:Code-Review+2")).isEmpty();
+    Change reviewMinus1Change = insert(ins2);
+    gApi.changes().id(reviewMinus1Change.getId().get()).current()
+        .review(ReviewInput.dislike());
 
-    assertResultEquals(change, queryOne("label:Code-Review>=0"));
-    assertResultEquals(change, queryOne("label:Code-Review>0"));
-    assertResultEquals(change, queryOne("label:Code-Review>=1"));
-    assertThat(query("label:Code-Review>1")).isEmpty();
-    assertThat(query("label:Code-Review>=2")).isEmpty();
+    Change noLabelChange = insert(ins3);
 
-    assertResultEquals(change, queryOne("label: Code-Review<=2"));
-    assertResultEquals(change, queryOne("label: Code-Review<2"));
-    assertResultEquals(change, queryOne("label: Code-Review<=1"));
-    assertThat(query("label:Code-Review<1")).isEmpty();
-    assertThat(query("label:Code-Review<=0")).isEmpty();
+    Change reviewPlus1Change = insert(ins4);
+    gApi.changes().id(reviewPlus1Change.getId().get()).current()
+        .review(ReviewInput.recommend());
 
-    assertThat(query("label:Code-Review=+1,anotheruser")).isEmpty();
-    assertResultEquals(change, queryOne("label:Code-Review=+1,user"));
-    assertResultEquals(change, queryOne("label:Code-Review=+1,user=user"));
-    assertResultEquals(change, queryOne("label:Code-Review=+1,Administrators"));
-    assertResultEquals(change, queryOne("label:Code-Review=+1,group=Administrators"));
+    Change reviewPlus2Change = insert(ins5);
+    gApi.changes().id(reviewPlus2Change.getId().get()).current()
+        .review(ReviewInput.approve());
+
+    Map<Integer, Change> changes = new LinkedHashMap<>(5);
+    changes.put(2, reviewPlus2Change);
+    changes.put(1, reviewPlus1Change);
+    changes.put(-1, reviewMinus1Change);
+    changes.put(-2, reviewMinus2Change);
+    changes.put(0, noLabelChange);
+
+    assertQuery("label:Code-Review=-2", reviewMinus2Change);
+    assertQuery("label:Code-Review-2", reviewMinus2Change);
+    assertQuery("label:Code-Review=-1", reviewMinus1Change);
+    assertQuery("label:Code-Review-1", reviewMinus1Change);
+    assertQuery("label:Code-Review=0", noLabelChange);
+    assertQuery("label:Code-Review=+1", reviewPlus1Change);
+    assertQuery("label:Code-Review=1", reviewPlus1Change);
+    assertQuery("label:Code-Review+1", reviewPlus1Change);
+    assertQuery("label:Code-Review=+2", reviewPlus2Change);
+    assertQuery("label:Code-Review=2", reviewPlus2Change);
+    assertQuery("label:Code-Review+2", reviewPlus2Change);
+
+    assertQuery("label:Code-Review>-3", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review>=-2", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review>-2", codeReviewInRange(changes, -1, 2));
+    assertQuery("label:Code-Review>=-1", codeReviewInRange(changes, -1, 2));
+    assertQuery("label:Code-Review>-1", codeReviewInRange(changes, 0, 2));
+    assertQuery("label:Code-Review>=0", codeReviewInRange(changes, 0, 2));
+    assertQuery("label:Code-Review>0", codeReviewInRange(changes, 1, 2));
+    assertQuery("label:Code-Review>=1", codeReviewInRange(changes, 1, 2));
+    assertQuery("label:Code-Review>1", reviewPlus2Change);
+    assertQuery("label:Code-Review>=2", reviewPlus2Change);
+    assertQuery("label:Code-Review>2");
+
+    assertQuery("label:Code-Review<=2", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review<2", codeReviewInRange(changes, -2, 1));
+    assertQuery("label:Code-Review<=1", codeReviewInRange(changes, -2, 1));
+    assertQuery("label:Code-Review<1", codeReviewInRange(changes, -2, 0));
+    assertQuery("label:Code-Review<=0", codeReviewInRange(changes, -2, 0));
+    assertQuery("label:Code-Review<0", codeReviewInRange(changes, -2, -1));
+    assertQuery("label:Code-Review<=-1", codeReviewInRange(changes, -2, -1));
+    assertQuery("label:Code-Review<-1", reviewMinus2Change);
+    assertQuery("label:Code-Review<=-2", reviewMinus2Change);
+    assertQuery("label:Code-Review<-2");
+
+    assertQuery("label:Code-Review=+1,anotheruser");
+    assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
   }
 
-  private void createGroup(String name, AccountGroup.Id owner, Account.Id member)
-      throws Exception {
-    CreateGroupArgs args = new CreateGroupArgs();
-    args.setGroupName(name);
-    args.ownerGroupId = owner;
-    args.initialMembers = ImmutableList.of(member);
-    performCreateGroupFactory.create(args).createGroup();
+  private Change[] codeReviewInRange(Map<Integer, Change> changes, int start,
+      int end) {
+    int size = 0;
+    Change[] range = new Change[end - start + 1];
+    for (int i : changes.keySet()) {
+      if (i >= start && i <= end) {
+        range[size] = changes.get(i);
+        size++;
+      }
+    }
+    return range;
+  }
+
+  private String createGroup(String name, String owner) throws Exception {
+    GroupInput in = new GroupInput();
+    in.name = name;
+    in.ownerId = owner;
+    gApi.groups().create(in);
+    return name;
+  }
+
+  private Account.Id createAccount(String name) throws Exception {
+    return accountManager.authenticate(
+        AuthRequest.forUser(name)).getAccountId();
   }
 
   @Test
   public void byLabelGroup() throws Exception {
-    Account.Id user1 = accountManager
-        .authenticate(AuthRequest.forUser("user1")).getAccountId();
-    Account.Id user2 = accountManager
-        .authenticate(AuthRequest.forUser("user2")).getAccountId();
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Account.Id user1 = createAccount("user1");
+    createAccount("user2");
+    TestRepository<Repo> repo = createProject("repo");
 
     // create group and add users
-    AccountGroup.Id adminGroup =
-        groupCache.get(new AccountGroup.NameKey("Administrators")).getId();
-    createGroup("group1", adminGroup, user1);
-    createGroup("group2", adminGroup, user2);
+    String g1 = createGroup("group1", "Administrators");
+    String g2 = createGroup("group2", "Administrators");
+    gApi.groups().id(g1).addMembers("user1");
+    gApi.groups().id(g2).addMembers("user2");
 
     // create a change
     ChangeInserter ins = newChange(repo, null, null, user1.get(), null);
-    Change change1 = ins.insert();
+    Change change1 = insert(ins);
 
     // post a review with user1
     requestContext.setContext(newRequestContext(user1));
-    ReviewInput input = new ReviewInput();
-    input.labels = ImmutableMap.<String, Short> of("Code-Review", (short) 1);
-    postReview.apply(new RevisionResource(
-        changes.parse(change1.getId()), ins.getPatchSet()), input);
+    gApi.changes().id(change1.getId().get()).current()
+      .review(new ReviewInput().label("Code-Review", 1));
 
     // verify that query with user1 will return results.
     requestContext.setContext(newRequestContext(userId));
-    assertResultEquals(change1, queryOne("label:Code-Review=+1,group1"));
-    assertResultEquals(change1, queryOne("label:Code-Review=+1,group=group1"));
-    assertResultEquals(change1, queryOne("label:Code-Review=+1,user=user1"));
-    assertThat(query("label:Code-Review=+1,user=user2")).isEmpty();
-    assertThat(query("label:Code-Review=+1,group=group2")).isEmpty();
+    assertQuery("label:Code-Review=+1,group1", change1);
+    assertQuery("label:Code-Review=+1,group=group1", change1);
+    assertQuery("label:Code-Review=+1,user=user1", change1);
+    assertQuery("label:Code-Review=+1,user=user2");
+    assertQuery("label:Code-Review=+1,group=group2");
   }
 
   @Test
   public void limit() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change last = null;
     int n = 5;
     for (int i = 0; i < n; i++) {
-      last = newChange(repo, null, null, null, null).insert();
+      last = insert(newChange(repo, null, null, null, null));
     }
 
-    List<ChangeInfo> results;
     for (int i = 1; i <= n + 2; i++) {
       int expectedSize;
       Boolean expectedMoreChanges;
@@ -605,272 +726,222 @@
         expectedSize = n;
         expectedMoreChanges = null;
       }
-      results = query("status:new limit:" + i);
-      String msg = "i=" + i;
-      assert_().withFailureMessage(msg).that(results).hasSize(expectedSize);
-      assertResultEquals(last, results.get(0));
-      assert_().withFailureMessage(msg)
-          .that(results.get(results.size() - 1)._moreChanges)
+      String q = "status:new limit:" + i;
+      List<ChangeInfo> results = newQuery(q).get();
+      assertThat(results).named(q).hasSize(expectedSize);
+      assertThat(results.get(results.size() - 1)._moreChanges).named(q)
           .isEqualTo(expectedMoreChanges);
+      assertThat(results.get(0)._number).isEqualTo(last.getId().get());
     }
   }
 
   @Test
   public void start() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 2; i++) {
-      changes.add(newChange(repo, null, null, null, null).insert());
+      changes.add(insert(newChange(repo, null, null, null, null)));
     }
 
-    QueryChanges q;
-    List<ChangeInfo> results;
-    results = query("status:new");
-    assertThat(results).hasSize(2);
-    assertResultEquals(changes.get(1), results.get(0));
-    assertResultEquals(changes.get(0), results.get(1));
-
-    q = newQuery("status:new");
-    q.setStart(1);
-    results = query(q);
-    assertThat(results).hasSize(1);
-    assertResultEquals(changes.get(0), results.get(0));
-
-    q = newQuery("status:new");
-    q.setStart(2);
-    results = query(q);
-    assertThat(results).isEmpty();
-
-    q = newQuery("status:new");
-    q.setStart(3);
-    results = query(q);
-    assertThat(results).isEmpty();
+    assertQuery("status:new", changes.get(1), changes.get(0));
+    assertQuery(newQuery("status:new").withStart(1), changes.get(0));
+    assertQuery(newQuery("status:new").withStart(2));
+    assertQuery(newQuery("status:new").withStart(3));
   }
 
   @Test
   public void startWithLimit() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 3; i++) {
-      changes.add(newChange(repo, null, null, null, null).insert());
+      changes.add(insert(newChange(repo, null, null, null, null)));
     }
 
-    QueryChanges q;
-    List<ChangeInfo> results;
-    results = query("status:new limit:2");
-    assertThat(results).hasSize(2);
-    assertResultEquals(changes.get(2), results.get(0));
-    assertResultEquals(changes.get(1), results.get(1));
+    assertQuery("status:new limit:2", changes.get(2), changes.get(1));
+    assertQuery(
+        newQuery("status:new limit:2").withStart(1),
+        changes.get(1), changes.get(0));
+    assertQuery(newQuery("status:new limit:2").withStart(2), changes.get(0));
+    assertQuery(newQuery("status:new limit:2").withStart(3));
+  }
 
-    q = newQuery("status:new limit:2");
-    q.setStart(1);
-    results = query(q);
-    assertThat(results).hasSize(2);
-    assertResultEquals(changes.get(1), results.get(0));
-    assertResultEquals(changes.get(0), results.get(1));
+  @Test
+  public void maxPages() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(newChange(repo, null, null, null, null));
 
-    q = newQuery("status:new limit:2");
-    q.setStart(2);
-    results = query(q);
-    assertThat(results).hasSize(1);
-    assertResultEquals(changes.get(0), results.get(0));
-
-    q = newQuery("status:new limit:2");
-    q.setStart(3);
-    results = query(q);
-    assertThat(results).isEmpty();
+    QueryRequest query = newQuery("status:new").withLimit(10);
+    assertQuery(query, change);
+    assertQuery(query.withStart(1));
+    assertQuery(query.withStart(99));
+    assertBadQuery(query.withStart(100));
+    assertQuery(query.withLimit(100).withStart(100));
   }
 
   @Test
   public void updateOrder() throws Exception {
-    clockStepMs = MILLISECONDS.convert(2, MINUTES);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestTimeUtil.resetWithClockStep(2, MINUTES);
+    TestRepository<Repo> repo = createProject("repo");
     List<ChangeInserter> inserters = Lists.newArrayList();
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 5; i++) {
       inserters.add(newChange(repo, null, null, null, null));
-      changes.add(inserters.get(i).insert());
+      changes.add(insert(inserters.get(i)));
     }
 
     for (int i : ImmutableList.of(2, 0, 1, 4, 3)) {
-      ReviewInput input = new ReviewInput();
-      input.message = "modifying " + i;
-      postReview.apply(
-          new RevisionResource(
-            this.changes.parse(changes.get(i).getId()),
-            inserters.get(i).getPatchSet()),
-          input);
-      changes.set(i, db.changes().get(changes.get(i).getId()));
+      gApi.changes().id(changes.get(i).getId().get()).current()
+          .review(new ReviewInput().message("modifying " + i));
     }
 
-    List<ChangeInfo> results = query("status:new");
-    assertThat(results).hasSize(5);
-    assertResultEquals(changes.get(3), results.get(0));
-    assertResultEquals(changes.get(4), results.get(1));
-    assertResultEquals(changes.get(1), results.get(2));
-    assertResultEquals(changes.get(0), results.get(3));
-    assertResultEquals(changes.get(2), results.get(4));
+    assertQuery(
+        "status:new",
+        changes.get(3),
+        changes.get(4),
+        changes.get(1),
+        changes.get(0),
+        changes.get(2));
   }
 
   @Test
   public void updatedOrderWithMinuteResolution() throws Exception {
-    clockStepMs = MILLISECONDS.convert(2, MINUTES);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestTimeUtil.resetWithClockStep(2, MINUTES);
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(ins1);
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
-    assertThat(lastUpdatedMs(change1) < lastUpdatedMs(change2)).isTrue();
+    assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
+    assertQuery("status:new", change2, change1);
 
-    List<ChangeInfo> results;
-    results = query("status:new");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-
-    ReviewInput input = new ReviewInput();
-    input.message = "toplevel";
-    postReview.apply(new RevisionResource(
-        changes.parse(change1.getId()), ins1.getPatchSet()), input);
+    gApi.changes().id(change1.getId().get()).current()
+        .review(new ReviewInput());
     change1 = db.changes().get(change1.getId());
 
-    assertThat(lastUpdatedMs(change1) > lastUpdatedMs(change2)).isTrue();
-    assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2)
-        > MILLISECONDS.convert(1, MINUTES)).isTrue();
+    assertThat(lastUpdatedMs(change1)).isGreaterThan(lastUpdatedMs(change2));
+    assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2))
+        .isGreaterThan(MILLISECONDS.convert(1, MINUTES));
 
-    results = query("status:new");
-    assertThat(results).hasSize(2);
     // change1 moved to the top.
-    assertResultEquals(change1, results.get(0));
-    assertResultEquals(change2, results.get(1));
+    assertQuery("status:new", change1, change2);
   }
 
   @Test
   public void updatedOrderWithSubMinuteResolution() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(ins1);
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
-    assertThat(lastUpdatedMs(change1) < lastUpdatedMs(change2)).isTrue();
+    assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
 
-    List<ChangeInfo> results;
-    results = query("status:new");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery("status:new", change2, change1);
 
-    ReviewInput input = new ReviewInput();
-    input.message = "toplevel";
-    postReview.apply(new RevisionResource(
-        changes.parse(change1.getId()), ins1.getPatchSet()), input);
+    gApi.changes().id(change1.getId().get()).current()
+        .review(new ReviewInput());
     change1 = db.changes().get(change1.getId());
 
-    assertThat(lastUpdatedMs(change1) > lastUpdatedMs(change2)).isTrue();
-    assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2)
-        < MILLISECONDS.convert(1, MINUTES)).isTrue();
+    assertThat(lastUpdatedMs(change1)).isGreaterThan(lastUpdatedMs(change2));
+    assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2))
+        .isLessThan(MILLISECONDS.convert(1, MINUTES));
 
-    results = query("status:new");
-    assertThat(results).hasSize(2);
     // change1 moved to the top.
-    assertResultEquals(change1, results.get(0));
-    assertResultEquals(change2, results.get(1));
+    assertQuery("status:new", change1, change2);
   }
 
   @Test
   public void filterOutMoreThanOnePageOfResults() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change = newChange(repo, null, null, userId.get(), null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(newChange(repo, null, null, userId.get(), null));
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     for (int i = 0; i < 5; i++) {
-      newChange(repo, null, null, user2, null).insert();
+      insert(newChange(repo, null, null, user2, null));
     }
 
-    assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
-    assertResultEquals(change,
-        queryOne("status:new ownerin:Administrators limit:2"));
+    assertQuery("status:new ownerin:Administrators", change);
+    assertQuery("status:new ownerin:Administrators limit:2", change);
   }
 
   @Test
   public void filterOutAllResults() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     for (int i = 0; i < 5; i++) {
-      newChange(repo, null, null, user2, null).insert();
+      insert(newChange(repo, null, null, user2, null));
     }
 
-    assertThat(query("status:new ownerin:Administrators")).isEmpty();
-    assertThat(query("status:new ownerin:Administrators limit:2")).isEmpty();
+    assertQuery("status:new ownerin:Administrators");
+    assertQuery("status:new ownerin:Administrators limit:2");
   }
 
   @Test
   public void byFileExact() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
-    assertThat(query("file:file")).isEmpty();
-    assertResultEquals(change, queryOne("file:dir"));
-    assertResultEquals(change, queryOne("file:file1"));
-    assertResultEquals(change, queryOne("file:file2"));
-    assertResultEquals(change, queryOne("file:dir/file1"));
-    assertResultEquals(change, queryOne("file:dir/file2"));
+    assertQuery("file:file");
+    assertQuery("file:dir", change);
+    assertQuery("file:file1", change);
+    assertQuery("file:file2", change);
+    assertQuery("file:dir/file1", change);
+    assertQuery("file:dir/file2", change);
   }
 
   @Test
   public void byFileRegex() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
-    assertThat(query("file:.*file.*")).isEmpty();
-    assertThat(query("file:^file.*")).isEmpty(); // Whole path only.
-    assertResultEquals(change, queryOne("file:^dir.file.*"));
+    assertQuery("file:.*file.*");
+    assertQuery("file:^file.*"); // Whole path only.
+    assertQuery("file:^dir.file.*", change);
   }
 
   @Test
   public void byPathExact() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
-    assertThat(query("path:file")).isEmpty();
-    assertThat(query("path:dir")).isEmpty();
-    assertThat(query("path:file1")).isEmpty();
-    assertThat(query("path:file2")).isEmpty();
-    assertResultEquals(change, queryOne("path:dir/file1"));
-    assertResultEquals(change, queryOne("path:dir/file2"));
+    assertQuery("path:file");
+    assertQuery("path:dir");
+    assertQuery("path:file1");
+    assertQuery("path:file2");
+    assertQuery("path:dir/file1", change);
+    assertQuery("path:dir/file2", change);
   }
 
   @Test
   public void byPathRegex() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
         .create());
-    Change change = newChange(repo, commit, null, null, null).insert();
+    Change change = insert(newChange(repo, commit, null, null, null));
 
-    assertThat(query("path:.*file.*")).isEmpty();
-    assertResultEquals(change, queryOne("path:^dir.file.*"));
+    assertQuery("path:.*file.*");
+    assertQuery("path:^dir.file.*", change);
   }
 
   @Test
   public void byComment() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
-    Change change = ins.insert();
+    Change change = insert(ins);
 
     ReviewInput input = new ReviewInput();
     input.message = "toplevel";
@@ -879,99 +950,75 @@
     comment.message = "inline";
     input.comments = ImmutableMap.<String, List<ReviewInput.CommentInput>> of(
         Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput> of(comment));
-    postReview.apply(new RevisionResource(
-        changes.parse(change.getId()), ins.getPatchSet()), input);
+    gApi.changes().id(change.getId().get()).current().review(input);
 
-    assertThat(query("comment:foo")).isEmpty();
-    assertResultEquals(change, queryOne("comment:toplevel"));
-    assertResultEquals(change, queryOne("comment:inline"));
+    assertQuery("comment:foo");
+    assertQuery("comment:toplevel", change);
+    assertQuery("comment:inline", change);
   }
 
   @Test
   public void byAge() throws Exception {
-    long thirtyHours = MILLISECONDS.convert(30, HOURS);
-    clockStepMs = thirtyHours;
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-    clockStepMs = 0; // Queried by AgePredicate constructor.
+    long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
+    TestTimeUtil.resetWithClockStep(thirtyHoursInMs, MILLISECONDS);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    // Queried by AgePredicate constructor.
+    TestTimeUtil.setClockStep(0, MILLISECONDS);
     long now = TimeUtil.nowMs();
-    assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1)).isEqualTo(thirtyHours);
-    assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHours);
+    assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1))
+        .isEqualTo(thirtyHoursInMs);
+    assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHoursInMs);
     assertThat(TimeUtil.nowMs()).isEqualTo(now);
 
-    assertThat(query("-age:1d")).isEmpty();
-    assertThat(query("-age:" + (30 * 60 - 1) + "m")).isEmpty();
-    assertResultEquals(change2, queryOne("-age:2d"));
-
-    List<ChangeInfo> results;
-    results = query("-age:3d");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-
-    assertThat(query("age:3d")).isEmpty();
-    assertResultEquals(change1, queryOne("age:2d"));
-
-    results = query("age:1d");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery("-age:1d");
+    assertQuery("-age:" + (30 * 60 - 1) + "m");
+    assertQuery("-age:2d", change2);
+    assertQuery("-age:3d", change2, change1);
+    assertQuery("age:3d");
+    assertQuery("age:2d", change1);
+    assertQuery("age:1d", change2, change1);
   }
 
   @Test
   public void byBefore() throws Exception {
-    clockStepMs = MILLISECONDS.convert(30, HOURS);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-    clockStepMs = 0;
+    TestTimeUtil.resetWithClockStep(30, HOURS);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    TestTimeUtil.setClockStep(0, MILLISECONDS);
 
-    assertThat(query("before:2009-09-29")).isEmpty();
-    assertThat(query("before:2009-09-30")).isEmpty();
-    assertThat(query("before:\"2009-09-30 16:59:00 -0400\"")).isEmpty();
-    assertThat(query("before:\"2009-09-30 20:59:00 -0000\"")).isEmpty();
-    assertThat(query("before:\"2009-09-30 20:59:00\"")).isEmpty();
-    assertResultEquals(change1,
-        queryOne("before:\"2009-09-30 17:02:00 -0400\""));
-    assertResultEquals(change1,
-        queryOne("before:\"2009-10-01 21:02:00 -0000\""));
-    assertResultEquals(change1,
-        queryOne("before:\"2009-10-01 21:02:00\""));
-    assertResultEquals(change1, queryOne("before:2009-10-01"));
-
-    List<ChangeInfo> results;
-    results = query("before:2009-10-03");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery("before:2009-09-29");
+    assertQuery("before:2009-09-30");
+    assertQuery("before:\"2009-09-30 16:59:00 -0400\"");
+    assertQuery("before:\"2009-09-30 20:59:00 -0000\"");
+    assertQuery("before:\"2009-09-30 20:59:00\"");
+    assertQuery("before:\"2009-09-30 17:02:00 -0400\"", change1);
+    assertQuery("before:\"2009-10-01 21:02:00 -0000\"", change1);
+    assertQuery("before:\"2009-10-01 21:02:00\"", change1);
+    assertQuery("before:2009-10-01", change1);
+    assertQuery("before:2009-10-03", change2, change1);
   }
 
   @Test
   public void byAfter() throws Exception {
-    clockStepMs = MILLISECONDS.convert(30, HOURS);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-    clockStepMs = 0;
+    TestTimeUtil.resetWithClockStep(30, HOURS);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    TestTimeUtil.setClockStep(0, MILLISECONDS);
 
-    assertThat(query("after:2009-10-03")).isEmpty();
-    assertResultEquals(change2,
-        queryOne("after:\"2009-10-01 20:59:59 -0400\""));
-    assertResultEquals(change2,
-        queryOne("after:\"2009-10-01 20:59:59 -0000\""));
-    assertResultEquals(change2, queryOne("after:2009-10-01"));
-
-    List<ChangeInfo> results;
-    results = query("after:2009-09-30");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery("after:2009-10-03");
+    assertQuery("after:\"2009-10-01 20:59:59 -0400\"", change2);
+    assertQuery("after:\"2009-10-01 20:59:59 -0000\"", change2);
+    assertQuery("after:2009-10-01", change2);
+    assertQuery("after:2009-09-30", change2, change1);
   }
 
   @Test
   public void bySize() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
 
     // added = 3, deleted = 0, delta = 3
     RevCommit commit1 = repo.parseBody(
@@ -980,37 +1027,58 @@
     RevCommit commit2 = repo.parseBody(
         repo.commit().parent(commit1).add("file1", "foo").create());
 
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
-    assertThat(query("added:>4")).isEmpty();
-    assertResultEquals(change1, queryOne("added:3"));
-    assertResultEquals(change1, queryOne("added:>2"));
-    assertResultEquals(change1, queryOne("added:>=3"));
-    assertResultEquals(change2, queryOne("added:<1"));
-    assertResultEquals(change2, queryOne("added:<=0"));
+    assertQuery("added:>4");
+    assertQuery("-added:<=4");
 
-    assertThat(query("deleted:>3")).isEmpty();
-    assertResultEquals(change2, queryOne("deleted:2"));
-    assertResultEquals(change2, queryOne("deleted:>1"));
-    assertResultEquals(change2, queryOne("deleted:>=2"));
-    assertResultEquals(change1, queryOne("deleted:<1"));
-    assertResultEquals(change1, queryOne("deleted:<=0"));
+    assertQuery("added:3", change1);
+    assertQuery("-(added:<3 OR added>3)", change1);
+
+    assertQuery("added:>2", change1);
+    assertQuery("-added:<=2", change1);
+
+    assertQuery("added:>=3", change1);
+    assertQuery("-added:<3", change1);
+
+    assertQuery("added:<1", change2);
+    assertQuery("-added:>=1", change2);
+
+    assertQuery("added:<=0", change2);
+    assertQuery("-added:>0", change2);
+
+    assertQuery("deleted:>3");
+    assertQuery("-deleted:<=3");
+
+    assertQuery("deleted:2", change2);
+    assertQuery("-(deleted:<2 OR deleted>2)", change2);
+
+    assertQuery("deleted:>1", change2);
+    assertQuery("-deleted:<=1", change2);
+
+    assertQuery("deleted:>=2", change2);
+    assertQuery("-deleted:<2", change2);
+
+    assertQuery("deleted:<1", change1);
+    assertQuery("-deleted:>=1", change1);
+
+    assertQuery("deleted:<=0", change1);
 
     for (String str : Lists.newArrayList("delta", "size")) {
-      assertThat(query(str + ":<2")).isEmpty();
-      assertResultEquals(change1, queryOne(str + ":3"));
-      assertResultEquals(change1, queryOne(str + ":>2"));
-      assertResultEquals(change1, queryOne(str + ":>=3"));
-      assertResultEquals(change2, queryOne(str + ":<3"));
-      assertResultEquals(change2, queryOne(str + ":<=2"));
+      assertQuery(str + ":<2");
+      assertQuery(str + ":3", change1);
+      assertQuery(str + ":>2", change1);
+      assertQuery(str + ":>=3", change1);
+      assertQuery(str + ":<3", change2);
+      assertQuery(str + ":<=2", change2);
     }
   }
 
   private List<Change> setUpHashtagChanges() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
 
     HashtagsInput in = new HashtagsInput();
     in.add = ImmutableSet.of("foo");
@@ -1026,122 +1094,311 @@
   public void byHashtagWithNotedb() throws Exception {
     assume().that(notesMigration.enabled()).isTrue();
     List<Change> changes = setUpHashtagChanges();
-    List<ChangeInfo> results = query("hashtag:foo");
-    assertThat(results).hasSize(2);
-    assertResultEquals(changes.get(1), results.get(0));
-    assertResultEquals(changes.get(0), results.get(1));
-    assertResultEquals(changes.get(1), queryOne("hashtag:bar"));
-    assertResultEquals(changes.get(1), queryOne("hashtag:\"a tag\""));
-    assertResultEquals(changes.get(1), queryOne("hashtag:\"a tag \""));
-    assertResultEquals(changes.get(1), queryOne("hashtag:\" a tag \""));
-    assertResultEquals(changes.get(1), queryOne("hashtag:\"#a tag\""));
-    assertResultEquals(changes.get(1), queryOne("hashtag:\"# #a tag\""));
+    assertQuery("hashtag:foo", changes.get(1), changes.get(0));
+    assertQuery("hashtag:bar", changes.get(1));
+    assertQuery("hashtag:\"a tag\"", changes.get(1));
+    assertQuery("hashtag:\"a tag \"", changes.get(1));
+    assertQuery("hashtag:\" a tag \"", changes.get(1));
+    assertQuery("hashtag:\"#a tag\"", changes.get(1));
+    assertQuery("hashtag:\"# #a tag\"", changes.get(1));
   }
 
   @Test
   public void byHashtagWithoutNotedb() throws Exception {
     assume().that(notesMigration.enabled()).isFalse();
     setUpHashtagChanges();
-    assertThat(query("hashtag:foo")).isEmpty();
-    assertThat(query("hashtag:bar")).isEmpty();
-    assertThat(query("hashtag:\" bar \"")).isEmpty();
-    assertThat(query("hashtag:\"a tag\"")).isEmpty();
-    assertThat(query("hashtag:\" a tag \"")).isEmpty();
-    assertThat(query("hashtag:#foo")).isEmpty();
-    assertThat(query("hashtag:\"# #foo\"")).isEmpty();
+    assertQuery("hashtag:foo");
+    assertQuery("hashtag:bar");
+    assertQuery("hashtag:\" bar \"");
+    assertQuery("hashtag:\"a tag\"");
+    assertQuery("hashtag:\" a tag \"");
+    assertQuery("hashtag:#foo");
+    assertQuery("hashtag:\"# #foo\"");
   }
 
   @Test
   public void byDefault() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
 
-    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change1 = insert(newChange(repo, null, null, null, null));
 
     RevCommit commit2 = repo.parseBody(
         repo.commit().message("foosubject").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
     RevCommit commit3 = repo.parseBody(
         repo.commit()
         .add("Foo.java", "foo contents")
         .create());
-    Change change3 = newChange(repo, commit3, null, null, null).insert();
+    Change change3 = insert(newChange(repo, commit3, null, null, null));
 
     ChangeInserter ins4 = newChange(repo, null, null, null, null);
-    Change change4 = ins4.insert();
+    Change change4 = insert(ins4);
     ReviewInput ri4 = new ReviewInput();
     ri4.message = "toplevel";
     ri4.labels = ImmutableMap.<String, Short> of("Code-Review", (short) 1);
-    postReview.apply(new RevisionResource(
-        changes.parse(change4.getId()), ins4.getPatchSet()), ri4);
+    gApi.changes().id(change4.getId().get()).current().review(ri4);
 
     ChangeInserter ins5 = newChange(repo, null, null, null, null);
     Change change5 = ins5.getChange();
     change5.setTopic("feature5");
-    ins5.insert();
+    insert(ins5);
 
-    Change change6 = newChange(repo, null, null, null, "branch6").insert();
+    Change change6 = insert(newChange(repo, null, null, null, "branch6"));
 
-    assertResultEquals(change1,
-        queryOne(Integer.toString(change1.getId().get())));
-    assertResultEquals(change1, queryOne(ChangeTriplet.format(change1)));
-    assertResultEquals(change2, queryOne("foosubject"));
-    assertResultEquals(change3, queryOne("Foo.java"));
-    assertResultEquals(change4, queryOne("Code-Review+1"));
-    assertResultEquals(change4, queryOne("toplevel"));
-    assertResultEquals(change5, queryOne("feature5"));
-    assertResultEquals(change6, queryOne("branch6"));
-    assertResultEquals(change6, queryOne("refs/heads/branch6"));
+    assertQuery(change1.getId().get(), change1);
+    assertQuery(ChangeTriplet.format(change1), change1);
+    assertQuery("foosubject", change2);
+    assertQuery("Foo.java", change3);
+    assertQuery("Code-Review+1", change4);
+    assertQuery("toplevel", change4);
+    assertQuery("feature5", change5);
+    assertQuery("branch6", change6);
+    assertQuery("refs/heads/branch6", change6);
 
-    assertThat(query("user@example.com")).hasSize(6);
-    assertThat(query("repo")).hasSize(6);
+    Change[] expected =
+        new Change[] {change6, change5, change4, change3, change2, change1};
+    assertQuery("user@example.com", expected);
+    assertQuery("repo", expected);
   }
 
   @Test
   public void implicitVisibleTo() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.DRAFT);
-    ins2.insert();
+    insert(ins2);
 
     String q = "project:repo";
-    List<ChangeInfo> results = query(q);
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery(q, change2, change1);
 
     // Second user cannot see first user's drafts.
     requestContext.setContext(newRequestContext(accountManager
         .authenticate(AuthRequest.forUser("anotheruser")).getAccountId()));
-    assertResultEquals(change1, queryOne(q));
+    assertQuery(q, change1);
   }
 
   @Test
   public void explicitVisibleTo() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, userId.get(), null));
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
     change2.setStatus(Change.Status.DRAFT);
-    ins2.insert();
+    insert(ins2);
 
     String q = "project:repo";
-    List<ChangeInfo> results = query(q);
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    assertQuery(q, change2, change1);
 
     // Second user cannot see first user's drafts.
     Account.Id user2 = accountManager
         .authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId();
-    assertResultEquals(change1, queryOne(q + " visibleto:" + user2.get()));
+    assertQuery(q + " visibleto:" + user2.get(), change1);
   }
 
+  @Test
+  public void byCommentBy() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+
+    ReviewInput input = new ReviewInput();
+    input.message = "toplevel";
+    ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
+    comment.line = 1;
+    comment.message = "inline";
+    input.comments = ImmutableMap.<String, List<ReviewInput.CommentInput>> of(
+        Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput> of(comment));
+    gApi.changes().id(change1.getId().get()).current().review(input);
+
+    input = new ReviewInput();
+    input.message = "toplevel";
+    gApi.changes().id(change2.getId().get()).current().review(input);
+
+    assertQuery("commentby:" + userId.get(), change2, change1);
+    assertQuery("commentby:" + user2);
+  }
+
+  @Test
+  public void byFrom() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    ChangeInserter ins2 = newChange(repo, null, null, user2, null);
+    Change change2 = insert(ins2);
+
+    ReviewInput input = new ReviewInput();
+    input.message = "toplevel";
+    ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
+    comment.line = 1;
+    comment.message = "inline";
+    input.comments = ImmutableMap.<String, List<ReviewInput.CommentInput>> of(
+        Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput> of(comment));
+    gApi.changes().id(change2.getId().get()).current().review(input);
+
+    assertQuery("from:" + userId.get(), change2, change1);
+    assertQuery("from:" + user2, change2);
+  }
+
+  @Test
+  public void conflicts() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    RevCommit commit1 = repo.parseBody(
+        repo.commit()
+            .add("file1", "contents1")
+            .add("dir/file2", "contents2")
+            .add("dir/file3", "contents3")
+            .create());
+    RevCommit commit2 = repo.parseBody(
+        repo.commit()
+            .add("file1", "contents1")
+            .create());
+    RevCommit commit3 = repo.parseBody(
+        repo.commit()
+            .add("dir/file2", "contents2 different")
+            .create());
+    RevCommit commit4 = repo.parseBody(
+        repo.commit()
+            .add("file4", "contents4")
+            .create());
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
+    Change change3 = insert(newChange(repo, commit3, null, null, null));
+    Change change4 = insert(newChange(repo, commit4, null, null, null));
+
+    assertQuery("conflicts:" + change1.getId().get(), change3);
+    assertQuery("conflicts:" + change2.getId().get());
+    assertQuery("conflicts:" + change3.getId().get(), change1);
+    assertQuery("conflicts:" + change4.getId().get());
+  }
+
+  @Test
+  public void reviewedBy() throws Exception {
+    TestTimeUtil.resetWithClockStep(2, MINUTES);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    Change change3 = insert(newChange(repo, null, null, null, null));
+
+    gApi.changes()
+      .id(change1.getId().get())
+      .current()
+      .review(new ReviewInput().message("comment"));
+
+    Account.Id user2 = accountManager
+        .authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId();
+    requestContext.setContext(newRequestContext(user2));
+
+    gApi.changes()
+        .id(change2.getId().get())
+        .current()
+        .review(new ReviewInput().message("comment"));
+
+    PatchSet.Id ps3_1 = change3.currentPatchSetId();
+    change3 = newPatchSet(repo, change3);
+    assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1);
+    // Response to previous patch set still counts as reviewing.
+    gApi.changes()
+        .id(change3.getId().get())
+        .revision(ps3_1.get())
+        .review(new ReviewInput().message("comment"));
+
+    List<ChangeInfo> actual;
+    actual = assertQuery(
+        newQuery("is:reviewed").withOption(REVIEWED),
+        change3, change2);
+    assertThat(actual.get(0).reviewed).isTrue();
+    assertThat(actual.get(1).reviewed).isTrue();
+
+    actual = assertQuery(
+        newQuery("-is:reviewed").withOption(REVIEWED),
+        change1);
+    assertThat(actual.get(0).reviewed).isNull();
+
+    actual = assertQuery("reviewedby:" + userId.get());
+
+    actual = assertQuery(
+        newQuery("reviewedby:" + user2.get()).withOption(REVIEWED),
+        change3, change2);
+    assertThat(actual.get(0).reviewed).isTrue();
+    assertThat(actual.get(1).reviewed).isTrue();
+  }
+
+  @Test
+  public void byCommitsOnBranchNotMerged() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    int n = 10;
+    List<String> shas = new ArrayList<>(n);
+    List<Integer> expectedIds = new ArrayList<>(n);
+    Branch.NameKey dest = null;
+    for (int i = 0; i < n; i++) {
+      ChangeInserter ins = newChange(repo, null, null, null, null);
+      insert(ins);
+      if (dest == null) {
+        dest = ins.getChange().getDest();
+      }
+      shas.add(ins.getPatchSet().getRevision().get());
+      expectedIds.add(ins.getChange().getId().get());
+    }
+
+    for (int i = 1; i <= 11; i++) {
+      Iterable<ChangeData> cds = internalChangeQuery.byCommitsOnBranchNotMerged(
+          repo.getRepository(), db, dest, shas, i);
+      Iterable<Integer> ids = FluentIterable.from(cds).transform(
+          new Function<ChangeData, Integer>() {
+            @Override
+            public Integer apply(ChangeData in) {
+              return in.getId().get();
+            }
+          });
+      String name = "limit " + i;
+      assertThat(ids).named(name).hasSize(n);
+      assertThat(ids).named(name)
+          .containsExactlyElementsIn(expectedIds);
+    }
+  }
+
+  @Test
+  public void prepopulatedFields() throws Exception {
+    assume().that(notesMigration.enabled()).isFalse();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(newChange(repo, null, null, null, null));
+
+    db = new DisabledReviewDb();
+    requestContext.setContext(newRequestContext(userId));
+    // Use QueryProcessor directly instead of API so we get ChangeDatas back.
+    List<ChangeData> cds = queryProcessor
+        .queryChanges(queryBuilder.parse(change.getId().toString()))
+        .changes();
+    assertThat(cds).hasSize(1);
+
+    ChangeData cd = cds.get(0);
+    cd.change();
+    cd.patchSets();
+    cd.currentApprovals();
+    cd.changedLines();
+    cd.reviewedBy();
+
+    // TODO(dborowitz): Swap out GitRepositoryManager somehow? Will probably be
+    // necessary for notedb anyway.
+    cd.isMergeable();
+
+    exception.expect(DisabledReviewDb.Disabled.class);
+    cd.messages();
+  }
+
+
   protected ChangeInserter newChange(
-      TestRepository<InMemoryRepository> repo,
+      TestRepository<Repo> repo,
       @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,
       @Nullable String branch) throws Exception {
     if (commit == null) {
@@ -1169,75 +1426,103 @@
 
     Change change = new Change(new Change.Key(key), id, ownerId,
         new Branch.NameKey(project, branch), TimeUtil.nowTs());
-    return changeFactory.create(
-        projectControlFactory.controlFor(project, userFactory.create(ownerId)),
-        change,
-        commit);
+    IdentifiedUser user = userFactory.create(Providers.of(db), ownerId);
+    RefControl refControl = projectControlFactory.controlFor(project, user)
+        .controlForRef(change.getDest());
+    return changeFactory.create(refControl, change, commit)
+        .setValidatePolicy(CommitValidators.Policy.NONE);
   }
 
-  protected void assertResultEquals(Change expected, ChangeInfo actual) {
-    assertThat(actual._number).isEqualTo(expected.getId().get());
-  }
-
-  protected void assertResultEquals(String message, Change expected,
-      ChangeInfo actual) {
-    assert_().withFailureMessage(message).that(actual._number)
-        .isEqualTo(expected.getId().get());
-  }
-
-  protected void assertBadQuery(Object query) throws Exception {
-    try {
-      query(query);
-      fail("expected BadRequestException for query: " + query);
-    } catch (BadRequestException e) {
-      // Expected.
+  protected Change insert(ChangeInserter ins) throws Exception {
+    try (BatchUpdate bu = updateFactory.create(
+        db, ins.getChange().getProject(), ins.getUser(),
+        ins.getChange().getCreatedOn())) {
+      bu.insertChange(ins);
+      bu.execute();
+      return ins.getChange();
     }
   }
 
-  protected TestRepository<InMemoryRepository> createProject(String name)
+  protected Change newPatchSet(TestRepository<Repo> repo, Change c)
       throws Exception {
-    CreateProject create = projectFactory.create(name);
-    create.apply(TLR, new ProjectInput());
+    // Add a new file so the patch set is not a trivial rebase, to avoid default
+    // Code-Review label copying.
+    int n = c.currentPatchSetId().get() + 1;
+    RevCommit commit = repo.parseBody(
+        repo.commit()
+            .message("message")
+            .add("file" + n, "contents " + n)
+            .create());
+    RefControl ctl = projectControlFactory.controlFor(c.getProject(), user)
+        .controlForRef(c.getDest());
+
+    PatchSetInserter inserter = patchSetFactory.create(
+          ctl, new PatchSet.Id(c.getId(), n), commit)
+        .setSendMail(false)
+        .setRunHooks(false)
+        .setValidatePolicy(CommitValidators.Policy.NONE);
+    try (BatchUpdate bu = updateFactory.create(
+        db, c.getProject(), user, TimeUtil.nowTs());
+        ObjectInserter oi = repo.getRepository().newObjectInserter()) {
+      bu.setRepository(repo.getRepository(), repo.getRevWalk(), oi);
+      bu.addOp(c.getId(), inserter);
+      bu.execute();
+    }
+
+    return inserter.getChange();
+  }
+
+  protected void assertBadQuery(Object query) throws Exception {
+    assertBadQuery(newQuery(query));
+  }
+
+  protected void assertBadQuery(QueryRequest query) throws Exception {
+    exception.expect(BadRequestException.class);
+    query.get();
+  }
+
+  protected TestRepository<Repo> createProject(String name) throws Exception {
+    gApi.projects().create(name).get();
     return new TestRepository<>(
         repoManager.openRepository(new Project.NameKey(name)));
   }
 
-  protected QueryChanges newQuery(Object query) {
-    QueryChanges q = queryProvider.get();
-    q.addQuery(query.toString());
-    return q;
+  protected QueryRequest newQuery(Object query) {
+    return gApi.changes().query(query.toString());
   }
 
-  @SuppressWarnings({"rawtypes", "unchecked"})
-  protected List<ChangeInfo> query(QueryChanges q) throws Exception {
-    Object result = q.apply(TLR);
-    assert_()
-        .withFailureMessage(
-            String.format("expected List<ChangeInfo>, found %s for [%s]",
-                result, q.getQuery(0))).that(result).isInstanceOf(List.class);
-    List results = (List) result;
-    if (!results.isEmpty()) {
-      assert_()
-          .withFailureMessage(
-              String.format("expected ChangeInfo, found %s for [%s]", result,
-                  q.getQuery(0))).that(results.get(0))
-          .isInstanceOf(ChangeInfo.class);
-    }
-    return (List<ChangeInfo>) result;
+  protected List<ChangeInfo> assertQuery(Object query, Change... changes)
+      throws Exception {
+    return assertQuery(newQuery(query), changes);
   }
 
-  protected List<ChangeInfo> query(Object query) throws Exception {
-    return query(newQuery(query));
+  protected List<ChangeInfo> assertQuery(QueryRequest query, Change... changes)
+      throws Exception {
+    List<ChangeInfo> result = query.get();
+    Iterable<Integer> ids = ids(result);
+    assertThat(ids).named(query.getQuery())
+        .containsExactlyElementsIn(ids(changes)).inOrder();
+    return result;
   }
 
-  protected ChangeInfo queryOne(Object query) throws Exception {
-    List<ChangeInfo> results = query(query);
-    assert_()
-        .withFailureMessage(
-            String.format(
-                "expected singleton List<ChangeInfo>, found %s for [%s]",
-                results, query)).that(results).hasSize(1);
-    return results.get(0);
+  protected static Iterable<Integer> ids(Change... changes) {
+    return FluentIterable.from(Arrays.asList(changes)).transform(
+        new Function<Change, Integer>() {
+          @Override
+          public Integer apply(Change in) {
+            return in.getId().get();
+          }
+        });
+  }
+
+  protected static Iterable<Integer> ids(Iterable<ChangeInfo> changes) {
+    return FluentIterable.from(changes).transform(
+        new Function<ChangeInfo, Integer>() {
+          @Override
+          public Integer apply(ChangeInfo in) {
+            return in._number;
+          }
+        });
   }
 
   protected static long lastUpdatedMs(Change c) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/ChangeDataTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/ChangeDataTest.java
new file mode 100644
index 0000000..ca1e2b1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testutil.TestChanges;
+
+import org.junit.Test;
+
+public class ChangeDataTest {
+  @Test
+  public void setPatchSetsClearsCurrentPatchSet() throws Exception {
+    ChangeData cd = ChangeData.createForTest(new Change.Id(1), 1);
+    cd.setChange(TestChanges.newChange(
+          new Project.NameKey("project"), new Account.Id(1000)));
+    PatchSet curr1 = cd.currentPatchSet();
+    int currId = curr1.getId().get();
+    PatchSet ps1 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 1));
+    PatchSet ps2 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 2));
+    cd.setPatchSets(ImmutableList.of(ps1, ps2));
+    PatchSet curr2 = cd.currentPatchSet();
+    assertThat(curr2).isNotSameAs(curr1);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 742c230..b503a13 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -14,14 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import static org.junit.Assert.assertTrue;
-
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -37,20 +35,20 @@
 
   @Test
   public void fullTextWithSpecialChars() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 =
         repo.parseBody(repo.commit().message("foo_bar_foo").create());
-    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    Change change1 = insert(newChange(repo, commit1, null, null, null));
     RevCommit commit2 =
         repo.parseBody(repo.commit().message("one.two.three").create());
-    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change2 = insert(newChange(repo, commit2, null, null, null));
 
-    assertTrue(query("message:foo_ba").isEmpty());
-    assertResultEquals(change1, queryOne("message:bar"));
-    assertResultEquals(change1, queryOne("message:foo_bar"));
-    assertResultEquals(change1, queryOne("message:foo bar"));
-    assertResultEquals(change2, queryOne("message:two"));
-    assertResultEquals(change2, queryOne("message:one.two"));
-    assertResultEquals(change2, queryOne("message:one two"));
+    assertQuery("message:foo_ba");
+    assertQuery("message:bar", change1);
+    assertQuery("message:foo_bar", change1);
+    assertQuery("message:foo bar", change1);
+    assertQuery("message:two", change2);
+    assertQuery("message:one.two", change2);
+    assertQuery("message:one two", change2);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
new file mode 100644
index 0000000..8b22ff5
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2015 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.server.query.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class LuceneQueryChangesV14Test extends LuceneQueryChangesTest {
+  @Override
+  protected Injector createInjector() {
+    Config luceneConfig = new Config(config);
+    InMemoryModule.setDefaults(luceneConfig);
+    // Latest version with a Lucene 4 index.
+    luceneConfig.setInt("index", "lucene", "testVersion", 14);
+    return Guice.createInjector(new InMemoryModule(luceneConfig));
+  }
+
+  @Override
+  @Ignore
+  @Test
+  public void byCommentBy() {
+    // Ignore.
+  }
+
+  @Override
+  @Ignore
+  @Test
+  public void byFrom() {
+    // Ignore.
+  }
+
+  @Override
+  @Ignore
+  @Test
+  public void byTopic() {
+    // Ignore.
+  }
+
+  @Override
+  @Ignore
+  @Test
+  public void reviewedBy() throws Exception {
+    // Ignore.
+  }
+
+  @Override
+  @Ignore
+  @Test
+  public void prepopulatedFields() throws Exception {
+    // Ignore.
+  }
+
+  @Test
+  public void isReviewed() throws Exception {
+    TestTimeUtil.resetWithClockStep(2, MINUTES);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(newChange(repo, null, null, null, null));
+    Change change2 = insert(newChange(repo, null, null, null, null));
+    Change change3 = insert(newChange(repo, null, null, null, null));
+
+    gApi.changes()
+      .id(change1.getId().get())
+      .current()
+      .review(new ReviewInput().message("comment"));
+
+    Account.Id user2 = accountManager
+        .authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId();
+    requestContext.setContext(newRequestContext(user2));
+
+    gApi.changes()
+        .id(change2.getId().get())
+        .current()
+        .review(ReviewInput.recommend());
+
+    PatchSet.Id ps3_1 = change3.currentPatchSetId();
+    change3 = newPatchSet(repo, change3);
+    assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1);
+    // Nonzero score on previous patch set does not count.
+    gApi.changes()
+        .id(change3.getId().get())
+        .revision(ps3_1.get())
+        .review(ReviewInput.recommend());
+
+    assertQuery("is:reviewed", change2);
+    assertQuery("-is:reviewed", change3, change1);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index 0c8157d..87b5322 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -56,13 +57,20 @@
   @Inject
   private InMemoryDatabase db;
 
+  private LifecycleManager lifecycle;
+
   @Before
   public void setUp() throws Exception {
+    lifecycle = new LifecycleManager();
     new InMemoryModule().inject(this);
+    lifecycle.start();
   }
 
   @After
   public void tearDown() throws Exception {
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
     InMemoryDatabase.drop(db);
   }
 
@@ -94,13 +102,10 @@
   private LabelTypes getLabelTypes() throws Exception {
     db.create();
     ProjectConfig c = new ProjectConfig(allProjects);
-    Repository repo = repoManager.openRepository(allProjects);
-    try {
+    try (Repository repo = repoManager.openRepository(allProjects)) {
       c.load(repo);
       return new LabelTypes(
           ImmutableList.copyOf(c.getLabelSections().values()));
-    } finally {
-      repo.close();
     }
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index 8686fe6..a161405 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -16,6 +16,8 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -24,7 +26,6 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -43,22 +44,29 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.UUID;
 
 public class SchemaUpdaterTest {
+  private LifecycleManager lifecycle;
   private InMemoryDatabase db;
 
   @Before
   public void setUp() throws Exception {
-    db = InMemoryDatabase.newDatabase();
+    lifecycle = new LifecycleManager();
+    db = InMemoryDatabase.newDatabase(lifecycle);
+    lifecycle.start();
   }
 
   @After
   public void tearDown() throws Exception {
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
     InMemoryDatabase.drop(db);
   }
 
@@ -67,7 +75,7 @@
       IOException {
     db.create();
 
-    final File site = new File(UUID.randomUUID().toString());
+    final Path site = Paths.get(UUID.randomUUID().toString());
     final SitePaths paths = new SitePaths(site);
     SchemaUpdater u = Guice.createInjector(new FactoryModule() {
       @Override
@@ -129,6 +137,6 @@
 
     db.assertSchemaVersion();
     final SystemConfig sc = db.getSystemConfig();
-    assertEquals(paths.site_path.getCanonicalPath(), sc.sitePath);
+    assertEquals(paths.site_path.toAbsolutePath().toString(), sc.sitePath);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
index 9c8e86a..c5d9151 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.tools.hooks;
 
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 
 import com.google.gerrit.server.util.HostPlatform;
 
@@ -30,10 +30,10 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.junit.Before;
-import org.junit.Rule;
+import org.junit.BeforeClass;
 import org.junit.Test;
-import org.junit.rules.TestName;
 
 import java.io.File;
 import java.io.IOException;
@@ -44,20 +44,14 @@
   private final String SOB1 = "Signed-off-by: J Author <ja@example.com>\n";
   private final String SOB2 = "Signed-off-by: J Committer <jc@example.com>\n";
 
-  @Rule public TestName test = new TestName();
-
-  private void skipIfWin32Platform() {
-    if (HostPlatform.isWin32()) {
-      System.err.println(" - Skipping " + test.getMethodName() + " on this system");
-      assumeTrue(false);
-    }
+  @BeforeClass
+  public static void skipIfWin32Platform() {
+    assume().that(HostPlatform.isWin32()).isFalse();
   }
 
   @Override
   @Before
   public void setUp() throws Exception {
-    skipIfWin32Platform();
-
     super.setUp();
     final Date when = author.getWhen();
     final TimeZone tz = author.getTimeZone();
@@ -443,15 +437,11 @@
 
       final RefUpdate ref = repository.updateRef(Constants.HEAD);
       ref.setNewObjectId(commitId);
-      switch (ref.forceUpdate()) {
-        case NEW:
-        case FAST_FORWARD:
-        case FORCED:
-        case NO_CHANGE:
-          break;
-        default:
-          fail(Constants.HEAD + " did not change: " + ref.getResult());
-      }
+      Result result = ref.forceUpdate();
+      assert_()
+        .withFailureMessage(Constants.HEAD + " did not change: " + ref.getResult())
+        .that(result)
+        .isAnyOf(Result.FAST_FORWARD, Result.FORCED, Result.NEW, Result.NO_CHANGE);
     }
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index 47ff6b0..f915d05 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -50,7 +50,7 @@
 
 package com.google.gerrit.server.tools.hooks;
 
-import static org.junit.Assert.fail;
+import static com.google.common.truth.Truth.assert_;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -99,44 +99,40 @@
       return hook;
     }
 
-    final String scproot = "com/google/gerrit/server/tools/root";
-    final String path = scproot + "/hooks/" + name;
+    String scproot = "com/google/gerrit/server/tools/root";
+    String path = scproot + "/hooks/" + name;
+    String errorMessage = "Cannot locate " + path + " in CLASSPATH";
     URL url = cl().getResource(path);
-    if (url == null) {
-      fail("Cannot locate " + path + " in CLASSPATH");
-    }
+    assert_()
+      .withFailureMessage(errorMessage)
+      .that(url).isNotNull();
 
-    if ("file".equals(url.getProtocol())) {
+    String protocol = url.getProtocol();
+    assert_()
+      .withFailureMessage("Cannot invoke " + url)
+      .that(protocol).isAnyOf("file", "jar");
+
+    if ("file".equals(protocol)) {
       hook = new File(url.getPath());
-      if (!hook.isFile()) {
-        fail("Cannot locate " + path + " in CLASSPATH");
-      }
+      assert_()
+        .withFailureMessage(errorMessage)
+        .that(hook.isFile()).isTrue();
       long time = hook.lastModified();
       hook.setExecutable(true);
       hook.setLastModified(time);
       hooks.put(name, hook);
-      return hook;
-    } else if ("jar".equals(url.getProtocol())) {
-      InputStream in = url.openStream();
-      try {
+    } else if ("jar".equals(protocol)) {
+      try (InputStream in = url.openStream()) {
         hook = File.createTempFile("hook_", ".sh");
         cleanup.add(hook);
-        FileOutputStream out = new FileOutputStream(hook);
-        try {
+        try (FileOutputStream out = new FileOutputStream(hook)) {
           ByteStreams.copy(in, out);
-        } finally {
-          out.close();
         }
-      } finally {
-        in.close();
       }
       hook.setExecutable(true);
       hooks.put(name, hook);
-      return hook;
-    } else {
-      fail("Cannot invoke " + url);
-      return null;
     }
+    return hook;
   }
 
   private ClassLoader cl() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
index 5ce8882..c060aaf 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
@@ -23,9 +23,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -34,6 +35,9 @@
 import java.net.UnknownHostException;
 
 public class SocketUtilTest {
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
   @Test
   public void testIsIPv6() throws UnknownHostException {
     final InetAddress ipv6 = getByName("1:2:3:4:5:6:7:8");
@@ -92,20 +96,20 @@
         parse("[foo.bar.example.com]:1234", 80));
     assertEquals(createUnresolved("foo.bar.example.com", 80), //
         parse("[foo.bar.example.com]", 80));
+  }
 
-    try {
-      parse("[:3", 80);
-      fail("did not throw exception");
-    } catch (IllegalArgumentException e) {
-      assertEquals("invalid IPv6: [:3", e.getMessage());
-    }
+  @Test
+  public void testParseInvalidIPv6() {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage("invalid IPv6: [:3");
+    parse("[:3", 80);
+  }
 
-    try {
-      parse("localhost:A", 80);
-      fail("did not throw exception");
-    } catch (IllegalArgumentException e) {
-      assertEquals("invalid port: localhost:A", e.getMessage());
-    }
+  @Test
+  public void testParseInvalidPort() {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage("invalid port: localhost:A");
+    parse("localhost:A", 80);
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index 67f56fc8..3945da7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -14,16 +14,19 @@
 
 package com.google.gerrit.server.util;
 
+import static org.easymock.EasyMock.createNiceMock;
 import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertEquals;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.lib.BlobBasedConfig;
@@ -32,17 +35,15 @@
 import org.junit.Test;
 
 import java.net.URI;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
 
 public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
   private static final String THIS_SERVER = "localhost";
-  private GitRepositoryManager repoManager;
+  private ProjectCache projectCache;
   private BlobBasedConfig bbc;
 
   @Override
@@ -50,16 +51,16 @@
   public void setUp() throws Exception {
     super.setUp();
 
-    repoManager = createStrictMock(GitRepositoryManager.class);
+    projectCache = createStrictMock(ProjectCache.class);
     bbc = createStrictMock(BlobBasedConfig.class);
   }
 
   private void doReplay() {
-    replay(repoManager, bbc);
+    replay(projectCache, bbc);
   }
 
   private void doVerify() {
-    verify(repoManager, bbc);
+    verify(projectCache, bbc);
   }
 
   @Test
@@ -87,7 +88,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
+    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -134,7 +135,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
+    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -159,9 +160,10 @@
     sectionsToReturn.put("a", new SubmoduleSection("ssh://review.source.com/a",
         "a", "."));
 
+    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
     execute(new Branch.NameKey(new Project.NameKey("super-project"),
         "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        new ArrayList<SubmoduleSubscription>());
+        expectedSubscriptions);
   }
 
   @Test
@@ -170,9 +172,10 @@
     sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
         "."));
 
+    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
     execute(new Branch.NameKey(new Project.NameKey("super-project"),
         "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        new ArrayList<SubmoduleSubscription>());
+        expectedSubscriptions);
   }
 
   @Test
@@ -181,9 +184,10 @@
     sectionsToReturn.put("project", new SubmoduleSection(
         "ssh://localhost/company/tools/project", "project", "."));
 
+    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
     execute(new Branch.NameKey(new Project.NameKey("super-project"),
         "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        new ArrayList<SubmoduleSubscription>());
+        expectedSubscriptions);
   }
 
   @Test
@@ -200,7 +204,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
+    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a/b"), "refs/heads/master"), "a/b"));
@@ -211,7 +215,7 @@
   private void execute(final Branch.NameKey superProjectBranch,
       final Map<String, SubmoduleSection> sectionsToReturn,
       final Map<String, String> reposToBeFound,
-      final List<SubmoduleSubscription> expectedSubscriptions) throws Exception {
+      final Set<SubmoduleSubscription> expectedSubscriptions) throws Exception {
     expect(bbc.getSubsections("submodule"))
         .andReturn(sectionsToReturn.keySet());
 
@@ -236,12 +240,11 @@
                 projectNameCandidate.length() - Constants.DOT_GIT_EXT.length());
           }
           if (reposToBeFound.containsValue(projectNameCandidate)) {
-            expect(repoManager.list()).andReturn(
-                new TreeSet<>(Collections.singletonList(
-                    new Project.NameKey(projectNameCandidate))));
+            expect(projectCache.get(new Project.NameKey(projectNameCandidate)))
+                .andReturn(createNiceMock(ProjectState.class));
           } else {
-            expect(repoManager.list()).andReturn(
-                new TreeSet<>(Collections.<Project.NameKey> emptyList()));
+            expect(projectCache.get(new Project.NameKey(projectNameCandidate)))
+                .andReturn(null);
           }
         }
       }
@@ -250,10 +253,10 @@
     doReplay();
 
     final SubmoduleSectionParser ssp =
-        new SubmoduleSectionParser(bbc, THIS_SERVER, superProjectBranch,
-            repoManager);
+        new SubmoduleSectionParser(projectCache, bbc, THIS_SERVER,
+            superProjectBranch);
 
-    List<SubmoduleSubscription> returnedSubscriptions = ssp.parseAllSections();
+    Set<SubmoduleSubscription> returnedSubscriptions = ssp.parseAllSections();
 
     doVerify();
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
index 707dd12..d769bcc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
@@ -20,6 +20,7 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 
 import org.junit.runner.Runner;
@@ -28,6 +29,7 @@
 import org.junit.runners.model.FrameworkMethod;
 import org.junit.runners.model.InitializationError;
 
+import java.lang.annotation.Annotation;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.lang.reflect.Field;
@@ -78,6 +80,9 @@
  *
  * Additionally, config values used by <strong>default</strong> can be set
  * in a method annotated with {@code @ConfigSuite.Default}.
+ * <p>
+ * The name of the config method corresponding to the currently-running test can
+ * be stored in a field annotated with {@code @ConfigSuite.Name}.
  */
 public class ConfigSuite extends Suite {
   private static final String DEFAULT = "default";
@@ -97,15 +102,22 @@
   public static @interface Parameter {
   }
 
+  @Target({FIELD})
+  @Retention(RUNTIME)
+  public static @interface Name {
+  }
+
   private static class ConfigRunner extends BlockJUnit4ClassRunner {
     private final Method configMethod;
     private final Field parameterField;
+    private final Field nameField;
     private final String name;
 
-    private ConfigRunner(Class<?> clazz, Field parameterField, String name,
-        Method configMethod) throws InitializationError {
+    private ConfigRunner(Class<?> clazz, Field parameterField, Field nameField,
+        String name, Method configMethod) throws InitializationError {
       super(clazz);
       this.parameterField = parameterField;
+      this.nameField = nameField;
       this.name = name;
       this.configMethod = configMethod;
     }
@@ -114,6 +126,9 @@
     public Object createTest() throws Exception {
       Object test = getTestClass().getJavaClass().newInstance();
       parameterField.set(test, callConfigMethod(configMethod));
+      if (nameField != null) {
+        nameField.set(test, name);
+      }
       return test;
     }
 
@@ -132,15 +147,23 @@
   private static List<Runner> runnersFor(Class<?> clazz) {
     Method defaultConfig = getDefaultConfig(clazz);
     List<Method> configs = getConfigs(clazz);
-    Field field = getParameterField(clazz);
+    Field parameterField = getOnlyField(clazz, Parameter.class);
+    checkArgument(parameterField != null, "No @ConfigSuite.Field found");
+    Field nameField = getOnlyField(clazz, Name.class);
     List<Runner> result = Lists.newArrayListWithCapacity(configs.size() + 1);
     try {
-      result.add(new ConfigRunner(clazz, field, null, defaultConfig));
+      result.add(new ConfigRunner(
+          clazz, parameterField, nameField, null, defaultConfig));
       for (Method m : configs) {
-        result.add(new ConfigRunner(clazz, field, m.getName(), m));
+        result.add(new ConfigRunner(
+            clazz, parameterField, nameField, m.getName(), m));
       }
       return result;
     } catch (InitializationError e) {
+      System.err.println("Errors initializing runners:");
+      for (Throwable t : e.getCauses()) {
+        t.printStackTrace();
+      }
       throw new RuntimeException(e);
     }
   }
@@ -191,16 +214,18 @@
     }
   }
 
-  private static Field getParameterField(Class<?> clazz) {
+  private static Field getOnlyField(Class<?> clazz,
+      Class<? extends Annotation> ann) {
     List<Field> fields = Lists.newArrayListWithExpectedSize(1);
     for (Field f : clazz.getFields()) {
-      if (f.getAnnotation(Parameter.class) != null) {
+      if (f.getAnnotation(ann) != null) {
         fields.add(f);
       }
     }
-    checkArgument(fields.size() == 1,
-        "expected 1 @ConfigSuite.Parameter field, found: %s", fields);
-    return fields.get(0);
+    checkArgument(fields.size() <= 1,
+        "expected 1 @ConfigSuite.%s field, found: %s",
+        ann.getSimpleName(), fields);
+    return Iterables.getFirst(fields, null);
   }
 
   public ConfigSuite(Class<?> clazz) throws InitializationError {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
new file mode 100644
index 0000000..c526cea
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2015 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.testutil;
+
+import com.google.gerrit.reviewdb.server.AccountAccess;
+import com.google.gerrit.reviewdb.server.AccountExternalIdAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
+import com.google.gerrit.reviewdb.server.AccountPatchReviewAccess;
+import com.google.gerrit.reviewdb.server.AccountProjectWatchAccess;
+import com.google.gerrit.reviewdb.server.AccountSshKeyAccess;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
+import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
+import com.google.gerrit.reviewdb.server.PatchSetAccess;
+import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
+import com.google.gerrit.reviewdb.server.StarredChangeAccess;
+import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
+import com.google.gerrit.reviewdb.server.SystemConfigAccess;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.StatementExecutor;
+
+/** ReviewDb that is disabled for testing. */
+public class DisabledReviewDb implements ReviewDb {
+  public static class Disabled extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    private Disabled() {
+      super("ReviewDb is disabled for this test");
+    }
+  }
+
+  @Override
+  public void close() {
+    // Do nothing.
+  }
+
+  @Override
+  public void commit() {
+    throw new Disabled();
+  }
+
+  @Override
+  public void rollback() {
+    throw new Disabled();
+  }
+
+  @Override
+  public void updateSchema(StatementExecutor e) {
+    throw new Disabled();
+  }
+
+  @Override
+  public void pruneSchema(StatementExecutor e) {
+    throw new Disabled();
+  }
+
+  @Override
+  public Access<?, ?>[] allRelations() {
+    throw new Disabled();
+  }
+
+  @Override
+  public SchemaVersionAccess schemaVersion() {
+    throw new Disabled();
+  }
+
+  @Override
+  public SystemConfigAccess systemConfig() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountAccess accounts() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountExternalIdAccess accountExternalIds() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountSshKeyAccess accountSshKeys() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountGroupAccess accountGroups() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountGroupNameAccess accountGroupNames() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountGroupMemberAccess accountGroupMembers() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
+    throw new Disabled();
+  }
+
+  @Override
+  public StarredChangeAccess starredChanges() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountProjectWatchAccess accountProjectWatches() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountPatchReviewAccess accountPatchReviews() {
+    throw new Disabled();
+  }
+
+  @Override
+  public ChangeAccess changes() {
+    throw new Disabled();
+  }
+
+  @Override
+  public PatchSetApprovalAccess patchSetApprovals() {
+    throw new Disabled();
+  }
+
+  @Override
+  public ChangeMessageAccess changeMessages() {
+    throw new Disabled();
+  }
+
+  @Override
+  public PatchSetAccess patchSets() {
+    throw new Disabled();
+  }
+
+  @Override
+  public PatchLineCommentAccess patchComments() {
+    throw new Disabled();
+  }
+
+  @Override
+  public SubmoduleSubscriptionAccess submoduleSubscriptions() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountGroupByIdAccess accountGroupById() {
+    throw new Disabled();
+  }
+
+  @Override
+  public AccountGroupByIdAudAccess accountGroupByIdAud() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextAccountId() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextAccountGroupId() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextChangeId() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextChangeMessageId() {
+    throw new Disabled();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
new file mode 100644
index 0000000..7adf721
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2015 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.testutil;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.EmailHeader;
+import com.google.gerrit.server.mail.EmailSender;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Email sender implementation that records messages in memory.
+ * <p>
+ * This class is mostly threadsafe. The only exception is that not all {@link
+ * EmailHeader} subclasses are immutable. In particular, if a caller holds a
+ * reference to an {@code AddressList} and mutates it after sending, the message
+ * returned by {@link #getMessages()} may or may not reflect mutations.
+ */
+@Singleton
+public class FakeEmailSender implements EmailSender {
+  private static final Logger log =
+      LoggerFactory.getLogger(FakeEmailSender.class);
+
+  public static class Module extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(EmailSender.class).to(FakeEmailSender.class);
+    }
+  }
+
+  @AutoValue
+  public abstract static class Message {
+    private static Message create(Address from, Collection<Address> rcpt,
+        Map<String, EmailHeader> headers, String body) {
+      return new AutoValue_FakeEmailSender_Message(from,
+          ImmutableList.copyOf(rcpt), ImmutableMap.copyOf(headers), body);
+    }
+
+    public abstract Address from();
+    public abstract ImmutableList<Address> rcpt();
+    public abstract ImmutableMap<String, EmailHeader> headers();
+    public abstract String body();
+  }
+
+  private final WorkQueue workQueue;
+  private final List<Message> messages;
+
+  @Inject
+  FakeEmailSender(WorkQueue workQueue) {
+    this.workQueue = workQueue;
+    messages = Collections.synchronizedList(new ArrayList<Message>());
+  }
+
+  @Override
+  public boolean isEnabled() {
+    return true;
+  }
+
+  @Override
+  public boolean canEmail(String address) {
+    return true;
+  }
+
+  @Override
+  public void send(Address from, Collection<Address> rcpt,
+      Map<String, EmailHeader> headers, String body) throws EmailException {
+    messages.add(Message.create(from, rcpt, headers, body));
+  }
+
+  public ImmutableList<Message> getMessages() {
+    waitForEmails();
+    synchronized (messages) {
+      return ImmutableList.copyOf(messages);
+    }
+  }
+
+  public ImmutableList<Message> getMessages(String changeId, String type) {
+    final String idFooter = "\nGerrit-Change-Id: " + changeId + "\n";
+    final String typeFooter = "\nGerrit-MessageType: " + type + "\n";
+    return FluentIterable.from(getMessages())
+        .filter(new Predicate<Message>() {
+          @Override
+          public boolean apply(Message in) {
+            return in.body().contains(idFooter)
+                && in.body().contains(typeFooter);
+          }
+        }).toList();
+  }
+
+  private void waitForEmails() {
+    // TODO(dborowitz): This is brittle; consider forcing emails to use
+    // a single thread in tests (tricky because most callers just use the
+    // default executor).
+    for (WorkQueue.Task<?> task : workQueue.getTasks()) {
+      if (task.toString().contains("send-email")) {
+        try {
+          task.get();
+        } catch (ExecutionException | InterruptedException e) {
+          log.warn("error finishing email task", e);
+        }
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index 49fcc96..458e100 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.testutil;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -27,6 +28,7 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -46,9 +48,10 @@
  * the JVM running the unit tests doesn't run out of heap space.
  */
 public class InMemoryDatabase implements SchemaFactory<ReviewDb> {
-  public static InMemoryDatabase newDatabase() {
-    return Guice.createInjector(new InMemoryModule())
-        .getInstance(InMemoryDatabase.class);
+  public static InMemoryDatabase newDatabase(LifecycleManager lifecycle) {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    lifecycle.add(injector);
+    return injector.getInstance(InMemoryDatabase.class);
   }
 
   private static int dbCnt;
@@ -108,17 +111,10 @@
   public InMemoryDatabase create() throws OrmException {
     if (!created) {
       created = true;
-      final ReviewDb c = open();
-      try {
-        try {
-          schemaCreator.create(c);
-        } catch (IOException e) {
-          throw new OrmException("Cannot create in-memory database", e);
-        } catch (ConfigInvalidException e) {
-          throw new OrmException("Cannot create in-memory database", e);
-        }
-      } finally {
-        c.close();
+      try (ReviewDb c = open()) {
+        schemaCreator.create(c);
+      } catch (IOException | ConfigInvalidException e) {
+        throw new OrmException("Cannot create in-memory database", e);
       }
     }
     return this;
@@ -139,25 +135,19 @@
   }
 
   public SystemConfig getSystemConfig() throws OrmException {
-    final ReviewDb c = open();
-    try {
+    try (ReviewDb c = open()) {
       return c.systemConfig().get(new SystemConfig.Key());
-    } finally {
-      c.close();
     }
   }
 
   public CurrentSchemaVersion getSchemaVersion() throws OrmException {
-    final ReviewDb c = open();
-    try {
+    try (ReviewDb c = open()) {
       return c.schemaVersion().get(new CurrentSchemaVersion.Key());
-    } finally {
-      c.close();
     }
   }
 
   public void assertSchemaVersion() throws OrmException {
-    final CurrentSchemaVersion act = getSchemaVersion();
-    assertEquals(SchemaVersion.getBinaryVersion(), act.versionNbr);
+    assertThat(getSchemaVersion().versionNbr)
+      .isEqualTo(SchemaVersion.getBinaryVersion());
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 76af6f1..d18712e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -17,15 +17,15 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.common.net.InetAddresses;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.DisabledChangeHooks;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
-import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
@@ -35,22 +35,19 @@
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
 import com.google.gerrit.server.git.ChangeCacheImplModule;
-import com.google.gerrit.server.git.EmailReviewCommentsExecutor;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.PerThreadRequestScope;
-import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.SendEmailExecutor;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
-import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.DiffExecutor;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.SchemaCreator;
@@ -73,11 +70,10 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
-import java.io.File;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.concurrent.ExecutorService;
 
 public class InMemoryModule extends FactoryModule {
@@ -89,16 +85,19 @@
 
   public static void setDefaults(Config cfg) {
     cfg.setEnum("auth", null, "type", AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT);
-    cfg.setString("gerrit", null, "basePath", "git");
     cfg.setString("gerrit", null, "allProjects", "Test-Projects");
+    cfg.setString("gerrit", null, "basePath", "git");
+    cfg.setString("gerrit", null, "canonicalWebUrl", "http://test/");
     cfg.setString("user", null, "name", "Gerrit Code Review");
     cfg.setString("user", null, "email", "gerrit@localhost");
-    cfg.setBoolean("sendemail", null, "enable", false);
-    cfg.setString("cache", null, "directory", null);
+    cfg.unset("cache", null, "directory");
     cfg.setString("index", null, "type", "lucene");
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
+    cfg.setInt("sendemail", null, "threadPoolSize", 0);
+    cfg.setBoolean("receive", null, "enableSignedPush", false);
+    cfg.setString("receive", null, "certNonceSeed", "sekret");
   }
 
   private final Config cfg;
@@ -117,6 +116,13 @@
 
   @Override
   protected void configure() {
+    // Do NOT bind @RemotePeer, as it is bound in a child injector of
+    // ChangeMergeQueue (bound via GerritGlobalModule below), so there cannot be
+    // a binding in the parent injector. If you need @RemotePeer, you must bind
+    // it in a child injector of the one containing InMemoryModule. But unless
+    // you really need to test something request-scoped, you likely don't
+    // actually need it.
+
     // For simplicity, don't create child injectors, just use this one to get a
     // few required modules.
     Injector cfgInjector = Guice.createInjector(new AbstractModule() {
@@ -132,10 +138,9 @@
 
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
 
-    bind(File.class).annotatedWith(SitePath.class).toInstance(new File("."));
+    // TODO(dborowitz): Use jimfs.
+    bind(Path.class).annotatedWith(SitePath.class).toInstance(Paths.get("."));
     bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
-    bind(SocketAddress.class).annotatedWith(RemotePeer.class).toInstance(
-        new InetSocketAddress(InetAddresses.forString("127.0.0.1"), 1234));
     bind(PersonIdent.class)
         .annotatedWith(GerritPersonIdent.class)
         .toProvider(GerritPersonIdentProvider.class);
@@ -180,8 +185,9 @@
       }
     });
     install(new DefaultCacheFactory.Module());
-    install(new SmtpEmailSender.Module());
+    install(new FakeEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
+    install(new GpgModule(cfg));
 
     IndexType indexType = null;
     try {
@@ -203,11 +209,9 @@
 
   @Provides
   @Singleton
-  @EmailReviewCommentsExecutor
-  public WorkQueue.Executor createEmailReviewCommentsExecutor(
-      @GerritServerConfig Config config, WorkQueue queues) {
-    int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
-    return queues.createQueue(poolSize, "EmailReviewComments");
+  @SendEmailExecutor
+  public ExecutorService createSendEmailExecutor() {
+    return MoreExecutors.newDirectExecutorService();
   }
 
   @Provides
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 0635464..656185d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -35,18 +35,27 @@
     return new Repo(name);
   }
 
-  private static class Description extends DfsRepositoryDescription {
+  public static class Description extends DfsRepositoryDescription {
+    private final Project.NameKey name;
     private String desc;
 
     private Description(Project.NameKey name) {
       super(name.get());
+      this.name = name;
       desc = "In-memory repository " + name.get();
     }
+
+    public Project.NameKey getProject() {
+      return name;
+    }
   }
 
-  private static class Repo extends InMemoryRepository {
+  public static class Repo extends InMemoryRepository {
     private Repo(Project.NameKey name) {
       super(new Description(name));
+      // TODO(dborowitz): Allow atomic transactions when this is supported:
+      // https://git.eclipse.org/r/#/c/61841/2/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java@313
+      setPerformsAtomicTransactions(false);
     }
 
     @Override
@@ -58,13 +67,13 @@
   private Map<String, Repo> repos = Maps.newHashMap();
 
   @Override
-  public InMemoryRepository openRepository(Project.NameKey name)
+  public synchronized Repo openRepository(Project.NameKey name)
       throws RepositoryNotFoundException {
     return get(name);
   }
 
   @Override
-  public InMemoryRepository createRepository(Project.NameKey name)
+  public synchronized Repo createRepository(Project.NameKey name)
       throws RepositoryCaseMismatchException, RepositoryNotFoundException {
     Repo repo;
     try {
@@ -80,13 +89,13 @@
   }
 
   @Override
-  public InMemoryRepository openMetadataRepository(Project.NameKey name)
-      throws RepositoryNotFoundException {
+  public synchronized Repo openMetadataRepository(
+      Project.NameKey name) throws RepositoryNotFoundException {
     return openRepository(name);
   }
 
   @Override
-  public SortedSet<Project.NameKey> list() {
+  public synchronized SortedSet<Project.NameKey> list() {
     SortedSet<Project.NameKey> names = Sets.newTreeSet();
     for (DfsRepository repo : repos.values()) {
       names.add(new Project.NameKey(repo.getDescription().getRepositoryName()));
@@ -95,13 +104,14 @@
   }
 
   @Override
-  public String getProjectDescription(Project.NameKey name)
+  public synchronized String getProjectDescription(Project.NameKey name)
       throws RepositoryNotFoundException {
     return get(name).getDescription().desc;
   }
 
   @Override
-  public void setProjectDescription(Project.NameKey name, String description) {
+  public synchronized void setProjectDescription(Project.NameKey name,
+      String description) {
     try {
       get(name).getDescription().desc = description;
     } catch (RepositoryNotFoundException e) {
@@ -109,7 +119,8 @@
     }
   }
 
-  private Repo get(Project.NameKey name) throws RepositoryNotFoundException {
+  private synchronized Repo get(Project.NameKey name)
+      throws RepositoryNotFoundException {
     Repo repo = repos.get(name.get().toLowerCase());
     if (repo != null) {
       return repo;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index 675634e..8b5e85a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeDraftUpdate;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -104,7 +104,7 @@
       IdentifiedUser user) throws OrmException {
     ChangeControl ctl = EasyMock.createNiceMock(ChangeControl.class);
     expect(ctl.getChange()).andStubReturn(c);
-    expect(ctl.getCurrentUser()).andStubReturn(user);
+    expect(ctl.getUser()).andStubReturn(user);
     ChangeNotes notes = new ChangeNotes(repoManager, migration, allUsers, c)
         .load();
     expect(ctl.getNotes()).andStubReturn(notes);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
new file mode 100644
index 0000000..4c71c57
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2015 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.testutil;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.joda.time.DateTimeZone;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Static utility methods for dealing with dates and times in tests. */
+public class TestTimeUtil {
+  private static Long clockStepMs;
+  private static AtomicLong clockMs;
+
+  /**
+   * Reset the clock to a known start point, then set the clock step.
+   * <p>
+   * The clock is initially set to 2009/09/30 17:00:00 -0400.
+   *
+   * @param clockStep amount to increment clock by on each lookup.
+   * @param clockStepUnit time unit for {@code clockStep}.
+   */
+  public static synchronized void resetWithClockStep(
+      long clockStep, TimeUnit clockStepUnit) {
+    // Set an arbitrary start point so tests are more repeatable.
+    clockMs = new AtomicLong(
+        new DateTime(2009, 9, 30, 17, 0, 0, DateTimeZone.forOffsetHours(-4))
+            .getMillis());
+    setClockStep(clockStep, clockStepUnit);
+  }
+
+  /**
+   * Set the clock step used by {@link com.google.gerrit.common.TimeUtil}.
+   *
+   * @param clockStep amount to increment clock by on each lookup.
+   * @param clockStepUnit time unit for {@code clockStep}.
+   */
+  public static synchronized void setClockStep(
+      long clockStep, TimeUnit clockStepUnit) {
+    checkState(clockMs != null, "call resetWithClockStep first");
+    clockStepMs = MILLISECONDS.convert(clockStep, clockStepUnit);
+    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+      @Override
+      public long getMillis() {
+        return clockMs.getAndAdd(clockStepMs);
+      }
+    });
+  }
+
+  /** Reset the clock to use the actual system clock. */
+  public static synchronized void useSystemTime() {
+    DateTimeUtils.setCurrentMillisSystem();
+  }
+
+  private TestTimeUtil() {
+  }
+}
diff --git a/gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt b/gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt
new file mode 100644
index 0000000..9edf6a4
--- /dev/null
+++ b/gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt
@@ -0,0 +1,1329 @@
+# Version 2016060601, Last Updated Tue Jun  7 07:07:01 2016 UTC
+# From http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+AAA
+AARP
+ABB
+ABBOTT
+ABBVIE
+ABOGADO
+ABUDHABI
+AC
+ACADEMY
+ACCENTURE
+ACCOUNTANT
+ACCOUNTANTS
+ACO
+ACTIVE
+ACTOR
+AD
+ADAC
+ADS
+ADULT
+AE
+AEG
+AERO
+#! AETNA
+AF
+AFL
+AG
+AGAKHAN
+AGENCY
+AI
+AIG
+AIRFORCE
+AIRTEL
+AKDN
+AL
+ALIBABA
+ALIPAY
+ALLFINANZ
+ALLY
+ALSACE
+AM
+AMICA
+AMSTERDAM
+ANALYTICS
+ANDROID
+ANQUAN
+AO
+APARTMENTS
+APP
+APPLE
+AQ
+AQUARELLE
+AR
+ARAMCO
+ARCHI
+ARMY
+ARPA
+ARTE
+AS
+ASIA
+ASSOCIATES
+AT
+ATTORNEY
+AU
+AUCTION
+AUDI
+AUDIO
+AUTHOR
+AUTO
+AUTOS
+AVIANCA
+AW
+AWS
+AX
+AXA
+AZ
+AZURE
+BA
+BABY
+BAIDU
+BAND
+BANK
+BAR
+BARCELONA
+BARCLAYCARD
+BARCLAYS
+BAREFOOT
+BARGAINS
+BAUHAUS
+BAYERN
+BB
+BBC
+BBVA
+BCG
+BCN
+BD
+BE
+BEATS
+BEER
+BENTLEY
+BERLIN
+BEST
+BET
+BF
+BG
+BH
+BHARTI
+BI
+BIBLE
+BID
+BIKE
+BING
+BINGO
+BIO
+BIZ
+BJ
+BLACK
+BLACKFRIDAY
+#! BLOG
+BLOOMBERG
+BLUE
+BM
+BMS
+BMW
+BN
+BNL
+BNPPARIBAS
+BO
+BOATS
+BOEHRINGER
+BOM
+BOND
+BOO
+BOOK
+BOOTS
+BOSCH
+BOSTIK
+BOT
+BOUTIQUE
+BR
+BRADESCO
+BRIDGESTONE
+BROADWAY
+BROKER
+BROTHER
+BRUSSELS
+BS
+BT
+BUDAPEST
+BUGATTI
+BUILD
+BUILDERS
+BUSINESS
+BUY
+BUZZ
+BV
+BW
+BY
+BZ
+BZH
+CA
+CAB
+CAFE
+CAL
+CALL
+CAMERA
+CAMP
+CANCERRESEARCH
+CANON
+CAPETOWN
+CAPITAL
+CAR
+CARAVAN
+CARDS
+CARE
+CAREER
+CAREERS
+CARS
+CARTIER
+CASA
+CASH
+CASINO
+CAT
+CATERING
+CBA
+CBN
+CC
+CD
+CEB
+CENTER
+CEO
+CERN
+CF
+CFA
+CFD
+CG
+CH
+CHANEL
+CHANNEL
+CHASE
+CHAT
+CHEAP
+CHLOE
+CHRISTMAS
+CHROME
+CHURCH
+CI
+CIPRIANI
+CIRCLE
+CISCO
+CITIC
+CITY
+CITYEATS
+CK
+CL
+CLAIMS
+CLEANING
+CLICK
+CLINIC
+CLINIQUE
+CLOTHING
+CLOUD
+CLUB
+CLUBMED
+CM
+CN
+CO
+COACH
+CODES
+COFFEE
+COLLEGE
+COLOGNE
+COM
+COMMBANK
+COMMUNITY
+COMPANY
+COMPARE
+COMPUTER
+COMSEC
+CONDOS
+CONSTRUCTION
+CONSULTING
+CONTACT
+CONTRACTORS
+COOKING
+COOL
+COOP
+CORSICA
+COUNTRY
+COUPON
+COUPONS
+COURSES
+CR
+CREDIT
+CREDITCARD
+CREDITUNION
+CRICKET
+CROWN
+CRS
+CRUISES
+CSC
+CU
+CUISINELLA
+CV
+CW
+CX
+CY
+CYMRU
+CYOU
+CZ
+DABUR
+DAD
+DANCE
+DATE
+DATING
+DATSUN
+DAY
+DCLK
+#! DDS
+DE
+DEALER
+DEALS
+DEGREE
+DELIVERY
+DELL
+DELOITTE
+DELTA
+DEMOCRAT
+DENTAL
+DENTIST
+DESI
+DESIGN
+DEV
+#! DHL
+DIAMONDS
+DIET
+DIGITAL
+DIRECT
+DIRECTORY
+DISCOUNT
+DJ
+DK
+DM
+DNP
+DO
+DOCS
+DOG
+DOHA
+DOMAINS
+#! DOT
+DOWNLOAD
+DRIVE
+#! DTV
+DUBAI
+DURBAN
+DVAG
+DZ
+EARTH
+EAT
+EC
+EDEKA
+EDU
+EDUCATION
+EE
+EG
+EMAIL
+EMERCK
+ENERGY
+ENGINEER
+ENGINEERING
+ENTERPRISES
+EPSON
+EQUIPMENT
+ER
+ERNI
+ES
+ESQ
+ESTATE
+ET
+EU
+EUROVISION
+EUS
+EVENTS
+EVERBANK
+EXCHANGE
+EXPERT
+EXPOSED
+EXPRESS
+EXTRASPACE
+FAGE
+FAIL
+FAIRWINDS
+FAITH
+FAMILY
+FAN
+FANS
+FARM
+FASHION
+FAST
+FEEDBACK
+FERRERO
+FI
+FILM
+FINAL
+FINANCE
+FINANCIAL
+FIRESTONE
+FIRMDALE
+FISH
+FISHING
+FIT
+FITNESS
+FJ
+FK
+FLICKR
+FLIGHTS
+#! FLIR
+FLORIST
+FLOWERS
+FLSMIDTH
+FLY
+FM
+FO
+FOO
+FOOTBALL
+FORD
+FOREX
+FORSALE
+FORUM
+FOUNDATION
+FOX
+FR
+FRESENIUS
+FRL
+FROGANS
+FRONTIER
+FTR
+FUND
+FURNITURE
+FUTBOL
+FYI
+GA
+GAL
+GALLERY
+GALLO
+GALLUP
+GAME
+#! GAMES
+GARDEN
+GB
+GBIZ
+GD
+GDN
+GE
+GEA
+GENT
+GENTING
+GF
+GG
+GGEE
+GH
+GI
+GIFT
+GIFTS
+GIVES
+GIVING
+GL
+GLASS
+GLE
+GLOBAL
+GLOBO
+GM
+GMAIL
+GMBH
+GMO
+GMX
+GN
+GOLD
+GOLDPOINT
+GOLF
+GOO
+GOOG
+GOOGLE
+GOP
+GOT
+GOV
+GP
+GQ
+GR
+GRAINGER
+GRAPHICS
+GRATIS
+GREEN
+GRIPE
+GROUP
+GS
+GT
+GU
+#! GUARDIAN
+GUCCI
+GUGE
+GUIDE
+GUITARS
+GURU
+GW
+GY
+HAMBURG
+HANGOUT
+HAUS
+HDFCBANK
+HEALTH
+HEALTHCARE
+HELP
+HELSINKI
+HERE
+HERMES
+HIPHOP
+#! HISAMITSU
+HITACHI
+HIV
+HK
+#! HKT
+HM
+HN
+HOCKEY
+HOLDINGS
+HOLIDAY
+HOMEDEPOT
+HOMES
+HONDA
+HORSE
+HOST
+HOSTING
+HOTELES
+HOTMAIL
+HOUSE
+HOW
+HR
+HSBC
+HT
+HTC
+HU
+HYUNDAI
+IBM
+ICBC
+ICE
+ICU
+ID
+IE
+IFM
+IINET
+IL
+IM
+IMAMAT
+IMMO
+IMMOBILIEN
+IN
+INDUSTRIES
+INFINITI
+INFO
+ING
+INK
+INSTITUTE
+INSURANCE
+INSURE
+INT
+INTERNATIONAL
+INVESTMENTS
+IO
+IPIRANGA
+IQ
+IR
+IRISH
+IS
+ISELECT
+ISMAILI
+IST
+ISTANBUL
+IT
+ITAU
+IWC
+JAGUAR
+JAVA
+JCB
+JCP
+JE
+JETZT
+JEWELRY
+JLC
+JLL
+JM
+JMP
+JNJ
+JO
+JOBS
+JOBURG
+JOT
+JOY
+JP
+JPMORGAN
+JPRS
+JUEGOS
+KAUFEN
+KDDI
+KE
+KERRYHOTELS
+KERRYLOGISTICS
+KERRYPROPERTIES
+KFH
+KG
+KH
+KI
+KIA
+KIM
+KINDER
+KITCHEN
+KIWI
+KM
+KN
+KOELN
+KOMATSU
+KP
+KPMG
+KPN
+KR
+KRD
+KRED
+KUOKGROUP
+KW
+KY
+KYOTO
+KZ
+LA
+LACAIXA
+LAMBORGHINI
+LAMER
+LANCASTER
+LAND
+LANDROVER
+LANXESS
+LASALLE
+LAT
+LATROBE
+LAW
+LAWYER
+LB
+LC
+LDS
+LEASE
+LECLERC
+LEGAL
+LEXUS
+LGBT
+LI
+LIAISON
+LIDL
+LIFE
+LIFEINSURANCE
+LIFESTYLE
+LIGHTING
+LIKE
+LIMITED
+LIMO
+LINCOLN
+LINDE
+LINK
+#! LIPSY
+LIVE
+LIVING
+LIXIL
+LK
+LOAN
+LOANS
+#! LOCKER
+LOCUS
+LOL
+LONDON
+LOTTE
+LOTTO
+LOVE
+LR
+LS
+LT
+LTD
+LTDA
+LU
+LUPIN
+LUXE
+LUXURY
+LV
+LY
+MA
+MADRID
+MAIF
+MAISON
+MAKEUP
+MAN
+MANAGEMENT
+MANGO
+MARKET
+MARKETING
+MARKETS
+MARRIOTT
+#! MATTEL
+MBA
+MC
+MD
+ME
+MED
+MEDIA
+MEET
+MELBOURNE
+MEME
+MEMORIAL
+MEN
+MENU
+MEO
+#! METLIFE
+MG
+MH
+MIAMI
+MICROSOFT
+MIL
+MINI
+MK
+ML
+#! MLB
+MLS
+MM
+MMA
+MN
+MO
+MOBI
+MOBILY
+MODA
+MOE
+MOI
+MOM
+MONASH
+MONEY
+MONTBLANC
+MORMON
+MORTGAGE
+MOSCOW
+MOTORCYCLES
+MOV
+MOVIE
+MOVISTAR
+MP
+MQ
+MR
+MS
+MT
+MTN
+MTPC
+MTR
+MU
+MUSEUM
+MUTUAL
+MUTUELLE
+MV
+MW
+MX
+MY
+MZ
+NA
+NADEX
+NAGOYA
+NAME
+NATURA
+NAVY
+NC
+NE
+NEC
+NET
+NETBANK
+#! NETFLIX
+NETWORK
+NEUSTAR
+NEW
+NEWS
+#! NEXT
+#! NEXTDIRECT
+NEXUS
+NF
+NG
+NGO
+NHK
+NI
+NICO
+NIKON
+NINJA
+NISSAN
+NISSAY
+NL
+NO
+NOKIA
+NORTHWESTERNMUTUAL
+NORTON
+NOWRUZ
+#! NOWTV
+NP
+NR
+NRA
+NRW
+NTT
+NU
+NYC
+NZ
+OBI
+OFFICE
+OKINAWA
+#! OLAYAN
+#! OLAYANGROUP
+#! OLLO
+OM
+OMEGA
+ONE
+ONG
+ONL
+ONLINE
+OOO
+ORACLE
+ORANGE
+ORG
+ORGANIC
+ORIGINS
+OSAKA
+OTSUKA
+#! OTT
+OVH
+PA
+PAGE
+PAMPEREDCHEF
+PANERAI
+PARIS
+PARS
+PARTNERS
+PARTS
+PARTY
+PASSAGENS
+#! PCCW
+PE
+PET
+PF
+PG
+PH
+PHARMACY
+PHILIPS
+PHOTO
+PHOTOGRAPHY
+PHOTOS
+PHYSIO
+PIAGET
+PICS
+PICTET
+PICTURES
+PID
+PIN
+PING
+PINK
+#! PIONEER
+PIZZA
+PK
+PL
+PLACE
+PLAY
+PLAYSTATION
+PLUMBING
+PLUS
+PM
+PN
+POHL
+POKER
+PORN
+POST
+PR
+PRAXI
+PRESS
+PRO
+PROD
+PRODUCTIONS
+PROF
+PROGRESSIVE
+PROMO
+PROPERTIES
+PROPERTY
+PROTECTION
+PS
+PT
+PUB
+PW
+PWC
+PY
+QA
+QPON
+QUEBEC
+QUEST
+RACING
+RE
+READ
+#! REALESTATE
+REALTOR
+REALTY
+RECIPES
+RED
+REDSTONE
+REDUMBRELLA
+REHAB
+REISE
+REISEN
+REIT
+REN
+RENT
+RENTALS
+REPAIR
+REPORT
+REPUBLICAN
+REST
+RESTAURANT
+REVIEW
+REVIEWS
+REXROTH
+RICH
+#! RICHARDLI
+RICOH
+RIO
+RIP
+RO
+ROCHER
+ROCKS
+RODEO
+ROOM
+RS
+RSVP
+RU
+RUHR
+RUN
+RW
+RWE
+RYUKYU
+SA
+SAARLAND
+SAFE
+SAFETY
+SAKURA
+SALE
+SALON
+SAMSUNG
+SANDVIK
+SANDVIKCOROMANT
+SANOFI
+SAP
+SAPO
+SARL
+SAS
+SAXO
+SB
+SBI
+SBS
+SC
+SCA
+SCB
+SCHAEFFLER
+SCHMIDT
+SCHOLARSHIPS
+SCHOOL
+SCHULE
+SCHWARZ
+SCIENCE
+SCOR
+SCOT
+SD
+SE
+SEAT
+SECURITY
+SEEK
+SELECT
+SENER
+SERVICES
+SEVEN
+SEW
+SEX
+SEXY
+SFR
+SG
+SH
+SHARP
+SHAW
+SHELL
+SHIA
+SHIKSHA
+SHOES
+#! SHOP
+SHOUJI
+SHOW
+SHRIRAM
+SI
+SINA
+SINGLES
+SITE
+SJ
+SK
+SKI
+SKIN
+SKY
+SKYPE
+SL
+SM
+SMILE
+SN
+SNCF
+SO
+SOCCER
+SOCIAL
+SOFTBANK
+SOFTWARE
+SOHU
+SOLAR
+SOLUTIONS
+SONG
+SONY
+SOY
+SPACE
+SPIEGEL
+SPOT
+SPREADBETTING
+SR
+SRL
+ST
+STADA
+STAR
+STARHUB
+STATEBANK
+STATEFARM
+STATOIL
+STC
+STCGROUP
+STOCKHOLM
+STORAGE
+STORE
+STREAM
+STUDIO
+STUDY
+STYLE
+SU
+SUCKS
+SUPPLIES
+SUPPLY
+SUPPORT
+SURF
+SURGERY
+SUZUKI
+SV
+SWATCH
+SWISS
+SX
+SY
+SYDNEY
+SYMANTEC
+SYSTEMS
+SZ
+TAB
+TAIPEI
+TALK
+TAOBAO
+TATAMOTORS
+TATAR
+TATTOO
+TAX
+TAXI
+TC
+TCI
+TD
+TEAM
+TECH
+TECHNOLOGY
+TEL
+TELECITY
+TELEFONICA
+TEMASEK
+TENNIS
+TEVA
+TF
+TG
+TH
+THD
+THEATER
+THEATRE
+TICKETS
+TIENDA
+TIFFANY
+TIPS
+TIRES
+TIROL
+TJ
+TK
+TL
+TM
+TMALL
+TN
+TO
+TODAY
+TOKYO
+TOOLS
+TOP
+TORAY
+TOSHIBA
+TOTAL
+TOURS
+TOWN
+TOYOTA
+TOYS
+TR
+TRADE
+TRADING
+TRAINING
+TRAVEL
+TRAVELERS
+TRAVELERSINSURANCE
+TRUST
+TRV
+TT
+TUBE
+TUI
+TUNES
+TUSHU
+TV
+TVS
+TW
+TZ
+UA
+UBS
+UG
+UK
+UNICOM
+UNIVERSITY
+UNO
+UOL
+#! UPS
+US
+UY
+UZ
+VA
+VACATIONS
+VANA
+VC
+VE
+VEGAS
+VENTURES
+VERISIGN
+VERSICHERUNG
+VET
+VG
+VI
+VIAJES
+VIDEO
+VIG
+VIKING
+VILLAS
+VIN
+VIP
+VIRGIN
+VISION
+VISTA
+VISTAPRINT
+VIVA
+VLAANDEREN
+VN
+VODKA
+VOLKSWAGEN
+VOTE
+VOTING
+VOTO
+VOYAGE
+VU
+VUELOS
+WALES
+WALTER
+WANG
+WANGGOU
+#! WARMAN
+WATCH
+WATCHES
+WEATHER
+WEATHERCHANNEL
+WEBCAM
+WEBER
+WEBSITE
+WED
+WEDDING
+WEIBO
+WEIR
+WF
+WHOSWHO
+WIEN
+WIKI
+WILLIAMHILL
+WIN
+WINDOWS
+WINE
+WME
+WOLTERSKLUWER
+WORK
+WORKS
+WORLD
+WS
+WTC
+WTF
+XBOX
+XEROX
+XIHUAN
+XIN
+XN--11B4C3D
+XN--1CK2E1B
+XN--1QQW23A
+XN--30RR7Y
+XN--3BST00M
+XN--3DS443G
+XN--3E0B707E
+XN--3PXU8K
+XN--42C2D9A
+XN--45BRJ9C
+XN--45Q11C
+XN--4GBRIM
+XN--55QW42G
+XN--55QX5D
+XN--5TZM5G
+XN--6FRZ82G
+XN--6QQ986B3XL
+XN--80ADXHKS
+XN--80AO21A
+XN--80ASEHDB
+XN--80ASWG
+XN--8Y0A063A
+XN--90A3AC
+XN--90AIS
+XN--9DBQ2A
+XN--9ET52U
+XN--9KRT00A
+XN--B4W605FERD
+XN--BCK1B9A5DRE4C
+XN--C1AVG
+XN--C2BR7G
+XN--CCK2B3B
+XN--CG4BKI
+XN--CLCHC0EA0B2G2A9GCD
+XN--CZR694B
+XN--CZRS0T
+XN--CZRU2D
+XN--D1ACJ3B
+XN--D1ALF
+XN--E1A4C
+XN--ECKVDTC9D
+XN--EFVY88H
+XN--ESTV75G
+XN--FCT429K
+XN--FHBEI
+XN--FIQ228C5HS
+XN--FIQ64B
+XN--FIQS8S
+XN--FIQZ9S
+XN--FJQ720A
+XN--FLW351E
+XN--FPCRJ9C3D
+XN--FZC2C9E2C
+XN--FZYS8D69UVGM
+XN--G2XX48C
+XN--GCKR3F0F
+XN--GECRJ9C
+XN--H2BRJ9C
+XN--HXT814E
+XN--I1B6B1A6A2E
+XN--IMR513N
+XN--IO0A7I
+XN--J1AEF
+XN--J1AMH
+XN--J6W193G
+XN--JLQ61U9W7B
+XN--JVR189M
+XN--KCRX77D1X4A
+XN--KPRW13D
+XN--KPRY57D
+XN--KPU716F
+XN--KPUT3I
+XN--L1ACC
+XN--LGBBAT1AD8J
+XN--MGB9AWBF
+XN--MGBA3A3EJT
+XN--MGBA3A4F16A
+XN--MGBA7C0BBN0A
+XN--MGBAAM7A8H
+XN--MGBAB2BD
+XN--MGBAYH7GPA
+XN--MGBB9FBPOB
+XN--MGBBH1A71E
+XN--MGBC0A9AZCG
+XN--MGBCA7DZDO
+XN--MGBERP4A5D4AR
+XN--MGBPL2FH
+XN--MGBT3DHD
+XN--MGBTX2B
+XN--MGBX4CD0AB
+XN--MIX891F
+XN--MK1BU44C
+XN--MXTQ1M
+XN--NGBC5AZD
+XN--NGBE9E0A
+XN--NODE
+XN--NQV7F
+XN--NQV7FS00EMA
+XN--NYQY26A
+XN--O3CW4H
+XN--OGBPF8FL
+XN--P1ACF
+XN--P1AI
+XN--PBT977C
+XN--PGBS0DH
+XN--PSSY2U
+XN--Q9JYB4C
+XN--QCKA1PMC
+XN--QXAM
+XN--RHQV96G
+XN--ROVU88B
+XN--S9BRJ9C
+XN--SES554G
+XN--T60B56A
+XN--TCKWE
+XN--UNUP4Y
+XN--VERMGENSBERATER-CTB
+XN--VERMGENSBERATUNG-PWB
+XN--VHQUV
+XN--VUQ861B
+XN--W4R85EL8FHU5DNRA
+XN--W4RS40L
+XN--WGBH1C
+XN--WGBL6A
+XN--XHQ521B
+XN--XKC2AL3HYE2A
+XN--XKC2DL3A5EE0H
+XN--Y9A3AQ
+XN--YFRO4I67O
+XN--YGBI2AMMX
+XN--ZFR164B
+XPERIA
+XXX
+XYZ
+YACHTS
+YAHOO
+YAMAXUN
+YANDEX
+YE
+YODOBASHI
+YOGA
+YOKOHAMA
+YOU
+YOUTUBE
+YT
+YUN
+ZA
+#! ZAPPOS
+ZARA
+ZERO
+ZIP
+ZM
+ZONE
+ZUERICH
+ZW
diff --git a/gerrit-solr/BUCK b/gerrit-solr/BUCK
deleted file mode 100644
index ec3c728..0000000
--- a/gerrit-solr/BUCK
+++ /dev/null
@@ -1,20 +0,0 @@
-java_library(
-  name = 'solr',
-  srcs = glob(['src/main/java/**/*.java']),
-  deps = [
-    '//gerrit-antlr:query_exception',
-    '//gerrit-extension-api:api',
-    '//gerrit-lucene:query_builder',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib/guice:guice',
-    '//lib/jgit:jgit',
-    '//lib/log:api',
-    '//lib/lucene:analyzers-common',
-    '//lib/lucene:core',
-    '//lib/solr:solrj',
-  ],
-  visibility = ['PUBLIC'],
-)
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
deleted file mode 100644
index ddb86c3..0000000
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2013 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.solr;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeSchemas;
-import com.google.inject.Inject;
-import com.google.inject.ProvisionException;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Map;
-
-class IndexVersionCheck implements LifecycleListener {
-  public static final Map<String, Integer> SCHEMA_VERSIONS = ImmutableMap.of(
-      SolrChangeIndex.CHANGES_OPEN, ChangeSchemas.getLatest().getVersion(),
-      SolrChangeIndex.CHANGES_CLOSED, ChangeSchemas.getLatest().getVersion());
-
-  public static File solrIndexConfig(SitePaths sitePaths) {
-    return new File(sitePaths.index_dir, "gerrit_index.config");
-  }
-
-  private final SitePaths sitePaths;
-
-  @Inject
-  IndexVersionCheck(SitePaths sitePaths) {
-    this.sitePaths = sitePaths;
-  }
-
-  @Override
-  public void start() {
-    // TODO Query schema version from a special meta-document
-    File file = solrIndexConfig(sitePaths);
-    try {
-      FileBasedConfig cfg = new FileBasedConfig(file, FS.detect());
-      cfg.load();
-      for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
-        int schemaVersion = cfg.getInt("index", e.getKey(), "schemaVersion", 0);
-        if (schemaVersion != e.getValue()) {
-          throw new ProvisionException(String.format(
-              "wrong index schema version for \"%s\": expected %d, found %d%s",
-              e.getKey(), e.getValue(), schemaVersion, upgrade()));
-        }
-      }
-    } catch (IOException e) {
-      throw new ProvisionException("unable to read " + file);
-    } catch (ConfigInvalidException e) {
-      throw new ProvisionException("invalid config file " + file);
-    }
-  }
-
-  @Override
-  public void stop() {
-    // Do nothing.
-  }
-
-  private final String upgrade() {
-    return "\nRun reindex to rebuild the index:\n"
-        + "$ java -jar gerrit.war reindex -d "
-        + sitePaths.site_path.getAbsolutePath();
-  }
-}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
deleted file mode 100644
index 78f5265..0000000
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
+++ /dev/null
@@ -1,338 +0,0 @@
-// Copyright (C) 2013 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.solr;
-
-import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
-import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
-import static com.google.gerrit.solr.IndexVersionCheck.SCHEMA_VERSIONS;
-import static com.google.gerrit.solr.IndexVersionCheck.solrIndexConfig;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.lucene.QueryBuilder;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.FieldDef.FillArgs;
-import com.google.gerrit.server.index.FieldType;
-import com.google.gerrit.server.index.IndexCollection;
-import com.google.gerrit.server.index.IndexRewriteImpl;
-import com.google.gerrit.server.index.Schema;
-import com.google.gerrit.server.index.Schema.Values;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Provider;
-
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
-import org.apache.lucene.analysis.util.CharArraySet;
-import org.apache.lucene.search.Query;
-import org.apache.solr.client.solrj.SolrQuery;
-import org.apache.solr.client.solrj.SolrQuery.SortClause;
-import org.apache.solr.client.solrj.SolrServer;
-import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.impl.CloudSolrServer;
-import org.apache.solr.common.SolrDocument;
-import org.apache.solr.common.SolrDocumentList;
-import org.apache.solr.common.SolrInputDocument;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/** Secondary index implementation using a remote Solr instance. */
-class SolrChangeIndex implements ChangeIndex, LifecycleListener {
-  public static final String CHANGES_OPEN = "changes_open";
-  public static final String CHANGES_CLOSED = "changes_closed";
-  private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
-
-  private final Provider<ReviewDb> db;
-  private final ChangeData.Factory changeDataFactory;
-  private final FillArgs fillArgs;
-  private final SitePaths sitePaths;
-  private final IndexCollection indexes;
-  private final CloudSolrServer openIndex;
-  private final CloudSolrServer closedIndex;
-  private final Schema<ChangeData> schema;
-  private final QueryBuilder queryBuilder;
-
-  SolrChangeIndex(
-      @GerritServerConfig Config cfg,
-      Provider<ReviewDb> db,
-      ChangeData.Factory changeDataFactory,
-      FillArgs fillArgs,
-      SitePaths sitePaths,
-      IndexCollection indexes,
-      Schema<ChangeData> schema,
-      String base) throws IOException {
-    this.db = db;
-    this.changeDataFactory = changeDataFactory;
-    this.fillArgs = fillArgs;
-    this.sitePaths = sitePaths;
-    this.indexes = indexes;
-    this.schema = schema;
-
-    String url = cfg.getString("index", null, "url");
-    if (Strings.isNullOrEmpty(url)) {
-      throw new IllegalStateException("index.url must be supplied");
-    }
-
-    queryBuilder = new QueryBuilder(
-        new StandardAnalyzer(CharArraySet.EMPTY_SET));
-
-    base = Strings.nullToEmpty(base);
-    openIndex = new CloudSolrServer(url);
-    openIndex.setDefaultCollection(base + CHANGES_OPEN);
-
-    closedIndex = new CloudSolrServer(url);
-    closedIndex.setDefaultCollection(base + CHANGES_CLOSED);
-  }
-
-  @Override
-  public void start() {
-    indexes.setSearchIndex(this);
-    indexes.addWriteIndex(this);
-  }
-
-  @Override
-  public void stop() {
-    openIndex.shutdown();
-    closedIndex.shutdown();
-  }
-
-  @Override
-  public Schema<ChangeData> getSchema() {
-    return schema;
-  }
-
-  @Override
-  public void close() {
-    stop();
-  }
-
-  @Override
-  public void replace(ChangeData cd) throws IOException {
-    String id = cd.getId().toString();
-    SolrInputDocument doc = toDocument(cd);
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        closedIndex.deleteById(id);
-        openIndex.add(doc);
-      } else {
-        openIndex.deleteById(id);
-        closedIndex.add(doc);
-      }
-    } catch (OrmException | SolrServerException e) {
-      throw new IOException(e);
-    }
-    commit(openIndex);
-    commit(closedIndex);
-  }
-
-  @Override
-  public void delete(Change.Id id) throws IOException {
-    String idString = Integer.toString(id.get());
-    delete(idString, openIndex);
-    delete(idString, closedIndex);
-  }
-
-  private void delete(String id, CloudSolrServer index) throws IOException {
-    try {
-      index.deleteById(id);
-      commit(index);
-    } catch (SolrServerException e) {
-      throw new IOException(e);
-    }
-  }
-
-  @Override
-  public void deleteAll() throws IOException {
-    try {
-      openIndex.deleteByQuery("*:*");
-      closedIndex.deleteByQuery("*:*");
-    } catch (SolrServerException e) {
-      throw new IOException(e);
-    }
-    commit(openIndex);
-    commit(closedIndex);
-  }
-
-  @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start, int limit)
-      throws QueryParseException {
-    Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
-    List<SolrServer> indexes = Lists.newArrayListWithCapacity(2);
-    if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
-      indexes.add(openIndex);
-    }
-    if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
-      indexes.add(closedIndex);
-    }
-    return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
-        getSorts());
-  }
-
-  private static List<SortClause> getSorts() {
-    return ImmutableList.of(
-        new SortClause(
-          ChangeField.UPDATED.getName(), SolrQuery.ORDER.desc),
-        new SortClause(
-          ChangeField.LEGACY_ID.getName(), SolrQuery.ORDER.desc));
-  }
-
-  private void commit(SolrServer server) throws IOException {
-    try {
-      server.commit();
-    } catch (SolrServerException e) {
-      throw new IOException(e);
-    }
-  }
-
-  private class QuerySource implements ChangeDataSource {
-    private final List<SolrServer> servers;
-    private final SolrQuery query;
-
-    public QuerySource(List<SolrServer> indexes, Query q, int start, int limit,
-        List<SortClause> sorts) {
-      this.servers = indexes;
-
-      query = new SolrQuery(q.toString());
-      query.setParam("shards.tolerant", true);
-      query.setParam("rows", Integer.toString(limit));
-      if (start != 0) {
-        query.setParam("start", Integer.toString(start));
-      }
-      query.setFields(ID_FIELD);
-      query.setSorts(sorts);
-    }
-
-    @Override
-    public int getCardinality() {
-      return 10; // TODO: estimate from solr?
-    }
-
-    @Override
-    public boolean hasChange() {
-      return false;
-    }
-
-    @Override
-    public String toString() {
-      return query.getQuery();
-    }
-
-    @Override
-    public ResultSet<ChangeData> read() throws OrmException {
-      try {
-        // TODO Sort documents during merge to select only top N.
-        SolrDocumentList docs = new SolrDocumentList();
-        for (SolrServer index : servers) {
-          docs.addAll(index.query(query).getResults());
-        }
-
-        List<ChangeData> result = Lists.newArrayListWithCapacity(docs.size());
-        for (SolrDocument doc : docs) {
-          Integer v = (Integer) doc.getFieldValue(ID_FIELD);
-          result.add(
-              changeDataFactory.create(db.get(), new Change.Id(v.intValue())));
-        }
-
-        final List<ChangeData> r = Collections.unmodifiableList(result);
-        return new ResultSet<ChangeData>() {
-          @Override
-          public Iterator<ChangeData> iterator() {
-            return r.iterator();
-          }
-
-          @Override
-          public List<ChangeData> toList() {
-            return r;
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        };
-      } catch (SolrServerException e) {
-        throw new OrmException(e);
-      }
-    }
-  }
-
-  private SolrInputDocument toDocument(ChangeData cd) {
-    SolrInputDocument result = new SolrInputDocument();
-    for (Values<ChangeData> values : schema.buildFields(cd, fillArgs)) {
-      add(result, values);
-    }
-    return result;
-  }
-
-  private void add(SolrInputDocument doc, Values<ChangeData> values) {
-    String name = values.getField().getName();
-    FieldType<?> type = values.getField().getType();
-
-    if (type == FieldType.INTEGER) {
-      for (Object value : values.getValues()) {
-        doc.addField(name, value);
-      }
-    } else if (type == FieldType.LONG) {
-      for (Object value : values.getValues()) {
-        doc.addField(name, value);
-      }
-    } else if (type == FieldType.TIMESTAMP) {
-      for (Object value : values.getValues()) {
-        doc.addField(name, ((Timestamp) value).getTime());
-      }
-    } else if (type == FieldType.EXACT
-        || type == FieldType.PREFIX
-        || type == FieldType.FULL_TEXT) {
-      for (Object value : values.getValues()) {
-        doc.addField(name, value);
-      }
-    } else {
-      throw QueryBuilder.badFieldType(type);
-    }
-  }
-
-  @Override
-  public void markReady(boolean ready) throws IOException {
-    // TODO Move the schema version information to a special meta-document
-    FileBasedConfig cfg = new FileBasedConfig(
-        solrIndexConfig(sitePaths),
-        FS.detect());
-    for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
-      cfg.setInt("index", e.getKey(), "schemaVersion",
-          ready ? e.getValue() : -1);
-    }
-    cfg.save();
-  }
-}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
deleted file mode 100644
index 38de6ee..0000000
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2013 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.solr;
-
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.ChangeSchemas;
-import com.google.gerrit.server.index.FieldDef.FillArgs;
-import com.google.gerrit.server.index.IndexCollection;
-import com.google.gerrit.server.index.IndexConfig;
-import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Provider;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-
-import java.io.IOException;
-
-public class SolrIndexModule extends LifecycleModule {
-  private final boolean checkVersion;
-  private final int threads;
-  private final String base;
-
-  public SolrIndexModule() {
-    this(true, 0, null);
-  }
-
-  public SolrIndexModule(boolean checkVersion, int threads, String base) {
-    this.checkVersion = checkVersion;
-    this.threads = threads;
-    this.base = base;
-  }
-
-  @Override
-  protected void configure() {
-    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
-    install(new IndexModule(threads));
-    bind(ChangeIndex.class).to(SolrChangeIndex.class);
-    listener().to(SolrChangeIndex.class);
-    if (checkVersion) {
-      listener().to(IndexVersionCheck.class);
-    }
-  }
-
-  @Provides
-  @Singleton
-  public SolrChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
-      Provider<ReviewDb> db,
-      ChangeData.Factory changeDataFactory,
-      SitePaths sitePaths,
-      IndexCollection indexes,
-      FillArgs fillArgs) throws IOException {
-    return new SolrChangeIndex(cfg, db, changeDataFactory, fillArgs, sitePaths,
-        indexes, ChangeSchemas.getLatest(), base);
-  }
-}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 701ef4d..5a7b539 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -28,6 +28,7 @@
     '//lib/mina:core',
     '//lib/mina:sshd',
     '//lib/jgit:jgit',
+    '//lib/jgit:jgit-archive',
   ],
   provided_deps = [
     '//lib/bouncycastle:bcprov',
@@ -50,8 +51,7 @@
     ':sshd',
     '//gerrit-extension-api:api',
     '//gerrit-server:server',
-    '//lib:guava',
-    '//lib:junit',
+    '//lib:truth',
     '//lib/mina:sshd',
   ],
   source_under_test = [':sshd'],
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index c0fd2ac..5c897e4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
@@ -51,13 +53,13 @@
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicReference;
 
 public abstract class BaseCommand implements Command {
   private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
-  public static final String ENC = "UTF-8";
+  public static final Charset ENC = UTF_8;
 
   private static final int PRIVATE_STATUS = 1 << 30;
   static final int STATUS_CANCEL = PRIVATE_STATUS | 1;
@@ -87,7 +89,7 @@
   private WorkQueue.Executor executor;
 
   @Inject
-  private Provider<CurrentUser> userProvider;
+  private Provider<CurrentUser> user;
 
   @Inject
   private Provider<SshScope.Context> contextProvider;
@@ -203,11 +205,7 @@
     final CmdLineParser clp = newCmdLineParser(options);
     try {
       clp.parseArgument(argv);
-    } catch (IllegalArgumentException err) {
-      if (!clp.wasHelpRequestedByOption()) {
-        throw new UnloggedFailure(1, "fatal: " + err.getMessage());
-      }
-    } catch (CmdLineException err) {
+    } catch (IllegalArgumentException | CmdLineException err) {
       if (!clp.wasHelpRequestedByOption()) {
         throw new UnloggedFailure(1, "fatal: " + err.getMessage());
       }
@@ -280,7 +278,7 @@
     final TaskThunk tt = new TaskThunk(thunk);
 
     if (isAdminHighPriorityCommand()
-        && userProvider.get().getCapabilities().canAdministrateServer()) {
+        && user.get().getCapabilities().canAdministrateServer()) {
       // Admin commands should not block the main work threads (there
       // might be an interactive shell there), nor should they wait
       // for the main work threads.
@@ -313,14 +311,7 @@
 
   /** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
   protected static PrintWriter toPrintWriter(final OutputStream o) {
-    try {
-      return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, ENC)));
-    } catch (UnsupportedEncodingException e) {
-      // Our default encoding is required by the specifications for the
-      // runtime APIs, this should never, ever happen.
-      //
-      throw new RuntimeException("JVM lacks " + ENC + " encoding", e);
-    }
+    return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, ENC)));
   }
 
   private int handleError(final Throwable e) {
@@ -338,12 +329,11 @@
       return 127;
     }
 
-    if (e instanceof UnloggedFailure) {
-    } else {
+    if (!(e instanceof UnloggedFailure)) {
       final StringBuilder m = new StringBuilder();
       m.append("Internal server error");
-      if (userProvider.get().isIdentifiedUser()) {
-        final IdentifiedUser u = (IdentifiedUser) userProvider.get();
+      if (user.get().isIdentifiedUser()) {
+        final IdentifiedUser u = user.get().asIdentifiedUser();
         m.append(" (user ");
         m.append(u.getAccount().getUserName());
         m.append(" account ");
@@ -361,6 +351,7 @@
         err.write((f.getMessage() + "\n").getBytes(ENC));
         err.flush();
       } catch (IOException e2) {
+        // Ignored
       } catch (Throwable e2) {
         log.warn("Cannot send failure message to client", e2);
       }
@@ -371,6 +362,7 @@
         err.write("fatal: internal server error\n".getBytes(ENC));
         err.flush();
       } catch (IOException e2) {
+        // Ignored
       } catch (Throwable e2) {
         log.warn("Cannot send internal server error message to client", e2);
       }
@@ -406,8 +398,8 @@
 
       StringBuilder m = new StringBuilder();
       m.append(context.getCommandLine());
-      if (userProvider.get().isIdentifiedUser()) {
-        IdentifiedUser u = (IdentifiedUser) userProvider.get();
+      if (user.get().isIdentifiedUser()) {
+        IdentifiedUser u = user.get().asIdentifiedUser();
         m.append(" (").append(u.getAccount().getUserName()).append(")");
       }
       this.taskName = m.toString();
@@ -455,10 +447,12 @@
           try {
             out.flush();
           } catch (Throwable e2) {
+            // Ignored
           }
           try {
             err.flush();
           } catch (Throwable e2) {
+            // Ignored
           }
           rc = handleError(e);
         } finally {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
index bafb9ee..f78aba4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -35,7 +35,7 @@
       final WorkQueue queues) {
     final int cores = Runtime.getRuntime().availableProcessors();
     poolSize = config.getInt("sshd", "threads", 3 * cores / 2);
-    batchThreads = config.getInt("sshd", "batchThreads", 0);
+    batchThreads = config.getInt("sshd", "batchThreads", cores == 1 ? 1 : 2);
     if (batchThreads > poolSize) {
       poolSize += batchThreads;
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 56441fba..6bd9c4c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -263,30 +263,33 @@
       switch (b) {
         case '\t':
         case ' ':
-          if (inquote || inDblQuote)
+          if (inquote || inDblQuote) {
             r.append(b);
-          else if (r.length() > 0) {
+          } else if (r.length() > 0) {
             list.add(r.toString());
             r = new StringBuilder();
           }
           continue;
         case '\"':
-          if (inquote)
+          if (inquote) {
             r.append(b);
-          else
+          } else {
             inDblQuote = !inDblQuote;
+          }
           continue;
         case '\'':
-          if (inDblQuote)
+          if (inDblQuote) {
             r.append(b);
-          else
+          } else {
             inquote = !inquote;
+          }
           continue;
         case '\\':
-          if (inquote || ip == commandLine.length())
+          if (inquote || ip == commandLine.length()) {
             r.append(b); // literal within a quote
-          else
+          } else {
             r.append(commandLine.charAt(ip++));
+          }
           continue;
         default:
           r.append(b);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 1a5e62cc..e964819 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.auto.value.AutoAnnotation;
 import com.google.inject.Key;
 
 import org.apache.sshd.server.Command;
@@ -47,35 +48,9 @@
   }
 
   /** Create a CommandName annotation for the supplied name. */
-  public static CommandName named(final String name) {
-    return new CommandName() {
-      @Override
-      public String value() {
-        return name;
-      }
-
-      @Override
-      public Class<? extends Annotation> annotationType() {
-        return CommandName.class;
-      }
-
-      @Override
-      public int hashCode() {
-        // This is specified in java.lang.Annotation.
-        return (127 * "value".hashCode()) ^ value().hashCode();
-      }
-
-      @Override
-      public boolean equals(final Object obj) {
-        return obj instanceof CommandName
-            && value().equals(((CommandName) obj).value());
-      }
-
-      @Override
-      public String toString() {
-        return "@" + CommandName.class.getName() + "(value=" + value() + ")";
-      }
-    };
+  @AutoAnnotation
+  public static CommandName named(final String value) {
+    return new AutoAnnotation_Commands_named(value);
   }
 
   /** Create a CommandName annotation for the supplied name. */
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index cc7b637..3ce4545 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -14,7 +14,11 @@
 
 package com.google.gerrit.sshd;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Preconditions;
+import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -33,10 +37,10 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Collection;
@@ -65,7 +69,7 @@
   DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
       final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
       final SitePaths site, final KeyPairProvider hostKeyProvider,
-      final @GerritServerConfig Config cfg, final SshScope s) {
+      @GerritServerConfig final Config cfg, final SshScope s) {
     sshKeyCache = skc;
     sshLog = l;
     userFactory = uf;
@@ -95,7 +99,7 @@
   public boolean authenticate(String username, PublicKey suppliedKey,
       ServerSession session) {
     SshSession sd = session.getAttribute(SshSession.KEY);
-    Preconditions.checkState(sd.getCurrentUser() == null);
+    Preconditions.checkState(sd.getUser() == null);
     if (PeerDaemonUser.USER_NAME.equals(username)) {
       if (myHostKeys.contains(suppliedKey)
           || getPeerKeys().contains(suppliedKey)) {
@@ -169,56 +173,48 @@
   }
 
   private static class PeerKeyCache {
-    private final File path;
+    private final Path path;
     private final long modified;
     final Set<PublicKey> keys;
 
-    PeerKeyCache(final File path) {
+    PeerKeyCache(Path path) {
       this.path = path;
-      this.modified = path.lastModified();
+      this.modified = FileUtil.lastModified(path);
       this.keys = read(path);
     }
 
-    private static Set<PublicKey> read(File path) {
-      try {
-        final BufferedReader br = new BufferedReader(new FileReader(path));
-        try {
-          final Set<PublicKey> keys = new HashSet<>();
-          String line;
-          while ((line = br.readLine()) != null) {
-            line = line.trim();
-            if (line.startsWith("#") || line.isEmpty()) {
-              continue;
-            }
-
-            try {
-              byte[] bin = Base64.decodeBase64(line.getBytes("ISO-8859-1"));
-              keys.add(new Buffer(bin).getRawPublicKey());
-            } catch (RuntimeException e) {
-              logBadKey(path, line, e);
-            } catch (SshException e) {
-              logBadKey(path, line, e);
-            }
+    private static Set<PublicKey> read(Path path) {
+      try (BufferedReader br = Files.newBufferedReader(path, UTF_8)) {
+        final Set<PublicKey> keys = new HashSet<>();
+        String line;
+        while ((line = br.readLine()) != null) {
+          line = line.trim();
+          if (line.startsWith("#") || line.isEmpty()) {
+            continue;
           }
-          return Collections.unmodifiableSet(keys);
-        } finally {
-          br.close();
-        }
-      } catch (FileNotFoundException noFile) {
-        return Collections.emptySet();
 
+          try {
+            byte[] bin = Base64.decodeBase64(line.getBytes(ISO_8859_1));
+            keys.add(new Buffer(bin).getRawPublicKey());
+          } catch (RuntimeException | SshException e) {
+            logBadKey(path, line, e);
+          }
+        }
+        return Collections.unmodifiableSet(keys);
+      } catch (NoSuchFileException noFile) {
+        return Collections.emptySet();
       } catch (IOException err) {
         log.error("Cannot read " + path, err);
         return Collections.emptySet();
       }
     }
 
-    private static void logBadKey(File path, String line, Exception e) {
+    private static void logBadKey(Path path, String line, Exception e) {
       log.warn("Invalid key in " + path + ":\n  " + line, e);
     }
 
     boolean isCurrent() {
-      return path.lastModified() == modified;
+      return modified == FileUtil.lastModified(path);
     }
 
     PeerKeyCache reload() {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index a7748ff..09222b7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -88,10 +88,11 @@
       checkRequiresCapability(cmd);
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
-        if (getName().isEmpty())
+        if (getName().isEmpty()) {
           bc.setName(commandName);
-        else
+        } else {
           bc.setName(getName() + " " + commandName);
+        }
         bc.setArguments(args.toArray(new String[args.size()]));
 
       } else if (!args.isEmpty()) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
index 241f853..3e6e2f5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
@@ -24,7 +24,8 @@
 import org.apache.sshd.common.util.SecurityUtils;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 
-import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -38,29 +39,29 @@
 
   @Override
   public KeyPairProvider get() {
-    final File objKey = site.ssh_key;
-    final File rsaKey = site.ssh_rsa;
-    final File dsaKey = site.ssh_dsa;
+    Path objKey = site.ssh_key;
+    Path rsaKey = site.ssh_rsa;
+    Path dsaKey = site.ssh_dsa;
 
     final List<String> stdKeys = new ArrayList<>(2);
-    if (rsaKey.exists()) {
-      stdKeys.add(rsaKey.getAbsolutePath());
+    if (Files.exists(rsaKey)) {
+      stdKeys.add(rsaKey.toAbsolutePath().toString());
     }
-    if (dsaKey.exists()) {
-      stdKeys.add(dsaKey.getAbsolutePath());
+    if (Files.exists(dsaKey)) {
+      stdKeys.add(dsaKey.toAbsolutePath().toString());
     }
 
-    if (objKey.exists()) {
+    if (Files.exists(objKey)) {
       if (stdKeys.isEmpty()) {
         SimpleGeneratorHostKeyProvider p = new SimpleGeneratorHostKeyProvider();
-        p.setPath(objKey.getAbsolutePath());
+        p.setPath(objKey.toAbsolutePath().toString());
         return p;
 
       } else {
         // Both formats of host key exist, we don't know which format
         // should be authoritative. Complain and abort.
         //
-        stdKeys.add(objKey.getAbsolutePath());
+        stdKeys.add(objKey.toAbsolutePath().toString());
         throw new ProvisionException("Multiple host keys exist: " + stdKeys);
       }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index a63abee..0207ede 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -187,6 +187,7 @@
         try {
           return new URL(url).getHost();
         } catch (MalformedURLException e) {
+          // Ignored
         }
       }
       return SystemReader.getInstance().getHostname();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 5fb1cfa..a8bc8dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -61,6 +61,7 @@
 import org.apache.sshd.common.cipher.CipherNone;
 import org.apache.sshd.common.cipher.TripleDESCBC;
 import org.apache.sshd.common.compression.CompressionNone;
+import org.apache.sshd.common.compression.CompressionZlib;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.FileSystemView;
 import org.apache.sshd.common.file.SshFile;
@@ -78,6 +79,8 @@
 import org.apache.sshd.common.mac.HMACMD596;
 import org.apache.sshd.common.mac.HMACSHA1;
 import org.apache.sshd.common.mac.HMACSHA196;
+import org.apache.sshd.common.mac.HMACSHA256;
+import org.apache.sshd.common.mac.HMACSHA512;
 import org.apache.sshd.common.random.BouncyCastleRandom;
 import org.apache.sshd.common.random.JceRandom;
 import org.apache.sshd.common.random.SingletonRandomFactory;
@@ -129,18 +132,20 @@
 /**
  * SSH daemon to communicate with Gerrit.
  * <p>
- * Use a Git URL such as {@code ssh://${email}@${host}:${port}/${path}},
+ * Use a Git URL such as <code>ssh://${email}@${host}:${port}/${path}</code>,
  * e.g. {@code ssh://sop@google.com@gerrit.com:8010/tools/gerrit.git} to
  * access the SSH daemon itself.
  * <p>
  * Versions of Git before 1.5.3 may require setting the username and port
  * properties in the user's {@code ~/.ssh/config} file, and using a host
- * alias through a URL such as <code>gerrit-alias:/tools/gerrit.git:
+ * alias through a URL such as {@code gerrit-alias:/tools/gerrit.git}:
  * <pre>
+ * {@code
  * Host gerrit-alias
  *  User sop@google.com
  *  Hostname gerrit.com
  *  Port 8010
+ * }
  * </pre>
  */
 @Singleton
@@ -214,6 +219,9 @@
     final String kerberosPrincipal = cfg.getString(
         "sshd", null, "kerberosPrincipal");
 
+    final boolean enableCompression = cfg.getBoolean(
+        "sshd", "enableCompression", false);
+
     SshSessionBackend backend = cfg.getEnum(
         "sshd", null, "backend", SshSessionBackend.MINA);
 
@@ -234,7 +242,7 @@
     initForwarding();
     initFileSystemFactory();
     initSubsystems();
-    initCompression();
+    initCompression(enableCompression);
     initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
@@ -444,7 +452,8 @@
         if ((n & -n) == n) {
           return (int)((n * (long) next(31)) >> 31);
         }
-        int bits, val;
+        int bits;
+        int val;
         do {
           bits = next(31);
           val = bits % n;
@@ -454,9 +463,9 @@
       throw new IllegalArgumentException();
     }
 
-    final protected int next(int numBits) {
+    protected final int next(int numBits) {
       int bytes = (numBits+7)/8;
-      byte next[] = new byte[bytes];
+      byte[] next = new byte[bytes];
       int ret = 0;
       random.nextBytes(next);
       for (int i = 0; i < bytes; i++) {
@@ -509,9 +518,13 @@
   }
 
   private void initMacs(final Config cfg) {
-    setMacFactories(filter(cfg, "mac", new HMACMD5.Factory(),
-        new HMACSHA1.Factory(), new HMACMD596.Factory(),
-        new HMACSHA196.Factory()));
+    setMacFactories(filter(cfg, "mac",
+        new HMACMD5.Factory(),
+        new HMACSHA1.Factory(),
+        new HMACMD596.Factory(),
+        new HMACSHA196.Factory(),
+        new HMACSHA256.Factory(),
+        new HMACSHA512.Factory()));
   }
 
   @SafeVarargs
@@ -592,13 +605,30 @@
         new SignatureECDSA.NISTP521Factory()));
   }
 
-  private void initCompression() {
-    // Always disable transparent compression. The majority of our data
-    // transfer is highly compressed Git pack files. We cannot make them
-    // any smaller than they already are.
+  private void initCompression(boolean enableCompression) {
+    List<NamedFactory<Compression>> compressionFactories =
+        Lists.newArrayList();
+
+    // Always support no compression over SSHD.
+    compressionFactories.add(new CompressionNone.Factory());
+
+    // In the general case, we want to disable transparent compression, since
+    // the majority of our data transfer is highly compressed Git pack files
+    // and we cannot make them any smaller than they already are.
     //
-    setCompressionFactories(Arrays
-        .<NamedFactory<Compression>> asList(new CompressionNone.Factory()));
+    // However, if there are CPU in abundance and the server is reachable through
+    // slow networks, gits with huge amount of refs can benefit from SSH-compression
+    // since git does not compress the ref announcement during the handshake.
+    //
+    // Compression can be especially useful when Gerrit slaves are being used
+    // for the larger clones and fetches and the master server mostly takes small
+    // receive-packs.
+
+    if (enableCompression) {
+      compressionFactories.add(new CompressionZlib.Factory());
+    }
+
+    setCompressionFactories(compressionFactories);
   }
 
   private void initChannels() {
@@ -689,7 +719,8 @@
           @Override
           public FileSystemView getNormalizedView() {
             return this;
-          }};
+          }
+        };
       }
     });
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index bacb167..4654069 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -105,10 +105,7 @@
           new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
       SshUtil.parse(key);
       return key;
-    } catch (NoSuchAlgorithmException e) {
-      throw new InvalidSshKeyException();
-
-    } catch (InvalidKeySpecException e) {
+    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
       throw new InvalidSshKeyException();
 
     } catch (NoSuchProviderException e) {
@@ -127,8 +124,7 @@
 
     @Override
     public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
+      try (ReviewDb db = schema.open()) {
         final AccountExternalId.Key key =
             new AccountExternalId.Key(SCHEME_USERNAME, username);
         final AccountExternalId user = db.accountExternalIds().get(key);
@@ -147,8 +143,6 @@
           return NO_KEYS;
         }
         return Collections.unmodifiableList(kl);
-      } finally {
-        db.close();
       }
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index b8b49eb..c2b6a16 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -206,7 +206,7 @@
 
   private LoggingEvent log(final String msg) {
     final SshSession sd = session.get();
-    final CurrentUser user = sd.getCurrentUser();
+    final CurrentUser user = sd.getUser();
 
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
@@ -223,10 +223,11 @@
 
     event.setProperty(P_SESSION, id(sd.getSessionId()));
 
-    String userName = "-", accountId = "-";
+    String userName = "-";
+    String accountId = "-";
 
     if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser u = (IdentifiedUser) user;
+      IdentifiedUser u = user.asIdentifiedUser();
       userName = u.getAccount().getUserName();
       accountId = "a/" + u.getAccountId().toString();
 
@@ -263,7 +264,7 @@
     } else {
       SshSession session = ctx.getSession();
       sessionId = IdGenerator.format(session.getSessionId());
-      currentUser = session.getCurrentUser();
+      currentUser = session.getUser();
       created = ctx.created;
     }
     auditService.dispatch(new SshAuditEvent(sessionId, currentUser,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
index 0c04749..2622fbd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
@@ -20,7 +20,6 @@
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
-import java.util.Date;
 import java.util.TimeZone;
 
 public final class SshLogLayout extends Layout {
@@ -35,15 +34,15 @@
   private final Calendar calendar;
   private long lastTimeMillis;
   private final char[] lastTimeString = new char[20];
-  private final char[] timeZone;
+  private final SimpleDateFormat tzFormat;
+  private char[] timeZone;
 
  public SshLogLayout() {
     final TimeZone tz = TimeZone.getDefault();
     calendar = Calendar.getInstance(tz);
 
-    final SimpleDateFormat sdf = new SimpleDateFormat("Z");
-    sdf.setTimeZone(tz);
-    timeZone = sdf.format(new Date()).toCharArray();
+    tzFormat = new SimpleDateFormat("Z");
+    tzFormat.setTimeZone(tz);
   }
 
   @Override
@@ -52,8 +51,6 @@
 
     buf.append('[');
     formatDate(event.getTimeStamp(), buf);
-    buf.append(' ');
-    buf.append(timeZone);
     buf.append(']');
 
     req(P_SESSION, buf, event);
@@ -92,11 +89,14 @@
         sbuf.append(',');
         sbuf.getChars(start, sbuf.length(), lastTimeString, 0);
         lastTimeMillis = rounded;
+        timeZone = tzFormat.format(calendar.getTime()).toCharArray();
       }
     } else {
       sbuf.append(lastTimeString);
     }
     sbuf.append(String.format("%03d", millis));
+    sbuf.append(' ');
+    sbuf.append(timeZone);
   }
 
   private String toTwoDigits(int input) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index cd09cfa..e3455e3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -80,10 +80,10 @@
     }
 
     @Override
-    public CurrentUser getCurrentUser() {
-      final CurrentUser user = session.getCurrentUser();
+    public CurrentUser getUser() {
+      CurrentUser user = session.getUser();
       if (user != null && user.isIdentifiedUser()) {
-        IdentifiedUser identifiedUser = userFactory.create(((IdentifiedUser) user).getAccountId());
+        IdentifiedUser identifiedUser = userFactory.create(user.getAccountId());
         identifiedUser.setAccessPath(user.getAccessPath());
         return identifiedUser;
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index f055b2f..ff160e0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -60,7 +60,7 @@
   }
 
   /** Identity of the authenticated user account on the socket. */
-  public CurrentUser getCurrentUser() {
+  public CurrentUser getUser() {
     return identity;
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index d4bb353..cca426d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -60,9 +60,7 @@
       }
       final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(s));
       return new Buffer(bin).getRawPublicKey();
-    } catch (RuntimeException re) {
-      throw new InvalidKeySpecException("Cannot parse key", re);
-    } catch (SshException e) {
+    } catch (RuntimeException | SshException e) {
       throw new InvalidKeySpecException("Cannot parse key", e);
     }
   }
@@ -123,7 +121,7 @@
   public static boolean success(final String username, final ServerSession session,
       final SshScope sshScope, final SshLog sshLog,
       final SshSession sd, final CurrentUser user) {
-    if (sd.getCurrentUser() == null) {
+    if (sd.getUser() == null) {
       sd.authenticationSuccess(username, user);
 
       // If this is the first time we've authenticated this
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 77738d0..c417f0a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
@@ -102,7 +104,7 @@
       if (!msg.endsWith("\n")) {
         msg += "\n";
       }
-      err.write(msg.getBytes("UTF-8"));
+      err.write(msg.getBytes(UTF_8));
       err.flush();
       onExit(1);
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index d448bd2..6ed70a2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -158,11 +158,7 @@
         }
       } catch (RepositoryNotFoundException notFound) {
         err.append("error: Project ").append(name).append(" not found\n");
-      } catch (IOException e) {
-        final String msg = "Cannot update project " + name;
-        log.error(msg, e);
-        err.append("error: ").append(msg).append("\n");
-      } catch (ConfigInvalidException e) {
+      } catch (IOException | ConfigInvalidException e) {
         final String msg = "Cannot update project " + name;
         log.error(msg, e);
         err.append("error: ").append(msg).append("\n");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 485d10f..f78b4df 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -70,7 +70,7 @@
       printCommits(r.newlyBanned, "The following commits were banned");
       printCommits(r.alreadyBanned, "The following commits were already banned");
       printCommits(r.ignored, "The following ids do not represent commits and were ignored");
-    } catch (RestApiException | IOException | InterruptedException e) {
+    } catch (RestApiException | IOException e) {
       throw die(e);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index aeb69d0..acbc50e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -33,7 +35,6 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -74,7 +75,8 @@
       @Override
       public String apply(AccountGroup.Id id) {
         return id.toString();
-      }});
+      }
+    });
     try {
       createAccountFactory.create(username).apply(TopLevelResource.INSTANCE, input);
     } catch (RestApiException e) {
@@ -82,14 +84,14 @@
     }
   }
 
-  private String readSshKey() throws UnsupportedEncodingException, IOException {
+  private String readSshKey() throws IOException {
     if (sshKey == null) {
       return null;
     }
     if ("-".equals(sshKey)) {
       sshKey = "";
       BufferedReader br =
-          new BufferedReader(new InputStreamReader(in, "UTF-8"));
+          new BufferedReader(new InputStreamReader(in, UTF_8));
       String line;
       while ((line = br.readLine()) != null) {
         sshKey += line + "\n";
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
index 55f6158..b1d09e9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
@@ -26,7 +26,7 @@
 
 /** Create a new branch. **/
 @CommandMetaData(name = "create-branch", description = "Create a new branch")
-final public class CreateBranchCommand extends SshCommand {
+public final class CreateBranchCommand extends SshCommand {
 
   @Argument(index = 0, required = true, metaVar = "PROJECT", usage = "name of the project")
   private ProjectControl project;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 75194dc..155c2eb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.CreateGroupArgs;
-import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.server.validators.GroupCreationValidationListener;
-import com.google.gerrit.server.validators.ValidationException;
+import com.google.gerrit.server.group.AddIncludedGroups;
+import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.group.CreateGroup;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
@@ -71,36 +76,75 @@
   }
 
   @Inject
-  private PerformCreateGroup.Factory performCreateGroupFactory;
+  private CreateGroup.Factory createGroupFactory;
 
   @Inject
-  private DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners;
+  private GroupsCollection groups;
+
+  @Inject
+  private AddMembers addMembers;
+
+  @Inject
+  private AddIncludedGroups addIncludedGroups;
 
   @Override
   protected void run() throws Failure, OrmException {
     try {
-      CreateGroupArgs args = new CreateGroupArgs();
-      args.setGroupName(groupName);
-      args.groupDescription = groupDescription;
-      args.visibleToAll = visibleToAll;
-      args.ownerGroupId = ownerGroupId;
-      args.initialMembers = initialMembers;
-      args.initialGroups = initialGroups;
+      GroupResource rsrc = createGroup();
 
-      for (GroupCreationValidationListener l : groupCreationValidationListeners) {
-        try {
-          l.validateNewGroup(args);
-        } catch (ValidationException e) {
-          die(e);
-        }
+      if (!initialMembers.isEmpty()) {
+        addMembers(rsrc);
       }
 
-      performCreateGroupFactory.create(args).createGroup();
-    } catch (PermissionDeniedException e) {
-      throw die(e);
-
-    } catch (NameAlreadyUsedException e) {
+      if (!initialGroups.isEmpty()) {
+        addIncludedGroups(rsrc);
+      }
+    } catch (RestApiException e) {
       throw die(e);
     }
   }
+
+  private GroupResource createGroup() throws RestApiException, OrmException {
+    GroupInput input = new GroupInput();
+    input.description = groupDescription;
+    input.visibleToAll = visibleToAll;
+
+    if (ownerGroupId != null) {
+      input.ownerId = String.valueOf(ownerGroupId.get());
+    }
+
+    GroupInfo group = createGroupFactory.create(groupName)
+        .apply(TopLevelResource.INSTANCE, input);
+    return groups.parse(TopLevelResource.INSTANCE,
+        IdString.fromUrl(group.id));
+  }
+
+  private void addMembers(GroupResource rsrc) throws RestApiException,
+      OrmException {
+    AddMembers.Input input =
+        AddMembers.Input.fromMembers(FluentIterable
+            .from(initialMembers)
+            .transform(new Function<Account.Id, String>() {
+              @Override
+              public String apply(Account.Id id) {
+                return String.valueOf(id.get());
+              }
+            })
+            .toList());
+    addMembers.apply(rsrc, input);
+  }
+
+  private void addIncludedGroups(GroupResource rsrc) throws RestApiException,
+      OrmException {
+    AddIncludedGroups.Input input =
+        AddIncludedGroups.Input.fromGroups(FluentIterable.from(initialGroups)
+            .transform(new Function<AccountGroup.UUID, String>() {
+              @Override
+              public String apply(AccountGroup.UUID id) {
+                return id.get();
+              }
+            }).toList());
+
+    addIncludedGroups.apply(rsrc, input);
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 4e151b3..3ad5156 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -46,15 +46,6 @@
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
 @CommandMetaData(name = "create-project", description = "Create a new project and associated Git repository")
 final class CreateProjectCommand extends SshCommand {
-  @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
-  void setProjectNameFromOption(String name) {
-    if (projectName != null) {
-      throw new IllegalArgumentException("NAME already supplied");
-    } else {
-      projectName = name;
-    }
-  }
-
   @Option(name = "--suggest-parents", aliases = {"-S"}, usage = "suggest parent candidates, "
       + "if this option is used all other options and arguments are ignored")
   private boolean suggestParent;
@@ -109,7 +100,8 @@
     requireChangeID = InheritableBoolean.TRUE;
   }
 
-  @Option(name = "--create-new-change-for-all-not-in-target", aliases = {"--ncfa"}, usage = "if a new change will be created for every commit not in target branch")
+  @Option(name = "--create-new-change-for-all-not-in-target", aliases = {"--ncfa"},
+      usage = "if a new change will be created for every commit not in target branch")
   void setNewChangeForAllNotInTarget(@SuppressWarnings("unused") boolean on) {
     createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
   }
@@ -124,19 +116,12 @@
   @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
   private String maxObjectSizeLimit;
 
-  @Option(name = "--plugin-config", usage = "plugin configuration parameter with format '<plugin-name>.<parameter-name>=<value>'")
+  @Option(name = "--plugin-config",
+      usage = "plugin configuration parameter with format '<plugin-name>.<parameter-name>=<value>'")
   private List<String> pluginConfigValues;
 
-  private String projectName;
-
   @Argument(index = 0, metaVar = "NAME", usage = "name of project to be created")
-  void setProjectNameFromArgument(String name) {
-    if (projectName != null) {
-      throw new IllegalArgumentException("--name already supplied");
-    } else {
-      projectName = name;
-    }
-  }
+  private String projectName;
 
   @Inject
   private GerritApi gApi;
@@ -181,7 +166,7 @@
           input.pluginConfigValues = parsePluginConfigValues(pluginConfigValues);
         }
 
-        gApi.projects().name(projectName).create(input);
+        gApi.projects().create(input);
       } else {
         List<Project.NameKey> parentCandidates =
             suggestParentCandidates.getNameKeys();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 492aaa6..c6eaebb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.sshd.CommandModule;
 import com.google.gerrit.sshd.CommandName;
@@ -34,12 +34,11 @@
 
   @Override
   protected void configure() {
-    final CommandName git = Commands.named("git");
-    final CommandName gerrit = Commands.named("gerrit");
-    CommandName index = Commands.named(gerrit, "index");
-    final CommandName logging = Commands.named(gerrit, "logging");
-    final CommandName plugin = Commands.named(gerrit, "plugin");
-    final CommandName testSubmit = Commands.named(gerrit, "test-submit");
+    CommandName git = Commands.named("git");
+    CommandName gerrit = Commands.named("gerrit");
+    CommandName logging = Commands.named(gerrit, "logging");
+    CommandName plugin = Commands.named(gerrit, "plugin");
+    CommandName testSubmit = Commands.named(gerrit, "test-submit");
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
     command(gerrit, AproposCommand.class);
@@ -58,10 +57,6 @@
     command(gerrit, VersionCommand.class);
     command(gerrit, GarbageCollectionCommand.class);
 
-    command(index).toProvider(new DispatchCommandProvider(index));
-    command(index, IndexActivateCommand.class);
-    command(index, IndexStartCommand.class);
-
     command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
     command(plugin, PluginLsCommand.class);
     command(plugin, PluginEnableCommand.class);
@@ -81,17 +76,24 @@
     if (sshEnabled()) {
       command("git-upload-pack").to(Commands.key(git, "upload-pack"));
       command(git, "upload-pack").to(Upload.class);
+      command("git-upload-archive").to(Commands.key(git, "upload-archive"));
+      command(git, "upload-archive").to(UploadArchive.class);
     }
     command("suexec").to(SuExec.class);
     listener().to(ShowCaches.StartupListener.class);
 
-    // The following commands can only be ran on a server in Master mode
     command(gerrit, CreateAccountCommand.class);
     command(gerrit, CreateGroupCommand.class);
     command(gerrit, CreateProjectCommand.class);
+    command(gerrit, SetHeadCommand.class);
     command(gerrit, AdminQueryShell.class);
 
-    if (!slaveMode) {
+    if (slaveMode) {
+      command("git-receive-pack").to(NotSupportedInSlaveModeFailureCommand.class);
+      command("gerrit-receive-pack").to(NotSupportedInSlaveModeFailureCommand.class);
+      command(git, "receive-pack").to(NotSupportedInSlaveModeFailureCommand.class);
+      command(gerrit, "test-submit").to(NotSupportedInSlaveModeFailureCommand.class);
+    } else {
       if (sshEnabled()) {
         command("git-receive-pack").to(Commands.key(git, "receive-pack"));
         command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
@@ -112,7 +114,6 @@
     command(gerrit, SetAccountCommand.class);
     command(gerrit, AdminSetParent.class);
 
-    command(gerrit, CreateAccountCommand.class);
     command(testSubmit, TestSubmitRuleCommand.class);
     command(testSubmit, TestSubmitTypeCommand.class);
 
@@ -124,8 +125,6 @@
   }
 
   private boolean sshEnabled() {
-    return downloadConfig.getDownloadSchemes().contains(DownloadScheme.SSH)
-        || downloadConfig.getDownloadSchemes().contains(
-            DownloadScheme.DEFAULT_DOWNLOADS);
+    return downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.SSH);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 6dfcb51..2bcd4cf 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
 import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH;
 import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.ListCaches;
@@ -36,7 +37,7 @@
 import java.util.List;
 
 /** Causes the caches to purge all entries and reload. */
-@RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@RequiresAnyCapability({FLUSH_CACHES, MAINTAIN_SERVER})
 @CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory",
   runsAt = MASTER_OR_SLAVE)
 final class FlushCaches extends SshCommand {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index c533f4f..a3fbcb2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GarbageCollectionResult;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.project.ProjectCache;
@@ -35,7 +36,7 @@
 import java.util.List;
 
 /** Runs the Git garbage collection. */
-@RequiresCapability(GlobalCapability.RUN_GC)
+@RequiresAnyCapability({RUN_GC, MAINTAIN_SERVER})
 @CommandMetaData(name = "gc", description = "Run Git garbage collection",
   runsAt = MASTER_OR_SLAVE)
 public class GarbageCollectionCommand extends SshCommand {
@@ -46,6 +47,9 @@
   @Option(name = "--show-progress", usage = "progress information is shown")
   private boolean showProgress;
 
+  @Option(name = "--aggressive", usage = "run aggressive garbage collection")
+  private boolean aggressive;
+
   @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
       usage = "projects for which the Git garbage collection should be run")
   private List<ProjectControl> projects = new ArrayList<>();
@@ -85,7 +89,8 @@
     }
 
     GarbageCollectionResult result =
-        garbageCollectionFactory.create().run(projectNames, showProgress ? stdout : null);
+        garbageCollectionFactory.create().run(projectNames, aggressive,
+            showProgress ? stdout : null);
     if (result.hasErrors()) {
       for (GarbageCollectionResult.Error e : result.getErrors()) {
         String msg;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
new file mode 100644
index 0000000..3e7b293
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 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.sshd.commands;
+
+import com.google.gerrit.sshd.CommandModule;
+import com.google.gerrit.sshd.CommandName;
+import com.google.gerrit.sshd.Commands;
+import com.google.gerrit.sshd.DispatchCommandProvider;
+
+public class IndexCommandsModule extends CommandModule {
+
+  @Override
+  protected void configure() {
+    CommandName gerrit = Commands.named("gerrit");
+    CommandName index = Commands.named(gerrit, "index");
+    command(index).toProvider(new DispatchCommandProvider(index));
+    command(index, IndexActivateCommand.class);
+    command(index, IndexStartCommand.class);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 95beaf3..db9b0cc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -34,7 +36,7 @@
 
 /** Kill a task in the work queue. */
 @AdminHighPriorityCommand
-@RequiresCapability(GlobalCapability.KILL_TASK)
+@RequiresAnyCapability({KILL_TASK, MAINTAIN_SERVER})
 final class KillCommand extends SshCommand {
   @Inject
   private TasksCollection tasksCollection;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index 82ad16f..75072e8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -18,14 +18,16 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupJson;
-import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gerrit.server.group.ListGroups;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -71,12 +73,13 @@
         final Provider<IdentifiedUser> identifiedUser,
         final IdentifiedUser.GenericFactory userFactory,
         final Provider<GetGroups> accountGetGroups,
-        final GroupJson json) {
+        final GroupJson json,
+        GroupBackend groupBackend) {
       super(groupCache, groupControlFactory, genericGroupControlFactory,
-          identifiedUser, userFactory, accountGetGroups, json);
+          identifiedUser, userFactory, accountGetGroups, json, groupBackend);
     }
 
-    void display(final PrintWriter out) throws OrmException {
+    void display(final PrintWriter out) throws OrmException, BadRequestException {
       final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
       for (final GroupInfo info : get()) {
         formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a"));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index c41fcdc..a70d581 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -92,10 +92,22 @@
 
     IdentifiedUser user = userFactory.create(userAccount.getId());
     ProjectControl userProjectControl = projectControl.forUser(user);
-    Repository repo;
-    try {
-      repo = repoManager.openRepository(userProjectControl.getProject()
-              .getNameKey());
+    try (Repository repo = repoManager.openRepository(
+        userProjectControl.getProject().getNameKey())) {
+      try {
+        Map<String, Ref> refsMap =
+            new VisibleRefFilter(tagCache, changeCache, repo, userProjectControl,
+                db, true).filter(repo.getRefDatabase().getRefs(ALL), false);
+
+        for (final String ref : refsMap.keySet()) {
+          if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
+            stdout.println(ref);
+          }
+        }
+      } catch (IOException e) {
+        throw new Failure(1, "fatal: Error reading refs: '"
+            + projectControl.getProject().getNameKey(), e);
+      }
     } catch (RepositoryNotFoundException e) {
       throw new UnloggedFailure("fatal: '"
           + projectControl.getProject().getNameKey() + "': not a git archive");
@@ -103,22 +115,5 @@
       throw new UnloggedFailure("fatal: Error opening: '"
           + projectControl.getProject().getNameKey());
     }
-
-    try {
-      Map<String, Ref> refsMap =
-          new VisibleRefFilter(tagCache, changeCache, repo, userProjectControl,
-              db, true).filter(repo.getRefDatabase().getRefs(ALL), false);
-
-      for (final String ref : refsMap.keySet()) {
-        if (!onlyRefsHeads || ref.startsWith(Branch.R_HEADS)) {
-          stdout.println(ref);
-        }
-      }
-    } catch (IOException e) {
-      throw new Failure(1, "fatal: Error reading refs: '"
-          + projectControl.getProject().getNameKey(), e);
-    } finally {
-      repo.close();
-    }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/NotSupportedInSlaveModeFailureCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/NotSupportedInSlaveModeFailureCommand.java
new file mode 100644
index 0000000..39e6d4a
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/NotSupportedInSlaveModeFailureCommand.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2015 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.sshd.commands;
+
+import com.google.gerrit.sshd.SshCommand;
+
+/* Failure command, that produces verbose failure message in slave mode */
+public class NotSupportedInSlaveModeFailureCommand extends SshCommand {
+  @Override
+  protected void run() throws UnloggedFailure {
+    throw die(getName() + ": is not supported in slave mode");
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 799686d..5d6621f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -107,6 +107,7 @@
       try {
         data.close();
       } catch (IOException err) {
+        // Ignored
       }
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index 4157515..4e195ca 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -49,9 +49,7 @@
     } else {
       try {
         loader.reload(names);
-      } catch (InvalidPluginException e) {
-        throw die(e.getMessage());
-      } catch (PluginInstallException e) {
+      } catch (InvalidPluginException | PluginInstallException e) {
         throw die(e.getMessage());
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 8f26bba..6c07fae 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -32,7 +34,6 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
@@ -65,11 +66,10 @@
 
   @Inject
   QueryShell(final SchemaFactory<ReviewDb> dbFactory,
-      @Assisted final InputStream in, @Assisted final OutputStream out)
-          throws UnsupportedEncodingException {
+      @Assisted final InputStream in, @Assisted final OutputStream out) {
     this.dbFactory = dbFactory;
-    this.in = new BufferedReader(new InputStreamReader(in, "UTF-8"));
-    this.out = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
+    this.in = new BufferedReader(new InputStreamReader(in, UTF_8));
+    this.out = new PrintWriter(new OutputStreamWriter(out, UTF_8));
   }
 
   public void setOutputFormat(OutputFormat fmt) {
@@ -95,10 +95,7 @@
         db.close();
         db = null;
       }
-    } catch (OrmException err) {
-      out.println("fatal: Cannot open connection: " + err.getMessage());
-
-    } catch (SQLException err) {
+    } catch (OrmException | SQLException err) {
       out.println("fatal: Cannot open connection: " + err.getMessage());
     } finally {
       out.flush();
@@ -123,10 +120,7 @@
         db.close();
         db = null;
       }
-    } catch (OrmException err) {
-      out.println("fatal: Cannot open connection: " + err.getMessage());
-
-    } catch (SQLException err) {
+    } catch (OrmException | SQLException err) {
       out.println("fatal: Cannot open connection: " + err.getMessage());
     } finally {
       out.flush();
@@ -758,7 +752,9 @@
       r.append(" (");
       boolean first = true;
       for (String c : columns.values()) {
-        if (!first) r.append(", ");
+        if (!first) {
+          r.append(", ");
+        }
         r.append(c);
         first = false;
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 38535a4..c8ebb6c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.group.PutName;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
@@ -34,19 +37,20 @@
   private String newGroupName;
 
   @Inject
-  private PerformRenameGroup.Factory performRenameGroupFactory;
+  private GroupsCollection groups;
+
+  @Inject
+  private PutName putName;
 
   @Override
   protected void run() throws Failure {
     try {
-      performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
-    } catch (OrmException e) {
-      throw die(e);
-    } catch (InvalidNameException e) {
-      throw die(e);
-    } catch (NameAlreadyUsedException e) {
-      throw die(e);
-    } catch (NoSuchGroupException e) {
+      GroupResource rsrc = groups.parse(TopLevelResource.INSTANCE,
+          IdString.fromDecoded(groupName));
+      PutName.Input input = new PutName.Input();
+      input.name = newGroupName;
+      putName.apply(rsrc, input);
+    } catch (RestApiException | OrmException | NoSuchGroupException e) {
       throw die(e);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index f84ed5a..1e3cf67 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.common.io.CharStreams;
@@ -50,7 +52,6 @@
 
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -73,7 +74,9 @@
 
   private final Set<PatchSet> patchSets = new HashSet<>();
 
-  @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review")
+  @Argument(index = 0, required = true, multiValued = true,
+      metaVar = "{COMMIT | CHANGE,PATCHSET}",
+      usage = "list of commits or patch sets to review")
   void addPatchSetId(final String token) {
     try {
       PatchSet ps = CommandUtils.parsePatchSet(token, db, projectControl,
@@ -119,6 +122,10 @@
   @Option(name = "--json", aliases = "-j", usage = "read review input json from stdin")
   private boolean json;
 
+  @Option(name = "--strict-labels", usage = "Strictly check if the labels "
+      + "specified can be applied to the given patch set(s)")
+  private boolean strictLabels;
+
   @Option(name = "--label", aliases = "-l", usage = "custom label(s) to assign", metaVar = "LABEL=VALUE")
   void addLabel(final String token) {
     LabelVote v = LabelVote.parseWithEquals(token);
@@ -247,8 +254,7 @@
   }
 
   private ReviewInput reviewFromJson() throws UnloggedFailure {
-    try (InputStreamReader r =
-          new InputStreamReader(in, StandardCharsets.UTF_8)) {
+    try (InputStreamReader r = new InputStreamReader(in, UTF_8)) {
       return OutputFormat.JSON.newGson().
           fromJson(CharStreams.toString(r), ReviewInput.class);
     } catch (IOException | JsonSyntaxException e) {
@@ -267,7 +273,7 @@
     review.notify = notify;
     review.labels = Maps.newTreeMap();
     review.drafts = ReviewInput.DraftHandling.PUBLISH;
-    review.strictLabels = false;
+    review.strictLabels = strictLabels;
     for (ApproveOption ao : optionList) {
       Short v = ao.value();
       if (v != null) {
@@ -354,6 +360,7 @@
     try {
       err.write(msg.getBytes(ENC));
     } catch (IOException e) {
+      // Ignored
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index bdc4cef..194e65f9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -22,6 +22,8 @@
  */
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.tools.ToolsCatalog.Entry;
 import com.google.gerrit.sshd.BaseCommand;
@@ -188,7 +190,7 @@
       }
     }
 
-    out.write("E\n".getBytes("UTF-8"));
+    out.write("E\n".getBytes(UTF_8));
     out.flush();
     readAck();
   }
@@ -210,7 +212,7 @@
     buf.append(" ");
     buf.append(dir.getName());
     buf.append("\n");
-    out.write(buf.toString().getBytes("UTF-8"));
+    out.write(buf.toString().getBytes(UTF_8));
     out.flush();
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 1cb442d..a6b2810 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -225,7 +228,7 @@
       in.raw = new RawInput() {
         @Override
         public InputStream getInputStream() throws IOException {
-          return new ByteArrayInputStream(sshKey.getBytes("UTF-8"));
+          return new ByteArrayInputStream(sshKey.getBytes(UTF_8));
         }
 
         @Override
@@ -270,7 +273,7 @@
 
   private void addEmail(String email) throws UnloggedFailure, RestApiException,
       OrmException {
-    CreateEmail.Input in = new CreateEmail.Input();
+    EmailInput in = new EmailInput();
     in.email = email;
     in.noConfirmation = true;
     try {
@@ -311,7 +314,7 @@
       if (idx >= 0) {
         StringBuilder sshKey = new StringBuilder();
         BufferedReader br =
-            new BufferedReader(new InputStreamReader(in, "UTF-8"));
+            new BufferedReader(new InputStreamReader(in, UTF_8));
         String line;
         while ((line = br.readLine()) != null) {
           sshKey.append(line)
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
new file mode 100644
index 0000000..b1d1605
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.sshd.commands;
+
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.SetHead;
+import com.google.gerrit.server.project.SetHead.Input;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@CommandMetaData(name = "set-head", description = "Change HEAD reference for a project")
+public class SetHeadCommand extends SshCommand {
+
+  @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project")
+  private ProjectControl project;
+
+  @Option(name = "--new-head", required = true, metaVar = "REF", usage = "new HEAD reference")
+  private String newHead;
+
+  private final SetHead setHead;
+
+  @Inject
+  SetHeadCommand(SetHead setHead) {
+    this.setHead = setHead;
+  }
+
+  @Override
+  protected void run() throws Exception {
+    Input input = new SetHead.Input();
+    input.ref = newHead;
+    try {
+      setHead.apply(new ProjectResource(project), input);
+    } catch (UnprocessableEntityException e) {
+      throw new UnloggedFailure("fatal: " + e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 3fd9b4d..ab1353b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -161,11 +161,7 @@
       }
     } catch (RepositoryNotFoundException notFound) {
       err.append("error: Project ").append(name).append(" not found\n");
-    } catch (IOException e) {
-      final String msg = "Cannot update project " + name;
-      log.error(msg, e);
-      err.append("error: ").append(msg).append("\n");
-    } catch (ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException e) {
       final String msg = "Cannot update project " + name;
       log.error(msg, e);
       err.append("error: ").append(msg).append("\n");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 8521058..e9043f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -270,6 +270,7 @@
     try {
       err.write((type + ": " + msg + "\n").getBytes(ENC));
     } catch (IOException e) {
+      // Ignored
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 1c1fb60..b5801bf 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
@@ -53,7 +54,7 @@
 import java.util.Map.Entry;
 
 /** Show the current cache states. */
-@RequiresCapability(GlobalCapability.VIEW_CACHES)
+@RequiresAnyCapability({VIEW_CACHES, MAINTAIN_SERVER})
 @CommandMetaData(name = "show-caches", description = "Display current cache statistics",
   runsAt = MASTER_OR_SLAVE)
 final class ShowCaches extends SshCommand {
@@ -154,7 +155,7 @@
     printDiskCaches(caches);
     stdout.print('\n');
 
-    if (self.get().getCapabilities().canAdministrateServer()) {
+    if (self.get().getCapabilities().canMaintainServer()) {
       sshSummary();
 
       SummaryInfo summary =
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 108df96..8ac9887 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -179,9 +179,9 @@
       return "";
     }
 
-    final CurrentUser user = sd.getCurrentUser();
+    final CurrentUser user = sd.getUser();
     if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser u = (IdentifiedUser) user;
+      IdentifiedUser u = user.asIdentifiedUser();
 
       if (!numeric) {
         String name = u.getAccount().getUserName();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index e756d86..379f1b9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
-import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.EventSource;
@@ -40,7 +41,7 @@
 
 @RequiresCapability(GlobalCapability.STREAM_EVENTS)
 @CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time",
-  runsAt = MASTER_OR_SLAVE)
+  runsAt = MASTER)
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
@@ -130,7 +131,7 @@
       if (!msg.endsWith("\n")) {
         msg += "\n";
       }
-      err.write(msg.getBytes("UTF-8"));
+      err.write(msg.getBytes(UTF_8));
       err.flush();
       onExit(1);
       return;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
new file mode 100644
index 0000000..c3dae3a
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2014 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.sshd.commands;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.GetArchive;
+import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.api.ArchiveCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PacketLineIn;
+import org.eclipse.jgit.transport.PacketLineOut;
+import org.eclipse.jgit.transport.SideBandOutputStream;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Allows getting archives for Git repositories over SSH using the Git
+ * upload-archive protocol.
+ */
+public class UploadArchive extends AbstractGitCommand {
+  /**
+   * Options for parsing Git commands.
+   * <p>
+   * These options are not passed on command line, but received through input
+   * stream in pkt-line format.
+   */
+  static class Options {
+    @Option(name = "-f", aliases = {"--format"}, usage = "Format of the"
+        + " resulting archive: tar or zip... If this option is not given, and"
+        + " the output file is specified, the format is inferred from the"
+        + " filename if possible (e.g. writing to \"foo.zip\" makes the output"
+        + " to be in the zip format). Otherwise the output format is tar.")
+    private String format = "tar";
+
+    @Option(name = "--prefix",
+        usage = "Prepend <prefix>/ to each filename in the archive.")
+    private String prefix;
+
+    @Option(name = "-0", usage = "Store the files instead of deflating them.")
+    private boolean level0;
+    @Option(name = "-1")
+    private boolean level1;
+    @Option(name = "-2")
+    private boolean level2;
+    @Option(name = "-3")
+    private boolean level3;
+    @Option(name = "-4")
+    private boolean level4;
+    @Option(name = "-5")
+    private boolean level5;
+    @Option(name = "-6")
+    private boolean level6;
+    @Option(name = "-7")
+    private boolean level7;
+    @Option(name = "-8")
+    private boolean level8;
+    @Option(name = "-9", usage = "Highest and slowest compression level. You "
+        + "can specify any number from 1 to 9 to adjust compression speed and "
+        + "ratio.")
+    private boolean level9;
+
+    @Argument(index = 0, required = true, usage = "The tree or commit to "
+        + "produce an archive for.")
+    private String treeIsh = "master";
+
+    @Argument(index = 1, multiValued = true, usage =
+        "Without an optional path parameter, all files and subdirectories of "
+        + "the current working directory are included in the archive. If one "
+        + "or more paths are specified, only these are included.")
+    private List<String> path;
+  }
+
+  @Inject
+  private GetArchive.AllowedFormats allowedFormats;
+  @Inject
+  private Provider<ReviewDb> db;
+  private Options options = new Options();
+
+  /**
+   * Read and parse arguments from input stream.
+   * This method gets the arguments from input stream, in Pkt-line format,
+   * then parses them to fill the options object.
+   */
+  protected void readArguments() throws IOException, Failure {
+    String argCmd = "argument ";
+    List<String> args = Lists.newArrayList();
+
+    // Read arguments in Pkt-Line format
+    PacketLineIn packetIn = new PacketLineIn(in);
+    for (;;) {
+      String s = packetIn.readString();
+      if (s == PacketLineIn.END) {
+        break;
+      }
+      if (!s.startsWith(argCmd)) {
+        throw new Failure(1, "fatal: 'argument' token or flush expected");
+      }
+      String[] parts = s.substring(argCmd.length()).split("=", 2);
+      for(String p : parts) {
+        args.add(p);
+      }
+    }
+
+    try {
+      // Parse them into the 'options' field
+      CmdLineParser parser = new CmdLineParser(options);
+      parser.parseArgument(args);
+      if (options.path == null || Arrays.asList(".").equals(options.path)) {
+        options.path = Collections.emptyList();
+      }
+    } catch (CmdLineException e) {
+      throw new Failure(2, "fatal: unable to parse arguments, " + e);
+    }
+  }
+
+  @Override
+  protected void runImpl() throws IOException, Failure {
+    PacketLineOut packetOut = new PacketLineOut(out);
+    packetOut.setFlushOnEnd(true);
+    packetOut.writeString("ACK");
+    packetOut.end();
+
+    try {
+      // Parse Git arguments
+      readArguments();
+
+      ArchiveFormat f = allowedFormats.getExtensions().get("." + options.format);
+      if (f == null) {
+        throw new Failure(3, "fatal: upload-archive not permitted");
+      }
+
+      // Find out the object to get from the specified reference and paths
+      ObjectId treeId = repo.resolve(options.treeIsh);
+      if (treeId == null) {
+        throw new Failure(4, "fatal: reference not found");
+      }
+
+      // Verify the user has permissions to read the specified reference
+      if (!projectControl.allRefsAreVisible() && !canRead(treeId)) {
+          throw new Failure(5, "fatal: cannot perform upload-archive operation");
+      }
+
+        // The archive is sent in DATA sideband channel
+      try (SideBandOutputStream sidebandOut =
+            new SideBandOutputStream(SideBandOutputStream.CH_DATA,
+                SideBandOutputStream.MAX_BUF, out)) {
+        new ArchiveCommand(repo)
+            .setFormat(f.name())
+            .setFormatOptions(getFormatOptions(f))
+            .setTree(treeId)
+            .setPaths(options.path.toArray(new String[0]))
+            .setPrefix(options.prefix)
+            .setOutputStream(sidebandOut)
+            .call();
+        sidebandOut.flush();
+      } catch (GitAPIException e) {
+        throw new Failure(7, "fatal: git api exception, " + e);
+      }
+    } catch (Failure f) {
+      // Report the error in ERROR sideband channel
+      try (SideBandOutputStream sidebandError =
+          new SideBandOutputStream(SideBandOutputStream.CH_ERROR,
+              SideBandOutputStream.MAX_BUF, out)) {
+        sidebandError.write(f.getMessage().getBytes(UTF_8));
+        sidebandError.flush();
+      }
+      throw f;
+    } finally {
+      // In any case, cleanly close the packetOut channel
+      packetOut.end();
+    }
+  }
+
+  private Map<String, Object> getFormatOptions(ArchiveFormat f) {
+    if (f == ArchiveFormat.ZIP) {
+      int value = Arrays.asList(options.level0, options.level1, options.level2,
+          options.level3, options.level4, options.level5, options.level6,
+          options.level7, options.level8, options.level9).indexOf(true);
+      if (value >= 0) {
+        return ImmutableMap.<String, Object> of(
+            "level", Integer.valueOf(value));
+      }
+    }
+    return Collections.emptyMap();
+  }
+
+  private boolean canRead(ObjectId revId) throws IOException {
+    try (RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(revId);
+      return projectControl.canReadCommit(db.get(), rw, commit);
+    }
+  }
+}
diff --git a/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java b/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
index 4f48eea..70ea632 100644
--- a/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
+++ b/gerrit-sshd/src/test/java/com/google/gerrit/sshd/commands/ProjectConfigParamParserTest.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
 
@@ -40,8 +38,8 @@
     Map<String, Map<String, ConfigValue>> r =
         cmd.parsePluginConfigValues(Collections.singletonList(in));
     ConfigValue configValue = r.get("a").get("b");
-    assertEquals("c", configValue.value);
-    assertNull(configValue.values);
+    assertThat(configValue.value).isEqualTo("c");
+    assertThat(configValue.values).isNull();
   }
 
   @Test
@@ -50,7 +48,7 @@
     Map<String, Map<String, ConfigValue>> r =
         cmd.parsePluginConfigValues(Collections.singletonList(in));
     ConfigValue configValue = r.get("a").get("b");
-    assertArrayEquals(new String[] {"c", "d", "e"}, configValue.values.toArray());
-    assertNull(configValue.value);
+    assertThat(configValue.values).containsExactly("c", "d", "e").inOrder();
+    assertThat(configValue.value).isNull();
   }
 }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 734b7045..e70ab72 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -202,8 +202,9 @@
     for (int argi = 0; argi < args.length; argi++) {
       final String str = args[argi];
       if (str.equals("--")) {
-        while (argi < args.length)
+        while (argi < args.length) {
           tmp.add(args[argi++]);
+        }
         break;
       }
 
diff --git a/gerrit-util-http/BUCK b/gerrit-util-http/BUCK
index 7041c0a..e9b2a3d 100644
--- a/gerrit-util-http/BUCK
+++ b/gerrit-util-http/BUCK
@@ -5,13 +5,33 @@
   visibility = ['PUBLIC'],
 )
 
+TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java'])
+
+java_library(
+  name = 'testutil',
+  srcs = TESTUTIL_SRCS,
+  deps = [
+    '//gerrit-extension-api:api',
+    '//lib:guava',
+    '//lib:servlet-api-3_1',
+    '//lib/httpcomponents:httpclient',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
 java_test(
   name = 'http_tests',
-  srcs = glob(['src/test/java/**/*.java']),
+  srcs = glob(
+    ['src/test/java/**/*.java'],
+    excludes = TESTUTIL_SRCS,
+  ),
   deps = [
     ':http',
+    ':testutil',
     '//lib:junit',
     '//lib:servlet-api-3_1',
+    '//lib:truth',
     '//lib/easymock:easymock',
   ],
   source_under_test = [':http'],
diff --git a/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java b/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java
index 922a8d5..888ad2f 100644
--- a/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java
+++ b/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java
@@ -35,6 +35,7 @@
    *     without decoding URL-encoded characters.
    */
   public static String getEncodedPathInfo(HttpServletRequest req) {
+    // CS IGNORE LineLength FOR NEXT 3 LINES. REASON: URL.
     // Based on com.google.guice.ServletDefinition$1#getPathInfo() from:
     // https://github.com/google/guice/blob/41c126f99d6309886a0ded2ac729033d755e1593/extensions/servlet/src/com/google/inject/servlet/ServletDefinition.java
     String servletPath = req.getServletPath();
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
index cfa0111..e656e56 100644
--- a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
@@ -14,78 +14,51 @@
 
 package com.google.gerrit.util.http;
 
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
-import org.junit.After;
-import org.junit.Before;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
-
 public class RequestUtilTest {
-  private List<Object> mocks;
-
-  @Before
-  public void setUp() {
-    mocks = Collections.synchronizedList(new ArrayList<>());
-  }
-
-  @After
-  public void tearDown() {
-    for (Object mock : mocks) {
-      verify(mock);
-    }
-  }
-
   @Test
   public void emptyContextPath() {
-    assertEquals("/foo/bar", RequestUtil.getEncodedPathInfo(
-        mockRequest("/s/foo/bar", "", "/s")));
-    assertEquals("/foo%2Fbar", RequestUtil.getEncodedPathInfo(
-        mockRequest("/s/foo%2Fbar", "", "/s")));
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("", "/s", "/foo/bar"))).isEqualTo("/foo/bar");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("", "/s", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
   }
 
   @Test
   public void emptyServletPath() {
-    assertEquals("/foo/bar", RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/foo/bar", "/c", "")));
-    assertEquals("/foo%2Fbar", RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/foo%2Fbar", "/c", "")));
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("", "/c", "/foo/bar"))).isEqualTo("/foo/bar");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("", "/c", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
   }
 
   @Test
   public void trailingSlashes() {
-    assertEquals("/foo/bar/", RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo/bar/", "/c", "/s")));
-    assertEquals("/foo/bar/", RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo/bar///", "/c", "/s")));
-    assertEquals("/foo%2Fbar/", RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo%2Fbar/", "/c", "/s")));
-    assertEquals("/foo%2Fbar/", RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo%2Fbar///", "/c", "/s")));
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("/c", "/s", "/foo/bar/"))).isEqualTo("/foo/bar/");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("/c", "/s", "/foo/bar///"))).isEqualTo("/foo/bar/");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("/c", "/s", "/foo%2Fbar/"))).isEqualTo("/foo%2Fbar/");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("/c", "/s", "/foo%2Fbar///"))).isEqualTo("/foo%2Fbar/");
   }
 
   @Test
-  public void servletPathMatchesRequestPath() {
-    assertEquals(null, RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s", "/c", "/s")));
+  public void emptyPathInfo() {
+    assertThat(RequestUtil.getEncodedPathInfo(
+        fakeRequest("/c", "/s", ""))).isNull();
   }
 
-  private HttpServletRequest mockRequest(String uri, String contextPath, String servletPath) {
-    HttpServletRequest req = createMock(HttpServletRequest.class);
-    expect(req.getRequestURI()).andStubReturn(uri);
-    expect(req.getContextPath()).andStubReturn(contextPath);
-    expect(req.getServletPath()).andStubReturn(servletPath);
-    replay(req);
-    mocks.add(req);
-    return req;
+  private FakeHttpServletRequest fakeRequest(String contextPath,
+      String servletPath, String pathInfo) {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "gerrit.example.com", 80, contextPath, servletPath);
+    return req.setPathInfo(pathInfo);
   }
 }
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
new file mode 100644
index 0000000..3991b95
--- /dev/null
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -0,0 +1,488 @@
+// Copyright (C) 2015 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.util.http.testutil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.Url;
+
+import org.apache.http.client.utils.DateUtils;
+
+import java.io.BufferedReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+import javax.servlet.http.Part;
+
+/** Simple fake implementation of {@link HttpServletRequest}. */
+public class FakeHttpServletRequest implements HttpServletRequest {
+  public static final String SERVLET_PATH = "/b";
+
+  private final Map<String, Object> attributes;
+  private final ListMultimap<String, String> headers;
+
+  private ListMultimap<String, String> parameters;
+  private String hostName;
+  private int port;
+  private String contextPath;
+  private String servletPath;
+  private String path;
+
+  public FakeHttpServletRequest() {
+    this("gerrit.example.com", 80, "", SERVLET_PATH);
+  }
+
+  public FakeHttpServletRequest(String hostName, int port, String contextPath,
+      String servletPath) {
+    this.hostName = checkNotNull(hostName, "hostName");
+    checkArgument(port > 0);
+    this.port = port;
+    this.contextPath = checkNotNull(contextPath, "contextPath");
+    this.servletPath = checkNotNull(servletPath, "servletPath");
+    attributes = Maps.newConcurrentMap();
+    parameters = LinkedListMultimap.create();
+    headers = LinkedListMultimap.create();
+  }
+
+  @Override
+  public Object getAttribute(String name) {
+    return attributes.get(name);
+  }
+
+  @Override
+  public Enumeration<String> getAttributeNames() {
+    return Collections.enumeration(attributes.keySet());
+  }
+
+  @Override
+  public String getCharacterEncoding() {
+    return UTF_8.name();
+  }
+
+  @Override
+  public int getContentLength() {
+    return -1;
+  }
+
+  @Override
+  public String getContentType() {
+    return null;
+  }
+
+  @Override
+  public ServletInputStream getInputStream() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getLocalAddr() {
+    return "1.2.3.4";
+  }
+
+  @Override
+  public String getLocalName() {
+    return hostName;
+  }
+
+  @Override
+  public int getLocalPort() {
+    return port;
+  }
+
+  @Override
+  public Locale getLocale() {
+    return Locale.US;
+  }
+
+  @Override
+  public Enumeration<Locale> getLocales() {
+    return Collections.enumeration(Collections.singleton(Locale.US));
+  }
+
+  @Override
+  public String getParameter(String name) {
+    return Iterables.getFirst(parameters.get(name), null);
+  }
+
+  private static final Function<Collection<String>, String[]> STRING_COLLECTION_TO_ARRAY =
+      new Function<Collection<String>, String[]>() {
+        @Override
+        public String[] apply(Collection<String> values) {
+          return values.toArray(new String[0]);
+        }
+      };
+
+  @Override
+  public Map<String, String[]> getParameterMap() {
+    return Collections.unmodifiableMap(
+        Maps.transformValues(parameters.asMap(), STRING_COLLECTION_TO_ARRAY));
+  }
+
+  @Override
+  public Enumeration<String> getParameterNames() {
+    return Collections.enumeration(parameters.keySet());
+  }
+
+  @Override
+  public String[] getParameterValues(String name) {
+    return STRING_COLLECTION_TO_ARRAY.apply(parameters.get(name));
+  }
+
+  public void setQueryString(String qs) {
+    ListMultimap<String, String> params = LinkedListMultimap.create();
+    for (String entry : Splitter.on('&').split(qs)) {
+      List<String> kv = Splitter.on('=').limit(2).splitToList(entry);
+      try {
+        params.put(URLDecoder.decode(kv.get(0), UTF_8.name()),
+            kv.size() == 2 ? URLDecoder.decode(kv.get(1), UTF_8.name()) : "");
+      } catch (UnsupportedEncodingException e) {
+        throw new IllegalArgumentException(e);
+      }
+    }
+    parameters = params;
+  }
+
+  @Override
+  public String getProtocol() {
+    return "HTTP/1.1";
+  }
+
+  @Override
+  public BufferedReader getReader() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public String getRealPath(String path) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getRemoteAddr() {
+    return "5.6.7.8";
+  }
+
+  @Override
+  public String getRemoteHost() {
+    return "remotehost";
+  }
+
+  @Override
+  public int getRemotePort() {
+    return 1234;
+  }
+
+  @Override
+  public RequestDispatcher getRequestDispatcher(String path) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getScheme() {
+    return port == 443 ? "https" : "http";
+  }
+
+  @Override
+  public String getServerName() {
+    return hostName;
+  }
+
+  @Override
+  public int getServerPort() {
+    return port;
+  }
+
+  @Override
+  public boolean isSecure() {
+    return port == 443;
+  }
+
+  @Override
+  public void removeAttribute(String name) {
+    attributes.remove(name);
+  }
+
+  @Override
+  public void setAttribute(String name, Object value) {
+    attributes.put(name, value);
+  }
+
+  @Override
+  public void setCharacterEncoding(String env) throws UnsupportedOperationException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getAuthType() {
+    return null;
+  }
+
+  @Override
+  public String getContextPath() {
+    return contextPath;
+  }
+
+  @Override
+  public Cookie[] getCookies() {
+    return new Cookie[0];
+  }
+
+  @Override
+  public long getDateHeader(String name) {
+    String v = getHeader(name);
+    return v != null ? DateUtils.parseDate(v).getTime() : 0;
+  }
+
+  @Override
+  public String getHeader(String name) {
+    return Iterables.getFirst(headers.get(name), null);
+  }
+
+  @Override
+  public Enumeration<String> getHeaderNames() {
+    return Collections.enumeration(headers.keySet());
+  }
+
+  @Override
+  public Enumeration<String> getHeaders(String name) {
+    return Collections.enumeration(headers.get(name));
+  }
+
+  @Override
+  public int getIntHeader(String name) {
+    return Integer.parseInt(getHeader(name));
+  }
+
+  @Override
+  public String getMethod() {
+    return "GET";
+  }
+
+  @Override
+  public String getPathInfo() {
+    return path;
+  }
+
+  public FakeHttpServletRequest setPathInfo(String path) {
+    this.path = path;
+    return this;
+  }
+
+  @Override
+  public String getPathTranslated() {
+    return path;
+  }
+
+  @Override
+  public String getQueryString() {
+    return paramsToString(parameters);
+  }
+
+  @Override
+  public String getRemoteUser() {
+    return null;
+  }
+
+  @Override
+  public String getRequestURI() {
+    String uri = contextPath + servletPath + path;
+    if (!parameters.isEmpty()) {
+      uri += "?" + paramsToString(parameters);
+    }
+    return uri;
+  }
+
+  @Override
+  public StringBuffer getRequestURL() {
+    return null;
+  }
+
+  @Override
+  public String getRequestedSessionId() {
+    return null;
+  }
+
+  @Override
+  public String getServletPath() {
+    return servletPath;
+  }
+
+  @Override
+  public HttpSession getSession() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public HttpSession getSession(boolean create) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Principal getUserPrincipal() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isRequestedSessionIdFromCookie() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isRequestedSessionIdFromURL() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public boolean isRequestedSessionIdFromUrl() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isRequestedSessionIdValid() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isUserInRole(String role) {
+    throw new UnsupportedOperationException();
+  }
+
+  private static String paramsToString(ListMultimap<String, String> params) {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    for (Map.Entry<String, String> e : params.entries()) {
+      if (!first) {
+        sb.append('&');
+      } else {
+        first = false;
+      }
+      sb.append(Url.encode(e.getKey()));
+      if (!"".equals(e.getValue())) {
+        sb.append('=').append(Url.encode(e.getValue()));
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public AsyncContext getAsyncContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DispatcherType getDispatcherType() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ServletContext getServletContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isAsyncStarted() {
+    return false;
+  }
+
+  @Override
+  public boolean isAsyncSupported() {
+    return false;
+  }
+
+  @Override
+  public AsyncContext startAsync() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public AsyncContext startAsync(ServletRequest req, ServletResponse res) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean authenticate(HttpServletResponse res) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Part getPart(String name) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Collection<Part> getParts() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void login(String username, String password) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void logout() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long getContentLengthLong() {
+    return getContentLength();
+  }
+
+  @Override
+  public String changeSessionId() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T extends HttpUpgradeHandler> T upgrade(
+      Class<T> httpUpgradeHandlerClass) {
+    throw new UnsupportedOperationException();
+  }
+
+  public FakeHttpServletRequest addHeader(String name, String value) {
+    headers.put(name, value);
+    return this;
+  }
+}
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
new file mode 100644
index 0000000..442c474
--- /dev/null
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
@@ -0,0 +1,289 @@
+// Copyright (C) 2015 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.util.http.testutil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.net.HttpHeaders;
+
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Locale;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+/** Simple fake implementation of {@link HttpServletResponse}. */
+public class FakeHttpServletResponse implements HttpServletResponse {
+  private final ByteArrayOutputStream actualBody = new ByteArrayOutputStream();
+  private final ListMultimap<String, String> headers = LinkedListMultimap.create();
+
+  private int status = SC_OK;
+  private boolean committed;
+  private ServletOutputStream outputStream;
+  private PrintWriter writer;
+
+  public FakeHttpServletResponse() {
+  }
+
+  @Override
+  public synchronized void flushBuffer() throws IOException {
+    if (outputStream != null) {
+      outputStream.flush();
+    }
+    if (writer != null) {
+      writer.flush();
+    }
+  }
+
+  @Override
+  public int getBufferSize() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getCharacterEncoding() {
+    return UTF_8.name();
+  }
+
+  @Override
+  public String getContentType() {
+    return null;
+  }
+
+  @Override
+  public Locale getLocale() {
+    return Locale.US;
+  }
+
+  @Override
+  public synchronized ServletOutputStream getOutputStream() {
+    checkState(writer == null, "getWriter() already called");
+    if (outputStream == null) {
+      outputStream = new ServletOutputStream() {
+        @Override
+        public void write(int c) throws IOException {
+          actualBody.write(c);
+        }
+
+        @Override
+        public boolean isReady() {
+          return true;
+        }
+
+        @Override
+        public void setWriteListener(WriteListener listener) {
+          throw new UnsupportedOperationException();
+        }
+      };
+    }
+    return outputStream;
+  }
+
+  @Override
+  public synchronized PrintWriter getWriter() {
+    checkState(outputStream == null, "getOutputStream() already called");
+    if (writer == null) {
+      writer = new PrintWriter(actualBody);
+    }
+    return writer;
+  }
+
+  @Override
+  public synchronized boolean isCommitted() {
+    return committed;
+  }
+
+  @Override
+  public void reset() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void resetBuffer() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setBufferSize(int sz) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setCharacterEncoding(String name) {
+    checkArgument(UTF_8.equals(Charset.forName(name)),
+        "unsupported charset: %s", name);
+  }
+
+  @Override
+  public void setContentLength(int length) {
+    setContentLengthLong(length);
+  }
+
+  @Override
+  public void setContentLengthLong(long length) {
+    headers.removeAll(HttpHeaders.CONTENT_LENGTH);
+    addHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(length));
+  }
+
+  @Override
+  public void setContentType(String type) {
+    headers.removeAll(HttpHeaders.CONTENT_TYPE);
+    addHeader(HttpHeaders.CONTENT_TYPE, type);
+  }
+
+  @Override
+  public void setLocale(Locale locale) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addCookie(Cookie cookie) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addDateHeader(String name, long value) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addHeader(String name, String value) {
+    headers.put(name.toLowerCase(), value);
+  }
+
+  @Override
+  public void addIntHeader(String name, int value) {
+    addHeader(name, Integer.toString(value));
+  }
+
+  @Override
+  public boolean containsHeader(String name) {
+    return headers.containsKey(name.toLowerCase());
+  }
+
+  @Override
+  public String encodeRedirectURL(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public String encodeRedirectUrl(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String encodeURL(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public String encodeUrl(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public synchronized void sendError(int sc) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  public synchronized void sendError(int sc, String msg) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  public synchronized void sendRedirect(String loc) {
+    status = SC_FOUND;
+    setHeader(HttpHeaders.LOCATION, loc);
+    committed = true;
+  }
+
+  @Override
+  public void setDateHeader(String name, long value) {
+    setHeader(name, Long.toString(value));
+  }
+
+  @Override
+  public void setHeader(String name, String value) {
+    headers.removeAll(name.toLowerCase());
+    addHeader(name, value);
+  }
+
+  @Override
+  public void setIntHeader(String name, int value) {
+    headers.removeAll(name.toLowerCase());
+    addIntHeader(name, value);
+  }
+
+  @Override
+  public synchronized void setStatus(int sc) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  @Deprecated
+  public synchronized void setStatus(int sc, String msg) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  public synchronized int getStatus() {
+    return status;
+  }
+
+  @Override
+  public String getHeader(String name) {
+    return Iterables.getFirst(
+        headers.get(checkNotNull(name.toLowerCase())), null);
+  }
+
+  @Override
+  public Collection<String> getHeaderNames() {
+    return headers.keySet();
+  }
+
+  @Override
+  public Collection<String> getHeaders(String name) {
+    return headers.get(checkNotNull(name.toLowerCase()));
+  }
+
+  public byte[] getActualBody() {
+    return actualBody.toByteArray();
+  }
+
+  public String getActualBodyString() {
+    return RawParseUtils.decode(getActualBody());
+  }
+}
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index 35f6084..d5c85ad 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -6,6 +6,7 @@
   deps = [
     '//gerrit-cache-h2:cache-h2',
     '//gerrit-extension-api:api',
+    '//gerrit-gpg:gpg',
     '//gerrit-httpd:httpd',
     '//gerrit-lucene:lucene',
     '//gerrit-oauth:oauth',
@@ -17,7 +18,6 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//gerrit-server/src/main/prolog:common',
-    '//gerrit-solr:solr',
     '//gerrit-sshd:sshd',
     '//lib:guava',
     '//lib:gwtorm',
@@ -38,7 +38,6 @@
   name = 'webapp_assets',
   cmd = 'cd src/main/webapp; zip -qr $OUT .',
   srcs = glob(['src/main/webapp/**/*']),
-  deps = [],
   out = 'webapp_assets.zip',
   visibility = ['//:'],
 )
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index ae70ef36..eaa9bea 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,11 +2,11 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.11.11</version>
+  <version>2.12.9-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
-  <url>http://code.google.com/p/gerrit/</url>
+  <url>https://www.gerritcodereview.com/</url>
 
   <licenses>
     <license>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
index 6bbbd8f..ea4a3ea 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
@@ -20,7 +20,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -47,21 +48,19 @@
   public void init() {
     try {
       if (sitePath != null) {
-        File site = new File(sitePath);
-        LOG.info(String.format("Initializing site at %s",
-            site.getAbsolutePath()));
+        Path site = Paths.get(sitePath);
+        LOG.info("Initializing site at " + site.toRealPath().normalize());
         new BaseInit(site, false, true, pluginsDistribution, pluginsToInstall).run();
         return;
       }
 
       try (Connection conn = connectToDb()) {
-        File site = getSiteFromReviewDb(conn);
+        Path site = getSiteFromReviewDb(conn);
         if (site == null && initPath != null) {
-          site = new File(initPath);
+          site = Paths.get(initPath);
         }
         if (site != null) {
-          LOG.info(String.format("Initializing site at %s",
-              site.getAbsolutePath()));
+          LOG.info("Initializing site at " + site.toRealPath().normalize());
           new BaseInit(site, new ReviewDbDataSourceProvider(), false, false,
               pluginsDistribution, pluginsToInstall).run();
         }
@@ -76,12 +75,12 @@
     return new ReviewDbDataSourceProvider().get().getConnection();
   }
 
-  private File getSiteFromReviewDb(Connection conn) {
+  private Path getSiteFromReviewDb(Connection conn) {
     try (Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(
           "SELECT site_path FROM system_config")) {
       if (rs.next()) {
-        return new File(rs.getString(1));
+        return Paths.get(rs.getString(1));
       }
     } catch (SQLException e) {
       return null;
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
index b97df3f..33f9ae3 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
@@ -22,12 +22,13 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.List;
 
-/** Provides {@link java.io.File} annotated with {@link SitePath}. */
-class SitePathFromSystemConfigProvider implements Provider<File> {
-  private final File path;
+/** Provides {@link Path} annotated with {@link SitePath}. */
+class SitePathFromSystemConfigProvider implements Provider<Path> {
+  private final Path path;
 
   @Inject
   SitePathFromSystemConfigProvider(SchemaFactory<ReviewDb> schemaFactory)
@@ -36,26 +37,23 @@
   }
 
   @Override
-  public File get() {
+  public Path get() {
     return path;
   }
 
-  private static File read(SchemaFactory<ReviewDb> schemaFactory)
+  private static Path read(SchemaFactory<ReviewDb> schemaFactory)
       throws OrmException {
-    ReviewDb db = schemaFactory.open();
-    try {
+    try (ReviewDb db = schemaFactory.open()) {
       List<SystemConfig> all = db.systemConfig().all().toList();
       switch (all.size()) {
         case 1:
-          return new File(all.get(0).sitePath);
+          return Paths.get(all.get(0).sitePath);
         case 0:
           throw new OrmException("system_config table is empty");
         default:
           throw new OrmException("system_config must have exactly 1 row;"
               + " found " + all.size() + " rows instead");
       }
-    } finally {
-      db.close();
     }
   }
 }
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index addac98..b76f0ec 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -19,15 +19,18 @@
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.auth.oauth.OAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.change.ChangeCleanupRunner;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -35,17 +38,15 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
-import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.contact.ContactStoreModule;
-import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.ChangeCacheImplModule;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.mime.MimeUtil2Module;
@@ -61,11 +62,11 @@
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
-import com.google.gerrit.solr.SolrIndexModule;
 import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
+import com.google.gerrit.sshd.commands.IndexCommandsModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -83,8 +84,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -106,9 +108,10 @@
   private static final Logger log =
       LoggerFactory.getLogger(WebAppInitializer.class);
 
-  private File sitePath;
+  private Path sitePath;
   private Injector dbInjector;
   private Injector cfgInjector;
+  private Config config;
   private Injector sysInjector;
   private Injector webInjector;
   private Injector sshInjector;
@@ -116,6 +119,7 @@
   private GuiceFilter filter;
 
   private ServletContext servletContext;
+  private IndexType indexType;
 
   @Override
   public void doFilter(ServletRequest req, ServletResponse res,
@@ -127,7 +131,7 @@
     if (manager == null) {
       final String path = System.getProperty("gerrit.site_path");
       if (path != null) {
-        sitePath = new File(path);
+        sitePath = Paths.get(path);
       }
 
       if (System.getProperty("gerrit.init") != null) {
@@ -165,6 +169,9 @@
       }
 
       cfgInjector = createCfgInjector();
+      initIndexType();
+      config = cfgInjector.getInstance(
+          Key.get(Config.class, GerritServerConfig.class));
       sysInjector = createSysInjector();
       if (!sshdOff()) {
         sshInjector = createSshInjector();
@@ -204,8 +211,7 @@
   }
 
   private boolean sshdOff() {
-    Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
-    return new SshAddressesModule().getListenAddresses(cfg).isEmpty();
+    return new SshAddressesModule().getListenAddresses(config).isEmpty();
   }
 
   private Injector createDbInjector() {
@@ -216,7 +222,7 @@
       Module sitePathModule = new AbstractModule() {
         @Override
         protected void configure() {
-          bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+          bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
         }
       };
       modules.add(sitePathModule);
@@ -268,7 +274,7 @@
       modules.add(new AbstractModule() {
         @Override
         protected void configure() {
-          bind(File.class).annotatedWith(SitePath.class).toProvider(
+          bind(Path.class).annotatedWith(SitePath.class).toProvider(
               SitePathFromSystemConfigProvider.class).in(SINGLETON);
         }
       });
@@ -283,6 +289,7 @@
 
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
+    modules.add(new LogFileCompressor.Module());
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
@@ -296,18 +303,14 @@
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginRestApiModule());
     modules.add(new RestCacheAdminModule());
-    AbstractModule changeIndexModule;
-    switch (IndexModule.getIndexType(cfgInjector)) {
+    modules.add(new GpgModule(config));
+    switch (indexType) {
       case LUCENE:
-        changeIndexModule = new LuceneIndexModule();
-        break;
-      case SOLR:
-        changeIndexModule = new SolrIndexModule();
+        modules.add(new LuceneIndexModule());
         break;
       default:
-        throw new IllegalStateException("unsupported index.type");
+        throw new IllegalStateException("unsupported index.type = " + indexType);
     }
-    modules.add(changeIndexModule);
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -315,7 +318,6 @@
       }
     });
     modules.add(SshKeyCacheImpl.module());
-    modules.add(new MasterNodeStartup());
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
@@ -323,15 +325,23 @@
       }
     });
     modules.add(new GarbageCollectionModule());
+    modules.add(new ChangeCleanupRunner.Module());
     return cfgInjector.createChildInjector(modules);
   }
 
+  private void initIndexType() {
+    indexType = IndexModule.getIndexType(cfgInjector);
+  }
+
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new SshHostKeyModule());
     modules.add(new DefaultCommandModule(false,
         sysInjector.getInstance(DownloadConfig.class)));
+    if (indexType == IndexType.LUCENE) {
+      modules.add(new IndexCommandsModule());
+    }
     return sysInjector.createChildInjector(modules);
   }
 
@@ -347,9 +357,7 @@
       modules.add(new NoSshModule());
     }
     modules.add(H2CacheBasedWebSession.module());
-    modules.add(HttpContactStoreConnection.module());
     modules.add(new HttpPluginModule());
-    modules.add(new ContactStoreModule());
 
     AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
     if (authConfig.getAuthType() == AuthType.OPENID) {
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index ef64f3b..8bc9bb2 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -16,7 +16,7 @@
 log4j.appender.stderr=org.apache.log4j.ConsoleAppender
 log4j.appender.stderr.target=System.err
 log4j.appender.stderr.layout=org.apache.log4j.PatternLayout
-log4j.appender.stderr.layout.ConversionPattern=[%d] %-5p %c %x: %m%n
+log4j.appender.stderr.layout.ConversionPattern=[%d] [%t] %-5p %c %x: %m%n
 
 # Silence non-critical messages from MINA SSHD.
 #
diff --git a/lib/BUCK b/lib/BUCK
index 38eb330..d481433 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -27,15 +27,21 @@
 define_license(name = 'DO_NOT_DISTRIBUTE')
 
 maven_jar(
-  name = 'gwtorm',
-  id = 'com.google.gerrit:gwtorm:1.14-14-gf54f1f1',
-  bin_sha1 = 'c02267e0245dd06930ea64a2d7c5ddc5ba6d9cfb',
-  src_sha1 = '3d17ae8a173eb34d89098c748f28cddd5080adbc',
+  name = 'gwtorm_client',
+  id = 'com.google.gerrit:gwtorm:1.14-20-gec13fdc',
+  bin_sha1 = '60c2f2a5584959343ad1b21c3c79ba0fe825ceac',
+  src_sha1 = '4c562a3aafd1c3828217ee178568ed3d34ec86eb',
   license = 'Apache2.0',
-  deps = [':protobuf'],
   repository = GERRIT,
 )
 
+java_library(
+  name = 'gwtorm',
+  exported_deps = [':gwtorm_client'],
+  deps = [':protobuf'],
+  visibility = ['PUBLIC'],
+)
+
 maven_jar(
   name = 'gwtjsonrpc',
   id = 'gwtjsonrpc:gwtjsonrpc:1.7-2-g272ca32',
@@ -47,15 +53,15 @@
 
 maven_jar(
   name = 'gson',
-  id = 'com.google.code.gson:gson:2.1',
-  sha1 = '2e66da15851f9f5b5079228f856c2f090ba98c38',
+  id = 'com.google.code.gson:gson:2.3.1',
+  sha1 = 'ecb6e1f8e4b0e84c4b886c2f14a1500caf309757',
   license = 'Apache2.0',
 )
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:18.0',
-  sha1 = 'cce0823396aa693798f8882e64213b1772032b09',
+  id = 'com.google.guava:guava:19.0-rc2',
+  sha1 = '93e17f60bc524c2610b41c494bb829c11ca89436',
   license = 'Apache2.0',
 )
 
@@ -81,8 +87,8 @@
 
 maven_jar(
   name = 'servlet-api-3_1',
-  id = 'org.apache.tomcat:tomcat-servlet-api:8.0.5',
-  sha1 = '9ef01afc25481b82aa8f3615db536869f2dc961e',
+  id = 'org.apache.tomcat:tomcat-servlet-api:8.0.24',
+  sha1 = '5d9e2e895e3111622720157d0aa540066d5fce3a',
   license = 'Apache2.0',
   exclude = ['META-INF/NOTICE', 'META-INF/LICENSE'],
 )
@@ -148,16 +154,24 @@
 )
 
 maven_jar(
+  name = 'derby',
+  id = 'org.apache.derby:derby:10.11.1.1',
+  sha1 = 'df4b50061e8e4c348ce243b921f53ee63ba9bbe1',
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
   name = 'h2',
-  id = 'com.h2database:h2:1.3.174',
-  sha1 = '2fb55391f525bc3ef9f320a379d19350af96a554',
+  id = 'com.h2database:h2:1.3.176',
+  sha1 = 'fd369423346b2f1525c413e33f8cf95b09c92cbd',
   license = 'h2',
 )
 
 maven_jar(
   name = 'postgresql',
-  id = 'postgresql:postgresql:9.1-901-1.jdbc4',
-  sha1 = '9bfabe48876ec38f6cbaa6931bad05c64a9ea942',
+  id = 'org.postgresql:postgresql:9.4.1211.jre7',
+  sha1 = '56b01e9e667f408818a6ef06a89598dbab80687d',
   license = 'postgresql',
   attach_source = False,
 )
@@ -171,12 +185,22 @@
   license = 'protobuf',
 )
 
+# Test-only dependencies below.
+
+maven_jar(
+  name = 'jimfs',
+  id = 'com.google.jimfs:jimfs:1.0',
+  sha1 = 'edd65a2b792755f58f11134e76485a928aab4c97',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [':guava'],
+)
+
 maven_jar(
   name = 'junit',
-  id = 'junit:junit:4.10',
-  sha1 = 'e4f1766ce7404a08f45d859fb9c226fc9e41a861',
+  id = 'junit:junit:4.11',
+  sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
   license = 'DO_NOT_DISTRIBUTE',
-  deps = [':hamcrest-core'],
+  exported_deps = [':hamcrest-core'],
 )
 
 maven_jar(
@@ -189,10 +213,10 @@
 
 maven_jar(
   name = 'truth',
-  id = 'com.google.truth:truth:0.25',
-  sha1 = '503ba892e8482976b81eb2b2df292858fbac3782',
+  id = 'com.google.truth:truth:0.27',
+  sha1 = 'bd17774d2dc0fffa884d42c07d2537e86c67acd6',
   license = 'DO_NOT_DISTRIBUTE',
-  deps = [
+  exported_deps = [
     ':guava',
     ':junit',
   ],
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index e6f10b1..18726c83 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -36,15 +36,15 @@
     '//lib:args4j',
     '//lib:guava',
     '//lib/lucene:analyzers-common',
-    '//lib/lucene:core',
+    '//lib/lucene:core-and-backward-codecs',
   ],
   visibility = ['//tools/eclipse:classpath'],
 )
 
 maven_jar(
   name = 'asciidoctor',
-  id = 'org.asciidoctor:asciidoctorj:1.5.0',
-  sha1 = '192df5660f72a0fb76966dcc64193b94fba65f99',
+  id = 'org.asciidoctor:asciidoctorj:1.5.2',
+  sha1 = '39d33f739ec1c46f6e908a725264eb74b23c9f99',
   license = 'asciidoctor',
   visibility = [],
   attach_source = False,
@@ -52,8 +52,8 @@
 
 maven_jar(
   name = 'jruby',
-  id = 'org.jruby:jruby-complete:1.7.4',
-  sha1 = '74984d84846523bd7da49064679ed1ccf199e1db',
+  id = 'org.jruby:jruby-complete:1.7.18',
+  sha1 = 'a1be3e1790aace5c99614a87785454d875eb21c2',
   license = 'DO_NOT_DISTRIBUTE',
   visibility = [],
   attach_source = False,
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index c7562df..dce939d 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -132,31 +132,31 @@
       return;
     }
 
-    ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile));
-    for (String inputFile : inputFiles) {
-      if (!inputFile.endsWith(inExt)) {
-        // We have to use UNSAFE mode in order to make embedding work. But in
-        // UNSAFE mode we'll also need css file in the same directory, so we
-        // have to add css files into the SRCS.
-        continue;
-      }
+    try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile))) {
+      for (String inputFile : inputFiles) {
+        if (!inputFile.endsWith(inExt)) {
+          // We have to use UNSAFE mode in order to make embedding work. But in
+          // UNSAFE mode we'll also need css file in the same directory, so we
+          // have to add css files into the SRCS.
+          continue;
+        }
 
-      String outName = mapInFileToOutFile(inputFile, inExt, outExt);
-      File out = new File(tmpdir, outName);
-      out.getParentFile().mkdirs();
-      Options options = createOptions(out);
-      renderInput(options, new File(inputFile));
-      zipFile(out, outName, zip);
+        String outName = mapInFileToOutFile(inputFile, inExt, outExt);
+        File out = new File(tmpdir, outName);
+        out.getParentFile().mkdirs();
+        Options options = createOptions(out);
+        renderInput(options, new File(inputFile));
+        zipFile(out, outName, zip);
+      }
     }
-    zip.close();
   }
 
   public static void zipFile(File file, String name, ZipOutputStream zip)
       throws IOException {
     zip.putNextEntry(new ZipEntry(name));
-    FileInputStream input = new FileInputStream(file);
-    ByteStreams.copy(input, zip);
-    input.close();
+    try (FileInputStream input = new FileInputStream(file)) {
+      ByteStreams.copy(input, zip);
+    }
     zip.closeEntry();
   }
 
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 7eb70c1..aa29d35 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.server.documentation.Constants;
 
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
@@ -25,7 +27,6 @@
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
 import org.apache.lucene.store.IndexInput;
 import org.apache.lucene.store.RAMDirectory;
-import org.apache.lucene.util.Version;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
@@ -51,8 +52,6 @@
 import java.util.zip.ZipOutputStream;
 
 public class DocIndexer {
-  @SuppressWarnings("deprecation")
-  private static final Version LUCENE_VERSION = Version.LUCENE_4_10_1;
   private static final Pattern SECTION_HEADER = Pattern.compile("^=+ (.*)");
 
   @Option(name = "-o", usage = "output JAR file")
@@ -84,81 +83,78 @@
       return;
     }
 
-    byte[] compressedIndex = zip(index());
-    JarOutputStream jar = new JarOutputStream(new FileOutputStream(outFile));
-    JarEntry entry = new JarEntry(
-        String.format("%s/%s", Constants.PACKAGE, Constants.INDEX_ZIP));
-    entry.setSize(compressedIndex.length);
-    jar.putNextEntry(entry);
-    jar.write(compressedIndex);
-    jar.closeEntry();
-    jar.close();
+    try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(outFile))) {
+      byte[] compressedIndex = zip(index());
+      JarEntry entry = new JarEntry(
+          String.format("%s/%s", Constants.PACKAGE, Constants.INDEX_ZIP));
+      entry.setSize(compressedIndex.length);
+      jar.putNextEntry(entry);
+      jar.write(compressedIndex);
+      jar.closeEntry();
+    }
   }
 
   private RAMDirectory index() throws IOException,
       UnsupportedEncodingException, FileNotFoundException {
     RAMDirectory directory = new RAMDirectory();
     IndexWriterConfig config = new IndexWriterConfig(
-        LUCENE_VERSION,
         new StandardAnalyzer(CharArraySet.EMPTY_SET));
     config.setOpenMode(OpenMode.CREATE);
-    IndexWriter iwriter = new IndexWriter(directory, config);
-    for (String inputFile : inputFiles) {
-      File file = new File(inputFile);
-      if (file.length() == 0) {
-        continue;
-      }
+    config.setCommitOnClose(true);
+    try (IndexWriter iwriter = new IndexWriter(directory, config)) {
+      for (String inputFile : inputFiles) {
+        File file = new File(inputFile);
+        if (file.length() == 0) {
+          continue;
+        }
 
-      BufferedReader titleReader = new BufferedReader(
-          new InputStreamReader(new FileInputStream(file), "UTF-8"));
-      String title = titleReader.readLine();
-      if (title != null && title.startsWith("[[")) {
-        // Generally the first line of the txt is the title. In a few cases the
-        // first line is a "[[tag]]" and the second line is the title.
-        title = titleReader.readLine();
-      }
-      titleReader.close();
-      Matcher matcher = SECTION_HEADER.matcher(title);
-      if (matcher.matches()) {
-        title = matcher.group(1);
-      }
+        String title;
+        try (BufferedReader titleReader = new BufferedReader(
+            new InputStreamReader(new FileInputStream(file), UTF_8))) {
+          title = titleReader.readLine();
+          if (title != null && title.startsWith("[[")) {
+            // Generally the first line of the txt is the title. In a few cases the
+            // first line is a "[[tag]]" and the second line is the title.
+            title = titleReader.readLine();
+          }
+        }
+        Matcher matcher = SECTION_HEADER.matcher(title);
+        if (matcher.matches()) {
+          title = matcher.group(1);
+        }
 
-      String outputFile = AsciiDoctor.mapInFileToOutFile(
-          inputFile, inExt, outExt);
-      FileReader reader = new FileReader(file);
-      Document doc = new Document();
-      doc.add(new TextField(Constants.DOC_FIELD, reader));
-      doc.add(new StringField(
+        String outputFile = AsciiDoctor.mapInFileToOutFile(
+            inputFile, inExt, outExt);
+        try (FileReader reader = new FileReader(file)) {
+          Document doc = new Document();
+          doc.add(new TextField(Constants.DOC_FIELD, reader));
+          doc.add(new StringField(
             Constants.URL_FIELD, prefix + outputFile, Field.Store.YES));
-      doc.add(new TextField(Constants.TITLE_FIELD, title, Field.Store.YES));
-      iwriter.addDocument(doc);
-      reader.close();
+          doc.add(new TextField(Constants.TITLE_FIELD, title, Field.Store.YES));
+          iwriter.addDocument(doc);
+        }
+      }
     }
-    iwriter.close();
     return directory;
   }
 
   private byte[] zip(RAMDirectory dir) throws IOException {
     ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    ZipOutputStream zip = new ZipOutputStream(buf);
-
-    for (String name : dir.listAll()) {
-      IndexInput in = dir.openInput(name, null);
-      try {
-        int len = (int) in.length();
-        byte[] tmp = new byte[len];
-        ZipEntry entry = new ZipEntry(name);
-        entry.setSize(len);
-        in.readBytes(tmp, 0, len);
-        zip.putNextEntry(entry);
-        zip.write(tmp, 0, len);
-        zip.closeEntry();
-      } finally {
-        in.close();
+    try (ZipOutputStream zip = new ZipOutputStream(buf)) {
+      for (String name : dir.listAll()) {
+        try (IndexInput in = dir.openInput(name, null)) {
+          int len = (int) in.length();
+          byte[] tmp = new byte[len];
+          ZipEntry entry = new ZipEntry(name);
+          entry.setSize(len);
+          in.readBytes(tmp, 0, len);
+          zip.putNextEntry(entry);
+          zip.write(tmp, 0, len);
+          zip.closeEntry();
+        }
       }
     }
 
-    zip.close();
     return buf.toByteArray();
   }
 
diff --git a/lib/auto/BUCK b/lib/auto/BUCK
index a596420..c688ee4 100644
--- a/lib/auto/BUCK
+++ b/lib/auto/BUCK
@@ -2,10 +2,10 @@
 
 maven_jar(
   name = 'auto-value',
-  id = 'com.google.auto.value:auto-value:1.0',
-  sha1 = '5d13e60f5d190003176ca6ba4a410fae2e3f6315',
+  id = 'com.google.auto.value:auto-value:1.1',
+  sha1 = 'f6951c141ea3e89c0f8b01da16834880a1ebf162',
   # Exclude un-relocated dependencies and replace with our own versions; see
-  # https://github.com/google/auto/blob/auto-value-1.0/value/pom.xml#L147
+  # https://github.com/google/auto/blob/auto-value-1.1/value/pom.xml#L151
   exclude = ['org/apache/*'],
   deps = ['//lib:velocity'],
   license = 'Apache2.0',
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK
index d1ec48d..0ce5817 100644
--- a/lib/bouncycastle/BUCK
+++ b/lib/bouncycastle/BUCK
@@ -2,19 +2,19 @@
 
 # This version must match the version that also appears in
 # gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
-VERSION = '1.51'
+VERSION = '1.52'
 
 maven_jar(
   name = 'bcprov',
   id = 'org.bouncycastle:bcprov-jdk15on:' + VERSION,
-  sha1 = '9ab8afcc2842d5ef06eb775a0a2b12783b99aa80',
+  sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
 )
 
 maven_jar(
   name = 'bcpg',
   id = 'org.bouncycastle:bcpg-jdk15on:' + VERSION,
-  sha1 = 'b5fa4c280dfbf8bf7c260bc1e78044c7a1de5133',
+  sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
   deps = [':bcprov'],
 )
@@ -22,7 +22,7 @@
 maven_jar(
   name = 'bcpkix',
   id = 'org.bouncycastle:bcpkix-jdk15on:' + VERSION,
-  sha1 = '6c8c1f61bf27a09f9b1a8abc201523669bba9597',
+  sha1 = 'b8ffac2bbc6626f86909589c8cc63637cc936504',
   license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
   deps = [':bcprov'],
 )
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 4c235e4b..6187b3b 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -3,8 +3,8 @@
 include_defs('//lib/codemirror/closure.defs')
 
 REPO = MAVEN_CENTRAL
-VERSION = '5.0'
-SHA1 = '24982be364be130fd7b2930c41f7203b63dbd86c'
+VERSION = '5.8'
+SHA1 = '1cbe267adf1da9659dae49253305649dae2391e9'
 
 if REPO == MAVEN_CENTRAL:
   URL = REPO + 'org/webjars/codemirror/%s/codemirror-%s.jar' % (VERSION, VERSION)
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index db87b25..abb6d92 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -9,10 +9,13 @@
   'lib/codemirror.js',
   'mode/meta.js',
   'keymap/vim.js',
+  'keymap/emacs.js',
 ]
 
 CM_ADDONS = [
   'dialog/dialog.js',
+  'edit/closebrackets.js',
+  'edit/matchbrackets.js',
   'edit/trailingspace.js',
   'scroll/annotatescrollbar.js',
   'scroll/simplescrollbars.js',
@@ -64,6 +67,7 @@
   'php',
   'pig',
   'properties',
+  'puppet',
   'python',
   'r',
   'rst',
@@ -77,6 +81,7 @@
   'tcl',
   'velocity',
   'verilog',
+  'vhdl',
   'xml',
   'yaml',
 ]
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 0582628..6922f4d 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -23,7 +23,6 @@
   sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
-  visibility = ['//lib/jgit:jgit-archive'],
 )
 
 maven_jar(
@@ -41,8 +40,8 @@
 
 maven_jar(
   name = 'lang',
-  id = 'commons-lang:commons-lang:2.5',
-  sha1 = 'b0236b252e86419eef20c31a44579d2aee2f0a69',
+  id = 'commons-lang:commons-lang:2.6',
+  sha1 = '0ce1edb914c94ebc388f086c6827e8bdeec71ac2',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
 )
@@ -73,17 +72,15 @@
   exclude = ['META-INF/LICENSE'],
 )
 
-maven_jar(
-  name = 'io',
-  id = 'commons-io:commons-io:1.4',
-  sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
-  license = 'Apache2.0',
-)
-
+# When updating the version of commons-validator, also update the
+# list of supported TLDs in:
+#    gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt
+# from:
+#    http://data.iana.org/TLD/tlds-alpha-by-domain.txt
 maven_jar(
   name = 'validator',
-  id = 'commons-validator:commons-validator:1.4.1',
-  sha1 = '2231238e391057a53f92bde5bbc588622c1956c3',
+  id = 'commons-validator:commons-validator:1.5.1',
+  sha1 = '86d05a46e8f064b300657f751b5a98c62807e2a0',
   license = 'Apache2.0',
 )
 
diff --git a/lib/easymock/BUCK b/lib/easymock/BUCK
index 5ab30d2..c0cb77b 100644
--- a/lib/easymock/BUCK
+++ b/lib/easymock/BUCK
@@ -2,8 +2,9 @@
 
 maven_jar(
   name = 'easymock',
-  id = 'org.easymock:easymock:3.2',
-  sha1 = '00c82f7fa3ef377d8954b1db25123944b5af2ba4',
+  id = 'org.easymock:easymock:3.3.1', # When bumping the version
+  # number, make sure to also move powermock to a compatible version
+  sha1 = 'a497d7f00c9af78b72b6d8f24762d9210309148a',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':cglib-2_2',
@@ -21,8 +22,8 @@
 
 maven_jar(
   name = 'objenesis',
-  id = 'org.objenesis:objenesis:1.2',
-  sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
+  id = 'org.objenesis:objenesis:2.1',
+  sha1 = '87c0ea803b69252868d09308b4618f766f135a96',
   license = 'DO_NOT_DISTRIBUTE',
   visibility = ['//lib/powermock:powermock-reflect'],
   attach_source = False,
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
index 3e2f411..6876dfe 100644
--- a/lib/gwt/BUCK
+++ b/lib/gwt/BUCK
@@ -15,15 +15,6 @@
   id = 'com.google.gwt:gwt-dev:' + VERSION,
   sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982',
   license = 'Apache2.0',
-  exported_deps = [
-    ':javax-validation',
-    ':javax-validation_src',
-    '//lib/ow2:ow2-asm',
-    '//lib/ow2:ow2-asm-analysis',
-    '//lib/ow2:ow2-asm-commons',
-    '//lib/ow2:ow2-asm-tree',
-    '//lib/ow2:ow2-asm-util',
-  ],
   attach_source = False,
   exclude = ['org/eclipse/jetty/*'],
 )
@@ -34,7 +25,7 @@
   bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
   src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
   license = 'Apache2.0',
-  visibility = [],
+  visibility = ['PUBLIC'],
 )
 
 maven_jar(
@@ -54,5 +45,5 @@
   id = 'org.javassist:javassist:3.18.1-GA',
   sha1 = 'd9a09f7732226af26bf99f19e2cffe0ae219db5b',
   license = 'Apache2.0',
-  visibility = [],
+  visibility = ['PUBLIC'],
 )
diff --git a/lib/httpcomponents/BUCK b/lib/httpcomponents/BUCK
index 50e463d..03669f2 100644
--- a/lib/httpcomponents/BUCK
+++ b/lib/httpcomponents/BUCK
@@ -1,10 +1,21 @@
 include_defs('//lib/maven.defs')
 
+VERSION = '4.4.1'
+
+maven_jar(
+  name = 'fluent-hc',
+  id = 'org.apache.httpcomponents:fluent-hc:' + VERSION,
+  bin_sha1 = '96fb842b68a44cc640c661186828b60590c71261',
+  src_sha1 = '702515612b2b94ce3374ed5b579d38cbd308eb4f',
+  license = 'Apache2.0',
+  deps = [':httpclient']
+)
+
 maven_jar(
   name = 'httpclient',
-  id = 'org.apache.httpcomponents:httpclient:4.3.4',
-  bin_sha1 = 'a9a1fef2faefed639ee0d0fba5b3b8e4eb2ff2d8',
-  src_sha1 = '7a14aafed8c5e2c4e360a2c1abd1602efa768b1f',
+  id = 'org.apache.httpcomponents:httpclient:' + VERSION,
+  bin_sha1 = '016d0bc512222f1253ee6b64d389c84e22f697f0',
+  src_sha1 = '30cb4791019c7280227e027b01814f4964a02482',
   license = 'Apache2.0',
   deps = [
     '//lib/commons:codec',
@@ -15,17 +26,16 @@
 
 maven_jar(
   name = 'httpcore',
-  id = 'org.apache.httpcomponents:httpcore:4.3.2',
-  bin_sha1 = '31fbbff1ddbf98f3aa7377c94d33b0447c646b6e',
-  src_sha1 = '4809f38359edeea9487f747e09aa58ec8d3a54c5',
+  id = 'org.apache.httpcomponents:httpcore:' + VERSION,
+  bin_sha1 = 'f5aa318bda4c6c8d688c9d00b90681dcd82ce636',
+  src_sha1 = '9700be0d0a331691654a8e901943c9a74e33c5fc',
   license = 'Apache2.0',
 )
 
 maven_jar(
   name = 'httpmime',
-  id = 'org.apache.httpcomponents:httpmime:4.3.4',
-  bin_sha1 = '54ffde537682aea984c22fbcf0106f21397c5f9b',
-  src_sha1 = '0651e21152b0963661068f948d84ed08c18094f8',
+  id = 'org.apache.httpcomponents:httpmime:' + VERSION,
+  bin_sha1 = '2f8757f5ac5e38f46c794e5229d1f3c522e9b1df',
+  src_sha1 = '5394d3715181a87009032335a55b0a9789f6e26f',
   license = 'Apache2.0',
 )
-
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
index 891fcec..342ae61 100644
--- a/lib/jetty/BUCK
+++ b/lib/jetty/BUCK
@@ -1,12 +1,12 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '9.2.9.v20150224'
+VERSION = '9.2.13.v20150730'
 EXCLUDE = ['about.html']
 
 maven_jar(
   name = 'servlet',
   id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
-  sha1 = '1797875a3cc524d181733f323866a5f7bbca03a7',
+  sha1 = '5ad6e38015a97ae9a60b6c2ad744ccfa9cf93a50',
   license = 'Apache2.0',
   deps = [':security'],
   exclude = EXCLUDE,
@@ -15,7 +15,7 @@
 maven_jar(
   name = 'security',
   id = 'org.eclipse.jetty:jetty-security:' + VERSION,
-  sha1 = '1747a52b01afbf96b58b0ae0f352185560768fc2',
+  sha1 = 'cc7c7f27ec4cc279253be1675d9e47e58b995943',
   license = 'Apache2.0',
   deps = [':server'],
   exclude = EXCLUDE,
@@ -25,7 +25,7 @@
 maven_jar(
   name = 'servlets',
   id = 'org.eclipse.jetty:jetty-servlets:' + VERSION,
-  sha1 = '9b04f638c23a4db7c8e2dbfe31ab7370ce972ade',
+  sha1 = '23eb48f1d889d45902e400750460d4cd94d74663',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [
@@ -37,7 +37,7 @@
 maven_jar(
   name = 'server',
   id = 'org.eclipse.jetty:jetty-server:' + VERSION,
-  sha1 = 'd30a52e992c3484569f58763f55097a1da3202ee',
+  sha1 = '5be7d1da0a7abffd142de3091d160717c120b6ab',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -49,7 +49,7 @@
 maven_jar(
   name = 'jmx',
   id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
-  sha1 = 'e0a9df505fbcc7c0481209325a106b922097468d',
+  sha1 = 'a2ebbbcb47ed98ecd23be550f77e8dadc9f9a800',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -61,7 +61,7 @@
 maven_jar(
   name = 'continuation',
   id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
-  sha1 = '476cae89c420170549b4851ed58dca25f349d16d',
+  sha1 = 'f6bd4e6871ecd0a5e7a5e5addcea160cd73f81bb',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
@@ -69,7 +69,7 @@
 maven_jar(
   name = 'http',
   id = 'org.eclipse.jetty:jetty-http:' + VERSION,
-  sha1 = '8b30ddc8304df24a36efbfa267acc24b7403b692',
+  sha1 = '23a745d9177ef67ef53cc46b9b70c5870082efc2',
   license = 'Apache2.0',
   exported_deps = [':io'],
   exclude = EXCLUDE,
@@ -78,7 +78,7 @@
 maven_jar(
   name = 'io',
   id = 'org.eclipse.jetty:jetty-io:' + VERSION,
-  sha1 = '06a4a23ee9decf2762d052bc2ae0501c08cc9023',
+  sha1 = '7a351e6a1b63dfd56b6632623f7ca2793ffb67ad',
   license = 'Apache2.0',
   exported_deps = [':util'],
   exclude = EXCLUDE,
@@ -88,7 +88,7 @@
 maven_jar(
   name = 'util',
   id = 'org.eclipse.jetty:jetty-util:' + VERSION,
-  sha1 = 'b5fb774a02158e9f66fed949581159a8d0dfcbe1',
+  sha1 = 'c101476360a7cdd0670462de04053507d5e70c97',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index 46bcc38..b5708a8 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -67,17 +67,23 @@
   license = 'Apache2.0',
 )
 
-prebuilt_jar(
+gwt_module(
   name = 'Edit',
-  binary_jar = ':jgit_edit_src',
+  srcs = [':jgit_edit_src'],
+  deps = [':edit_src'],
   visibility = ['PUBLIC'],
 )
 
+prebuilt_jar(
+  name = 'edit_src',
+  binary_jar = ':jgit_edit_src',
+)
+
 genrule(
   name = 'jgit_edit_src',
   cmd = 'unzip -qd $TMP $(location :jgit_src) ' +
     'org/eclipse/jgit/diff/Edit.java;' +
     'cd $TMP;' +
     'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java',
-  out = 'edit-src.jar',
+  out = 'edit.src.zip',
 )
diff --git a/lib/joda/BUCK b/lib/joda/BUCK
index d45ce78..420a8ac 100644
--- a/lib/joda/BUCK
+++ b/lib/joda/BUCK
@@ -7,8 +7,8 @@
 
 maven_jar(
   name = 'joda-time',
-  id = 'joda-time:joda-time:2.3',
-  sha1 = '56498efd17752898cfcc3868c1b6211a07b12b8f',
+  id = 'joda-time:joda-time:2.8',
+  sha1 = '9f2785d7184b97d005a44241ccaf980f43b9ccdb',
   deps = [':joda-convert'],
   license = 'Apache2.0',
   exclude = EXCLUDE,
diff --git a/lib/local.defs b/lib/local.defs
deleted file mode 100644
index 6eec581..0000000
--- a/lib/local.defs
+++ /dev/null
@@ -1,33 +0,0 @@
-def local_jar(
-    name,
-    jar,
-    src = None,
-    deps = [],
-    visibility = ['PUBLIC']):
-  binjar = name + '.jar'
-  srcjar = name + '-src.jar'
-  genrule(
-    name = '%s__local_bin' % name,
-    cmd = 'ln -s %s $OUT' % jar,
-    out = binjar)
-  if src:
-    genrule(
-      name = '%s__local_src' % name,
-      cmd = 'ln -s %s $OUT' % src,
-      out = srcjar)
-    prebuilt_jar(
-      name = '%s_src' % name,
-      binary_jar = ':%s__local_src' % name,
-      visibility = visibility,
-    )
-  else:
-    srcjar = None
-
-  prebuilt_jar(
-    name = name,
-    deps = deps,
-    binary_jar = ':%s__local_bin' % name,
-    source_jar = ':%s__local_src' % name if srcjar else None,
-    visibility = visibility,
- )
-
diff --git a/lib/log/BUCK b/lib/log/BUCK
index cadc7e7..a5201f3 100644
--- a/lib/log/BUCK
+++ b/lib/log/BUCK
@@ -10,6 +10,14 @@
 )
 
 maven_jar(
+  name = 'nop',
+  id = 'org.slf4j:slf4j-nop:' + VER,
+  sha1 = '6cca9a3b999ff28b7a35ca762b3197cd7e4c2ad1',
+  license = 'slf4j',
+  deps = [':api'],
+)
+
+maven_jar(
   name = 'impl_log4j',
   id = 'org.slf4j:slf4j-log4j12:' + VER,
   sha1 = '58f588119ffd1702c77ccab6acb54bfb41bed8bd',
@@ -31,3 +39,18 @@
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
 )
+
+maven_jar(
+  name = 'jsonevent-layout',
+  id = 'net.logstash.log4j:jsonevent-layout:1.7',
+  sha1 = '507713504f0ddb75ba512f62763519c43cf46fde',
+  license = 'Apache2.0',
+  deps = [':json-smart', '//lib/commons:lang']
+)
+
+maven_jar(
+  name = 'json-smart',
+  id = 'net.minidev:json-smart:1.1.1',
+  sha1 = '24a2f903d25e004de30ac602c5b47f2d4e420a59',
+  license = 'Apache2.0',
+)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 9026f79..c5107d5 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,23 +1,36 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.10.2'
+VERSION = '5.3.0'
+
+# core and backward-codecs both provide
+# META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
+merge_maven_jars(
+  name = 'core-and-backward-codecs',
+  srcs = [
+    ':backward-codecs_jar',
+    ':core_jar',
+  ],
+  visibility = ['PUBLIC'],
+)
 
 maven_jar(
-  name = 'core',
+  name = 'core_jar',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'c01e3d675d277e0a93e7890d03cc3246b2cdecaa',
+  sha1 = '9e12bb7c39e964a544e3a23b9c8ffa9599d38f10',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
     'META-INF/NOTICE.txt',
   ],
+  visibility = [],
 )
 
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = 'f977f8c443e8f4e9d1fd7fdfda80a6cf60b3e7c2',
+  sha1 = '1502beac94cf437baff848ffbbb8f76172befa6b',
   license = 'Apache2.0',
+  deps = [':core-and-backward-codecs'],
   exclude = [
     'META-INF/LICENSE.txt',
     'META-INF/NOTICE.txt',
@@ -25,8 +38,38 @@
 )
 
 maven_jar(
-  name = 'query-parser',
-  id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'd70f54e1060d553ba7aeb4d49a71fd0c068499e8',
+  name = 'backward-codecs_jar',
+  id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
+  sha1 = 'f654901e55fe56bdbe4be202767296929c2f8d9e',
   license = 'Apache2.0',
+  deps = [':core_jar'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+  visibility = [],
+)
+
+maven_jar(
+  name = 'misc',
+  id = 'org.apache.lucene:lucene-misc:' + VERSION,
+  sha1 = 'd03ce6d1bb8ab3926b3acc717418c474a49ade69',
+  license = 'Apache2.0',
+  deps = [':core-and-backward-codecs'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'queryparser',
+  id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
+  sha1 = '2c5e08580316c90b56a52e3cb686e1cf69db3f9e',
+  license = 'Apache2.0',
+  deps = [':core-and-backward-codecs'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
 )
diff --git a/lib/maven.defs b/lib/maven.defs
index c68eb92..665d39c 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -12,8 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-include_defs('//lib/local.defs')
-
 ECLIPSE = 'ECLIPSE:'
 GERRIT = 'GERRIT:'
 GERRIT_API = 'GERRIT_API:'
@@ -47,9 +45,14 @@
   from os import path
 
   parts = id.split(':')
-  if len(parts) != 3:
-    raise NameError('expected id="groupId:artifactId:version"')
-  group, artifact, version = parts
+  if len(parts) not in [3, 4]:
+    raise NameError('%s:\nexpected id="groupId:artifactId:version[:classifier]"'
+                    % id)
+  if len(parts) == 4:
+    group, artifact, version, classifier = parts
+  else:
+    group, artifact, version = parts
+    classifier = None
 
   # SNAPSHOT artifacts are handled differently on Google storage bucket:
   # 'SNAPSHOT' is discarded from the directory name. However on other
@@ -63,7 +66,11 @@
   else:
     file_version = version
 
+  if classifier is not None:
+    file_version += '-' + classifier
+
   jar = path.join(name, artifact.lower() + '-' + file_version)
+
   url = '/'.join([
     repository,
     group.replace('.', '/'), artifact, version,
@@ -115,7 +122,7 @@
   else:
     srcjar = None
     genrule(
-      name = '%s__download_src' % name,
+      name = '%s_src' % name,
       cmd = ':>$OUT',
       out = '__%s__no_src' % name,
     )
@@ -142,3 +149,30 @@
       visibility = visibility,
     )
 
+
+def merge_maven_jars(
+    name,
+    srcs,
+    visibility = []):
+
+  def cmd(jars):
+    return ('$(location //tools:merge_jars) $OUT '
+            + ' '.join(['$(location %s)' % j for j in jars]))
+
+  genrule(
+    name = '%s__merged_bin' % name,
+    cmd = cmd(['%s__download_bin' % s for s in srcs]),
+    out = '%s__merged.jar' % name,
+  )
+  genrule(
+    name = '%s__merged_src' % name,
+    cmd = cmd(['%s__download_src' % s for s in srcs]),
+    # tools/eclipse/project.py requires -src.jar suffix.
+    out = '%s__merged-src.jar' % name,
+  )
+  prebuilt_jar(
+    name = name,
+    binary_jar = ':%s__merged_bin' % name,
+    source_jar = ':%s__merged_src' % name,
+    visibility = visibility,
+  )
diff --git a/lib/mina/BUCK b/lib/mina/BUCK
index 0c9b41e..869fd5c 100644
--- a/lib/mina/BUCK
+++ b/lib/mina/BUCK
@@ -10,6 +10,7 @@
   name = 'sshd',
   id = 'org.apache.sshd:sshd-core:0.14.0',
   sha1 = 'cb12fa1b1b07fb5ce3aa4f99b189743897bd4fca',
+  src_sha1 = '44d7e868fcfc85c64b20337d694290792af8281c',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = EXCLUDE,
@@ -19,6 +20,7 @@
   name = 'core',
   id = 'org.apache.mina:mina-core:2.0.8',
   sha1 = 'd6ff69fa049aeaecdf0c04cafbb1ab53b7487883',
+  src_sha1 = 'c7b30746336f59d395d766b03c78a3a0a732ab26',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
diff --git a/lib/powermock/BUCK b/lib/powermock/BUCK
index 3a2eeac..5ac97c4 100644
--- a/lib/powermock/BUCK
+++ b/lib/powermock/BUCK
@@ -1,11 +1,12 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '1.5'
+VERSION = '1.6.2' # When bumping VERSION, make sure to also move
+# easymock to a compatible version
 
 maven_jar(
   name = 'powermock-module-junit4',
   id = 'org.powermock:powermock-module-junit4:' + VERSION,
-  sha1 = '9f6f8d0485249171f9d870e2b269048fa8cad43b',
+  sha1 = 'dff58978da716e000463bc1b08013d6a7cf3d696',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-module-junit4-common',
@@ -16,7 +17,7 @@
 maven_jar(
   name = 'powermock-module-junit4-common',
   id = 'org.powermock:powermock-module-junit4-common:' + VERSION,
-  sha1 = '43db4720ff57af42a1bd5c73fb5cdfebeebd564c',
+  sha1 = '48dd7406e11a14fe2ae4ab641e1f27510e896640',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-reflect',
@@ -27,7 +28,7 @@
 maven_jar(
   name = 'powermock-reflect',
   id = 'org.powermock:powermock-reflect:' + VERSION,
-  sha1 = '8df1548eeabb8492ba97d4f3eb84ae4d5f69215e',
+  sha1 = '1af1bbd1207c3ecdcf64973e6f9d57dcd17cc145',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     '//lib:junit',
@@ -38,7 +39,7 @@
 maven_jar(
   name = 'powermock-api-easymock',
   id = 'org.powermock:powermock-api-easymock:' + VERSION,
-  sha1 = 'a485b570b9debb46b53459a8e866a40343b2cfe2',
+  sha1 = 'addd25742ac9fe3e0491cbd68e2515e3b06c77fd',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-api-support',
@@ -49,7 +50,7 @@
 maven_jar(
   name = 'powermock-api-support',
   id = 'org.powermock:powermock-api-support:' + VERSION,
-  sha1 = '7c1b2e4555cfa333aec201c4612345c092820a38',
+  sha1 = '93b21413b4ee99b7bc0dd34e1416fdca96866aaf',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-core',
@@ -61,7 +62,7 @@
 maven_jar(
   name = 'powermock-core',
   id = 'org.powermock:powermock-core:' + VERSION,
-  sha1 = '4415337ff3fdb7ceb484f11fd08e39711e408976',
+  sha1 = 'ea04e79244e19dcf0c3ccf6863c5b028b4b58c9c',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':powermock-reflect',
diff --git a/lib/prolog/BUCK b/lib/prolog/BUCK
index 1f3e425..77fe5ac 100644
--- a/lib/prolog/BUCK
+++ b/lib/prolog/BUCK
@@ -1,15 +1,53 @@
 include_defs('//lib/maven.defs')
 
+VERSION = '1.4.1'
+REPO = GERRIT
+
 maven_jar(
-  name = 'prolog-cafe',
-  id = 'com.googlecode.prolog-cafe:PrologCafe:1.3',
-  sha1 = '5e0fbf18e8c98c4113f9acc978306884a1152870',
+  name = 'runtime',
+  id = 'com.googlecode.prolog-cafe:prolog-runtime:' + VERSION,
+  sha1 = 'c5d9f92e49c485969dcd424dfc0c08125b5f8246',
   license = 'prologcafe',
-  repository = GERRIT,
+  repository = REPO,
+)
+
+maven_jar(
+  name = 'compiler',
+  id = 'com.googlecode.prolog-cafe:prolog-compiler:' + VERSION,
+  sha1 = 'ac24044c6ec166fdcb352b78b80d187ead3eff41',
+  license = 'prologcafe',
+  repository = REPO,
+  deps = [
+    ':io',
+    ':runtime',
+  ],
+)
+
+maven_jar(
+  name = 'io',
+  id = 'com.googlecode.prolog-cafe:prolog-io:' + VERSION,
+  sha1 = 'b072426a4b1b8af5e914026d298ee0358a8bb5aa',
+  license = 'prologcafe',
+  repository = REPO,
+  deps = [':runtime'],
+  visibility = [],
+)
+
+maven_jar(
+  name = 'cafeteria',
+  id = 'com.googlecode.prolog-cafe:prolog-cafeteria:' + VERSION,
+  sha1 = '8cbc3b0c19e7167c42d3f11667b21cb21ddec641',
+  license = 'prologcafe',
+  repository = REPO,
+  deps = [
+    ':io',
+    ':runtime',
+  ],
+  visibility = ['//gerrit-pgm:'],
 )
 
 java_binary(
-  name = 'compiler',
+  name = 'compiler_bin',
   main_class = 'BuckPrologCompiler',
   deps = [':compiler_lib'],
   visibility = ['PUBLIC'],
@@ -18,6 +56,9 @@
 java_library(
   name = 'compiler_lib',
   srcs = ['java/BuckPrologCompiler.java'],
-  deps = [':prolog-cafe'],
+  deps = [
+    ':compiler',
+    ':runtime',
+  ],
   visibility = ['//tools/eclipse:classpath'],
 )
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BuckPrologCompiler.java
index 17d2d76..6010be2 100644
--- a/lib/prolog/java/BuckPrologCompiler.java
+++ b/lib/prolog/java/BuckPrologCompiler.java
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
 import com.googlecode.prolog_cafe.compiler.Compiler;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -47,11 +47,8 @@
   private static void jar(File jar, File classes) throws IOException {
     File tmp = File.createTempFile("prolog", ".jar", tmpdir);
     try {
-      JarOutputStream out = new JarOutputStream(new FileOutputStream(tmp));
-      try {
+      try (JarOutputStream out = new JarOutputStream(new FileOutputStream(tmp))) {
         add(out, classes, "");
-      } finally {
-        out.close();
       }
       if (!tmp.renameTo(jar)) {
         throw new IOException("Cannot create " + jar);
@@ -63,7 +60,11 @@
 
   private static void add(JarOutputStream out, File classes, String prefix)
       throws IOException {
-    for (String name : classes.list()) {
+    String[] list = classes.list();
+    if (list == null) {
+      return;
+    }
+    for (String name : list) {
       File f = new File(classes, name);
       if (f.isDirectory()) {
         add(out, f, prefix + name + "/");
@@ -71,8 +72,7 @@
       }
 
       JarEntry e = new JarEntry(prefix + name);
-      FileInputStream in = new FileInputStream(f);
-      try {
+      try (FileInputStream in = new FileInputStream(f)) {
         e.setTime(f.lastModified());
         out.putNextEntry(e);
         byte[] buf = new byte[16 << 10];
@@ -81,7 +81,6 @@
           out.write(buf, 0, n);
         }
       } finally {
-        in.close();
         out.closeEntry();
       }
     }
diff --git a/lib/prolog/prolog.defs b/lib/prolog/prolog.defs
index d0636f5..e74c21d 100644
--- a/lib/prolog/prolog.defs
+++ b/lib/prolog/prolog.defs
@@ -19,7 +19,7 @@
     visibility = []):
   genrule(
     name = name + '__pl2j',
-    cmd = '$(exe //lib/prolog:compiler)' +
+    cmd = '$(exe //lib/prolog:compiler_bin)' +
       ' $TMP $OUT ' +
       ' '.join(srcs),
     srcs = srcs,
@@ -28,12 +28,11 @@
   java_library(
     name = name + '__lib',
     srcs = [':' + name + '__pl2j'],
-    deps = ['//lib/prolog:prolog-cafe'] + deps,
+    deps = ['//lib/prolog:runtime'] + deps,
   )
   genrule(
     name = name + '__ln',
     cmd = 'ln -s $(location :%s__lib) $OUT' % name,
-    deps = [':%s__lib' % name],
     out = name + '.jar',
   )
   prebuilt_jar(
diff --git a/lib/solr/BUCK b/lib/solr/BUCK
deleted file mode 100644
index cd39742..0000000
--- a/lib/solr/BUCK
+++ /dev/null
@@ -1,33 +0,0 @@
-include_defs('//lib/maven.defs')
-
-# Java client library to use Solr over the network.
-maven_jar(
-  name = 'solrj',
-  id = 'org.apache.solr:solr-solrj:4.3.1',
-  sha1 = '433fe37796e67eaeb4452f69eb1fae2de27cb7a8',
-  license = 'Apache2.0',
-  deps = [
-    ':noggit',
-    ':zookeeper',
-    '//lib/httpcomponents:httpclient',
-    '//lib/httpcomponents:httpmime',
-    '//lib/commons:io',
-  ],
-)
-
-maven_jar(
-  name = 'noggit',
-  id = 'org.noggit:noggit:0.5',
-  sha1 = '8e6e65624d2e09a30190c6434abe23b7d4e5413c',
-  license = 'Apache2.0',
-  visibility = [],
-)
-
-maven_jar(
-  name = 'zookeeper',
-  id = 'org.apache.zookeeper:zookeeper:3.4.5',
-  sha1 = 'c0f69fb36526552a8f0bc548a6c33c49cf08e562',
-  license = 'Apache2.0',
-  deps = ['//lib/log:api'],
-  visibility = [],
-)
diff --git a/plugins/BUCK b/plugins/BUCK
index 134f275..9948720 100644
--- a/plugins/BUCK
+++ b/plugins/BUCK
@@ -6,12 +6,15 @@
   'reviewnotes',
   'singleusergroup'
 ]
+CUSTOM = [
+  # Add custom core plugins here
+]
 
 # buck audit parses and resolves all deps even if not reachable
 # from the root(s) passed to audit. Filter dependencies to only
 # the ones that currently exist to allow buck to parse cleanly.
 # TODO(sop): buck should more lazily resolve deps
-def filter(names):
+def core_plugins(names):
   from os import path
   h, n = [], []
   for p in names:
@@ -20,7 +23,7 @@
     else:
       n.append(p)
   return h, n
-HAVE, NEED = filter(CORE)
+HAVE, NEED = core_plugins(CORE + CUSTOM)
 
 genrule(
   name = 'core',
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index 7923b67..8d295ed 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit 7923b67392164dcc65ada85f723fa5111b265484
+Subproject commit 8d295ed48e8f52eef5661b6eb10d6402d197c776
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 0f50526..f9b7f69 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 0f5052695546844f92e9730d619062957006055d
+Subproject commit f9b7f6994c579d93cd2e5887174d5ab9b477b095
diff --git a/plugins/download-commands b/plugins/download-commands
index 63e7cf5..8ad70a0 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 63e7cf5f24045ede2ee9e5a220e594716b2b6ce4
+Subproject commit 8ad70a0ec142b4b718b97fbbdf97bd8e8dc7fb7a
diff --git a/plugins/replication b/plugins/replication
index 85685f6..51d6be2 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 85685f618eecf41bc0bc3a65bc1c849b96bca4e8
+Subproject commit 51d6be2b49b8136d594309743edf26a74948bd23
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 4efc9a1..8de8884 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 4efc9a167fe66dc019f495abe848b2296553b4a0
+Subproject commit 8de8884603ce21de9c7282b955cb5ef2077277c3
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 691c9c9..f6df712 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 691c9c9c4fa6c0a533ee8386e991a41224c87243
+Subproject commit f6df7121d2704e73c2a315a660e5cc4e12ab1ab9
diff --git a/tools/BUCK b/tools/BUCK
index ee26062..489dffc 100644
--- a/tools/BUCK
+++ b/tools/BUCK
@@ -6,6 +6,12 @@
 )
 
 python_binary(
+  name = 'merge_jars',
+  main = 'merge_jars.py',
+  visibility = ['PUBLIC'],
+)
+
+python_binary(
   name = 'pack_war',
   main = 'pack_war.py',
   deps = [':util'],
@@ -36,10 +42,9 @@
   return environ.get('PATH')
 
 genrule(
-  name = 'buck.properties',
+  name = 'buck',
   cmd = 'echo buck=`which buck`>$OUT;' +
     ("echo PATH=\''%s'\' >>$OUT;" % shquote(os_path())),
-  deps = [],
   out = 'buck.properties',
   visibility = ['PUBLIC'],
 )
diff --git a/tools/build.defs b/tools/build.defs
index 8b858cd..893abba 100644
--- a/tools/build.defs
+++ b/tools/build.defs
@@ -42,15 +42,13 @@
     ):
   cmd = ['$(exe //tools:pack_war)', '-o', '$OUT', '--tmp', '$TMP']
   for l in libs:
-    cmd.extend(['--lib', l])
+    cmd.extend(['--lib', '$(classpath %s)' % l])
   for l in pgmlibs:
-    cmd.extend(['--pgmlib', l])
+    cmd.extend(['--pgmlib', '$(classpath %s)' % l])
 
-  dep = []
   if docs:
     cmd.append('$(location %s)' % DOCS_HTML)
-    dep.append(DOCS_LIB)
-    cmd.extend(['--lib', DOCS_LIB])
+    cmd.extend(['--lib', '$(classpath %s)' % DOCS_LIB])
   if context:
     for t in context:
       cmd.append('$(location %s)' % t)
@@ -58,7 +56,6 @@
   genrule(
     name = name,
     cmd = ' '.join(cmd),
-    deps = libs + pgmlibs + dep,
     out = name + '.war',
     visibility = visibility,
   )
@@ -71,8 +68,8 @@
     context = [
       '//gerrit-main:main_bin',
       '//gerrit-war:webapp_assets',
-      '//gerrit-gwtui:' + ui,
-    ] + context,
+    ] + (['//gerrit-gwtui:' + ui] if ui else []) +
+    context,
     docs = docs,
     visibility = visibility,
   )
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
index a9f8cfe..1bb40f7 100644
--- a/tools/checkstyle.xml
+++ b/tools/checkstyle.xml
@@ -14,10 +14,12 @@
   <property name="severity" value="warning"/>
   <property name="charset" value="UTF-8"/>
   <module name="TreeWalker">
+    <module name="FileContentsHolder"/>
     <module name="OuterTypeFilename"/>
     <module name="LineLength">
       <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
-      <property name="max" value="100"/>
+      <property name="max" value="150"/>
+      <property name="tabWidth" value="2"/>
     </module>
     <module name="OneTopLevelClass"/>
     <module name="NoLineWrap"/>
@@ -27,9 +29,8 @@
     </module>
     <module name="NeedBraces"/>
     <module name="LeftCurly">
-      <property name="maxLineLength" value="100"/>
+      <property name="maxLineLength" value="150"/>
     </module>
-    <module name="RightCurly"/>
     <module name="RightCurly">
       <property name="option" value="alone"/>
       <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, STATIC_INIT, INSTANCE_INIT"/>
@@ -47,8 +48,6 @@
     <module name="OneStatementPerLine"/>
     <module name="MultipleVariableDeclarations"/>
     <module name="ArrayTypeStyle"/>
-    <module name="MissingSwitchDefault"/>
-    <module name="FallThrough"/>
     <module name="UpperEll"/>
     <module name="ModifierOrder"/>
     <module name="EmptyLineSeparator">
@@ -100,4 +99,12 @@
     <property name="eachLine" value="true"/>
     <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
   </module>
+  <module name="SuppressWithNearbyCommentFilter">
+    <property name="commentFormat" value="CS IGNORE (\w+) FOR NEXT (\d+) LINES\. REASON\: \w+"/>
+    <property name="checkFormat" value="$1"/>
+    <property name="influenceFormat" value="$2"/>
+  </module>
+  <module name="SuppressionFilter">
+    <property name="file" value="${samedir}/checkstyle_suppressions.xml"/>
+  </module>
 </module>
diff --git a/tools/checkstyle_suppressions.xml b/tools/checkstyle_suppressions.xml
new file mode 100644
index 0000000..5f5d9ee
--- /dev/null
+++ b/tools/checkstyle_suppressions.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE suppressions PUBLIC
+  "-//Puppy Crawl//DTD Suppressions 1.1//EN"
+  "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+  <suppress files="[/\\].apt_generated[/\\]" checks=".*"/>
+</suppressions>
diff --git a/tools/default.defs b/tools/default.defs
index a6a65b3..90096b2 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -18,11 +18,16 @@
 include_defs('//tools/gwt-constants.defs')
 include_defs('//tools/java_doc.defs')
 include_defs('//tools/java_sources.defs')
+include_defs('//tools/git.defs')
 import copy
+import traceback
+import os
+from multiprocessing import cpu_count
 
 # Set defaults on java rules:
 #  - Add AutoValue annotation processing support.
 #  - Treat source files as UTF-8.
+#  - std_out_log_level = info (the default is too spammy)
 
 _buck_java_library = java_library
 def java_library(*args, **kwargs):
@@ -32,9 +37,9 @@
 _buck_java_test = java_test
 def java_test(*args, **kwargs):
   _munge_args(kwargs)
+  _do_not_spam_std_out(kwargs)
   _buck_java_test(*args, **kwargs)
 
-
 # Munge kwargs to set Gerrit-specific defaults.
 def _munge_args(kwargs):
   _set_auto_value(kwargs)
@@ -52,6 +57,11 @@
 
   extra_args.extend(['-encoding', 'UTF-8'])
 
+def _do_not_spam_std_out(kwargs):
+  level = 'std_out_log_level'
+  if level not in kwargs:
+    kwargs[level] = 'INFO'
+
 def _set_auto_value(kwargs):
   apk = 'annotation_processors'
   if apk not in kwargs:
@@ -63,7 +73,8 @@
     kwargs[apdk] = []
   apds = kwargs.get(apdk, [])
 
-  if AUTO_VALUE_DEP in kwargs.get('deps', []):
+  all_deps = kwargs.get('deps', []) + kwargs.get('exported_deps', [])
+  if AUTO_VALUE_DEP in all_deps:
     aps.extend(AUTO_VALUE_PROCESSORS)
     apds.extend(AUTO_VALUE_PROCESSOR_DEPS)
 
@@ -127,9 +138,12 @@
     manifest_file = None,
     manifest_entries = [],
     type = 'plugin',
-    visibility = ['PUBLIC']):
-  from multiprocessing import cpu_count
-  mf_cmd = 'v=\$(git describe HEAD);'
+    visibility = ['PUBLIC'],
+    target_suffix = ''):
+  tb = traceback.extract_stack()
+  calling_BUCK_file = tb[-2][0]
+  calling_BUCK_dir = os.path.abspath(os.path.dirname(calling_BUCK_file))
+  mf_cmd = 'v=%s;' % git_describe(calling_BUCK_dir)
   if manifest_file:
     mf_src = [manifest_file]
     mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT'
@@ -186,7 +200,7 @@
     gwt_binary(
       name = name + '__gwt_application',
       modules = [gwt_module],
-      deps = GWT_PLUGIN_DEPS + ['//lib/gwt:dev'],
+      deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ['//lib/gwt:dev'],
       module_deps = [':%s__gwt_module' % name],
       local_workers = cpu_count(),
       strict = True,
@@ -195,7 +209,7 @@
     )
 
   java_binary(
-    name = name,
+    name = name + target_suffix,
     manifest_file = ':%s__manifest' % name,
     merge_manifests = False,
     deps = [
diff --git a/tools/download_all.py b/tools/download_all.py
index 241d20b..58316ca 100755
--- a/tools/download_all.py
+++ b/tools/download_all.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,7 +32,7 @@
   if m:
     n = m.group(1)
     if args.src and n.endswith('__download_bin'):
-      n = n[:-4] + '_src'
+      n = n[:-13] + 'src'
     targets.add(n)
 r = p.wait()
 if r != 0:
diff --git a/tools/download_file.py b/tools/download_file.py
index 88ab41a..97d982f 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,7 +25,11 @@
 from zipfile import ZipFile, BadZipfile, LargeZipFile
 
 GERRIT_HOME = path.expanduser('~/.gerritcodereview')
-CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
+CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache', 'downloaded-artifacts')
+# LEGACY_CACHE_DIR is only used to allow existing workspaces to move already
+# downloaded files to the new cache directory.
+# Please remove after 3 months (2015-10-07).
+LEGACY_CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
 LOCAL_PROPERTIES = 'local.properties'
 
 
@@ -85,6 +89,15 @@
   name = '%s-%s' % (path.basename(args.o), h)
   return path.join(CACHE_DIR, name)
 
+# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
+def legacy_cache_entry(args):
+  if args.v:
+    h = args.v
+  else:
+    h = sha1(args.u.encode('utf-8')).hexdigest()
+  name = '%s-%s' % (path.basename(args.o), h)
+  return path.join(LEGACY_CACHE_DIR, name)
+
 
 opts = OptionParser()
 opts.add_option('-o', help='local output file')
@@ -103,8 +116,19 @@
 
 redirects = download_properties(root_dir)
 cache_ent = cache_entry(args)
+legacy_cache_ent = legacy_cache_entry(args)
 src_url = resolve_url(args.u, redirects)
 
+# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
+if not path.exists(cache_ent) and path.exists(legacy_cache_ent):
+  try:
+    safe_mkdirs(path.dirname(cache_ent))
+  except OSError as err:
+    print('error creating directory %s: %s' %
+          (path.dirname(cache_ent), err), file=stderr)
+    exit(1)
+  shutil.move(legacy_cache_ent, cache_ent)
+
 if not path.exists(cache_ent):
   try:
     safe_mkdirs(path.dirname(cache_ent))
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index 865c9d7..1e13515 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -4,6 +4,7 @@
   name = 'classpath',
   deps = LIBS + PGMLIBS + [
     '//gerrit-acceptance-tests:lib',
+    '//gerrit-gpg:gpg_tests',
     '//gerrit-gwtdebug:gwtdebug',
     '//gerrit-gwtui:ui_module',
     '//gerrit-gwtui:ui_tests',
@@ -20,6 +21,8 @@
     '//lib/bouncycastle:bcprov',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcpkix',
+    '//lib/gwt:javax-validation',
+    '//lib/gwt:javax-validation_src',
     '//lib/jetty:servlets',
     '//lib/prolog:compiler_lib',
     '//Documentation:index_lib',
diff --git a/tools/eclipse/gerrit_gwt_debug.launch b/tools/eclipse/gerrit_gwt_debug.launch
index 3533211..c3c58ff 100644
--- a/tools/eclipse/gerrit_gwt_debug.launch
+++ b/tools/eclipse/gerrit_gwt_debug.launch
@@ -16,7 +16,7 @@
 </listAttribute>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gerrit.gwtdebug.GerritGwtDebugLauncher"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui com.google.gerrit.GerritGwtUI -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui com.google.gerrit.GerritGwtUI -src ${resource_loc:/gerrit}/gerrit-plugin-gwtui/src/main/java -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx1024M&#10;-XX:MaxPermSize=256M&#10;-Dgerrit.disable-gwtui-recompile=true"/>
 </launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index dd6f248..754c1c8 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -119,7 +119,7 @@
       gwt_lib.add(p)
       continue
 
-    if p.startswith('buck-out/gen/lib/gwt/'):
+    if 'buck-out/gen/lib/gwt/' in p:
       # gwt_module() depends on huge shaded GWT JARs that import
       # incorrect versions of classes for Gerrit. Collect into
       # a private grouping for later use.
@@ -165,11 +165,11 @@
         if path.exists(p):
           classpathentry('src', p, out=o)
 
-  for libs in [gwt_lib, lib]:
+  for libs in [lib, gwt_lib]:
     for j in sorted(libs):
       s = None
       if j.endswith('.jar'):
-        s = j[:-4] + '-src.jar'
+        s = j[:-4] + '_src.jar'
         if not path.exists(s):
           s = None
       if args.plugins:
@@ -178,7 +178,8 @@
         classpathentry('lib', j, s)
   for s in sorted(gwt_src):
     p = path.join(ROOT, s, 'src', 'main', 'java')
-    classpathentry('lib', p, out='buck-out/eclipse/gwtsrc')
+    if path.exists(p):
+      classpathentry('lib', p, out='buck-out/eclipse/gwtsrc')
 
   classpathentry('con', JRE)
   classpathentry('output', 'buck-out/eclipse/classes')
@@ -223,8 +224,8 @@
   gen_factorypath()
 
   try:
-    targets = ['//tools:buck.properties'] + MAIN + GWT
-    check_call(['buck', 'build'] + targets)
+    targets = ['//tools:buck'] + MAIN + GWT
+    check_call(['buck', 'build', '--deep'] + targets)
   except CalledProcessError as err:
     exit(1)
 except KeyboardInterrupt:
diff --git a/tools/gerrit.importorder b/tools/gerrit.importorder
index 831c5fe..398130e 100644
--- a/tools/gerrit.importorder
+++ b/tools/gerrit.importorder
@@ -1,9 +1,12 @@
 #Organize Import Order
-#Wed Jan 14 10:19:45 JST 2015
-6=javax
-5=java
-4=org
-3=net
-2=junit
-1=com
-0=com.google
+#Mon Mar 23 17:27:34 PDT 2015
+9=javax
+8=java
+7=org
+6=net
+5=junit
+4=eu
+3=dk
+2=com
+1=com.google
+0=\#
diff --git a/tools/git.defs b/tools/git.defs
index c58ac88..859f173 100644
--- a/tools/git.defs
+++ b/tools/git.defs
@@ -12,10 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-def git_describe():
+def git_describe(directory = None):
   import subprocess
-  cmd = ['git', 'describe', '--match', 'v[0-9].*', '--dirty']
-  p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+  cmd = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty']
+  if not directory:
+    p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+  else:
+    p = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = directory)
   v = p.communicate()[0].strip()
   r = p.returncode
   if r != 0:
diff --git a/tools/gitlog2asciidoc.py b/tools/gitlog2asciidoc.py
deleted file mode 100755
index dfbad82..0000000
--- a/tools/gitlog2asciidoc.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/bin/python
-from optparse import OptionParser
-import re
-import subprocess
-import sys
-
-"""
-This script generates a release note from the output of git log
-between the specified tags.
-
-Options:
---issues          Show output the commits with issues associated with them.
---issue-numbers   Show outputs issue numbers of the commits with issues
-                  associated with them
-
-Arguments:
-since -- tag name
-until -- tag name
-
-Example Input:
-
-   * <commit subject>
-   +
-   <commit message>
-
-   Bug: issue 123
-   Change-Id: <change id>
-   Signed-off-by: <name>
-
-Expected Output:
-
-   * issue 123 <commit subject>
-   +
-   <commit message>
-"""
-
-parser = OptionParser(usage='usage: %prog [options] <since> <until>')
-
-parser.add_option('-i', '--issues', action='store_true',
-                  dest='issues_only', default=False,
-                  help='only output the commits with issues association')
-
-parser.add_option('-n', '--issue-numbers', action='store_true',
-                  dest='issue_numbers_only', default=False,
-                  help='only outputs issue numbers of the commits with \
-                        issues association')
-
-(options, args) = parser.parse_args()
-
-if len(args) != 2:
-    parser.error("wrong number of arguments")
-
-issues_only = options.issues_only
-issue_numbers_only = options.issue_numbers_only
-
-since_until = args[0] + '..' + args[1]
-proc = subprocess.Popen(['git', 'log', '--reverse', '--no-merges',
-                         since_until, "--format=* %s%n+%n%b"],
-                         stdout=subprocess.PIPE,
-                         stderr=subprocess.STDOUT,)
-
-stdout_value = proc.communicate()[0]
-
-subject = ""
-message = []
-is_issue = False
-
-# regex pattern to match following cases such as Bug: 123, Issue Bug: 123,
-# Bug: GERRIT-123, Bug: issue 123, Bug issue: 123, issue: 123, issue: bug 123
-p = re.compile('bug: GERRIT-|bug(:? issue)?:? |issue(:? bug)?:? ',
-               re.IGNORECASE)
-
-if issue_numbers_only:
-    for line in stdout_value.splitlines(True):
-        if p.match(line):
-            sys.stdout.write(p.sub('', line))
-else:
-    for line in stdout_value.splitlines(True):
-        # Move issue number to subject line
-        if p.match(line):
-            line = p.sub('issue ', line).replace('\n',' ')
-            subject = subject[:2] + line + subject[2:]
-            is_issue = True
-        elif line.startswith('* '):
-            # Write change log for a commit
-            if subject != "":
-                if (not issues_only or is_issue):
-                    # Write subject
-                    sys.stdout.write(subject)
-                    # Write message lines
-                    if message != []:
-                        # Clear + from last line in commit message
-                        message[-1] = '\n'
-                    for m in message:
-                        sys.stdout.write(m)
-            # Start new commit block
-            message = []
-            subject = line
-            is_issue = False
-        # Remove commit footers
-        elif re.match(r'((\w+-)+\w+:)', line):
-            continue
-        # Don't add extra blank line if last one is already blank
-        elif line == '\n' and message and message[-1] != '+\n':
-                message.append('+\n')
-        elif line != '\n':
-            message.append(line)
diff --git a/tools/gwt-constants.defs b/tools/gwt-constants.defs
index cc09d3e..8bafddb 100644
--- a/tools/gwt-constants.defs
+++ b/tools/gwt-constants.defs
@@ -2,6 +2,9 @@
 
 GWT_COMPILER_ARGS = [
   '-XdisableClassMetadata',
+]
+
+GWT_COMPILER_ARGS_RELEASE_MODE = GWT_COMPILER_ARGS + [
   '-XdisableCastChecking',
 ]
 
@@ -9,3 +12,13 @@
   '//gerrit-plugin-gwtui:gwtui-api-lib',
   '//lib/gwt:user',
 ]
+
+GWT_TRANSITIVE_DEPS = [
+  '//lib/gwt:javax-validation',
+  '//lib/gwt:javax-validation_src',
+  '//lib/ow2:ow2-asm',
+  '//lib/ow2:ow2-asm-analysis',
+  '//lib/ow2:ow2-asm-commons',
+  '//lib/ow2:ow2-asm-tree',
+  '//lib/ow2:ow2-asm-util',
+]
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index 4d53783..fcd77c0 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -1,25 +1,30 @@
 include_defs('//VERSION')
 include_defs('//tools/maven/package.defs')
+include_defs('//tools/maven/repository.defs')
 
-URL = 'https://oss.sonatype.org/content/repositories/snapshots' \
-      if GERRIT_VERSION.endswith('-SNAPSHOT') else \
-        'https://oss.sonatype.org/service/local/staging/deploy/maven2'
+if GERRIT_VERSION.endswith('-SNAPSHOT'):
+  URL = MAVEN_SNAPSHOT_URL
+else:
+  URL = MAVEN_RELEASE_URL
 
 maven_package(
-  repository = 'sonatype-nexus-staging',
+  repository = MAVEN_REPOSITORY,
   url = URL,
   version = GERRIT_VERSION,
   jar = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework',
     'gerrit-extension-api': '//gerrit-extension-api:extension-api',
     'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api',
     'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api',
   },
   src = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework-src',
     'gerrit-extension-api': '//gerrit-extension-api:extension-api-src',
     'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-src',
     'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-src',
   },
   doc = {
+    'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework-javadoc',
     'gerrit-extension-api': '//gerrit-extension-api:extension-api-javadoc',
     'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-javadoc',
     'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-javadoc',
@@ -30,5 +35,4 @@
 python_binary(
   name = 'mvn',
   main = 'mvn.py',
-  deps = ['//tools:util'],
 )
diff --git a/tools/maven/api.py b/tools/maven/api.py
new file mode 100755
index 0000000..600de6a
--- /dev/null
+++ b/tools/maven/api.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+# Copyright (C) 2015 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.
+
+from __future__ import print_function
+from argparse import ArgumentParser
+from json import loads
+from os import environ, path, remove
+from subprocess import check_call, check_output, Popen, PIPE
+from sys import stderr
+from tempfile import mkstemp
+
+
+def locations():
+  d = Popen('buck audit dependencies api'.split(),
+            stdin=None, stdout=PIPE, stderr=PIPE)
+  t = Popen('xargs buck targets --show_output'.split(),
+            stdin=d.stdout, stdout=PIPE, stderr=PIPE)
+  out = t.communicate()[0]
+  d.wait()
+  targets = []
+  outs = []
+  for e in out.strip().split('\n'):
+    t, o = e.split()
+    targets.append(t)
+    outs.append(o)
+  return dict(zip(targets, outs))
+
+parser = ArgumentParser()
+parser.add_argument('-n', '--dryrun', action='store_true')
+parser.add_argument('-v', '--verbose', action='store_true')
+
+subparsers = parser.add_subparsers(help='action', dest='action')
+subparsers.add_parser('deploy', help='Deploy to Maven (remote)')
+subparsers.add_parser('install', help='Install to Maven (local)')
+
+args = parser.parse_args()
+
+root = path.abspath(__file__)
+while not path.exists(path.join(root, '.buckconfig')):
+  root = path.dirname(root)
+
+if not args.dryrun:
+  check_call('buck build api'.split())
+target = check_output(('buck targets --json api_%s' % args.action).split())
+
+s = loads(target)[0]['cmd']
+
+fd, tempfile = mkstemp()
+s = s.replace('$(exe //tools/maven:mvn)', path.join(root, 'tools/maven/mvn.py'))
+s = s.replace('-o $OUT', '-o %s' % tempfile)
+
+locations = locations()
+
+while '$(location' in s:
+  start = s.index('$(location')
+  end = s.index(')', start)
+  target = s[start+11:end]
+  s = s.replace(s[start:end+1], locations[target])
+
+try:
+  if args.verbose or args.dryrun or environ.get('VERBOSE'):
+    print(s, file=stderr)
+  if not args.dryrun:
+    check_call(s.split())
+finally:
+  remove(tempfile)
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
old mode 100644
new mode 100755
index cc10816..7017406
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
index a9dc60c..8fe9a13 100644
--- a/tools/maven/package.defs
+++ b/tools/maven/package.defs
@@ -22,16 +22,13 @@
     war = {}):
   cmd = ['$(exe //tools/maven:mvn)', '-v', version, '-o', '$OUT']
   api_cmd = []
-  api_deps = []
   for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]:
     for a,t in d.iteritems():
       api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
-      api_deps.append(t)
 
   genrule(
     name = 'api_install',
     cmd = ' '.join(cmd + api_cmd + ['-a', 'install']),
-    deps = api_deps + ['//tools/maven:mvn'],
     out = 'api_install.info',
   )
 
@@ -42,20 +39,16 @@
         '-a', 'deploy',
         '--repository', repository,
         '--url', url]),
-      deps = api_deps + ['//tools/maven:mvn'],
       out = 'api_deploy.info',
     )
 
   war_cmd = []
-  war_deps = []
   for a,t in war.iteritems():
     war_cmd.append('-s %s:war:$(location %s)' % (a,t))
-    war_deps.append(t)
 
   genrule(
     name = 'war_install',
     cmd = ' '.join(cmd + war_cmd + ['-a', 'install']),
-    deps = war_deps + ['//tools/maven:mvn'],
     out = 'war_install.info',
   )
 
@@ -66,6 +59,5 @@
         '-a', 'deploy',
         '--repository', repository,
         '--url', url]),
-      deps = war_deps + ['//tools/maven:mvn'],
       out = 'war_deploy.info',
     )
diff --git a/tools/maven/repository.defs b/tools/maven/repository.defs
new file mode 100644
index 0000000..c4e8fbf
--- /dev/null
+++ b/tools/maven/repository.defs
@@ -0,0 +1,3 @@
+MAVEN_REPOSITORY = 'sonatype-nexus-staging'
+MAVEN_SNAPSHOT_URL = 'https://oss.sonatype.org/content/repositories/snapshots'
+MAVEN_RELEASE_URL = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
diff --git a/tools/merge_jars.py b/tools/merge_jars.py
new file mode 100755
index 0000000..46016c0
--- /dev/null
+++ b/tools/merge_jars.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+# Copyright (C) 2015 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.
+
+from __future__ import print_function
+import collections
+import sys
+import zipfile
+
+
+if len(sys.argv) < 3:
+  print('usage: %s <out.zip> <in.zip>...' % sys.argv[0], file=sys.stderr)
+  exit(1)
+
+outfile = sys.argv[1]
+infiles = sys.argv[2:]
+seen = set()
+SERVICES = 'META-INF/services/'
+
+try:
+  with zipfile.ZipFile(outfile, 'w') as outzip:
+    services = collections.defaultdict(lambda: '')
+    for infile in infiles:
+      with zipfile.ZipFile(infile) as inzip:
+        for info in inzip.infolist():
+          n = info.filename
+          if n in seen:
+            continue
+          elif n.startswith(SERVICES):
+            # Concatenate all provider configuration files.
+            services[n] += inzip.read(n)
+            continue
+          outzip.writestr(info, inzip.read(n))
+          seen.add(n)
+
+    for n, v in services.iteritems():
+      outzip.writestr(n, v)
+except Exception as err:
+  exit('Failed to merge jars: %s' % err)
diff --git a/tools/pack_war.py b/tools/pack_war.py
index 7e7d895..8525a56 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,7 +16,7 @@
 from __future__ import print_function
 from optparse import OptionParser
 from os import chdir, makedirs, path, symlink
-from subprocess import check_call, check_output
+from subprocess import check_call
 import sys
 
 opts = OptionParser()
@@ -30,17 +30,14 @@
 root = war[:war.index('buck-out')]
 jars = set()
 
+def prune(l):
+ return [j[j.find('buck-out'):] for e in l for j in e.split(':')]
 
 def link_jars(libs, directory):
   makedirs(directory)
   while not path.isfile('.buckconfig'):
     chdir('..')
-  try:
-    cp = check_output(['buck', 'audit', 'classpath'] + libs)
-  except Exception as e:
-    print('call to buck audit failed: %s' % e, file=sys.stderr)
-    exit(1)
-  for j in cp.strip().splitlines():
+  for j in libs:
     if j not in jars:
       jars.add(j)
       n = path.basename(j)
@@ -49,9 +46,9 @@
       symlink(path.join(root, j), path.join(directory, n))
 
 if args.lib:
-  link_jars(args.lib, path.join(war, 'WEB-INF', 'lib'))
+  link_jars(prune(args.lib), path.join(war, 'WEB-INF', 'lib'))
 if args.pgmlib:
-  link_jars(args.pgmlib, path.join(war, 'WEB-INF', 'pgm-lib'))
+  link_jars(prune(args.pgmlib), path.join(war, 'WEB-INF', 'pgm-lib'))
 try:
   for s in ctx:
     check_call(['unzip', '-q', '-d', war, s])
diff --git a/tools/util_test.py b/tools/util_test.py
index f116171..30647ba 100644
--- a/tools/util_test.py
+++ b/tools/util_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/version.py b/tools/version.py
index 28f6b65..9f03a59 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2014 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,12 +19,6 @@
 import re
 import sys
 
-version_text = """# Maven style API version (e.g. '2.x-SNAPSHOT').
-# Used by :api_install and :api_deploy targets
-# when talking to the destination repository.
-#
-GERRIT_VERSION = '%s'
-"""
 parser = OptionParser()
 opts, args = parser.parse_args()
 
@@ -33,31 +27,34 @@
 elif len(args) > 1:
   parser.error('too many arguments')
 
-new_version = args[0]
-pattern = re.compile(r'(\s*)<version>[-.\w]+</version>')
+DEST_PATTERN = r'\g<1>%s\g<3>' % args[0]
 
-for project in ['gerrit-extension-api', 'gerrit-plugin-api',
-                'gerrit-plugin-archetype', 'gerrit-plugin-gwt-archetype',
-                'gerrit-plugin-gwtui', 'gerrit-plugin-js-archetype',
-                'gerrit-war']:
-  pom = os.path.join(project, 'pom.xml')
+
+def replace_in_file(filename, src_pattern):
   try:
-    outxml = ""
-    found = False
-    for line in open(pom, "r"):
-      m = pattern.match(line)
-      if m and not found:
-        outxml += "%s<version>%s</version>\n" % (m.group(1), new_version)
-        found = True
-      else:
-        outxml += line
-    with open(pom, "w") as outfile:
-      outfile.write(outxml)
+    f = open(filename, "r")
+    s = f.read()
+    f.close()
+    s = re.sub(src_pattern, DEST_PATTERN, s)
+    f = open(filename, "w")
+    f.write(s)
+    f.close()
   except IOError as err:
-    print('error updating %s: %s' % (pom, err), file=sys.stderr)
+    print('error updating %s: %s' % (filename, err), file=sys.stderr)
 
-try:
-  with open('VERSION', "w") as version_file:
-    version_file.write(version_text % new_version)
-except IOError as err:
-  print('error updating VERSION: %s' % err, file=sys.stderr)
+
+src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$',
+                         re.MULTILINE)
+for project in ['gerrit-acceptance-framework', 'gerrit-extension-api',
+                'gerrit-plugin-api', 'gerrit-plugin-archetype',
+                'gerrit-plugin-gwt-archetype', 'gerrit-plugin-gwtui',
+                'gerrit-plugin-js-archetype', 'gerrit-war']:
+  pom = os.path.join(project, 'pom.xml')
+  replace_in_file(pom, src_pattern)
+
+src_pattern = re.compile(r"^(GERRIT_VERSION = ')([-.\w]+)(')$", re.MULTILINE)
+replace_in_file('VERSION', src_pattern)
+
+src_pattern = re.compile(r'^(\s*-DarchetypeVersion=)([-.\w]+)(\s*\\)$',
+                         re.MULTILINE)
+replace_in_file(os.path.join('Documentation', 'dev-plugins.txt'), src_pattern)
diff --git a/website/releases/index.html b/website/releases/index.html
index 8aedc6b..456f0f9 100644
--- a/website/releases/index.html
+++ b/website/releases/index.html
@@ -30,7 +30,7 @@
 <body>
 
 <h1>Gerrit Code Review - Releases</h1>
-<a href="http://code.google.com/p/gerrit">
+<a href="https://www.gerritcodereview.com/">
   <img id="diffy_logo" src="https://gerrit-review.googlesource.com/static/diffy1.cache.png">
 </a>
 
@@ -44,6 +44,7 @@
   var doc = document;
   var frg = doc.createDocumentFragment();
   var rx = /^gerrit(?:-full)?-([0-9.]+(?:-rc[0-9]+)?)[.]war/;
+  var dl = 'https://www.gerritcodereview.com/download/';
   var docs = 'https://gerrit-documentation.storage.googleapis.com/';
   var src = 'https://gerrit.googlesource.com/gerrit/+/'
 
@@ -91,7 +92,7 @@
     var tr = doc.createElement('tr');
     var td = doc.createElement('td');
     var a = doc.createElement('a');
-    a.href = f.name;
+    a.href = dl + f.name;
     if (v) {
       a.appendChild(doc.createTextNode('Gerrit ' + v[1]));
     } else {