Merge branch 'stable-2.11'

* stable-2.11:
  Update replication plugin to latest revision
  Release notes for Gerrit 2.11.3
  ContainerAuthFilter: honor username provided by container
  Stop logging Unknown GroupMembership for UUID: null
  Set version to 2.11.3

Change-Id: I6b90e911f58a5e9274723aa58dea3a89de1d4b13
diff --git a/.buckconfig b/.buckconfig
index e4a19f1..7b75225 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -9,9 +9,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 +27,4 @@
 
 [cache]
   mode = dir
-  dir = ~/.gerritcodereview/buck-cache/cache
+  dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
diff --git a/.buckversion b/.buckversion
index 9c09744..46408a5 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-79d36de9f5284f6e833cca81867d6088a25685fb
+8204fddf60b25a3c2090f3ef0742fca5d466d562
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..f2cb839 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,4 @@
 /gwt-unitCache
 *.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..f66a0ff 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -7,6 +7,7 @@
 org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
 org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.doc.comment.support=enabled
 org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
 org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
 org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
@@ -28,12 +29,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
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
index d4218a5..7397758 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
 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..4dd69c3 100644
--- a/BUCK
+++ b/BUCK
@@ -1,11 +1,12 @@
 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-extension-api:extension-api',
@@ -25,7 +26,6 @@
     ['cd $TMP'] +
     ['ln -s $(location %s) .' % n for n in API_DEPS] +
     ['zip -q0 $OUT *']),
-  deps = API_DEPS,
   out = 'api.zip',
 )
 
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 48a6525..dfac617 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'] + JSUI
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
 
 genasciidoc(
@@ -29,11 +30,19 @@
 
 genrule(
   name = 'licenses.txt',
-  cmd = '$(exe :gen_licenses) >$OUT',
-  deps = [':gen_licenses'] + MAIN,
+  cmd = '$(exe :gen_licenses) --asciidoc ' + ' '.join(MAIN) + ' >$OUT',
+  deps = MAIN,
   out = 'licenses.txt',
 )
 
+# Required by Google for gerrit-review.
+genrule(
+  name = 'js_licenses.txt',
+  cmd = '$(exe :gen_licenses) --partial ' + ' '.join(JSUI) + ' >$OUT',
+  deps = JSUI,
+  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..ef653cc 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.
 
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-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-gerrit.txt b/Documentation/config-gerrit.txt
index 837469f..7b3431e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -430,8 +430,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
@@ -871,6 +872,41 @@
 +
 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::
++
+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 to be submitted.
++
+Defaults to "Submit all ${topicSize} changes within the topic".
+
 [[change.replyLabel]]change.replyLabel::
 +
 Label name for the reply button. In the user interface an ellipsis (…)
@@ -886,6 +922,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
 
@@ -1188,7 +1300,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[
@@ -1425,10 +1537,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]
@@ -1436,11 +1573,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
 
@@ -1448,6 +1591,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.
@@ -1480,6 +1632,7 @@
 * `mon, month, months` (`1 month` is treated as `30 days`)
 * `y, year, years` (`1 year` is treated as `365 days`)
 
+[[schedule-examples]]
 Examples::
 +
 ----
@@ -1554,6 +1707,18 @@
 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.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
 +
 Optional command to install the `commit-msg` hook. Typically of the
@@ -1724,7 +1889,7 @@
 +
 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)
@@ -1800,6 +1965,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
@@ -2131,10 +2301,6 @@
 +
 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`.
@@ -2156,6 +2322,36 @@
 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.
+
 ==== Lucene configuration
 
 Open and closed changes are indexed in separate indexes named
@@ -2225,17 +2421,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
 
@@ -2581,6 +2766,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
 
@@ -2657,6 +2858,13 @@
 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
@@ -2674,6 +2882,40 @@
   maxObjectSizeLimit = 40 m
 ----
 
+[[receive.enableSignedPush]]receive.enableSignedPush::
++
+If true, server-side signed push validation is enabled.
++
+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/gpg-keys` branch of `All-Users`.
++
+Defaults to false.
+
+[[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.checkMagicRefs]]receive.checkMagicRefs::
 +
 If true, Gerrit will verify the destination repository has
@@ -2788,9 +3030,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::
 +
@@ -2862,6 +3117,16 @@
 +
 By default, true, allowing notifications to be sent.
 
+[[sendemail.allowRegisterNewEmail]]sendemail.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.
+
 [[sendemail.connectTimeout]]sendemail.connectTimeout::
 +
 The connection timeout of opening a socket connected to a
@@ -2908,7 +3173,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
@@ -3025,6 +3290,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
@@ -3284,7 +3566,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::
 +
@@ -3501,6 +3785,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
@@ -3530,7 +3866,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
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-plugins.txt b/Documentation/config-plugins.txt
index 608a3ac..e5614cd 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -118,9 +118,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 +169,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[
@@ -357,6 +360,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
 
@@ -386,6 +402,17 @@
 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]
+
 [[project-download-commands]]
 === project-download-commands
 
@@ -418,6 +445,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..0d3ff58 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -154,6 +154,16 @@
 +
 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.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 7f30b14..c3dd12e 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
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..b3b72c4 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -146,6 +146,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..de3e6de 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
@@ -116,6 +120,20 @@
 ----
 
 
+=== 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.war
+----
+
 === Extension and Plugin API JAR Files
 
 To build the extension, plugin and GWT API JAR files:
@@ -279,33 +297,36 @@
 [[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`:
@@ -489,7 +510,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 +519,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,14 +615,14 @@
 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/api/group/.AddRemoveGroupMembersIT/
 ----
 
 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
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group:AddRemoveGroupMembersIT
+  TESTING //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group:AddRemoveGroupMembersIT
   PASS  14,9s  8 Passed   0 Failed   com.google.gerrit.acceptance.rest.group.AddRemoveGroupMembersIT
   TESTS PASSED
 ----
@@ -633,6 +655,46 @@
 buck test --no-results-cache
 ----
 
+== 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 0a44542..281e68c 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:http://code.google.com/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..7e1ca10 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -79,6 +79,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 05e9e47..24f34af 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.3 \
+    -DarchetypeVersion=2.12-SNAPSHOT \
     -DgroupId=com.googlesource.gerrit.plugins.testplugin \
     -DartifactId=testplugin
 ----
@@ -404,6 +404,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
 
@@ -958,6 +962,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 +1625,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 +1763,35 @@
 }
 ----
 
+[[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.
+
 [[settings-screen]]
 == Plugin Settings Screen
 
@@ -1717,17 +1852,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 +1898,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 +1981,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 +2083,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 bcf4a588..b64973a 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
 
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/gen_licenses.py b/Documentation/gen_licenses.py
index af7cd88..8b2d096 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,12 @@
 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('targets', nargs='+')
+args = parser.parse_args()
+
 KNOWN_PROVIDED_DEPS = [
   '//lib/bouncycastle:bcpg',
   '//lib/bouncycastle:bcpkix',
@@ -36,13 +42,23 @@
   while not path.isfile('.buckconfig'):
     chdir('..')
   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 +76,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 +86,8 @@
   queue.extend(graph[target])
 used = sorted(licenses.keys())
 
-print("""\
+if args.asciidoc:
+  print("""\
 Gerrit Code Review - Licenses
 =============================
 
@@ -122,26 +139,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 6b83644..2a0082a 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,13 +50,8 @@
 == 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]
@@ -76,7 +69,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 +78,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:http://code.google.com/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 6138a28..4a64892 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]]
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..4584841 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
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/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 7aec531..096cf04 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/name HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "username": "jdoe"
+  }
+----
+
+As response the new username is returned.
+
 [[get-active]]
 === Get Active
 --
@@ -1242,6 +1296,26 @@
 [[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.
+|`contact_filed_on`  |optional|
+The link:rest-api.html#timestamp[timestamp] of when contact information
+for this account was filed. Not set if no contact information was
+filed.
+|=================================
+
 [[account-info]]
 === AccountInfo
 The `AccountInfo` entity contains information about an account.
@@ -1324,6 +1398,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.
@@ -1523,42 +1600,45 @@
 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`      ||
+|`download_scheme`              ||
 The type of download URL the user prefers to use.
-|`download_command`     ||
+|`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.
+|============================================
 
 [[preferences-input]]
 === PreferencesInput
@@ -1566,42 +1646,45 @@
 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.
+|============================================
 
 [[query-limit-info]]
 === QueryLimitInfo
@@ -1631,6 +1714,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 1b9a705..94f8ac9 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -231,13 +231,6 @@
   `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,13 @@
 * `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].
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -1061,6 +1066,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 +1417,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 +1580,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
@@ -1862,6 +2239,7 @@
 
   )]}'
   {
+    "commit": "674ac754f91e64a0efb8087e59a176484bd534d1",
     "parents": [
       {
         "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
@@ -2108,6 +2486,7 @@
         "_change_number": 58478,
         "_revision_number": 2,
         "_current_revision_number": 2
+        "status": "NEW"
       },
       {
         "change_id": "I5e4fc08ce34d33c090c9e0bf320de1b17309f774",
@@ -2129,6 +2508,7 @@
         "_change_number": 58081,
         "_revision_number": 10,
         "_current_revision_number": 10
+        "status": "NEW"
       }
     ]
   }
@@ -2306,18 +2686,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
@@ -2586,7 +2958,7 @@
 ----
 
 [[list-drafts]]
-=== List Drafts
+=== List Revision Drafts
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/'
 --
@@ -2594,9 +2966,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
 ----
@@ -2765,7 +3136,7 @@
 ----
 
 [[list-comments]]
-=== List Comments
+=== List Revision Comments
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/'
 --
@@ -3390,8 +3761,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.
@@ -3494,6 +3864,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. +
@@ -3576,25 +3949,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
@@ -3768,6 +4144,21 @@
 Not set for binary files or if no lines were deleted.
 |=============================
 
+[[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
@@ -3800,14 +4191,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
@@ -3934,6 +4328,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]]
@@ -4013,7 +4409,9 @@
 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 handling that defines to whom email notifications should be sent
@@ -4076,9 +4474,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
@@ -4108,6 +4503,12 @@
 |`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.
 |===========================
 
 [[rule-input]]
@@ -4132,15 +4533,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
@@ -4159,11 +4560,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..c641f94 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,110 @@
 |`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].
+|=============================
+
+[[contact-store-info]]
+=== ContactStoreInfo
+The `ContactStoreInfo` entity contains information about the contact
+store.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`url`      |
+The link:config-gerrit.html#contactstore.url[URL of the contact store].
+|=======================
+
+[[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 +1188,78 @@
 `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.)
+|`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 +1322,109 @@
 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.
+|`contact_store`           |optional|
+Information about the contact store configuration as
+link:#contact-store-info[ContactStoreInfo] 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 +1536,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..afdf36d 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -592,6 +592,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 +1191,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 a909ab4..4658e2c 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -731,6 +731,7 @@
     "use_content_merge": "INHERIT",
     "use_signed_off_by": "INHERIT",
     "create_new_change_for_all_not_in_target": "INHERIT",
+    "enable_signed_push": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -774,6 +775,11 @@
       "configured_value": "TRUE",
       "inherited_value": true
     },
+    "enable_signed_push": {
+      "value": true,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "max_object_size_limit": {
       "value": "10m",
       "configured_value": "10m",
@@ -1902,6 +1908,9 @@
 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|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+signed push validation is enabled 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 +2125,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 5ad6b39..e262cb7 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.
 
@@ -145,6 +144,13 @@
 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
 
diff --git a/Documentation/user-named-queries.txt b/Documentation/user-named-queries.txt
new file mode 100644
index 0000000..0ddbbef
--- /dev/null
+++ b/Documentation/user-named-queries.txt
@@ -0,0 +1,32 @@
+= 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 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 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..1015e0f 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -94,7 +94,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"]
@@ -408,9 +408,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 +536,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 +571,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[
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3de45d2..bb60a7e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -99,6 +99,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 +153,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 +251,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 +268,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::
 +
@@ -296,8 +311,9 @@
 
 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.
+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:submitted::
 +
@@ -327,6 +343,25 @@
 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.
+
 
 == 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..573042d
--- /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://code.google.com/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 all
+
+## 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.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
new file mode 100644
index 0000000..4a266e9
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -0,0 +1,41 @@
+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:* Upgrading to 2.12.x requires the server be first upgraded to 2.8 (or
+2.9) and then to 2.12.x. If you are upgrading from 2.8.x or later, 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].
+
+
+Release Highlights
+------------------
+
+* TODO
+
+
+New Features
+------------
+
+* TODO
+
+
+Upgrades
+--------
+
+* Upgrade gson to 2.3.1
+
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 a0c44b1..262dc4f 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -146,4 +146,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 8136a17..83d3d2a 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.3'
+GERRIT_VERSION = '2.12-SNAPSHOT'
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/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-tests/BUCK b/gerrit-acceptance-tests/BUCK
index c7bea4e..219575c 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -19,21 +19,22 @@
 
     '//lib:args4j',
     '//lib:gson',
-    '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
     '//lib:h2',
     '//lib:jsch',
-    '//lib:junit',
     '//lib:servlet-api-3_1',
     '//lib:truth',
 
+    '//lib/auto:auto-value',
+    '//lib/httpcomponents:fluent-hc',
     '//lib/httpcomponents:httpclient',
     '//lib/httpcomponents:httpcore',
     '//lib/log:impl_log4j',
     '//lib/log:log4j',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
     '//lib/mina:sshd',
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
index d890fa4..12e26b4 100644
--- 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
@@ -14,33 +14,38 @@
 
 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.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 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.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+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.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.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.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;
@@ -57,29 +62,42 @@
 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.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.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.EnumSet;
 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;
 
@@ -93,7 +111,7 @@
   protected GerritApi gApi;
 
   @Inject
-  private AcceptanceTestRequestScope atrScope;
+  protected AcceptanceTestRequestScope atrScope;
 
   @Inject
   private IdentifiedUser.GenericFactory identifiedUserFactory;
@@ -120,9 +138,24 @@
   protected Provider<InternalChangeQuery> queryProvider;
 
   @Inject
-  protected @GerritServerConfig Config cfg;
+  @CanonicalWebUrl
+  protected Provider<String> canonicalWebUrl;
 
-  protected Git git;
+  @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;
@@ -133,36 +166,36 @@
   protected Project.NameKey project;
 
   @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 {
-          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();
+          beforeTest(description);
+          try {
+            base.evaluate();
+          } finally {
+            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");
+  @AfterClass
+  public static void stopCommonServer() throws Exception {
+    if (commonServer != null) {
+      commonServer.stop();
+      commonServer = null;
     }
-    if (cfgs != null) {
-      return ConfigAnnotationParser.parse(baseConfig, cfgs);
-    } else if (cfg != null) {
-      return ConfigAnnotationParser.parse(baseConfig, cfg);
-    } else {
-      return baseConfig;
-    }
+    TempFileUtil.cleanup();
   }
 
   protected static Config submitWholeTopicEnabledConfig() {
@@ -185,9 +218,24 @@
     return cfg.getBoolean("change", null, "submitWholeTopic", false);
   }
 
-  private void beforeTest(Config cfg, boolean memory, boolean enableHttpd) throws Exception {
-    server = startServer(cfg, memory, enableHttpd);
+  private void beforeTest(Description description) throws Exception {
+    GerritServer.Description classDesc =
+      GerritServer.Description.forTestClass(description, configName);
+    GerritServer.Description methodDesc =
+      GerritServer.Description.forTestMethod(description, configName);
+
+    if (classDesc.equals(methodDesc)) {
+      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();
     adminSession = new RestSession(server, admin);
@@ -197,56 +245,167 @@
     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());
+    sshSession.open();
+    resourcePrefix = UNSAFE_PROJECT_NAME.matcher(
+        description.getClassName() + "_"
+        + description.getMethodName() + "_").replaceAll("");
+
+    project = createProject(projectInput(description));
+    testRepo = cloneProject(project, getCloneAsAccount(description));
   }
 
-  protected GerritServer startServer(Config cfg, boolean memory,
-      boolean enableHttpd) throws Exception {
-    return GerritServer.start(cfg, memory, enableHttpd);
+  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 {
+    ProjectInput in = new ProjectInput();
+    in.name = name(nameSuffix);
+    in.parent = parent != null ? parent.get() : null;
+    in.createEmptyCommit = createEmptyCommit;
+    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();
-    server.stop();
-    TempFileUtil.cleanup();
+    if (server != commonServer) {
+      server.stop();
+    }
   }
 
-  protected PushOneCommit.Result createChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master");
+  protected TestRepository<?>.CommitBuilder commitBuilder() throws Exception {
+    return testRepo.branch("HEAD").commit().insertChangeId();
+  }
+
+  protected TestRepository<?>.CommitBuilder amendBuilder() throws Exception {
+    ObjectId head = repo().getRef("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;
   }
 
   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 {
+      throws Exception {
     return amendChange(changeId, "refs/for/master");
   }
 
   protected PushOneCommit.Result amendChange(String changeId, String ref)
-      throws GitAPIException, IOException {
+      throws Exception {
     Collections.shuffle(RANDOM);
     PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
-    return push.to(git, ref);
-  }
-
-  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);
+    return push.to(ref);
   }
 
   protected ChangeInfo info(String id)
@@ -266,9 +425,8 @@
 
   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);
+    return gApi.changes().id(id).get(
+        Sets.newEnumSet(Arrays.asList(options), ListChangesOption.class));
   }
 
   protected List<ChangeInfo> query(String q) throws RestApiException {
@@ -284,6 +442,10 @@
     return atrScope.set(newRequestContext(account));
   }
 
+  protected Context setApiUserAnonymous() {
+    return atrScope.newContext(reviewDbProvider, null, anonymousUser.get());
+  }
+
   protected static Gson newGson() {
     return OutputFormat.JSON_COMPACT.newGson();
   }
@@ -301,10 +463,31 @@
     saveProjectConfig(project, cfg);
   }
 
-  protected void allowGlobalCapability(String capabilityName,
-      AccountGroup.UUID id) throws Exception {
+  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();
-    Util.allow(cfg, capabilityName, id);
+    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);
   }
 
@@ -378,9 +561,22 @@
     saveProjectConfig(project, cfg);
   }
 
-  protected PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
+  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();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 2a578c2..0ff4709 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -41,7 +41,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;
@@ -162,6 +162,21 @@
     return old;
   }
 
+  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-tests/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-tests/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-tests/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-tests/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/DisabledReviewDb.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java
new file mode 100644
index 0000000..44d3d7f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java
@@ -0,0 +1,206 @@
+// 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.gerrit.reviewdb.server.AccountAccess;
+import com.google.gerrit.reviewdb.server.AccountDiffPreferenceAccess;
+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.PatchSetAncestorAccess;
+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. */
+class DisabledReviewDb implements ReviewDb {
+  private static final String MESSAGE = "ReviewDb is disabled for this test";
+
+  @Override
+  public void close() {
+    // Do nothing.
+  }
+
+  @Override
+  public void commit() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public void rollback() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public void updateSchema(StatementExecutor e) {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public void pruneSchema(StatementExecutor e) {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public Access<?, ?>[] allRelations() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public SchemaVersionAccess schemaVersion() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public SystemConfigAccess systemConfig() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountAccess accounts() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountExternalIdAccess accountExternalIds() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountSshKeyAccess accountSshKeys() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupAccess accountGroups() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupNameAccess accountGroupNames() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupMemberAccess accountGroupMembers() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountDiffPreferenceAccess accountDiffPreferences() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public StarredChangeAccess starredChanges() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountProjectWatchAccess accountProjectWatches() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountPatchReviewAccess accountPatchReviews() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public ChangeAccess changes() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchSetApprovalAccess patchSetApprovals() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public ChangeMessageAccess changeMessages() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchSetAccess patchSets() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchSetAncestorAccess patchSetAncestors() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchLineCommentAccess patchComments() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public SubmoduleSubscriptionAccess submoduleSubscriptions() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupByIdAccess accountGroupById() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupByIdAudAccess accountGroupByIdAud() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextAccountId() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextAccountGroupId() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextChangeId() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextChangeMessageId() {
+    throw new AssertionError(MESSAGE);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java b/gerrit-acceptance-tests/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-tests/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-tests/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-tests/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/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 3be8195..948f178 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/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.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;
@@ -47,10 +53,55 @@
 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.
+          testDesc.getTestClass().getAnnotation(NoHttpd.class) == null,
+          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
+            && testDesc.getTestClass().getAnnotation(NoHttpd.class) == null,
+          testDesc.getAnnotation(GerritConfig.class),
+          testDesc.getAnnotation(GerritConfigs.class));
+    }
+
+    @Nullable abstract String configName();
+    abstract boolean memory();
+    abstract boolean httpd();
+    @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 +109,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 +128,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 +140,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 +155,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 +186,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 +206,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 +226,8 @@
     return InetAddress.getLoopbackAddress();
   }
 
+  private final Description desc;
+
   private Daemon daemon;
   private ExecutorService daemonService;
   private Injector testInjector;
@@ -174,8 +235,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 +269,10 @@
     return testInjector;
   }
 
+  Description getDescription() {
+    return desc;
+  }
+
   void stop() throws Exception {
     daemon.getLifecycleManager().stop();
     if (daemonService != null) {
@@ -216,4 +282,9 @@
     }
     RepositoryCache.clear();
   }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).addValue(desc).toString();
+  }
 }
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
index dee36ef..e8f8925 100644
--- 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
@@ -14,46 +14,43 @@
 
 package com.google.gerrit.acceptance;
 
-import static com.google.common.base.Preconditions.checkState;
-
+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.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.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.PersonIdent;
+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.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;
+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();
@@ -77,131 +74,75 @@
     });
   }
 
-  public static void createProject(SshSession s, String name)
-      throws JSchException, IOException {
-    createProject(s, name, null);
+  /**
+   * 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 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");
+  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);
     }
-    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());
-    }
+    return testRepo;
   }
 
-  public static Git cloneProject(String url) throws GitAPIException, IOException {
-    return cloneProject(url, true);
+  public static TestRepository<InMemoryRepository> cloneProject(
+      Project.NameKey project, SshSession sshSession) throws Exception {
+    return cloneProject(project, sshSession.getUrl() + "/" + project.get());
   }
 
-  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)
+  public static void fetch(TestRepository<?> testRepo, String spec)
       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();
+    FetchCommand fetch = testRepo.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(TestRepository<?> testRepo, String ref,
+      boolean pushTags) throws GitAPIException {
+    return pushHead(testRepo, ref, pushTags, false);
   }
 
-  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();
+  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) {
@@ -211,21 +152,14 @@
     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 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();
     }
-
-    public RevCommit getCommit() {
-      return commit;
-    }
-
-    public String getChangeId() {
-      return changeId;
-    }
+    return Optional.of(ids.get(ids.size() - 1));
   }
 }
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
index f765e7a..1e0920e 100644
--- 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
@@ -16,12 +16,9 @@
 
 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 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;
@@ -29,33 +26,18 @@
 public class HttpSession {
 
   protected final String url;
-  private final TestAccount account;
-  private HttpClient client;
+  private final Executor executor;
 
   public HttpSession(GerritServer server, TestAccount account) {
     this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
-    this.account = account;
+    URI uri = URI.create(url);
+    this.executor = Executor
+        .newInstance()
+        .auth(new HttpHost(uri.getHost(), uri.getPort()),
+            account.username, account.httpPassword);
   }
 
-  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;
+  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-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 8548b5c..8f4c2d4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/gerrit-acceptance-tests/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("UNIT_TEST_GERRIT_SITE"));
 
     bind(GitRepositoryManager.class)
       .toInstance(new InMemoryRepositoryManager());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
new file mode 100644
index 0000000..c02b9e5
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -0,0 +1,352 @@
+// 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 getCurrentUser() {
+      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();
+        }
+
+        IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+        rp.setRefLogIdent(user.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/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 67b6f51..77c38c8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-tests/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,38 @@
 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;
-
 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 +82,7 @@
     PushOneCommit create(
         ReviewDb db,
         PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content,
@@ -106,7 +112,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 +121,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 +140,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 +154,48 @@
       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.getClock()));
+  }
+
+  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);
+  }
+
+  private 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 +206,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 +221,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 +234,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 +246,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 +283,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/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
index bf6f928..98459b3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -21,10 +21,7 @@
 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;
@@ -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()));
     }
-    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,19 +100,18 @@
   }
 
   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()));
     }
-    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));
   }
 
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-tests/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-tests/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-tests/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-tests/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-tests/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestProjectInput.java
new file mode 100644
index 0000000..4ad37e2
--- /dev/null
+++ b/gerrit-acceptance-tests/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/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 8945a22d..0ecbc7e 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
@@ -16,14 +16,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 
 import org.junit.Test;
 
-public class AccountIT extends AbstractDaemonTest {
+import java.util.List;
 
+public class AccountIT extends AbstractDaemonTest {
   @Test
   public void get() throws Exception {
     AccountInfo info = gApi
@@ -49,14 +53,54 @@
   @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);
   }
 }
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..967575a 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,13 @@
 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.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,6 +29,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+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;
@@ -33,11 +42,17 @@
 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.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -48,19 +63,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();
@@ -126,7 +141,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 +150,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 +159,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 +167,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 +203,7 @@
         .id(r.getChangeId())
         .addReviewer(in);
 
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(user.id));
   }
 
@@ -204,7 +219,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,7 +227,7 @@
     gApi.changes()
         .id(r.getChangeId())
         .addReviewer(in);
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
   }
 
@@ -235,38 +250,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 +297,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 +308,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 +322,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 +336,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 +391,112 @@
         .get(EnumSet.of(ListChangesOption.CHECK))
         .problems).isEmpty();
   }
+
+  @Test
+  public void commitFooters() throws Exception {
+    LabelType verified = category("Verified",
+        value(1, "Failed"), value(0, "No score"), value(-1, "Passes"));
+    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(), verified);
+    cfg.getLabelSections().put(custom2.getName(), verified);
+    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();
+
+    String expected = SUBJECT + "\n"
+        + "\n"
+        + "Change-Id: " + r2.getChangeId() + "\n"
+        + "Reviewed-on: "
+            + canonicalWebUrl.get() + r2.getChange().getId() + "\n"
+        + "Reviewed-by: Administrator <admin@example.com>\n"
+        + "Custom2: Administrator <admin@example.com>\n"
+        + "Tested-by: Administrator <admin@example.com>\n";
+    assertThat(actual.revisions.get(r2.getCommit().getName()).commitWithFooters)
+        .isEqualTo(expected);
+  }
+
+  @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();
+  }
 }
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..1152d88
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK
@@ -0,0 +1,6 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  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..332459a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
@@ -0,0 +1,22 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  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: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..5b8b87f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -0,0 +1,542 @@
+// 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 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/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/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 9cc8f9c..5429b95 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,8 +17,8 @@
 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 org.eclipse.jgit.lib.Constants.HEAD;
-import static org.junit.Assert.fail;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
@@ -41,20 +41,21 @@
 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;
@@ -75,7 +76,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 +109,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 +122,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 +153,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 +170,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 +208,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 +222,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 +234,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 +273,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 +301,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 +329,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 +342,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 +361,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().getRef(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.
   }
@@ -505,6 +521,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 +544,39 @@
       .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(), StandardCharsets.UTF_8);
+    DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
+    ChangeInfo change = changeApi.get();
+    RevisionInfo rev = change.revisions.get(change.currentRevision);
+    String date = dateFormat.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/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 8a02831..50bafbe 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,31 @@
 
 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,12 +57,12 @@
 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.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;
@@ -69,13 +70,17 @@
 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;
 
@@ -108,19 +113,8 @@
   private PatchSet ps;
   private PatchSet ps2;
 
-  @Before
-  public void setUp() throws Exception {
-    db = reviewDbProvider.open();
-    changeId = newChange(git, admin.getIdent());
-    ps = getCurrentPatchSet(changeId);
-    amendChange(git, admin.getIdent(), changeId);
-    change = getChange(changeId);
-    assertThat(ps).isNotNull();
-    changeId2 = newChange2(git, admin.getIdent());
-    change2 = getChange(changeId2);
-    assertThat(change2).isNotNull();
-    ps2 = getCurrentPatchSet(changeId2);
-    assertThat(ps2).isNotNull();
+  @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());
@@ -132,9 +126,28 @@
     });
   }
 
+  @AfterClass
+  public static void restoreTime() {
+    DateTimeUtils.setCurrentMillisSystem();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    db = reviewDbProvider.open();
+    changeId = newChange(admin.getIdent());
+    ps = getCurrentPatchSet(changeId);
+    amendChange(admin.getIdent(), changeId);
+    change = getChange(changeId);
+    assertThat(ps).isNotNull();
+    changeId2 = newChange2(admin.getIdent());
+    change2 = getChange(changeId2);
+    assertThat(change2).isNotNull();
+    ps2 = getCurrentPatchSet(changeId2);
+    assertThat(ps2).isNotNull();
+  }
+
   @After
   public void cleanup() {
-    DateTimeUtils.setCurrentMillisSystem();
     db.close();
   }
 
@@ -156,7 +169,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 +192,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 +285,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 +307,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,20 +328,24 @@
   }
 
   @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);
 
-    try {
-      modifier.modifyMessage(
-          edit.get(),
-          edit.get().getEditCommit().getFullMessage());
-      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(),
+        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);
 
     String msg = String.format("New commit message\n\nChange-Id: %s",
         change.getKey());
@@ -334,6 +361,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
@@ -359,6 +391,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
@@ -402,12 +439,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
@@ -419,12 +453,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
@@ -432,12 +463,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
@@ -452,12 +480,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
@@ -473,6 +498,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();
@@ -483,12 +532,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
@@ -588,14 +634,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
@@ -628,24 +671,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();
@@ -672,25 +713,58 @@
     assertThat(approvals.get(0).value).isEqualTo(1);
   }
 
-  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 {
+  private List<ChangeInfo> queryEdits() throws Exception {
+    return query("project:{" + project.get() + "} has:edit");
+  }
+
+  private String newChange(PersonIdent ident) throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME2,
+        pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME,
+            new String(CONTENT_OLD, StandardCharsets.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, StandardCharsets.UTF_8), changeId);
-    return push.to(git, "refs/for/master").getChangeId();
+    return push.to("refs/for/master").getChangeId();
   }
 
-  private String newChange2(Git git, PersonIdent ident) throws Exception {
+  private String newChange2(PersonIdent ident) throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME,
+        pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME,
             new String(CONTENT_OLD, StandardCharsets.UTF_8));
-    return push.rm(git, "refs/for/master").getChangeId();
+    return push.rm("refs/for/master").getChangeId();
   }
 
   private Change getChange(String changeId) throws Exception {
@@ -761,4 +835,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 8893ecb..080b767 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,36 +16,25 @@
 
 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 com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 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.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.PushResult;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.List;
 import java.util.Set;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
@@ -58,6 +47,7 @@
   private NotesMigration notesMigration;
 
   protected enum Protocol {
+    // TODO(dborowitz): TEST.
     SSH, HTTP
   }
 
@@ -68,7 +58,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:
@@ -80,20 +70,18 @@
       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 testPushForMasterWithTopic() throws Exception {
     // specify topic in ref
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
@@ -107,8 +95,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);
@@ -131,8 +118,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);
@@ -156,8 +142,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();
@@ -170,8 +155,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());
@@ -185,65 +169,59 @@
   }
 
   @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);
 
     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");
     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);
   }
 
   @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();
@@ -256,23 +234,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();
@@ -287,25 +264,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");
@@ -313,59 +288,26 @@
   }
 
   @Test
-  public void testPushSameCommitTwiceUsingMagicBranchBaseOption()
-      throws Exception {
-    grant(Permission.PUSH, project, "refs/heads/master");
-    PushOneCommit.Result rBase = pushTo("refs/heads/master");
-    rBase.assertOkStatus();
-
-    gApi.projects()
-        .name(project.get())
-        .branch("foo")
-        .create(new BranchInput());
-
-    PushOneCommit push =
-        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
-            "b.txt", "anotherContent");
-
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
-    r.assertOkStatus();
-
-    PushResult pr = GitUtil.pushHead(
-        git, "refs/for/foo%base=" + rBase.getCommitId().name(), false, false);
-    assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done");
-
-    List<ChangeInfo> changes = query(r.getCommitId().name());
-    assertThat(changes).hasSize(2);
-    ChangeInfo c1 = get(changes.get(0).id);
-    ChangeInfo c2 = get(changes.get(1).id);
-    assertThat(c1.project).isEqualTo(c2.project);
-    assertThat(c1.branch).isNotEqualTo(c2.branch);
-    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,
+        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();
 
     setUseSignedOffBy(InheritableBoolean.TRUE);
     blockForgeCommitter(project, "refs/heads/master");
 
-    push = pushFactory.create(db, admin.getIdent(),
+    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(git, "refs/for/master");
+    r = push.to("refs/for/master");
     r.assertOkStatus();
 
-    push = pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
-        "b.txt", "anotherContent");
-    r = push.to(git, "refs/for/master");
+    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");
   }
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..2dbbb16
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.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.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 createSubscription(TestRepository<?> repo, String branch,
+      String subscribeToRepo, String subscribeToBranch) throws Exception {
+    Config config = new Config();
+    prepareSubscriptionConfigEntry(config, subscribeToRepo, subscribeToBranch);
+    pushSubscriptionConfig(repo, branch, config);
+  }
+
+  protected void prepareSubscriptionConfigEntry(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);
+    config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+  }
+
+  protected void pushSubscriptionConfig(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();
+
+    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);
+  }
+}
\ No newline at end of file
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 f36c447..446a183 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
@@ -5,8 +5,11 @@
     'DraftChangeBlockedIT.java',
     'ForcePushIT.java',
     'SubmitOnPushIT.java',
+    'SubmoduleSubscriptionsWholeTopicMergeIT.java',
+    'SubmoduleSubscriptionsIT.java',
     'VisibleRefFilterIT.java',
   ],
+  deps = [':submodule_util'],
   labels = ['git'],
 )
 
@@ -21,3 +24,9 @@
   srcs = ['AbstractPushForReview.java'],
   deps = ['//gerrit-acceptance-tests:lib'],
 )
+
+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..26db819 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().getRef(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().getRef(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..9a8bb51 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());
@@ -90,9 +84,9 @@
     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 +98,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 +110,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().getRef("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().getRef("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 +190,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 +206,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");
@@ -255,11 +247,11 @@
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
       RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
-      assertThat(c.getParentCount()).is(2);
+      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());
     }
   }
 
@@ -290,17 +282,16 @@
   }
 
   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..707852f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -0,0 +1,247 @@
+// 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 org.eclipse.jgit.junit.TestRepository;
+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;
+
+public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
+
+  @Test
+  public void testSubscriptionToEmptyRepo() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    createSubscription(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");
+    createSubscription(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");
+    createSubscription(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");
+    createSubscription(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");
+    createSubscription(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");
+    createSubscription(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");
+
+    createSubscription(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");
+    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubscription(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..086c205
--- /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");
+    createSubscription(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();
+    prepareSubscriptionConfigEntry(config, "sub1", "master");
+    prepareSubscriptionConfigEntry(config, "sub2", "master");
+    prepareSubscriptionConfigEntry(config, "sub3", "master");
+    pushSubscriptionConfig(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..21efc87 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
@@ -68,22 +68,35 @@
 
   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,15 +104,18 @@
         .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());
@@ -111,8 +127,6 @@
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(repo.getRef("refs/heads/branch").getObjectId());
       assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
-    } finally {
-      repo.close();
     }
   }
 
@@ -126,10 +140,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 +157,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 +175,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 +187,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 +201,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 +276,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/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/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/GetAccountDetailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
new file mode 100644
index 0000000..d9d1ceb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.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.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.AccountCache;
+import com.google.gerrit.server.account.GetDetail.AccountDetailInfo;
+import com.google.inject.Inject;
+
+import org.junit.Test;
+
+public class GetAccountDetailIT extends AbstractDaemonTest {
+  @Inject
+  private AccountCache accountCache;
+
+  @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());
+    assertThat(info.contactFiledOn).isEqualTo(account.getContactFiledOn());
+  }
+}
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/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..6fcdbce 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
@@ -18,29 +18,29 @@
 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.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 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.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.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.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;
@@ -50,19 +50,17 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.Event;
 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,12 +71,15 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 public abstract class AbstractSubmit extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
   private Map<String, String> mergeResults;
 
   @Inject
@@ -109,7 +110,6 @@
       }
 
     }, listenerUser);
-    project = new Project.NameKey("p2");
   }
 
   @After
@@ -120,10 +120,9 @@
   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());
   }
@@ -131,104 +130,81 @@
   @Test
   public void submitWholeTopic() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
-    Git git = createProject();
     PushOneCommit.Result change1 =
-        createChange(git, "Change 1", "a.txt", "content", "test-topic");
+        createChange("Change 1", "a.txt", "content", "test-topic");
     PushOneCommit.Result change2 =
-        createChange(git, "Change 2", "b.txt", "content", "test-topic");
+        createChange("Change 2", "b.txt", "content", "test-topic");
+    PushOneCommit.Result change3 =
+        createChange("Change 3", "c.txt", "content", "test-topic");
     approve(change1.getChangeId());
     approve(change2.getChangeId());
-    submit(change2.getChangeId());
+    approve(change3.getChangeId());
+    submit(change3.getChangeId());
     change1.assertChange(Change.Status.MERGED, "test-topic", admin);
     change2.assertChange(Change.Status.MERGED, "test-topic", admin);
+    change3.assertChange(Change.Status.MERGED, "test-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);
   }
 
-  protected Git createProject() throws JSchException, IOException,
-      GitAPIException {
-    return createProject(true);
-  }
-
-  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();
+  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 into the git repository by Administrator");
     }
   }
 
-  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();
+  @Override
+  protected void updateProjectInput(ProjectInput in) {
+    in.submitType = getSubmitType();
+    if (in.useContentMerge == InheritableBoolean.INHERIT) {
+      in.useContentMerge = InheritableBoolean.FALSE;
+    }
   }
 
-  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();
-  }
-
-  protected PushOneCommit.Result createChange(Git git) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master");
-  }
-
-  protected PushOneCommit.Result createChange(Git git, String subject,
-      String fileName, String content) throws GitAPIException, IOException {
+  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 {
+  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 submit(String changeId) throws Exception {
     submit(changeId, HttpStatus.SC_OK);
   }
 
-  protected void submitWithConflict(String changeId) throws IOException {
+  protected void submitWithConflict(String changeId) throws Exception {
     submit(changeId, HttpStatus.SC_CONFLICT);
   }
 
-  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);
-  }
-
-  private void submit(String changeId, int expectedStatus) throws IOException {
+  private void submit(String changeId, int expectedStatus) throws Exception {
     approve(changeId);
     SubmitInput subm = new SubmitInput();
-    subm.waitForMerge = true;
     RestResponse r =
         adminSession.post("/changes/" + changeId + "/submit", subm);
     assertThat(r.getStatusCode()).isEqualTo(expectedStatus);
@@ -247,51 +223,56 @@
     // 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 {
+    try (Repository repo =
+        repoManager.openRepository(new Project.NameKey(c.project))) {
       Ref ref = repo.getRef(
           new PatchSet.Id(new Change.Id(c._number), expectedNum).toRefName());
       assertThat(ref).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 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 +283,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,19 +319,31 @@
     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 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/" + branch).getObjectId()));
+      return Lists.newArrayList(rw);
     }
   }
 
   protected List<RevCommit> getRemoteLog() throws IOException {
-    try (Repository repo = repoManager.openRepository(project);
-        RevWalk rw = new RevWalk(repo)) {
-      rw.markStart(rw.parseCommit(
-          repo.getRef("refs/heads/master").getObjectId()));
-      return Lists.newArrayList(rw);
-    }
+    return getRemoteLog(project, "master");
   }
 
   private RevCommit getHead(Repository repo, String name) throws IOException {
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..98aae37 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,19 +65,17 @@
   }
 
   @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");
+        createChange("Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId());
     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..24bb72b 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,102 @@
       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(
+          "Clicking the button would fail for other changes");
+    } 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");
     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 +172,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/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 3c76355..a56a7f2 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
@@ -73,15 +73,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 +94,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/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..d0bcfd9 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
@@ -19,15 +19,18 @@
 
 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 org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
+@NoHttpd
 public class CreateChangeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config allowDraftsDisabled() {
@@ -38,9 +41,8 @@
   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 +50,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 +90,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 +101,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..621d94a 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,7 +78,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);
@@ -90,7 +94,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..6ef53ff 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
@@ -16,218 +16,206 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-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.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);
-  }
-
   @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..b99b30a 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,37 @@
   }
 
   @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");
+        createChange("Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId());
     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 +135,55 @@
 
   @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");
+        createChange("Change 3", "b.txt", "different content");
     submitWithConflict(change3.getChangeId());
     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,12 +207,11 @@
 
   @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
@@ -240,64 +226,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..2655789 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,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.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ActionInfo;
 
-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 +34,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,17 +44,53 @@
   }
 
   @Test
+  public void submitTwoChangesWithFastForward() throws Exception {
+    PushOneCommit.Result change = createChange();
+    PushOneCommit.Result change2 = createChange();
+
+    approve(change.getChangeId());
+    submit(change2.getChangeId());
+
+    RevCommit head = getRemoteHead();
+    assertThat(head.getId()).isEqualTo(change2.getCommitId());
+    assertThat(head.getParent(0).getId()).isEqualTo(change.getCommitId());
+    assertSubmitter(change.getChangeId(), 1);
+    assertSubmitter(change2.getChangeId(), 1);
+    assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
+    assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+  }
+
+  @Test
+  public void submitTwoChangesWithFastForward_missingDependency() throws Exception {
+    RevCommit oldHead = getRemoteHead();
+    createChange();
+    PushOneCommit.Result change2 = createChange();
+
+    submitWithConflict(change2.getChangeId());
+
+    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");
+        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());
     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..032cb7d 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,284 @@
 
   @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());
+    } 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());
+
+    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..d1d3bdf 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,12 +15,12 @@
 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.google.gerrit.extensions.client.SubmitType;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
@@ -32,10 +32,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 +43,50 @@
     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(serverIdent.get(), head.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());
-    assertRebase(git, true);
+    assertRebase(testRepo, true);
     RevCommit head = getRemoteHead();
     assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
@@ -93,22 +96,21 @@
   }
 
   @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");
+        createChange("Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId());
     RevCommit head = getRemoteHead();
     assertThat(head).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
-    assertSubmitter(change2.getChangeId(), 1);
+    assertNoSubmitter(change2.getChangeId(), 1);
   }
 }
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 7fee9f4..ed6740a 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,38 +16,42 @@
 
 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;
@@ -55,22 +59,20 @@
   @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);
   }
 
   @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 +84,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 +93,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 +116,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 +144,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 +162,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,19 +199,19 @@
     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, "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);
   }
 
@@ -217,60 +223,47 @@
   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, 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());
+    name = name(name);
+    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 + "@example.com", fullName, groupNames);
   }
 }
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..9d8320ad
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.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.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);
+  }
+}
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..f75780c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -0,0 +1,175 @@
+// 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);
+
+    // contactstore
+    assertThat(i.contactStore).isNull();
+
+    // 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);
+
+    // contactstore
+    assertThat(i.contactStore).isNull();
+
+    // 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..ffdfa8b 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
@@ -2,23 +2,6 @@
 
 acceptance_tests(
   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..50e6b84 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
@@ -15,9 +15,8 @@
     'BranchAssert.java',
   ],
   deps = [
-    '//lib:guava',
-    '//lib:junit',
     '//lib:truth',
+    '//gerrit-extension-api:api',
     '//gerrit-server:server',
   ],
 )
@@ -32,8 +31,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..030897b 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,19 @@
 
 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.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 +40,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 +55,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 +68,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 +119,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 +154,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 +172,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,39 +200,42 @@
   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");
@@ -209,15 +243,18 @@
 
   @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);
   }
 
   private AccountGroup.UUID groupUuid(String groupName) {
@@ -226,13 +263,10 @@
 
   private void assertHead(String projectName, String expectedRef)
       throws RepositoryNotFoundException, IOException {
-    Repository repo =
-        repoManager.openRepository(new Project.NameKey(projectName));
-    try {
+    try (Repository repo =
+        repoManager.openRepository(new Project.NameKey(projectName))) {
       assertThat(repo.getRef(Constants.HEAD).getTarget().getName())
         .isEqualTo(expectedRef);
-    } finally {
-      repo.close();
     }
   }
 
@@ -251,4 +285,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..1b79bc3 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,77 @@
 
 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.ListBranchesRequest;
+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("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 +94,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 +137,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 ListBranchesRequest 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 ac90ac0..67d4d1f 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;
@@ -28,8 +27,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));
@@ -39,8 +37,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));
@@ -72,15 +69,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 c6cf647..7efefa7 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
@@ -50,16 +50,16 @@
     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();
 
     List<TagInfo> result =
@@ -86,16 +86,16 @@
     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 =
@@ -121,9 +121,9 @@
     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 =
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..721c712 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;
@@ -34,21 +41,21 @@
 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() {
@@ -64,7 +71,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 +87,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 +101,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 +124,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 +144,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 +158,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 +172,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 +196,228 @@
     }
   }
 
-  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 listChangeDrafts() 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");
+
+
+    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);
   }
 
-  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 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 {
+    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));
+    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 +440,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..27f8fc8
--- /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().getRef("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().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 {
+    String ref = "refs/heads/master";
+    // Detach head so we're allowed to delete ref.
+    testRepo.reset(testRepo.getRepository().getRef(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..9a88f56 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,15 @@
 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.GitUtil;
 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;
@@ -32,158 +31,243 @@
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 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.api.ResetCommand.ResetType;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 import java.io.IOException;
 import java.util.List;
 
 public class GetRelatedIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config byGroup() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "getRelatedByAncestors", false);
+    return cfg;
+  }
+
+  @ConfigSuite.Config
+  public static Config byAncestors() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "getRelatedByAncestors", true);
+    return cfg;
+  }
+
   @Inject
   private ChangeEditUtil editUtil;
 
   @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);
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_2 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    String id2 = getChangeId(c2_2);
+    pushHead(testRepo, "refs/for/master", false);
 
-    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 (RevCommit c : ImmutableList.of(c2_2, c1_1)) {
+      assertRelated(getPatchSetId(c),
+          changeAndCommit(id2, c2_2, 1, 1),
+          changeAndCommit(id1, c1_1, 1, 1));
     }
   }
 
   @Test
   public void getRelatedReorder() throws Exception {
     // 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();
+    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);
+    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(id1, c1_2, 2, 2),
+          changeAndCommit(id2, c2_2, 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(id2, c2_1, 1, 2),
+          changeAndCommit(id1, c1_1, 1, 2));
     }
   }
 
   @Test
   public void getRelatedReorderAndExtend() throws Exception {
     // 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);
+    ObjectId initial = repo().getRef("HEAD").getObjectId();
+    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);
+    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);
+    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();
+    String id3 = getChangeId(c3_1);
+    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(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(ps3_1, ps2_2, ps1_2)) {
+      assertRelated(ps,
+          changeAndCommit(id3, c3_1, 1, 1),
+          changeAndCommit(id1, c1_2, 2, 2),
+          changeAndCommit(id2, c2_2, 2, 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());
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(id3, c3_1, 1, 1),
+          changeAndCommit(id2, c2_1, 1, 2),
+          changeAndCommit(id1, c1_1, 1, 2));
     }
   }
 
   @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);
+    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);
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "3")
+        .message("subject: 3")
+        .create();
+    String id3 = getChangeId(c3_1);
+    pushHead(testRepo, "refs/for/master", false);
 
-    Change ch2 = getChange(c2).change();
+    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(id3, c3_1, 1, 1),
+          changeAndCommit(id2, c2_1, 1, 1),
+          changeAndCommit(id1, c1_1, 1, 1));
+    }
+
+    assertRelated(ps2_edit,
+        changeAndCommit(id3, c3_1, 1, 1),
+        changeAndCommit(id2, editRev, 0, 1),
+        changeAndCommit(id1, c1_1, 1, 1));
+  }
+
+  @Test
+  public void pushNewPatchSetWhenParentHasNullGroup() throws Exception {
+    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);
+    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(id2, c2_1, 1, 1),
+          changeAndCommit(id1, c1_1, 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(id2, c2_2, 2, 2),
+        changeAndCommit(id1, c1_1, 1, 1));
   }
 
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
@@ -198,7 +282,11 @@
         RelatedInfo.class).changes;
   }
 
-  private PatchSet.Id getPatchSetId(Commit c) throws OrmException {
+  private String getChangeId(RevCommit c) throws Exception {
+    return GitUtil.getChangeId(testRepo, c).get();
+  }
+
+  private PatchSet.Id getPatchSetId(ObjectId c) throws OrmException {
     return getChange(c).change().currentPatchSetId();
   }
 
@@ -206,8 +294,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(String changeId,
+      ObjectId commitId, int revisionNum, int currentRevisionNum) {
+    ChangeAndCommit result = new ChangeAndCommit();
+    result.changeId = changeId;
+    result.commit = new CommitInfo();
+    result.commit.commit = commitId.name();
+    result._revisionNumber = revisionNum;
+    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).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.changeId).named("Change-Id of " + name)
+          .isEqualTo(e.changeId);
+      assertThat(a.commit.commit).named("commit of " + name)
+          .isEqualTo(e.commit.commit);
+      // Don't bother checking _changeNumber; assume changeId is sufficient.
+      assertThat(a._revisionNumber).named("revision of " + name)
+          .isEqualTo(e._revisionNumber);
+      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..b80d6d9 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,14 +15,10 @@
 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.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
@@ -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..6ac34ce
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -0,0 +1,194 @@
+// 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.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.ProjectConfig;
+
+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;
+import java.util.Arrays;
+import java.util.List;
+
+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
+  public void testCherryPickWithoutAncestors() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getProject().setSubmitType(SubmitType.CHERRY_PICK);
+    saveProjectConfig(project, cfg);
+
+    // 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);
+  }
+
+  private void assertSubmittedTogether(String chId, String... expected)
+      throws Exception {
+    List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
+    assertThat(actual).hasSize(expected.length);
+    assertThat(Arrays.asList(expected))
+        .containsExactlyElementsIn(
+            Iterables.transform(actual, new Function<ChangeInfo, String>() {
+              @Override
+              public String apply(ChangeInfo input) {
+                return input.changeId;
+              }
+            })).inOrder();
+  }
+
+  private RevCommit getRemoteHead() throws IOException {
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      return rw.parseCommit(repo.getRef("refs/heads/master").getObjectId());
+    }
+  }
+
+  private String getChangeId(RevCommit c) throws Exception {
+    return GitUtil.getChangeId(testRepo, c).get();
+  }
+}
\ No newline at end of file
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..2ec6f7a 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,13 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
+FLAKY_TEST_CASES=['ProjectWatchIT.java']
+
 acceptance_tests(
-  srcs = glob(['*IT.java']),
+  srcs = glob(['*IT.java'], excludes=FLAKY_TEST_CASES),
   labels = ['server'],
 )
+
+acceptance_tests(
+  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 cd76c12..da83157 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
@@ -28,7 +28,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;
@@ -39,29 +38,29 @@
 @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"));
 
   @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/*");
-    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();
@@ -69,12 +68,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();
@@ -82,12 +81,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();
@@ -95,12 +94,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();
@@ -111,9 +110,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();
@@ -121,18 +120,8 @@
   }
 
   private void saveLabelConfig() throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    cfg.getLabelSections().put(Q.getName(), Q);
-    saveProjectConfig(cfg);
-  }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
-    projectCache.evict(allProjects);
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().put(label.getName(), label);
+    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..5d31c77 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;
@@ -28,6 +27,7 @@
 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.server.project.Util;
 import com.google.gerrit.testutil.ConfigSuite;
 
 import org.eclipse.jgit.lib.Config;
@@ -46,15 +46,15 @@
 
   @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 +143,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 +162,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 +180,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 +207,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 +232,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 +262,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 +288,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 +330,9 @@
   private void merge(PushOneCommit.Result r) throws Exception {
     revision(r).review(ReviewInput.approve());
     revision(r).submit();
-    Repository repo = repoManager.openRepository(project);
-    try {
+    try (Repository repo = repoManager.openRepository(project)) {
       assertThat(repo.getRef("refs/heads/master").getObjectId()).isEqualTo(
           r.getCommitId());
-    } finally {
-      repo.close();
     }
   }
 
@@ -356,6 +353,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..d067b34 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
@@ -2,5 +2,6 @@
 
 acceptance_tests(
   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-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..2335b8d1 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,33 @@
     }
   }
 
+  /**
+   * 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 {
+      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-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index c45d9f9..9a30696 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,6 +24,7 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Set;
 
@@ -53,7 +53,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 +71,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 +86,10 @@
     }
   }
 
+  public static void loadJARs(Path... jars) {
+    loadJARs(Arrays.asList(jars));
+  }
+
   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..c80d867 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
@@ -33,6 +33,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 = "/";
@@ -128,6 +129,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 +140,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..0ca0207 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
@@ -30,10 +30,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);
 
@@ -51,8 +47,4 @@
   @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..81bca20 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
@@ -18,7 +18,6 @@
 import com.google.gerrit.common.auth.SignInRequired;
 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,11 +35,6 @@
 
   @Audit
   @SignInRequired
-  void changePreferences(AccountGeneralPreferences pref,
-      AsyncCallback<VoidResult> gerritCallback);
-
-  @Audit
-  @SignInRequired
   void changeDiffPreferences(AccountDiffPreference diffPref,
       AsyncCallback<VoidResult> 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/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/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index 430c23c..db94b95 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
@@ -26,10 +26,10 @@
   public Account account;
   public AccountDiffPreference accountDiffPref;
   public String xGerritAuth;
-  public GerritConfig config;
   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/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 046df1d..7bab43a 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
@@ -57,6 +57,8 @@
   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,
@@ -65,7 +67,8 @@
       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() {
@@ -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/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-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/pom.xml b/gerrit-extension-api/pom.xml
index 4b64557..a2476eb 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.3</version>
+  <version>2.12-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..a4abfe6 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
@@ -23,6 +23,7 @@
 
   void starChange(String id) throws RestApiException;
   void unstarChange(String id) throws RestApiException;
+  void addEmail(EmailInput input) throws RestApiException;
 
   /**
    * A default implementation which allows source compatibility
@@ -43,5 +44,10 @@
     public void unstarChange(String id) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void addEmail(EmailInput input) 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/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/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index dd2ce92..873c560 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;
@@ -61,7 +61,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 {
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..ab09e5f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -0,0 +1,167 @@
+// 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;
+
+    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 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;
+    }
+  }
+}
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..b973806
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
@@ -0,0 +1,29 @@
+// 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 {
+  public String ref;
+  public String revision;
+  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..102b1ce 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;
+
+  ListBranchesRequest branches();
+
+  public abstract class ListBranchesRequest {
+    private int limit;
+    private int start;
+    private String substring;
+    private String regex;
+
+    public abstract List<BranchInfo> get() throws RestApiException;
+
+    public ListBranchesRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public ListBranchesRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public ListBranchesRequest withSubstring(String substring) {
+      this.substring = substring;
+      return this;
+    }
+
+    public ListBranchesRequest 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,10 @@
    * 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;
 
   /**
    * A default implementation which allows source compatibility
@@ -53,12 +111,43 @@
     }
 
     @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 ListBranchesRequest branches() {
+      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();
     }
   }
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-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/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 54617a7..c1edd6a 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,9 +46,6 @@
   /** Set the reviewed boolean for the caller. */
   REVIEWED(11),
 
-  /** Include draft comments for the caller. */
-  DRAFT_COMMENTS(12),
-
   /** Include download commands for the caller. */
   DOWNLOAD_COMMANDS(13),
 
@@ -58,7 +56,10 @@
   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);
 
   private final int value;
 
@@ -87,7 +88,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/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/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-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..bc0fa6d 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,5 @@
   public CommitInfo commit;
   public Map<String, FileInfo> files;
   public Map<String, ActionInfo> actions;
+  public String commitWithFooters;
 }
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-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..c99f233 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.
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..92fefed 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
@@ -196,8 +196,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();
     }
   }
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index 8e531c1..189445a 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -74,7 +74,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..f48a663 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,14 @@
 
   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();
+    try (InputStream in = r.getContents(logger)) {
+      final byte[] buf = new byte[2048];
+      int n;
+      while ((n = in.read(buf)) >= 0) {
+        tmp.write(buf, 0, n);
       }
+      tmp.close();
     } catch (IOException e) {
       final UnableToCompleteException ute = new UnableToCompleteException();
       ute.initCause(e);
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..4b78991 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());
   }
 
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..9c9cf06 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("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..3977487 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,7 +15,8 @@
   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'],
 )
 
@@ -20,7 +30,8 @@
   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'],
 )
 
@@ -52,3 +63,16 @@
   name = 'diffy_image_files',
   resources = DIFFY,
 )
+
+java_test(
+  name = 'client_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':client',
+    '//lib:junit',
+    '//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..37e63af
--- /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 final static 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..0db7ea4 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,90 @@
 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();
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
new file mode 100644
index 0000000..60c7641
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountInfo.java
@@ -0,0 +1,108 @@
+// 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.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 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; }-*/;
+
+  public final Timestamp contactFiledOn() {
+    if (contactFiledOnRaw() != null) {
+      Timestamp ts = _getContactFiledOn();
+      if (ts == null) {
+        ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(contactFiledOnRaw());
+        _setContactFiledOn(ts);
+      }
+      return ts;
+    }
+    return null;
+  }
+
+  private final native String contactFiledOnRaw() /*-{ return this.contact_filed_on; }-*/;
+  private final native Timestamp _getContactFiledOn() /*-{ return this._cts; }-*/;
+  private final native void _setContactFiledOn(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 hasAvatarInfo()
+  /*-{ return this.hasOwnProperty('avatars') }-*/;
+
+  public final AvatarInfo avatar(int sz) {
+    JsArray<AvatarInfo> a = avatars();
+    for (int i = 0; a != null && i < a.length(); i++) {
+      AvatarInfo r = a.get(i);
+      if (r.height() == sz) {
+        return r;
+      }
+    }
+    return null;
+  }
+
+  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,
+        'username': username};
+  }-*/;
+
+  protected AccountInfo() {
+  }
+
+  public static class AvatarInfo extends JavaScriptObject {
+    public static final int DEFAULT_SIZE = 26;
+    public final native String url() /*-{ return this.url }-*/;
+    public final native int height() /*-{ return this.height || 0 }-*/;
+    public final native int width() /*-{ return this.width || 0 }-*/;
+
+    protected AvatarInfo() {
+    }
+  }
+}
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 78%
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..4482fd0 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,9 +12,8 @@
 // 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.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
@@ -27,32 +26,38 @@
 
 import java.util.List;
 
-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 }-*/;
@@ -183,7 +188,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 +197,6 @@
   final native void initMy() /*-{ this.my = []; }-*/;
   final native void addMy(TopMenuItem m) /*-{ this.my.push(m); }-*/;
 
-  protected Preferences() {
+  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 69%
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 fff792f..695c126 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,19 +296,18 @@
     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 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 static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
@@ -273,7 +319,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;
         }
       });
     }
@@ -282,8 +328,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);
@@ -323,7 +369,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() {
     }
@@ -357,7 +403,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() {
@@ -365,8 +411,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..660ad90
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/DownloadInfo.java
@@ -0,0 +1,122 @@
+// 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 Set<String> schemes() {
+    return Natives.keys(_schemes());
+  }
+
+  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 Set<String> commandNames() {
+      return Natives.keys(_commands());
+    }
+
+    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 Set<String> cloneCommandNames() {
+      return Natives.keys(_cloneCommands());
+    }
+
+    public final Set<DownloadCommandInfo> cloneCommands(String project) {
+      Set<DownloadCommandInfo> commands = new HashSet<>();
+      for (String commandName : cloneCommandNames()) {
+        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 87%
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..b21078e 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,9 +24,9 @@
 
 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; }-*/;
 
@@ -64,7 +63,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..f0f3b66
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.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.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 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/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
new file mode 100644
index 0000000..b487f2e
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.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.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 ContactStoreInfo contactStore() /*-{ return this.contact_store; }-*/;
+  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 hasContactStore() {
+    return contactStore() != null;
+  }
+
+  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 ContactStoreInfo extends JavaScriptObject {
+    public final native String url() /*-{ return this.url; }-*/;
+
+    protected ContactStoreInfo() {
+    }
+  }
+
+  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/Natives.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
index 564e95a..7087888 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,34 +91,6 @@
     return arr;
   }
 
-  @SuppressWarnings("unchecked")
-  public static <T extends JavaScriptObject> T parseJSON(String json) {
-    if (json.startsWith("\"")) {
-      return (T) NativeString.wrap(parseString(parser, json));
-    }
-    return Natives.<T> parseObject(parser, json); // javac generics bug
-  }
-
-  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();
-  }
-
   private Natives() {
   }
 }
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/BUCK b/gerrit-gwtui/BUCK
index 9b7ec49..90fdcab 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -1,6 +1,5 @@
 include_defs('//gerrit-gwtui/gwt.defs')
 include_defs('//tools/gwt-constants.defs')
-from multiprocessing import cpu_count
 
 DEPS = [
   '//gerrit-gwtexpui:CSS',
@@ -8,59 +7,8 @@
   '//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..9322a1d 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,14 @@
   }
 
   private void loadAvatar(AccountInfo account, int size, boolean addPopup) {
+    if (!Gerrit.info().plugin().hasAvatars()) {
+      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 +125,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..e7381f8 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,7 @@
 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_EXTENSION;
 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,7 +36,6 @@
 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;
@@ -50,6 +50,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;
@@ -64,6 +65,7 @@
 import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.admin.ProjectScreen;
 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 +95,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 +202,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 +244,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 +480,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,
@@ -695,7 +550,7 @@
         }
 
         if (matchExact(SETTINGS_AGREEMENTS, token)
-            && Gerrit.getConfig().isUseContributorAgreements()) {
+            && Gerrit.info().auth().useContributorAgreements()) {
           return new MyAgreementsScreen();
         }
 
@@ -707,16 +562,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 +696,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());
             }
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..80aa9cc 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,41 @@
 
 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 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 +69,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 +78,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 +103,8 @@
         acct.getPreferredEmail(),
         acct.getUsername());
   }
+
+  private static AccountFormatter createAccountFormatter() {
+    return new AccountFormatter(Gerrit.info().user().anonymousCowardName());
+  }
 }
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..fb85f62 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
@@ -20,17 +20,20 @@
 
 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.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 +44,10 @@
 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.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 +63,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,13 +104,18 @@
       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 String xGerritAuth;
@@ -266,9 +274,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 +290,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 +303,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,6 +313,11 @@
     return xGerritAuth;
   }
 
+  /** @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; null if no diff preferences defined for the account */
   public static AccountDiffPreference getAccountDiffPreference() {
     return myAccountDiffPref;
@@ -324,7 +329,7 @@
 
   /** @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 +388,9 @@
   }
 
   static void deleteSessionCookie() {
-    myAccount = null;
+    myAccount = AccountInfo.create(0, null, null, null);
     myAccountDiffPref = null;
+    myPrefs = AccountPreferencesInfo.createDefault();
     xGerritAuth = null;
     refreshMenuBar();
 
@@ -426,25 +432,65 @@
     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.xGerritAuth != null) {
+          xGerritAuth = result.xGerritAuth;
+          // 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.addFinal(new GerritCallback<AccountPreferencesInfo>() {
+            @Override
+            public void onSuccess(AccountPreferencesInfo prefs) {
+              myPrefs = prefs;
+              onModuleLoad2(result);
+            }
+          }));
+        } else {
+          myAccount = AccountInfo.create(0, null, null, null);
+          myPrefs = AccountPreferencesInfo.createDefault();
+          onModuleLoad2(result);
+        }
       }
-    });
+    }));
   }
 
   private static void initHostname() {
@@ -471,9 +517,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,7 +584,6 @@
     };
     gBody.add(body);
 
-    RpcStatus.INSTANCE = new RpcStatus();
     JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
     JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
@@ -560,7 +605,7 @@
 
     applyUserPreferences();
     populateBottomMenu(bottomMenu, hpd);
-    refreshMenuBar(false);
+    refreshMenuBar();
 
     History.addValueChangeHandler(new ValueChangeHandler<String>() {
       @Override
@@ -574,12 +619,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 +632,7 @@
             }
             display(token);
           }
-        }));
+        });
   }
 
   private void saveDefaultTheme() {
@@ -600,17 +642,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 +661,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 {
@@ -691,7 +742,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 +755,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 +800,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 +851,70 @@
     });
   }
 
-  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);
+  }
+
+  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 +1033,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/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 2844b5e..c6eb2de 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
@@ -98,6 +98,7 @@
   String errorDialogErrorType();
   String errorDialogGlass();
   String errorDialogTitle();
+  String extensionPanel();
   String loadingPluginsDialog();
   String fileColumnHeader();
   String fileCommentBorder();
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..20afa19 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
@@ -76,23 +76,29 @@
     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:");
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..adaee55
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UrlAliasMatcher.java
@@ -0,0 +1,43 @@
+// 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> globalUrlAliases;
+
+  UrlAliasMatcher(Map<String, String> globalUrlAliases) {
+    this.globalUrlAliases = new HashMap<>();
+    if (globalUrlAliases != null) {
+      for (Map.Entry<String, String> e : globalUrlAliases.entrySet()) {
+        this.globalUrlAliases.put(RegExp.compile(e.getKey()), e.getValue());
+      }
+    }
+  }
+
+  public String replace(String token) {
+    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 cfb7e92..00036a8 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..a796f94 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,6 +15,7 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
@@ -52,6 +53,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 +136,15 @@
     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() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
deleted file mode 100644
index 1127374..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.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.client.account;
-
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
-
-public class AccountInfo extends JavaScriptObject {
-  public final native int _account_id() /*-{ 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; }-*/;
-
-  /**
-   * @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()
-  /*-{ return this.hasOwnProperty('avatars') }-*/;
-
-  public final AvatarInfo avatar(int sz) {
-    JsArray<AvatarInfo> a = avatars();
-    for (int i = 0; a != null && i < a.length(); i++) {
-      AvatarInfo r = a.get(i);
-      if (r.height() == sz) {
-        return r;
-      }
-    }
-    return null;
-  }
-
-  private final native JsArray<AvatarInfo> avatars()
-  /*-{ return this.avatars }-*/;
-
-  public static native AccountInfo create(int id, String name,
-      String email, String username) /*-{
-    return {'_account_id': id, 'name': name, 'email': email,
-        'username': username};
-  }-*/;
-
-  protected AccountInfo() {
-  }
-
-  public static class AvatarInfo extends JavaScriptObject {
-    public static final int DEFAULT_SIZE = 26;
-    public final native String url() /*-{ return this.url }-*/;
-    public final native int height() /*-{ return this.height || 0 }-*/;
-    public final native int width() /*-{ return this.width || 0 }-*/;
-
-    protected AvatarInfo() {
-    }
-  }
-}
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
index 4b8f0e2..14bf346 100644
--- 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
@@ -15,8 +15,8 @@
 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.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;
@@ -65,7 +65,7 @@
     hasContact.setStyleName(Gerrit.RESOURCES.css().accountContactOnFile());
     hasContact.setVisible(false);
 
-    if (Gerrit.getConfig().isUseContactInfo()) {
+    if (Gerrit.info().hasContactStore()) {
       body.add(privhtml);
       body.add(hasContact);
       body.add(infoSecure);
@@ -88,18 +88,18 @@
   }
 
   @Override
-  protected void display(final Account userAccount) {
-    super.display(userAccount);
-    displayHasContact(userAccount);
+  protected void display(AccountInfo account) {
+    super.display(account);
+    displayHasContact(account);
     addressTxt.setText("");
     countryTxt.setText("");
     phoneTxt.setText("");
     faxTxt.setText("");
   }
 
-  private void displayHasContact(final Account userAccount) {
-    if (userAccount.isContactFiled()) {
-      final Timestamp dt = userAccount.getContactFiledOn();
+  private void displayHasContact(AccountInfo account) {
+    if (account.contactFiledOn() != null) {
+      Timestamp dt = account.contactFiledOn();
       hasContact.setText(Util.M.contactOnFile(new Date(dt.getTime())));
       hasContact.setVisible(true);
     } else {
@@ -108,15 +108,15 @@
   }
 
   @Override
-  void onSaveSuccess(final Account userAccount) {
-    super.onSaveSuccess(userAccount);
-    displayHasContact(userAccount);
+  void onSaveSuccess(AccountInfo account) {
+    super.onSaveSuccess(account);
+    displayHasContact(account);
   }
 
   @Override
   ContactInformation toContactInformation() {
     final ContactInformation info;
-    if (Gerrit.getConfig().isUseContactInfo()) {
+    if (Gerrit.info().hasContactStore()) {
       info = new ContactInformation();
       info.setAddress(addressTxt.getText());
       info.setCountry(countryTxt.getText());
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..2fd53bf 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,7 +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;
@@ -48,7 +49,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 +102,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 +111,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 +169,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 +197,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 +239,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 +276,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 +326,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);
@@ -361,9 +363,9 @@
     Util.ACCOUNT_SEC.updateContact(newName, newEmail, info,
         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,10 +380,10 @@
         });
   }
 
-  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);
   }
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..54541e4 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
@@ -68,12 +68,44 @@
   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 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 }-*/;
@@ -90,23 +122,6 @@
   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 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 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 +134,12 @@
   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(); }
 
-  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/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/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/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..ac140ff 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,70 @@
 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.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);
+    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 +86,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..aa72300 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
@@ -250,8 +250,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..66a64b4 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,7 @@
   String useContributorAgreements();
   String useSignedOffBy();
   String createNewChangeForAllNotInTarget();
+  String enableSignedPush();
   String requireChangeID();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
@@ -67,6 +68,7 @@
   String headingParentProjectName();
   String columnProjectName();
   String headingAgreements();
+  String headingAuditLog();
 
   String headingProjectSubmitType();
   String projectSubmitType_FAST_FORWARD_ONLY();
@@ -88,6 +90,13 @@
   String columnGroupNotifications();
   String columnGroupVisibleToAll();
 
+  String columnDate();
+  String columnType();
+  String columnByUser();
+
+  String typeAdded();
+  String typeRemoved();
+
   String columnBranchName();
   String columnBranchRevision();
   String initialRevision();
@@ -103,6 +112,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..952ea5f 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,11 @@
 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
 requireChangeID = Require <code>Change-Id</code> in commit message
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
@@ -46,6 +47,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 +69,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
@@ -82,6 +91,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..134f869 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
@@ -22,7 +22,6 @@
 import com.google.gerrit.client.ui.Hyperlink;
 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 +43,7 @@
 
   public GroupListScreen() {
     setRequiresSignIn(true);
-    configurePageSize();
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
   public GroupListScreen(String params) {
@@ -65,17 +64,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();
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/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..13b3a54 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,7 +16,7 @@
 
 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;
@@ -121,7 +121,7 @@
       inheritsFrom.getStyle().setDisplay(Display.NONE);
     }
 
-    final GitwebLink c = Gerrit.getGitwebLink();
+    GitwebInfo c = Gerrit.info().gitweb();
     if (value.isConfigVisible() && c != null) {
       history.getStyle().setDisplay(Display.BLOCK);
       gitweb.setText(c.getLinkName());
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..0db4779 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;
   }
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 c5f7225..9a13158 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;
@@ -38,7 +38,6 @@
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.OnEditEnabler;
 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;
@@ -92,18 +91,7 @@
 
   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;
-    }
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
   private void parseToken() {
@@ -444,8 +432,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();
@@ -456,7 +445,7 @@
     }
 
     void populate(int row, BranchInfo k) {
-      final GitwebLink c = Gerrit.getGitwebLink();
+      GitwebInfo c = Gerrit.info().gitweb();
 
       if (k.canDelete()) {
         CheckBox sel = new CheckBox();
@@ -484,8 +473,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..cb87fc0 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;
@@ -66,6 +69,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 
 public class ProjectInfoScreen extends ProjectScreen {
   private boolean isOwner;
@@ -80,6 +84,7 @@
   private ListBox state;
   private ListBox contentMerge;
   private ListBox newChangeForAllNotInTarget;
+  private ListBox enableSignedPush;
   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,12 @@
     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);
+    }
+
     maxObjectSizeLimit = new NpTextBox();
     saveEnabler.listenTo(maxObjectSizeLimit);
     effectiveMaxObjectSizeLimit = new Label();
@@ -265,7 +291,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);
     }
@@ -306,12 +332,12 @@
       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 +349,7 @@
         box.removeItem(inheritedIndex);
       } else {
         box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
-            + inheritedBoolean.inherited_value() + ")");
+            + inheritedBoolean.inheritedValue() + ")");
       }
     }
   }
@@ -342,20 +368,23 @@
 
   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 (enableSignedPush != null) {
+      setBool(enableSignedPush, result.enableSignedPush());
+    }
+    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 +645,12 @@
   private void doSave() {
     enableForm(false);
     saveProject.setEnabled(false);
+    InheritableBoolean sp = enableSignedPush != null
+        ? getBool(enableSignedPush) : null;
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
+        sp,
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
@@ -672,33 +704,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 Set<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..f9904cd 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,8 +18,8 @@
 
 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;
@@ -30,7 +30,6 @@
 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;
@@ -57,10 +56,11 @@
   private Query query;
 
   public ProjectListScreen() {
-    configurePageSize();
+    pageSize = Gerrit.getUserPreferences().changesPerPage();
   }
 
   public ProjectListScreen(String params) {
+    this();
     for (String kvPair : params.split("[,;&]")) {
       String[] kv = kvPair.split("=", 2);
       if (kv.length != 2 || kv[0].isEmpty()) {
@@ -75,18 +75,6 @@
         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;
-    }
   }
 
   @Override
@@ -185,16 +173,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/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 8b29f86..b9ea8e0 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,25 @@
 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.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.projects.ConfigInfoCache;
 import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -85,6 +87,8 @@
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 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 +100,7 @@
 
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 
@@ -113,6 +118,7 @@
     String label_need();
     String replyBox();
     String selected();
+    String highlight();
     String hashtagName();
   }
 
@@ -131,11 +137,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 +150,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;
@@ -166,6 +174,7 @@
   @UiField Topic topic;
   @UiField Element actionText;
   @UiField Element actionDate;
+  @UiField SimplePanel changeExtension;
 
   @UiField Actions actions;
   @UiField Labels labels;
@@ -223,6 +232,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,15 +261,35 @@
           @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);
+  }
+
   void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
     RestApi call = ChangeApi.detail(changeId.get());
     ChangeList.addOptions(call, EnumSet.of(
-      ListChangesOption.CURRENT_ACTIONS,
+      ListChangesOption.CHANGE_ACTIONS,
       ListChangesOption.ALL_REVISIONS));
     if (!fg) {
       call.background();
@@ -256,6 +297,18 @@
     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 +334,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 +365,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 +377,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 +385,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 +396,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 +420,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 +443,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 +464,7 @@
     branchLink.setTargetHistoryToken(
         PageLinks.toChangeQuery(
             BranchLink.query(
-                info.project_name_key(),
+                info.projectNameKey(),
                 info.status(),
                 info.branch(),
                 null)));
@@ -505,7 +494,7 @@
         reviewMode.setVisible(false);
       }
 
-      if (rev.is_edit()) {
+      if (rev.isEdit()) {
         if (info.hasEditBasedOnCurrentPatchSet()) {
           publishEdit.setVisible(true);
         } else {
@@ -517,11 +506,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 +555,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 +822,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 +842,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 +860,7 @@
 
     CallbackGroup group = new CallbackGroup();
     Timestamp lastReply = myLastReply(info);
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       loadFileList(b, rev, lastReply, group, null, null);
     } else {
       loadDiff(b, rev, lastReply, group);
@@ -798,7 +874,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) {
@@ -806,16 +882,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();
         }
       }
@@ -872,16 +949,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
@@ -891,6 +971,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);
@@ -914,7 +1011,7 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       return;
     }
 
@@ -922,7 +1019,7 @@
         group.add(new AsyncCallback<CommitInfo>() {
           @Override
           public void onSuccess(CommitInfo info) {
-            rev.set_commit(info);
+            rev.setCommit(info);
           }
 
           @Override
@@ -933,7 +1030,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());
       }
@@ -963,7 +1059,7 @@
 
   private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
     RevisionInfo rev = resolveRevisionOrPatchSetId(info, revision,
-        info.current_revision());
+        info.currentRevision());
     if (rev != null) {
       revision = rev.name();
       return rev;
@@ -980,7 +1076,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");
     }
   }
@@ -1018,7 +1114,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()) {
@@ -1046,19 +1142,7 @@
   private void renderChangeInfo(ChangeInfo info) {
     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);
@@ -1067,7 +1151,6 @@
     initReplyButton(info, revision);
     initIncludedInAction(info);
     initChangeAction(info);
-    initRevisionsAction(info, revision);
     initDownloadAction(info, revision);
     initProjectLinks(info);
     initBranchLink(info);
@@ -1076,7 +1159,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);
@@ -1087,23 +1170,46 @@
       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())
+        || info.revision(revision).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);
+
+    RevisionInfo revisionInfo = info.revision(revision);
+    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);
 
@@ -1112,23 +1218,22 @@
       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();
+        : Gerrit.info().user().anonymousCowardName();
 
     if (info.owner().avatar(AvatarInfo.DEFAULT_SIZE) != null) {
       ownerPanel.insert(new AvatarImage(info.owner()), 0);
@@ -1142,7 +1247,7 @@
         ? 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 renderSubmitType(String action) {
@@ -1230,7 +1335,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..06ae4ca 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;
@@ -322,6 +321,19 @@
       height: 16px !important;
       vertical-align: bottom;
     }
+
+    .headerExtension {
+      display: inline-block;
+      float: right;
+    }
+
+    .headerExtension>div>div {
+      float: left;
+    }
+
+    .changeExtension {
+      padding-top: 5px;
+    }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.cs2}'>
@@ -333,6 +345,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 +383,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 +398,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'/>
@@ -472,6 +487,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..1e02ee6 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,15 +17,15 @@
 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;
@@ -80,7 +80,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 +93,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));
@@ -164,7 +164,7 @@
   }
 
   private void insertArchive() {
-    List<String> activated = Gerrit.getConfig().getArchiveFormats();
+    List<String> activated = Gerrit.info().download().archives();
     if (activated.isEmpty()) {
       return;
     }
@@ -237,24 +237,21 @@
   }
 
   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;
-        }
+    DownloadScheme pref = Gerrit.getUserPreferences().downloadScheme();
+    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;
@@ -262,13 +259,12 @@
 
   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);
+    AccountPreferencesInfo prefs = Gerrit.getUserPreferences();
+    if (Gerrit.isSignedIn() && scheme != null
+        && scheme != prefs.downloadScheme()) {
+      prefs.downloadScheme(scheme);
+      AccountPreferencesInfo in = AccountPreferencesInfo.create();
+      in.downloadScheme(scheme);
       AccountApi.self().view("preferences")
           .put(in, new AsyncCallback<JavaScriptObject>() {
             @Override
@@ -297,21 +293,4 @@
     }
     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..ad61446 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
@@ -22,7 +22,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 +87,7 @@
     String deltaColumn2();
     String inserted();
     String deleted();
-    String removeButton();
+    String restoreDelete();
   }
 
   public static enum Mode {
@@ -471,8 +471,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());
     }
 
@@ -516,8 +516,8 @@
       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();
+          inserted += info.linesInserted();
+          deleted += info.linesDeleted();
         }
       }
     }
@@ -548,7 +548,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 +592,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 +648,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 +662,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,16 +738,16 @@
       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());
           }
         }
       }
@@ -753,24 +758,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,7 +791,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.openTd().closeTd(); // path
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..9bb3a76c 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);
 
@@ -156,7 +152,6 @@
   void autoOpen() {
     if (commentList == null) {
       autoOpen = true;
-      history.load(info._revisionNumber());
     } else if (!commentList.isEmpty()) {
       setOpen(true);
     }
@@ -209,7 +204,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..ae1a608 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,21 +208,31 @@
 
     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())) {
+    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));
+    } else {
+      // TODO(sbeller): show only on latest revision
+      if (info.status().isOpen()) {
+        ChangeApi.change(info.legacyId().get()).view("submitted_together")
+            .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+                info.project(), revision));
+      }
     }
   }
 
@@ -211,7 +241,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 +349,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 +382,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..525f5a9 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,7 +281,11 @@
       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);
@@ -295,20 +306,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 +337,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..626e7c0 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,19 +140,19 @@
   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
@@ -180,8 +180,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 +291,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 +328,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 +336,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 +352,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 +373,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 +382,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 18e5e87..3937ade 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);
   }
 
@@ -198,22 +198,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());
       }
     }
 
@@ -227,8 +227,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);
@@ -241,13 +241,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..f0101cb 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;
 }
@@ -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..ca4c633 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,5 @@
   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();
 }
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..e348161 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,4 @@
 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:
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..2d3644e 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
@@ -60,16 +60,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..c109794 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
@@ -43,14 +43,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..6e65ccd 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 t) /*-{ this.t = t; }-*/;
+
+    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.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..800da91 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,6 +15,7 @@
 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;
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..99ecc9b 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;
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.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
index 62d2eac..e011091 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;
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 4f63425..6ff93e4 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
@@ -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;
@@ -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) {
@@ -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..a344e6b 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
@@ -45,7 +45,8 @@
 
     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/DownloadPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
index 350dbed..ea5df4c 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,9 +15,8 @@
 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;
@@ -25,41 +24,23 @@
 import java.util.Set;
 
 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 Set<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..55856a5 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,91 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+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.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.AuthType;
 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;
+  private enum KnownScheme {
+    ANON_GIT(DownloadScheme.ANON_GIT, "git", Util.M.anonymousDownload("Git")),
+    ANON_HTTP(DownloadScheme.ANON_HTTP, "anonymous http", Util.M.anonymousDownload("HTTP")),
+    SSH(DownloadScheme.SSH, "ssh", "SSH"),
+    HTTP(DownloadScheme.HTTP, "http", "HTTP");
 
-    protected DownloadRefUrlLink(DownloadScheme urlType,
-        String text, String project, String ref) {
-      super(urlType, text);
-      this.projectName = project;
-      this.ref = ref;
+    public final DownloadScheme downloadScheme;
+    public final String name;
+    public final String text;
+
+    private KnownScheme(DownloadScheme downloadScheme, String name, String text) {
+      this.downloadScheme = downloadScheme;
+      this.name = name;
+      this.text = text;
     }
 
-    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();
+    static KnownScheme get(String name) {
+      for (KnownScheme s : values()) {
+        if (s.name.equals(name)) {
+          return s;
         }
-        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();
+      return null;
     }
   }
 
-  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();
+    for (String s : Gerrit.info().download().schemes()) {
+      DownloadSchemeInfo scheme = Gerrit.info().download().scheme(s);
+      if (scheme.isAuthRequired() && !allowAnonymous) {
+        continue;
+      }
 
-    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));
+      KnownScheme knownScheme = KnownScheme.get(s);
+      if (knownScheme != null) {
+        urls.add(new DownloadUrlLink(downloadPanel, scheme,
+            knownScheme.downloadScheme, knownScheme.text));
+      } else {
+        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 DownloadScheme scheme;
+
+  public DownloadUrlLink(DownloadPanel downloadPanel,
+      DownloadSchemeInfo schemeInfo, String text) {
+    this(downloadPanel, schemeInfo, null, text);
   }
 
-  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) {
+  public DownloadUrlLink(DownloadPanel downloadPanel,
+      DownloadSchemeInfo schemeInfo, DownloadScheme urlType, String text) {
     super(text);
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
     addClickHandler(this);
 
-    if (!hostPageUrl.endsWith("/")) {
-      hostPageUrl += "/";
-    }
+    this.downloadPanel = downloadPanel;
+    this.schemeInfo = schemeInfo;
+    this.scheme = urlType;
   }
 
-  public String getUrlData() {
-    return urlData;
+  public DownloadScheme getUrlType() {
+    return scheme;
   }
 
   @Override
@@ -232,33 +109,34 @@
 
     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() && scheme != null
+        && scheme != prefs.downloadScheme()) {
+      prefs.downloadScheme(scheme);
+      AccountPreferencesInfo in = AccountPreferencesInfo.create();
+      in.downloadScheme(scheme);
+      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..f4a7e8a 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
@@ -23,10 +23,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());
   }
@@ -44,7 +42,7 @@
         if (first == null) {
           first = d;
         }
-        if (d.urlType == urlType) {
+        if (d.getUrlType() == urlType) {
           d.select();
           return;
         }
@@ -58,10 +56,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/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/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index dd36657..e5a5b53 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
@@ -25,11 +25,11 @@
 import com.google.gerrit.client.account.DiffPreferences;
 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;
@@ -185,7 +185,7 @@
         .get(group1.add(new AsyncCallback<DiffInfo>() {
           @Override
           public void onSuccess(DiffInfo diffInfo) {
-            diffLinks = diffInfo.web_links();
+            diffLinks = diffInfo.webLinks();
           }
 
           @Override
@@ -239,7 +239,9 @@
     super.registerKeys();
     cm.addKeyMap(KeyMap.create()
         .on("Ctrl-L", gotoLine())
-        .on("Cmd-L", gotoLine()));
+        .on("Cmd-L", gotoLine())
+        .on("Cmd-S", save())
+        .on("Ctrl-S", save()));
   }
 
   private Runnable gotoLine() {
@@ -367,9 +369,6 @@
         .set("keyMap", "default")
         .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 +376,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..a82dc49 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,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'>
     @external .CodeMirror, .CodeMirror-cursor;
 
     .header {
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..c26c437 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
    */
@@ -929,7 +946,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 **/
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..2420e7a 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;
@@ -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..8491c61 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;
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..f3bc092 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
@@ -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/UnifiedPatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
index a5c1484..bfa308a 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;
@@ -251,7 +251,7 @@
   }
 
   private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
-    return diffInfo.unified_web_links();
+    return diffInfo.unifiedWebLinks();
   }
 
   private String getSideBySideDiffUrl() {
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..b94f5d5 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,23 @@
 
 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.gerrit.reviewdb.client.RefNames;
 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();
+    return RefNames.shortName(ref());
   }
 
   public final native String ref() /*-{ return this.ref; }-*/;
   public final native String revision() /*-{ return this.revision; }-*/;
   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..3f3875a 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,26 @@
   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 SubmitType submitType() {
+    return SubmitType.valueOf(submitTypeRaw());
   }
 
   public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
@@ -63,7 +66,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 +78,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 +134,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 +155,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 +182,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 +192,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..53eba42 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
@@ -84,7 +84,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 +99,9 @@
       InheritableBoolean useContributorAgreements,
       InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
       InheritableBoolean createNewChangeForAllNotInTarget,
-      InheritableBoolean requireChangeId, String maxObjectSizeLimit,
+      InheritableBoolean requireChangeId,
+      InheritableBoolean enableSignedPush,
+      String maxObjectSizeLimit,
       SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
       AsyncCallback<ConfigInfo> cb) {
@@ -110,6 +112,9 @@
     in.setUseSignedOffBy(useSignedOffBy);
     in.setRequireChangeId(requireChangeId);
     in.setCreateNewChangeForAllNotInTarget(createNewChangeForAllNotInTarget);
+    if (enableSignedPush != null) {
+      in.setEnableSignedPush(enableSignedPush);
+    }
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -230,6 +235,12 @@
     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 native void setMaxObjectSizeLimit(String l)
     /*-{ if(l)this.max_object_size_limit=l; }-*/;
 
@@ -317,6 +328,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/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/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/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..782500d 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(),
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..8170c2b 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();
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/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..1eb88b1 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,8 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.io.ByteStreams;
+
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -21,14 +23,14 @@
 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.charset.StandardCharsets;
+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 +38,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 +50,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 = StandardCharsets.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 +72,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 +113,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 +131,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 +174,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/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 8c469a9..5d896df 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
@@ -26,6 +26,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 +169,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);
@@ -186,7 +191,7 @@
     return MoreObjects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
   }
 
-  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..33b9fed 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
@@ -260,8 +260,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/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/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 949f392..9ebf406 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;
@@ -109,13 +109,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..ccc945f 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
@@ -104,12 +104,8 @@
       rsp.setContentType("text/html");
       rsp.setCharacterEncoding("UTF-8");
       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 f58a719..5388048 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,11 +92,8 @@
     res.setContentType("text/html");
     res.setCharacterEncoding("UTF-8");
     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 81%
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..75e468e 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,18 @@
 
 package com.google.gerrit.httpd.gitweb;
 
-import com.google.gerrit.httpd.GitWebConfig;
+import static com.google.gerrit.common.FileUtil.lastModified;
+
 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 +34,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,10 +44,10 @@
   }
 
   @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());
     }
   }
 
@@ -55,14 +57,14 @@
   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();
+        modified = lastModified(src);
         raw_css = raw.getBytes(ENC);
         gz_css = HtmlDomUtil.compress(raw_css);
       } else {
@@ -99,11 +101,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 74%
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..e73cb11 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);
+    serve("/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 85%
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..e5af965 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,29 @@
 
 package com.google.gerrit.httpd.gitweb;
 
+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 +60,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 +67,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 +82,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 +99,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 +134,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 +152,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 +185,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 +203,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 +237,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 +251,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 +269,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>,
@@ -294,15 +305,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 +337,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 +414,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 +458,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 +470,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 +529,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());
@@ -569,7 +563,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 +628,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"))) {
+          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 64b754d..0ac2c4f 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,6 +14,7 @@
 
 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;
 
@@ -24,6 +25,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 +57,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 +305,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 +411,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) {
@@ -611,12 +613,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,36 +626,23 @@
   }
 
   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);
     }
   }
 
   private static byte[] readWholeEntry(PluginContentScanner scanner, PluginEntry entry)
       throws IOException {
     byte[] data = new byte[entry.getSize().get().intValue()];
-    InputStream in = scanner.getInputStream(entry);
-    try {
+    try (InputStream in = scanner.getInputStream(entry)) {
       IO.readFully(in, data, 0, data.length);
-    } finally {
-      in.close();
     }
     return data;
   }
@@ -674,9 +663,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..65f42e5 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
@@ -189,65 +189,56 @@
       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;
-    }
+    ObjectLoader blobLoader;
+    RevCommit fromCommit;
+    String suffix;
+    String path = patchKey.getFileName();
+    try (Repository repo = repoManager.openRepository(project.getNameKey())) {
+      try (ObjectReader reader = repo.newObjectReader();
+          RevWalk rw = new RevWalk(reader)) {
+        RevCommit c;
 
-    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";
 
-      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 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);
+        try (TreeWalk tw = TreeWalk.forPath(reader, path, fromCommit.getTree())) {
+          if (tw == null) {
+            rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+          }
 
-        } else {
-          rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-          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) {
+    } catch (RepositoryNotFoundException e) {
+      getServletContext().log("Cannot open repository", e);
+      rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      return;
+    } catch (IOException | RuntimeException 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 =
@@ -258,7 +249,6 @@
     CacheHeaders.setNotCacheable(rsp);
 
     OutputStream out;
-    @SuppressWarnings("resource")
     ZipOutputStream zo;
 
     final MimeType contentType = registry.getMimeType(path, raw);
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..e499109 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,13 +14,14 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
+
 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.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -30,6 +31,7 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+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;
@@ -47,14 +49,15 @@
 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;
@@ -68,12 +71,11 @@
 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;
@@ -84,21 +86,24 @@
   private final boolean refreshHeaderFooter;
   private final StaticServlet staticServlet;
   private final boolean isNoteDbEnabled;
+  private final Integer pluginsLoadTimeout;
   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,
+      StaticServlet ss,
+      NotesMigration migration)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
-    config = gc;
     plugins = webUiPlugins;
     messages = motd;
     signedOutTheme = themeFactory.getSignedOutTheme();
@@ -107,6 +112,7 @@
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
     staticServlet = ss;
     isNoteDbEnabled = migration.enabled();
+    pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
 
     final String pageName = "HostPage.html";
     template = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -122,54 +128,49 @@
     }
 
     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);
+        final 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 static int getPluginsLoadTimeout(final 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(final Object data, final 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;
   }
@@ -181,10 +182,6 @@
     final StringWriter w = new StringWriter();
     final 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(";");
@@ -216,13 +213,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();
     }
   }
 
@@ -288,16 +282,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);
     }
   }
 
@@ -317,8 +311,8 @@
 
       final HostPageData pageData = new HostPageData();
       pageData.version = Version.getVersion();
-      pageData.config = config;
       pageData.isNoteDbEnabled = isNoteDbEnabled;
+      pageData.pluginsLoadTimeout = pluginsLoadTimeout;
 
       final StringWriter w = new StringWriter();
       w.write("var " + HPD_ID + "=");
@@ -364,8 +358,8 @@
       }
     }
 
-    private FileInfo injectCssFile(final Document hostDoc, final String id,
-        final File src) throws IOException {
+    private FileInfo injectCssFile(Document hostDoc, String id, Path src)
+        throws IOException {
       final FileInfo info = new FileInfo(src);
       final Element banner = HtmlDomUtil.find(hostDoc, id);
       if (banner == null) {
@@ -376,7 +370,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,8 +380,8 @@
       return info;
     }
 
-    private FileInfo injectXmlFile(final Document hostDoc, final String id,
-        final File src) throws IOException {
+    private FileInfo injectXmlFile(Document hostDoc, String id, Path src)
+        throws IOException {
       final FileInfo info = new FileInfo(src);
       final Element banner = HtmlDomUtil.find(hostDoc, id);
       if (banner == null) {
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/RobotsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
index 1d8e74d..2f5bc3a 100644
--- 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
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.isReadable;
+
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -24,11 +27,11 @@
 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 java.nio.file.Files;
+import java.nio.file.Path;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -58,13 +61,13 @@
   private static final Logger log =
       LoggerFactory.getLogger(RobotsServlet.class);
 
-  private final File robotsFile;
+  private final Path robotsFile;
 
   @Inject
   RobotsServlet(@GerritServerConfig final Config config, final SitePaths sitePaths) {
-    File file = sitePaths.resolve(
+    Path file = sitePaths.resolve(
       config.getString("httpd", null, "robotsFile"));
-    if (file != null && (!file.exists() || !file.canRead())) {
+    if (file != null && (!exists(file) || !isReadable(file))) {
       log.warn("Cannot read httpd.robotsFile, using default");
       file = null;
     }
@@ -75,23 +78,16 @@
   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();
+    try (InputStream in = openRobotsFile();
+        OutputStream out = rsp.getOutputStream()) {
+      ByteStreams.copy(in, out);
     }
   }
 
   private InputStream openRobotsFile() {
     if (robotsFile != null) {
       try {
-        return new FileInputStream(robotsFile);
+        return Files.newInputStream(robotsFile);
       } catch (IOException e) {
         log.warn("Cannot read " + robotsFile + "; using default", e);
       }
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..5f526dd 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
@@ -32,20 +32,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")
@@ -88,11 +90,8 @@
     CacheHeaders.setNotCacheable(rsp);
     rsp.setCharacterEncoding("UTF-8");
     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/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
index 52b7a5c9..570ad57 100644
--- 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
@@ -17,6 +17,7 @@
 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 com.google.gerrit.common.FileUtil.lastModified;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
@@ -30,7 +31,7 @@
 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.FileUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -44,11 +45,11 @@
 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.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 
@@ -89,21 +90,19 @@
     return type != null ? type : "application/octet-stream";
   }
 
-  private final File staticBase;
-  private final String staticBasePath;
+  private final Path staticBase;
   private final boolean refresh;
   private final LoadingCache<String, Resource> cache;
 
   @Inject
   StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
-    File f;
+    Path p;
     try {
-      f = site.static_dir.getCanonicalFile();
+      p = site.static_dir.toRealPath().normalize();
     } catch (IOException e) {
-      f = site.static_dir.getAbsoluteFile();
+      p = site.static_dir.toAbsolutePath().normalize();
     }
-    staticBase = f;
-    staticBasePath = staticBase.getPath() + File.separator;
+    staticBase = p;
     refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
     cache = CacheBuilder.newBuilder()
         .maximumWeight(1 << 20)
@@ -131,7 +130,8 @@
     }
   }
 
-  private Resource getResource(HttpServletRequest req) throws ExecutionException {
+  private Resource getResource(HttpServletRequest req)
+      throws ExecutionException {
     String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
     if (isUnreasonableName(name)) {
       return Resource.NOT_FOUND;
@@ -150,13 +150,12 @@
   }
 
   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
+    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 "//..."
   }
 
   @Override
@@ -200,38 +199,28 @@
     rsp.setHeader(ETAG, r.etag);
     rsp.setContentType(r.contentType);
     rsp.setContentLength(tosend.length);
-    final OutputStream out = rsp.getOutputStream();
-    try {
+    try (OutputStream out = rsp.getOutputStream()) {
       out.write(tosend);
-    } finally {
-      out.close();
     }
   }
 
   private Resource loadResource(String name) throws IOException {
-    File p = new File(staticBase, name);
+    Path p = staticBase.resolve(name);
     try {
-      p = p.getCanonicalFile();
+      p = p.toRealPath().normalize();
     } catch (IOException e) {
       return Resource.NOT_FOUND;
     }
-    if (!p.getPath().startsWith(staticBasePath)) {
+    if (!p.startsWith(staticBase)) {
       return Resource.NOT_FOUND;
     }
 
-    long ts = p.lastModified();
-    FileInputStream in;
-    try {
-      in = new FileInputStream(p);
-    } catch (FileNotFoundException e) {
-      return Resource.NOT_FOUND;
-    }
-
+    long ts = FileUtil.lastModified(p);
     byte[] raw;
     try {
-      raw = ByteStreams.toByteArray(in);
-    } finally {
-      in.close();
+      raw = Files.readAllBytes(p);
+    } catch (NoSuchFileException e) {
+      return Resource.NOT_FOUND;
     }
     return new Resource(p, ts, contentType(name), raw);
   }
@@ -239,13 +228,13 @@
   static class Resource {
     static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
 
-    final File src;
+    final Path src;
     final long lastModified;
     final String contentType;
     final String etag;
     final byte[] raw;
 
-    Resource(File src, long lastModified, String contentType, byte[] raw) {
+    Resource(Path src, long lastModified, String contentType, byte[] raw) {
       this.src = src;
       this.lastModified = lastModified;
       this.contentType = contentType;
@@ -254,7 +243,7 @@
     }
 
     boolean isStale() {
-      return lastModified != src.lastModified();
+      return lastModified != lastModified(src);
     }
   }
 }
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..179b268 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
@@ -82,11 +82,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();
     }
   }
 
@@ -148,11 +145,8 @@
     rsp.setContentType("text/html");
     rsp.setCharacterEncoding("UTF-8");
     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/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 6f4cc08..44bf7aa 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,16 @@
         return;
       }
 
-      if (viewData.view instanceof RestModifyView<?, ?>) {
+      if (viewData.view instanceof RestReadView<?>
+          && "GET".equals(req.getMethod())) {
+        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 +335,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 +389,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 +434,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 +444,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 +461,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 +477,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 +523,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 +537,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 +546,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 +554,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 +656,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);
@@ -767,11 +767,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 +778,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)) {
@@ -829,7 +826,9 @@
       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 +957,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 +1002,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,10 +1053,15 @@
     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);
+    int maxSize = base64MaxSize(bin.getContentLength());
+    int estSize = Math.min(base64MaxSize(HEAP_EST_SIZE), maxSize);
+    TemporaryBuffer.Heap buf = heap(estSize, maxSize);
     OutputStream encoded = BaseEncoding.base64().encodingStream(
         new OutputStreamWriter(buf, ISO_8859_1));
     bin.writeTo(encoded);
@@ -1066,10 +1071,10 @@
 
   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 +1088,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/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/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index b2a6afc..55586b0 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
@@ -21,11 +21,9 @@
 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;
@@ -36,13 +34,9 @@
 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;
@@ -60,13 +54,10 @@
   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;
 
@@ -78,10 +69,8 @@
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser, final ContactStore cs,
       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,
@@ -90,16 +79,13 @@
     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 +93,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);
   }
@@ -221,25 +193,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..34066f1 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
@@ -22,12 +22,10 @@
 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.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
@@ -48,7 +46,6 @@
 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;
@@ -56,13 +53,11 @@
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
-      final AccountCache accountCache,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
       final ChangeQueryBuilder queryBuilder) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
-    this.accountCache = accountCache;
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
     this.queryBuilder = queryBuilder;
@@ -79,24 +74,6 @@
   }
 
   @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,
       AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>(){
@@ -201,8 +178,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/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 9fc95b1..3237873 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
@@ -91,7 +91,7 @@
   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;
@@ -150,7 +150,7 @@
         throw new NoSuchEntityException();
       }
     }
-    projectKey = control.getProject().getNameKey();
+    project = control.getProject().getNameKey();
     final PatchList list;
 
     try {
@@ -188,7 +188,7 @@
 
     detail = new PatchSetDetail();
     detail.setPatchSet(patchSet);
-    detail.setProject(projectKey);
+    detail.setProject(project);
 
     detail.setInfo(infoFactory.get(db, patchSet.getId()));
     detail.setPatches(patches);
@@ -251,12 +251,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/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/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 9437bbe..dff2cd0 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
@@ -14,13 +14,16 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.EmailException;
 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;
@@ -100,6 +103,7 @@
   protected Change.Id updateProjectConfig(ProjectControl ctl,
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, OrmException {
+    md.setInsertChangeId(true);
     Change.Id changeId = new Change.Id(db.nextChangeId());
     RevCommit commit =
         config.commitToNewRef(md, new PatchSet.Id(changeId,
@@ -109,7 +113,7 @@
     }
 
     Change change = new Change(
-        new Change.Key("I" + commit.name()),
+        getChangeId(commit),
         changeId,
         user.getAccountId(),
         new Branch.NameKey(
@@ -133,6 +137,14 @@
     return changeId;
   }
 
+  private static Change.Key getChangeId(RevCommit commit) {
+    List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID);
+    Change.Key changeKey = !idList.isEmpty()
+        ? new Change.Key(idList.get(idList.size() - 1).trim())
+        : new Change.Key("I" + commit.name());
+    return changeKey;
+  }
+
   private void addProjectOwnersAsReviewers(ChangeResource rsrc) {
     final String projectOwners =
         groupBackend.get(SystemGroupBackend.PROJECT_OWNERS).getName();
@@ -140,7 +152,7 @@
       AddReviewerInput input = new AddReviewerInput();
       input.reviewer = projectOwners;
       reviewersProvider.get().apply(rsrc, input);
-    } catch (Exception e) {
+    } catch (IOException | OrmException | RestApiException | EmailException e) {
       // one of the owner groups is not visible to the user and this it why it
       // can't be added as reviewer
     }
@@ -156,7 +168,7 @@
         AddReviewerInput input = new AddReviewerInput();
         input.reviewer = r.getGroup().getUUID().get();
         reviewersProvider.get().apply(rsrc, input);
-      } catch (Exception e) {
+      } catch (IOException | OrmException | RestApiException | EmailException 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-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 4ee9676..60404a3 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
@@ -46,11 +46,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 {
@@ -88,26 +88,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 +121,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 +167,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 +197,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 +247,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();
@@ -348,24 +346,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.
         //
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 2b45d2b..a146774 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -34,7 +34,9 @@
     '//lib/jgit:jgit',
     '//lib/log:api',
     '//lib/lucene:analyzers-common',
+    '//lib/lucene:backward-codecs',
     '//lib/lucene:core',
+    '//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..27ded17 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;
@@ -43,13 +42,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 +50,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 +58,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 +87,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();
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..55ed22d 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,6 +15,8 @@
 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;
@@ -29,8 +31,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,8 +41,8 @@
 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;
@@ -51,6 +54,8 @@
 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.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -64,9 +69,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,22 +84,26 @@
 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;
@@ -118,55 +130,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 +157,10 @@
     }
   }
 
+  private static String sortFieldName(FieldDef<?, ?> f) {
+    return f.getName() + "_SORT";
+  }
+
   static interface Factory {
     LuceneChangeIndex create(Schema<ChangeData> schema, String base);
   }
@@ -187,12 +169,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 +206,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,15 +239,9 @@
     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);
@@ -263,19 +251,46 @@
         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 +314,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 +333,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),
@@ -358,12 +371,19 @@
     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));
+    }
   }
 
   private class QuerySource implements ChangeDataSource {
@@ -376,7 +396,7 @@
     private QuerySource(List<SubIndex> indexes, Query query, int start,
         int limit, Sort sort) {
       this.indexes = indexes;
-      this.query = query;
+      this.query = checkNotNull(query, "null query from Lucene");
       this.start = start;
       this.limit = limit;
       this.sort = sort;
@@ -402,7 +422,7 @@
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
       try {
         int realLimit = start + limit;
-        TopDocs[] hits = new TopDocs[indexes.size()];
+        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);
@@ -462,18 +482,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 +512,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 +553,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 +596,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..7fd98aa 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;
@@ -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,17 @@
     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");
+    }
+    return queryBuilder.createPhraseQuery(p.getField().getName(), value);
   }
 
   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..5778008 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
@@ -28,17 +28,17 @@
 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.ExecutionException;
 import java.util.concurrent.Executor;
@@ -52,16 +52,19 @@
 
   private final Directory dir;
   private final TrackingIndexWriter writer;
-  private final SearcherManager searcherManager;
+  private final ReferenceManager<IndexSearcher> searcherManager;
   private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
   private final Set<NrtFuture> notDoneNrtFutures;
 
-  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;
     IndexWriter delegateWriter;
     long commitPeriod = writerConfig.getCommitWithinMs();
@@ -103,8 +106,8 @@
           }, 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();
 
@@ -124,6 +127,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 {
@@ -157,12 +162,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);
     }
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-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..bd7558b 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
@@ -319,11 +319,8 @@
     res.setContentType("text/html");
     res.setCharacterEncoding("UTF-8");
     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/XrdsServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
index 4719a84..d6ada97 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
@@ -63,11 +63,8 @@
     rsp.setContentType("application/xrds+xml");
     rsp.setCharacterEncoding(ENC);
 
-    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..eb4ebab 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
@@ -145,9 +145,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);
     }
 
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..b52e7af 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(
@@ -91,7 +92,7 @@
 
 java_library(
   name = 'pgm',
-  srcs = glob([SRCS + '*.java']),
+  srcs = glob([SRCS + '*.java', SRCS + 'rules/*.java']),
   resources = glob([RSRCS + '*']),
   deps = DEPS + [
     ':http',
@@ -102,11 +103,14 @@
     '//gerrit-lucene:lucene',
     '//gerrit-oauth:oauth',
     '//gerrit-openid:openid',
-    '//gerrit-solr:solr',
     '//lib:args4j',
     '//lib:gwtorm',
+    '//lib:protobuf',
     '//lib:servlet-api-3_1',
-    '//lib/prolog:prolog-cafe',
+    '//lib/auto:auto-value',
+    '//lib/prolog:cafeteria',
+    '//lib/prolog:compiler',
+    '//lib/prolog:runtime',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
@@ -125,7 +129,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..0a0d000 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,6 +15,7 @@
 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;
@@ -43,6 +44,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,7 +52,6 @@
 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;
@@ -76,7 +77,6 @@
 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;
@@ -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;
 
@@ -149,9 +149,10 @@
   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;
 
@@ -185,7 +186,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 +197,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 +204,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 +217,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,13 +258,18 @@
   }
 
   @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);
     }
@@ -281,6 +279,11 @@
       .setDbCfgInjector(dbInjector, cfgInjector);
     manager.add(dbInjector, cfgInjector, sysInjector);
 
+    if (!consoleLog) {
+      manager.add(ErrorLogFile.start(getSitePath(),
+          cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class))));
+    }
+
     sshd &= !sshdOff();
     if (sshd) {
       initSshd();
@@ -326,7 +329,11 @@
     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());
@@ -351,9 +358,6 @@
     } else {
       modules.add(NoSshKeyCache.module());
     }
-    if (!slave) {
-      modules.add(new MasterNodeStartup());
-    }
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
@@ -366,6 +370,7 @@
       }
     });
     modules.add(new GarbageCollectionModule());
+    modules.add(new ChangeCleanupRunner.Module());
     return cfgInjector.createChildInjector(modules);
   }
 
@@ -377,8 +382,6 @@
     switch (indexType) {
       case LUCENE:
         return luceneModule != null ? luceneModule : new LuceneIndexModule();
-      case SOLR:
-        return new SolrIndexModule();
       default:
         throw new IllegalStateException("unsupported index.type = " + indexType);
     }
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..e2775c1 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;
 
@@ -70,7 +70,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 +106,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()));
@@ -157,8 +157,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);
@@ -177,7 +177,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..c3cf914 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());
       }
-    } finally {
-      db.close();
     }
 
     final List<Worker> workers = new ArrayList<>(threads);
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..a77cc8c 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
@@ -47,14 +47,11 @@
           PrintWriter out = new PrintWriter(
               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();
         }
 
         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..8b90e54 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
@@ -113,13 +113,11 @@
         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();
@@ -143,7 +141,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 +156,8 @@
           log.error("Error rebuilding notedb", e);
           ok.set(false);
           break;
-        } finally {
-          repo.close();
         }
       }
-    } finally {
-      allUsersRepo.close();
     }
 
     double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
@@ -231,20 +225,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..5bedfe3 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;
@@ -122,9 +121,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 +145,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..9cf6892 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
@@ -17,15 +17,15 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
 
 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..ba1aea3 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
@@ -56,11 +56,8 @@
     try {
       CacheHeaders.setNotCacheable(res);
     } finally {
-      ServletOutputStream out = res.getOutputStream();
-      try {
+      try (ServletOutputStream out = res.getOutputStream()) {
         out.write(msg);
-      } finally {
-        out.close();
       }
     }
   }
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..5028e4d3 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
@@ -81,6 +81,8 @@
 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;
@@ -220,22 +222,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 +342,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;
@@ -518,49 +520,42 @@
   }
 
   private static void unpack(File srcwar, File dstwar) throws IOException {
-    final ZipFile zf = new ZipFile(srcwar);
-    try {
+    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()) 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;
+        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();
 
-        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();
+        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);
           }
-        } finally {
-          rawout.close();
         }
       }
-    } finally {
-      zf.close();
     }
   }
 
   private static void mkdir(File dir) throws IOException {
     if (!dir.isDirectory()) {
       mkdir(dir.getParentFile());
-      if (!dir.mkdir())
+      if (!dir.mkdir()) {
         throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
+      }
       dir.deleteOnExit();
     }
   }
@@ -627,14 +622,14 @@
         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();
+        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
@@ -662,12 +657,10 @@
     long start = TimeUtil.nowMs();
     Process rebuild = proc.start();
     byte[] out;
-    InputStream in = rebuild.getInputStream();
-    try {
+    try (InputStream in = rebuild.getInputStream()) {
       out = ByteStreams.toByteArray(in);
     } finally {
       rebuild.getOutputStream().close();
-      in.close();
     }
 
     int status;
@@ -687,12 +680,9 @@
   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 {
+    try (InputStream in = new FileInputStream(
+        new File(new File(gen, "tools"), "buck.properties"))) {
       properties.load(in);
-    } finally {
-      in.close();
     }
     return properties;
   }
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 c9e76c8..1632c82 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
@@ -56,9 +56,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;
@@ -86,12 +91,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);
@@ -120,19 +125,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;
   }
@@ -208,7 +208,7 @@
 
   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();
@@ -228,7 +228,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());
@@ -287,8 +287,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));
       }
@@ -408,15 +408,41 @@
     return sysInjector;
   }
 
-  private static void recursiveDelete(File path) {
-    File[] entries = path.listFiles();
-    if (entries != null) {
-      for (File e : entries) {
-        recursiveDelete(e);
-      }
-    }
-    if (!path.delete() && path.exists()) {
-      System.err.println("warn: Cannot remove " + path);
+  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);
     }
   }
 }
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..98bee3d 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;
   }
 
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..a0c24a6 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,8 @@
   protected void configure() {
     bind(SitePaths.class).toInstance(site);
     bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("db2")).to(DB2Initializer.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/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 c1f0090..b670d39 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
@@ -23,6 +23,7 @@
 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.AccountSshKey;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
@@ -30,6 +31,11 @@
 
 import org.apache.commons.validator.routines.EmailValidator;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Collections;
 
 public class InitAdminUser implements InitStep {
@@ -62,16 +68,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(
@@ -98,19 +104,56 @@
               new AccountGroupMember(new AccountGroupMember.Key(id,
                   new AccountGroup.Id(1)));
           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), StandardCharsets.UTF_8);
+    return new AccountSshKey(new AccountSshKey.Id(id, 0), content);
+  }
 }
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..60ff665 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;
@@ -28,11 +29,12 @@
 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 +58,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 +68,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(), FS.DETECTED);
           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 +110,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/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 72400a4..bb4c314 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,37 +111,39 @@
     for (PluginData plugin : plugins) {
       String pluginName = plugin.name;
       try {
-        final File tmpPlugin = plugin.pluginFile;
+        final Path tmpPlugin = plugin.pluginPath;
 
         if (!(initFlags.installPlugins.contains(pluginName) || ui.yesno(false,
             "Install plugin %s version %s", pluginName, plugin.version))) {
-          tmpPlugin.delete();
+          Files.deleteIfExists(tmpPlugin);
           continue;
         }
 
-        final File p = new File(site.plugins_dir, plugin.name + ".jar");
-        if (p.exists()) {
+        final Path p = site.plugins_dir.resolve(plugin.name + ".jar");
+        if (Files.exists(p)) {
           final String installedPluginVersion = getVersion(p);
           if (!ui.yesno(false,
               "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()) {
@@ -167,14 +170,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/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index ea8f0f1..869e1c4 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
@@ -39,6 +39,8 @@
 
   /* final */LibraryDownloader bouncyCastleProvider;
   /* final */LibraryDownloader bouncyCastleSSL;
+  /* final */LibraryDownloader db2Driver;
+  /* final */LibraryDownloader db2DriverLicense;
   /* final */LibraryDownloader mysqlDriver;
   /* final */LibraryDownloader oracleDriver;
 
@@ -53,9 +55,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 +89,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);
     }
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..9c9843e 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);
+          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..8a227ac 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;
     }
 
@@ -132,7 +131,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 +140,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..21cd3c8 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
@@ -30,13 +30,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 +65,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 +83,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 +96,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);
         }
       }
     }
@@ -256,23 +259,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..06d907a 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
@@ -40,6 +40,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 public class AllProjectsConfig extends VersionedMetaData {
 
@@ -68,21 +69,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;
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..02969d9 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
@@ -45,15 +45,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);
     }
   }
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..c8f03cc 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
@@ -47,11 +47,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..904af2f 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,15 @@
 
 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 +34,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 +55,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 +122,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 +148,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(), FS.DETECTED);
     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 78%
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..31c3be1 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,51 +217,48 @@
   }
 
   /** 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);
     }
   }
 
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..60d4acc 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
@@ -87,6 +87,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>>() {})
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..8a0d9a1 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] %-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..6eb313e 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()));
       }
@@ -198,7 +200,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 16bceee..20dc4ce 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,16 +15,16 @@
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleProvider"]
-  name = Bouncy Castle Crypto Provider v151
-  url = http://www.bouncycastle.org/download/bcprov-jdk15on-151.jar
-  sha1 = 9ab8afcc2842d5ef06eb775a0a2b12783b99aa80
+  name = Bouncy Castle Crypto Provider v152
+  url = http://www.bouncycastle.org/download/bcprov-jdk15on-152.jar
+  sha1 = 88a941faf9819d371e3174b5ed56a3f3f7d73269
   remove = bcprov-.*[.]jar
 
 # Version should match lib/bouncycastle/BUCK
 [library "bouncyCastleSSL"]
-  name = Bouncy Castle Crypto SSL v151
-  url = http://www.bouncycastle.org/download/bcpkix-jdk15on-151.jar
-  sha1 = 6c8c1f61bf27a09f9b1a8abc201523669bba9597
+  name = Bouncy Castle Crypto SSL v152
+  url = http://www.bouncycastle.org/download/bcpkix-jdk15on-152.jar
+  sha1 = b8ffac2bbc6626f86909589c8cc63637cc936504
   needs = bouncyCastleProvider
   remove = bcpkix-.*[.]jar
 
@@ -39,3 +39,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..6a4e4c0 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -33,7 +33,6 @@
     '//lib:jsch',
     '//lib:mime-util',
     '//lib:servlet-api-3_1',
-    '//lib/commons:io',
     '//lib/commons:lang',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index c0ad2b1..131666c 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.3</version>
+  <version>2.12-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 992ffaa..4035940 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.3</version>
+  <version>2.12-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 debe317..6e811a2 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.3</version>
+  <version>2.12-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..132ade5 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'],
@@ -25,14 +24,7 @@
   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-lib2'],
   provided_deps = DEPS + ['//lib/gwt:dev'],
   visibility = ['PUBLIC'],
 )
@@ -67,8 +59,12 @@
   paths = COMMON + GWTEXPUI,
   srcs = SRCS,
   deps = DEPS + [
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm_client',
     '//lib/gwt:dev__jar',
     '//gerrit-gwtui-common:client-lib2',
+    '//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 7f2ae55..a34a7b3 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.3</version>
+  <version>2.12-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..f52bbef
--- /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 final static 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-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index c87fc19..7cc87fe 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.3</version>
+  <version>2.12-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..6728ba6 100644
--- a/gerrit-prettify/BUCK
+++ b/gerrit-prettify/BUCK
@@ -12,14 +12,16 @@
   ]),
   deps = [
     ':google-code-prettify',
+    '//gerrit-gwtexpui:SafeHtml',
+  ],
+  exported_deps = [
     '//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/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..2750380 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,7 +14,7 @@
 
 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.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
@@ -111,8 +111,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;
     }
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 fe2cd96..0701771 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
@@ -145,6 +145,14 @@
       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);
+      return new Change.Id(Integer.parseInt(id));
+    }
+
     static int startIndex(String ref) {
       if (ref == null || !ref.startsWith(REFS_CHANGES)) {
         return -1;
@@ -243,8 +251,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 +283,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,7 +446,7 @@
 
   /**
    * 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.
    */
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 48623bd..4ec957e 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
@@ -20,6 +20,8 @@
 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 {
@@ -28,6 +30,41 @@
     return Id.fromRef(name) != null;
   }
 
+  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;
 
@@ -140,6 +177,18 @@
   @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;
+
   protected PatchSet() {
   }
 
@@ -187,6 +236,14 @@
     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();
   }
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..ce1b27f 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,8 @@
 
   protected InheritableBoolean createNewChangeForAllNotInTarget;
 
+  protected InheritableBoolean enableSignedPush;
+
   protected Project() {
   }
 
@@ -108,6 +110,7 @@
     requireChangeID = InheritableBoolean.INHERIT;
     useContentMerge = InheritableBoolean.INHERIT;
     createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
+    enableSignedPush = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -171,6 +174,14 @@
     this.createNewChangeForAllNotInTarget = useAllNotInTarget;
   }
 
+  public InheritableBoolean getEnableSignedPush() {
+    return enableSignedPush;
+  }
+
+  public void setEnableSignedPush(InheritableBoolean enable) {
+    enableSignedPush = enable;
+  }
+
   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..707664f 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,10 @@
 
 /** Constants and utilities for Gerrit-specific ref names. */
 public class RefNames {
+  public static final String REFS = "refs/";
+
+  public static final String REFS_HEADS = "refs/heads/";
+
   public static final String REFS_CHANGES = "refs/changes/";
 
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
@@ -26,7 +30,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 +55,27 @@
   /** Suffix of a meta ref in the notedb. */
   public static final String META_SUFFIX = "/meta";
 
+  public static final String EDIT_PREFIX = "edit-";
+
+  /**
+   * Special ref for GPG public keys used by {@link
+   * com.google.gerrit.server.git.gpg.SignedPushPreReceiveHook}.
+   */
+  public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
+
+  public static String fullName(String ref) {
+    return ref.startsWith(REFS) ? ref : REFS_HEADS + ref;
+  }
+
+  public static final String shortName(String ref) {
+    return ref.startsWith(REFS_HEADS)
+        ? ref.substring(REFS_HEADS.length())
+        : 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 +127,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/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/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
index 33da24a..87e5b88 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,34 @@
     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");
+  }
+
   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.isRef(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.isRef(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..b0981a7
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
@@ -0,0 +1,50 @@
+// 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");
+  }
+
+  @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..c7bd8c9 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,14 +60,15 @@
     '//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: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',
@@ -94,16 +96,18 @@
     ':server',
     '//gerrit-common:server',
     '//gerrit-cache-h2:cache-h2',
+    '//gerrit-extension-api:api',
     '//gerrit-lucene:lucene',
     '//gerrit-reviewdb:server',
-    '//lib:guava',
     '//lib:gwtorm',
     '//lib:h2',
-    '//lib:junit',
+    '//lib:truth',
+    '//lib/auto:auto-value',
     '//lib/guice:guice',
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
+    '//lib/log:api',
     '//lib/log:impl_log4j',
     '//lib/log:log4j',
   ],
@@ -132,9 +136,11 @@
   deps = [
     ':server',
     '//gerrit-common:server',
+    '//lib:guava',
     '//lib:junit',
+    '//lib:truth',
     '//lib/guice:guice',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
 )
 
@@ -155,7 +161,7 @@
     '//lib:truth',
     '//lib/jgit:jgit',
     '//lib/guice:guice',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
 )
 
@@ -176,9 +182,7 @@
     '//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',
@@ -209,14 +213,16 @@
     '//lib:grappa',
     '//lib:guava',
     '//lib:gwtorm',
-    '//lib:junit',
     '//lib:truth',
+    '//lib/bouncycastle:bcprov',
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcpkix',
     '//lib/guice:guice',
     '//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..285f1d4 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;
@@ -41,11 +41,11 @@
 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;
@@ -66,12 +66,14 @@
 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 +90,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 +101,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 +172,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 +246,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 +264,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 +296,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 +318,20 @@
      * @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);
+      }
     }
 
     /**
@@ -331,10 +339,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 +352,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 +374,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(change);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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(change);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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(change);
+      event.author =  eventFactory.asAccountAttribute(account);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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(change);
+      event.submitter = eventFactory.asAccountAttribute(account);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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(change);
+      event.submitter = eventFactory.asAccountAttribute(account);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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(change);
+      event.abandoner = eventFactory.asAccountAttribute(account);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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(change);
+      event.restorer = eventFactory.asAccountAttribute(account);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      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 +597,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 +610,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.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 +632,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.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);
@@ -653,7 +678,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 +705,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 +715,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 +803,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 +819,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 +840,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 +870,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 +901,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 +921,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 +943,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 +961,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 +996,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 +1013,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 +1026,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 +1035,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/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/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..961cc45 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
@@ -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..b3cf660 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
@@ -106,9 +106,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 +131,6 @@
           }
         }
         return labelNormalizer.normalize(ctl, byUser.values()).getNormalized();
-      } finally {
-        repo.close();
       }
     } catch (IOException e) {
       throw new OrmException(e);
@@ -142,7 +139,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 ffcd1bb..590d065 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,12 +14,12 @@
 
 package com.google.gerrit.server;
 
-import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
 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.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -32,7 +32,6 @@
 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.GitRepositoryManager;
@@ -75,12 +74,9 @@
 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 +94,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 +114,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();
@@ -191,7 +199,6 @@
   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;
@@ -203,7 +210,6 @@
       Provider<InternalChangeQuery> queryProvider,
       RevertedSender.Factory revertedSenderFactory,
       ChangeInserter.Factory changeInserterFactory,
-      PatchSetInserter.Factory patchSetInserterFactory,
       GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated,
       ChangeIndexer indexer) {
@@ -213,7 +219,6 @@
     this.queryProvider = queryProvider;
     this.revertedSenderFactory = revertedSenderFactory;
     this.changeInserterFactory = changeInserterFactory;
-    this.patchSetInserterFactory = patchSetInserterFactory;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
     this.indexer = indexer;
@@ -319,7 +324,7 @@
       ins.setMessage(cmsg).insert();
 
       try {
-        RevertedSender cm = revertedSenderFactory.create(change);
+        RevertedSender cm = revertedSenderFactory.create(change.getId());
         cm.setFrom(user().getAccountId());
         cm.setChangeMessage(cmsg);
         cm.send();
@@ -334,70 +339,6 @@
     }
   }
 
-  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(RECEIVE_COMMITS)
-          .setDraft(ps.isDraft())
-          .insert();
-
-      return change.getId();
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeId, e);
-    }
-  }
-
   public String getMessage(Change change)
       throws NoSuchChangeException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException {
@@ -427,28 +368,40 @@
     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.patchSetAncestors().delete(db.patchSetAncestors().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 +420,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 +435,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);
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..baba4bb 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,6 +14,8 @@
 
 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;
@@ -37,6 +39,7 @@
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 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.OutOfScopeException;
@@ -144,13 +147,13 @@
         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,
+        @DisableReverseDnsLookup final Boolean disableReverseDnsLookup,
 
-        final @RemotePeer Provider<SocketAddress> remotePeerProvider,
+        @RemotePeer final Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
@@ -327,35 +330,28 @@
   @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 | OrmRuntimeException e) {
+        log.warn("Cannot query starred changes", e);
       }
-      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 +370,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..15519cc 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 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/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..34f83f7 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,15 +16,44 @@
 
 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.HashSet;
 import java.util.Objects;
 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()) {
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/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 62615c6..66c7570 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
@@ -83,13 +83,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);
@@ -107,8 +104,7 @@
   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);
         if (id == null) {
@@ -118,8 +114,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");
           }
 
@@ -128,16 +124,14 @@
           return new AuthResult(id.getAccountId(), key, false);
         }
 
-      } finally {
-        db.close();
       }
-    } catch (OrmException e) {
+    } catch (OrmException | NameAlreadyUsedException | InvalidUserNameException e) {
       throw new AccountException("Authentication error", e);
     }
   }
 
   private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
-      throws OrmException {
+      throws OrmException, NameAlreadyUsedException, InvalidUserNameException {
     IdentifiedUser user = userFactory.create(extId.getAccountId());
     Account toUpdate = null;
 
@@ -165,7 +159,7 @@
 
     if (!realm.allowsEdit(Account.FieldName.USER_NAME)
         && !eq(user.getUserName(), who.getUserName())) {
-      changeUserNameFactory.create(db, user, who.getUserName());
+      changeUserNameFactory.create(db, user, who.getUserName()).call();
     }
 
     if (toUpdate != null) {
@@ -324,8 +318,7 @@
    */
   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);
@@ -334,7 +327,11 @@
         if (!extId.getAccountId().equals(to)) {
           throw new AccountException("Identity in use by another account");
         }
-        update(db, who, extId);
+        try {
+          update(db, who, extId);
+        } catch(NameAlreadyUsedException | InvalidUserNameException e) {
+          throw new AccountException("Account update failed", e);
+        }
 
       } else {
         extId = createId(to, who);
@@ -357,8 +354,6 @@
 
       return new AuthResult(to, key, false);
 
-    } finally {
-      db.close();
     }
   }
 
@@ -377,8 +372,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()
@@ -390,8 +384,6 @@
       }
       byIdCache.evict(to);
       return link(to, who);
-    } finally {
-      db.close();
     }
   }
 
@@ -417,8 +409,7 @@
    */
   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);
@@ -446,8 +437,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..80a451a 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
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..a8eec10 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
@@ -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/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 441213d..a0fadb5 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,7 +27,6 @@
 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.config.AuthConfig;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
@@ -40,16 +39,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class CreateEmail implements RestModifyView<AccountResource, Input> {
+public class CreateEmail implements RestModifyView<AccountResource, EmailInput> {
   private final Logger log = LoggerFactory.getLogger(getClass());
 
-  public static class Input {
-    @DefaultInput
-    public String email;
-    public boolean preferred;
-    public boolean noConfirmation;
-  }
-
   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,7 +82,7 @@
     }
 
     if (input == null) {
-      input = new Input();
+      input = new EmailInput();
     }
 
     if (!EmailValidator.getInstance().isValid(email)) {
@@ -102,17 +94,16 @@
       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 {
     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..7669cf9 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.mail.EmailSettings;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -27,13 +28,17 @@
 @Singleton
 public class DefaultRealm extends AbstractRealm {
   private final EmailExpander emailExpander;
+  private final EmailSettings emailSettings;
   private final AccountByEmailCache byEmail;
   private final AuthConfig authConfig;
 
   @Inject
-  DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail, final AuthConfig authConfig) {
+  DefaultRealm(EmailExpander emailExpander,
+      EmailSettings emailSettings,
+      AccountByEmailCache byEmail,
+      AuthConfig authConfig) {
     this.emailExpander = emailExpander;
+    this.emailSettings = emailSettings;
     this.byEmail = byEmail;
     this.authConfig = authConfig;
   }
@@ -47,12 +52,18 @@
         case FULL_NAME:
           return Strings.emptyToNull(authConfig.getHttpDisplaynameHeader()) == null;
         case REGISTER_NEW_EMAIL:
-          return Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
+          return emailSettings.allowRegisterNewEmail
+              && Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
         default:
           return true;
       }
     } else {
-      return true;
+      switch (field) {
+        case REGISTER_NEW_EMAIL:
+          return emailSettings.allowRegisterNewEmail;
+        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..3698844
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.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.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();
+    info.contactFiledOn = a.getContactFiledOn();
+    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 Timestamp contactFiledOn;
+
+    public AccountDetailInfo(Integer id) {
+      super(id);
+    }
+  }
+}
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..28c3be6 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
@@ -86,14 +86,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();
     }
   }
 
@@ -151,6 +148,7 @@
         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));
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/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..553392d 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
@@ -38,10 +38,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);
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/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/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..b12e7ce
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -0,0 +1,71 @@
+// 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.ValidationError;
+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 {
+    ValidationError.Sink errors = new ValidationError.Sink() {
+      @Override
+      public void error(ValidationError error) {
+        log.error("Error parsing file " + QueryList.FILE_NAME + ": " +
+            error.getMessage());
+      }
+    };
+    queryList = QueryList.parse(readUTF8(QueryList.FILE_NAME), errors);
+  }
+
+  @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..8527e5f 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,16 @@
 
 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.common.AccountInfo;
 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.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 +31,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-public class AccountApiImpl extends AccountApi.NotImplemented implements AccountApi {
+public class AccountApiImpl implements AccountApi {
   interface Factory {
     AccountApiImpl create(AccountResource account);
   }
@@ -38,18 +41,21 @@
   private final AccountLoader.Factory accountLoaderFactory;
   private final StarredChanges.Create starredChangesCreate;
   private final StarredChanges.Delete starredChangesDelete;
+  private final CreateEmail.Factory createEmailFactory;
 
   @Inject
   AccountApiImpl(AccountLoader.Factory ailf,
       ChangesCollection changes,
       StarredChanges.Create starredChangesCreate,
       StarredChanges.Delete starredChangesDelete,
+      CreateEmail.Factory createEmailFactory,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
     this.changes = changes;
     this.starredChangesCreate = starredChangesCreate;
     this.starredChangesDelete = starredChangesDelete;
+    this.createEmailFactory = createEmailFactory;
   }
 
   @Override
@@ -91,4 +97,15 @@
       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);
+    }
+  }
 }
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..3f578ae 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,6 +16,7 @@
 
 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;
@@ -24,24 +25,30 @@
 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
@@ -61,4 +68,32 @@
     }
     return api.create(new AccountResource((IdentifiedUser)self.get()));
   }
+
+  @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/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 161461d..868094e 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
@@ -26,11 +26,14 @@
 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.IdentifiedUser;
 import com.google.gerrit.server.change.Abandon;
 import com.google.gerrit.server.change.ChangeEdits;
 import com.google.gerrit.server.change.ChangeJson;
@@ -38,12 +41,15 @@
 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.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestReviewers;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gwtorm.server.OrmException;
@@ -54,13 +60,15 @@
 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;
@@ -69,32 +77,40 @@
   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,
       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 +118,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;
@@ -181,6 +200,15 @@
   }
 
   @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);
   }
@@ -244,7 +272,11 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
-      return changeJson.get().addOptions(s).format(change);
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        ((IdentifiedUser) u).clearStarredChanges();
+      }
+      return changeJson.create(s).format(change);
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
     }
@@ -289,6 +321,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..f7705a7 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,11 +24,12 @@
 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.IdentifiedUser;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.CreateChange;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -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;
@@ -123,6 +127,10 @@
     }
 
     try {
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        ((IdentifiedUser) u).clearStarredChanges();
+      }
       List<?> result = qc.apply(TopLevelResource.INSTANCE);
       if (result.isEmpty()) {
         return ImmutableList.of();
@@ -136,7 +144,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/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index c36faa2..0569e5f 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,18 @@
 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.RebaseChange;
 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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -60,7 +64,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);
   }
@@ -77,16 +81,18 @@
   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,
@@ -100,16 +106,18 @@
       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;
@@ -123,6 +131,7 @@
     this.putReviewed = putReviewed;
     this.deleteReviewed = deleteReviewed;
     this.listFiles = listFiles;
+    this.getPatch = getPatch;
     this.mergeable = mergeable;
     this.fileApi = fileApi;
     this.listComments = listComments;
@@ -132,6 +141,7 @@
     this.draftFactory = draftFactory;
     this.comments = comments;
     this.commentFactory = commentFactory;
+    this.revisionActions = revisionActions;
     this.revision = r;
   }
 
@@ -147,7 +157,6 @@
   @Override
   public void submit() throws RestApiException {
     SubmitInput in = new SubmitInput();
-    in.waitForMerge = true;
     submit(in);
   }
 
@@ -188,7 +197,7 @@
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
       return changes.id(rebase.apply(revision, in)._number);
-    } catch (OrmException | EmailException e) {
+    } catch (OrmException | EmailException | IOException e) {
       throw new RestApiException("Cannot rebase ps", e);
     }
   }
@@ -293,6 +302,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 +320,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 +341,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 +360,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..feddf54
--- /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.server.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..97ba5d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -0,0 +1,147 @@
+// 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());
+    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..dae08cd
--- /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.server.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..2e6b761 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
@@ -24,5 +24,6 @@
 
     factory(BranchApiImpl.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..84b219b5 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,122 @@
 
 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.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.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 Provider<ListBranches> listBranchesProvider;
 
   @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,
+      Provider<ListBranches> listBranchesProvider,
       @Assisted ProjectResource project) {
-    this(createProjectFactory, projectApi, projects, projectJson,
-        branchApiFactory, project, null);
+    this(user, createProjectFactory, projectApi, projects, getDescription,
+        putDescription, childApi, children, projectJson, branchApiFactory,
+        listBranchesProvider, 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,
+      Provider<ListBranches> listBranchesProvider,
       @Assisted String name) {
-    this(createProjectFactory, projectApi, projects, projectJson,
-        branchApiFactory, null, name);
+    this(user, createProjectFactory, projectApi, projects, getDescription,
+        putDescription, childApi, children, projectJson, branchApiFactory,
+        listBranchesProvider, 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,
+      Provider<ListBranches> listBranchesProvider,
       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.listBranchesProvider = listBranchesProvider;
   }
 
   @Override
@@ -102,24 +146,93 @@
       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 ListBranchesRequest branches() {
+    return new ListBranchesRequest() {
+      @Override
+      public List<BranchInfo> get() throws RestApiException {
+        return listBranches(this);
+      }
+    };
+  }
+
+  private List<BranchInfo> listBranches(ListBranchesRequest 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 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);
+  }
+
+  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/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 4c29c9b..966c77a 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);
     }
   }
 
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..0c09155 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
@@ -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/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 9060108..27779f2 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -75,9 +76,10 @@
 
   @Inject
   LdapRealm(
-      final Helper helper,
-      final AuthConfig authConfig,
-      final EmailExpander emailExpander,
+      Helper helper,
+      AuthConfig authConfig,
+      EmailExpander emailExpander,
+      EmailSettings emailSettings,
       @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 +98,9 @@
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.USER_NAME);
     }
+    if (!emailSettings.allowRegisterNewEmail) {
+      readOnlyAccountFields.add(Account.FieldName.REGISTER_NEW_EMAIL);
+    }
 
     fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
   }
@@ -134,7 +139,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);
   }
 
@@ -307,8 +312,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 +320,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..b54cd88 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.server.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..6723db8 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,7 +15,6 @@
 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.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -23,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.server.ReviewDb;
@@ -53,7 +53,7 @@
   private final ChangeHooks hooks;
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
   private final ChangeIndexer indexer;
   private final ChangeUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
@@ -62,7 +62,7 @@
   Abandon(ChangeHooks hooks,
       AbandonedSender.Factory abandonedSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json,
+      ChangeJson.Factory json,
       ChangeIndexer indexer,
       ChangeUpdate.Factory updateFactory,
       ChangeMessagesUtil cmUtil) {
@@ -89,14 +89,22 @@
     } else if (change.getStatus() == Change.Status.DRAFT) {
       throw new ResourceConflictException("draft changes cannot be abandoned");
     }
+    change = abandon(control, input.message, caller.getAccount());
+    return json.create(ChangeJson.NO_OPTIONS).format(change);
+  }
 
+  public Change abandon(ChangeControl control,
+      String msgTxt, Account acc) throws ResourceConflictException,
+      OrmException, IOException {
+    Change change;
     ChangeMessage message;
     ChangeUpdate update;
+    Change.Id changeId = control.getChange().getId();
     ReviewDb db = dbProvider.get();
-    db.changes().beginTransaction(change.getId());
+    db.changes().beginTransaction(changeId);
     try {
       change = db.changes().atomicUpdate(
-        change.getId(),
+        changeId,
         new AtomicUpdate<Change>() {
           @Override
           public Change update(Change change) {
@@ -110,12 +118,12 @@
         });
       if (change == null) {
         throw new ResourceConflictException("change is "
-            + status(db.changes().get(req.getChange().getId())));
+            + status(db.changes().get(changeId)));
       }
 
       //TODO(yyonas): atomic update was not propagated
       update = updateFactory.create(control, change.getLastUpdatedOn());
-      message = newMessage(input, caller, change);
+      message = newMessage(msgTxt, acc != null ? acc.getId() : null, change);
       cmUtil.addChangeMessage(db, update, message);
       db.commit();
     } finally {
@@ -123,24 +131,23 @@
     }
     update.commit();
 
-    CheckedFuture<?, IOException> indexFuture =
-        indexer.indexAsync(change.getId());
+    indexer.index(db, change);
     try {
-      ReplyToChangeSender cm = abandonedSenderFactory.create(change);
-      cm.setFrom(caller.getAccountId());
+      ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
+      if (acc != null) {
+        cm.setFrom(acc.getId());
+      }
       cm.setChangeMessage(message);
       cm.send();
     } catch (Exception e) {
       log.error("Cannot email update for change " + change.getChangeId(), e);
     }
-    indexFuture.checkedGet();
     hooks.doChangeAbandonedHook(change,
-        caller.getAccount(),
+        acc,
         db.patchSets().get(change.currentPatchSetId()),
-        Strings.emptyToNull(input.message),
+        Strings.emptyToNull(msgTxt),
         db);
-    ChangeInfo result = json.format(change);
-    return result;
+    return change;
   }
 
   @Override
@@ -153,20 +160,20 @@
           && resource.getControl().canAbandon());
   }
 
-  private ChangeMessage newMessage(AbandonInput input, IdentifiedUser caller,
+  private ChangeMessage newMessage(String msgTxt, Account.Id accId,
       Change change) throws OrmException {
     StringBuilder msg = new StringBuilder();
     msg.append("Abandoned");
-    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+    if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) {
       msg.append("\n\n");
-      msg.append(input.message.trim());
+      msg.append(msgTxt.trim());
     }
 
     ChangeMessage message = new ChangeMessage(
         new ChangeMessage.Key(
             change.getId(),
             ChangeUtil.messageUUID(dbProvider.get())),
-        caller.getAccountId(),
+        accId,
         change.getLastUpdatedOn(),
         change.currentPatchSetId());
     message.setMessage(msg.toString());
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..8fbeda4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -0,0 +1,104 @@
+// 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 {
+          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 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..fd20868 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) {
@@ -73,11 +69,14 @@
     Provider<CurrentUser> userProvider = Providers.of(ctl.getCurrentUser());
     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");
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..73f9cab 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 {
     }
@@ -407,7 +407,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 +432,7 @@
   }
 
   @Singleton
-  static class Get implements RestReadView<ChangeEditResource> {
+  public static class Get implements RestReadView<ChangeEditResource> {
     private final FileContentUtil fileContentUtil;
 
     @Inject
@@ -455,7 +455,7 @@
   }
 
   @Singleton
-  static class GetMeta implements RestReadView<ChangeEditResource> {
+  public static class GetMeta implements RestReadView<ChangeEditResource> {
     private final WebLinks webLinks;
 
     @Inject
@@ -481,8 +481,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 ff833f4..a9153d6 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
@@ -163,6 +164,11 @@
     return this;
   }
 
+  public ChangeInserter setGroups(Iterable<String> groups) {
+    patchSet.setGroups(groups);
+    return this;
+  }
+
   public ChangeInserter setHashtags(Set<String> hashtags) {
     this.hashtags = hashtags;
     return this;
@@ -205,6 +211,9 @@
     db.changes().beginTransaction(change.getId());
     try {
       ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+      if (patchSet.getGroups() == null) {
+        patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
+      }
       db.patchSets().insert(Collections.singleton(patchSet));
       db.changes().insert(Collections.singleton(change));
       LabelTypes labelTypes = projectControl.getLabelTypes();
@@ -233,10 +242,10 @@
     }
 
     CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
-
     if (!messageIsForChange()) {
       commitMessageNotForChange();
     }
+    f.checkedGet();
 
     if (sendMail) {
       Runnable sender = new Runnable() {
@@ -244,7 +253,7 @@
         public void run() {
           try {
             CreateChangeSender cm =
-                createChangeSenderFactory.create(change);
+                createChangeSenderFactory.create(change.getId());
             cm.setFrom(change.getOwner());
             cm.setPatchSet(patchSet, patchSetInfo);
             cm.addReviewers(reviewers);
@@ -266,7 +275,6 @@
         sender.run();
       }
     }
-    f.checkedGet();
 
     gitRefUpdated.fire(change.getProject(), patchSet.getRefName(),
         ObjectId.zeroId(), commit);
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..acdf004 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,22 @@
 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.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,7 +65,6 @@
 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.RevisionInfo;
@@ -80,37 +79,39 @@
 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.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.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;
@@ -122,17 +123,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 +147,54 @@
   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 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) {
+      @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.options = options.isEmpty()
+        ? EnumSet.noneOf(ListChangesOption.class)
+        : EnumSet.copyOf(options);
   }
 
   public ChangeJson fix(FixInput fix) {
@@ -223,24 +223,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 | OrmException | IOException
+        | RuntimeException e) {
       if (!has(CHECK)) {
-        throw e;
+        Throwables.propagateIfPossible(e, OrmException.class);
+        throw new OrmException(e);
       }
       return checkOnly(cd);
     }
@@ -248,7 +266,7 @@
 
   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)
@@ -267,16 +285,15 @@
     } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
       ChangeData.ensureCurrentPatchSetLoaded(all);
     }
-    Set<Change.Id> reviewed = Sets.newHashSet();
-    if (has(REVIEWED)) {
-      reviewed = loadReviewed(all);
+    if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
+      ChangeData.ensureReviewedByLoadedForOpenChanges(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);
+      List<ChangeInfo> infos = toChangeInfo(out, r.changes());
       if (r.moreChanges()) {
         infos.get(infos.size() - 1)._moreChanges = true;
       }
@@ -291,14 +308,15 @@
   }
 
   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 | OrmException | IOException
+            | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
           } else {
@@ -340,8 +358,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, OrmException, IOException {
     ChangeInfo out = new ChangeInfo();
 
     if (has(CHECK)) {
@@ -356,7 +375,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 +386,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 +398,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 = ((IdentifiedUser) user).getAccountId();
+      out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
+    }
 
     out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS));
 
@@ -412,7 +434,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 +601,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
@@ -787,57 +815,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, 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 +833,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 +857,11 @@
     return map;
   }
 
-  private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in) throws OrmException {
+  private RevisionInfo toRevisionInfo(ChangeControl ctl, PatchSet in)
+      throws PatchListNotAvailableException, 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 +869,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 +900,41 @@
         && 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;
-    }
     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 +990,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..8bee4a6 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;
@@ -35,7 +34,6 @@
 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;
@@ -54,6 +52,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 +68,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 +97,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 +120,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 +176,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);
@@ -217,7 +218,7 @@
         // 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);
+            repo, MergeUtil.createDryRunInserter(repo), key.strategyName);
         merger.setBase(prior.getParent(0));
         try {
           if (merger.merge(next.getParent(0), prior)
@@ -229,8 +230,6 @@
           // it was a rework.
         }
         return ChangeKind.REWORK;
-      } finally {
-        key.repo = null;
       }
     }
 
@@ -264,7 +263,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 +272,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 +287,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);
@@ -322,7 +322,7 @@
         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() &&
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..84658b2 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
@@ -23,7 +23,6 @@
 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,14 @@
     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));
+          : 0);
 
     byte[] buf = new byte[20];
     ObjectId noteId;
@@ -87,6 +80,14 @@
     for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
       hashObjectId(h, p.getConfig().getRevision(), buf);
     }
+  }
+
+  @Override
+  public String getETag() {
+    CurrentUser user = control.getCurrentUser();
+    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..f4b2f4e 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.getCurrentUser().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..08117d1 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
@@ -24,6 +24,7 @@
 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.project.ChangeControl;
@@ -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;
@@ -70,11 +71,7 @@
       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,9 +82,9 @@
     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) {
@@ -102,6 +99,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..34a7070 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,6 +14,7 @@
 
 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.reviewdb.client.Branch;
@@ -21,6 +22,7 @@
 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;
@@ -108,25 +110,26 @@
   }
 
   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 {
 
-    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();
+    String destinationBranch = RefNames.shortName(ref);
     IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
     try (Repository git = gitManager.openRepository(project);
         RevWalk revWalk = new RevWalk(git)) {
-      Ref destRef = git.getRef(destinationBranch);
+      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());
@@ -182,9 +185,13 @@
       } 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, changeKey, project,
             destRef, cherryPickCommit, refControl,
-            identifiedUser, change.getTopic());
+            identifiedUser, newTopic);
 
         addMessageToSourceChange(change, patch.getId(), destinationBranch,
             cherryPickCommit, identifiedUser, refControl);
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..7240f44 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,13 +14,19 @@
 
 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.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ProblemInfo;
@@ -28,13 +34,19 @@
 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.change.PatchSetInserter.ValidatePolicy;
 import com.google.gerrit.server.git.GitRepositoryManager;
 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.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -60,6 +72,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 +106,17 @@
   private final GitRepositoryManager repoManager;
   private final Provider<CurrentUser> user;
   private final Provider<PersonIdent> serverIdent;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
 
   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 +127,16 @@
       GitRepositoryManager repoManager,
       Provider<CurrentUser> user,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      PatchSetInfoFactory patchSetInfoFactory) {
+      ChangeControl.GenericFactory changeControlFactory,
+      PatchSetInfoFactory patchSetInfoFactory,
+      PatchSetInserter.Factory patchSetInserterFactory) {
     this.db = db;
     this.repoManager = repoManager;
     this.user = user;
     this.serverIdent = serverIdent;
+    this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
+    this.patchSetInserterFactory = patchSetInserterFactory;
     reset();
   }
 
@@ -210,17 +231,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 +241,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 +302,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 +317,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, currPsCommit, merged);
     }
+  }
+
+  private void checkMergedBitMatchesStatus(PatchSet ps, 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", ps.getId().get(), commit.name(),
           refName, tip.name(), change.getStatus()));
       if (fix != null) {
         fixMerged(p);
@@ -335,11 +361,125 @@
     } 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 ps = insertPatchSet(commit);
+          if (ps != null) {
+            checkMergedBitMatchesStatus(ps, 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 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 {
+      ChangeControl ctl = changeControlFactory.controlFor(change, user.get());
+      PatchSetInserter inserter =
+          patchSetInserterFactory.create(repo, rw, ctl, commit);
+      change = inserter.setValidatePolicy(ValidatePolicy.NONE)
+          .setRunHooks(false)
+          .setSendMail(false)
+          .setAllowClosed(true)
+          .setUploader(((IdentifiedUser) user.get()).getAccountId())
+          // TODO: fix setMessage to work without init()
+          .setMessage(
+              "Patch set for merged commit inserted by consistency checker")
+          .insert();
+      p.status = Status.FIXED;
+      p.outcome = "Inserted as patch set " + change.currentPatchSetId().get();
+      return inserter.getPatchSet();
+    } catch (InvalidChangeOperationException | OrmException | IOException
+        | NoSuchChangeException 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(),
@@ -457,6 +597,15 @@
     }
   }
 
+  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 +633,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 4b5afe2..6c0ee3b 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,6 +24,7 @@
 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.RestModifyView;
@@ -34,6 +35,7 @@
 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;
@@ -56,7 +58,6 @@
 
 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;
@@ -85,7 +86,7 @@
   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 boolean allowDrafts;
 
@@ -97,7 +98,7 @@
       ProjectsCollection projectsCollection,
       CommitValidators.Factory commitValidatorsFactory,
       ChangeInserter.Factory changeInserterFactory,
-      ChangeJson json,
+      ChangeJson.Factory json,
       ChangeUtil changeUtil,
       @GerritServerConfig Config config) {
     this.db = db;
@@ -107,7 +108,7 @@
     this.projectsCollection = projectsCollection;
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
-    this.json = json;
+    this.jsonFactory = json;
     this.changeUtil = changeUtil;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
   }
@@ -117,7 +118,7 @@
       ChangeInfo input) throws AuthException, OrmException,
       BadRequestException, UnprocessableEntityException, IOException,
       InvalidChangeOperationException, ResourceNotFoundException,
-      MethodNotAllowedException {
+      MethodNotAllowedException, ResourceConflictException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
@@ -141,11 +142,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();
@@ -162,6 +159,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) {
@@ -177,13 +175,15 @@
             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);
 
@@ -220,17 +220,23 @@
       validateCommit(git, refControl, c, me, ins);
       updateRef(git, rw, c, change, ins.getPatchSet());
 
-      change.setTopic(input.topic);
+      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);
       ins.insert();
 
+      ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
       return Response.created(json.format(change.getId()));
     }
   }
 
   private void validateCommit(Repository git, RefControl refControl,
       RevCommit c, IdentifiedUser me, ChangeInserter ins)
-      throws InvalidChangeOperationException {
+      throws ResourceConflictException {
     PatchSet newPatchSet = ins.getPatchSet();
     CommitValidators commitValidators =
         commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
@@ -247,7 +253,7 @@
     try {
       commitValidators.validateForGerritCommits(commitReceivedEvent);
     } catch (CommitValidationException e) {
-      throw new InvalidChangeOperationException(e.getMessage());
+      throw new ResourceConflictException(e.getMessage());
     }
   }
 
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/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 6330e34..122156b 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.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -24,7 +25,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.EmailReviewCommentsExecutor;
-import com.google.gerrit.server.git.WorkQueue.Executor;
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.util.RequestContext;
@@ -40,9 +40,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);
@@ -57,7 +56,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;
@@ -73,7 +72,7 @@
 
   @Inject
   EmailReviewComments (
-      @EmailReviewCommentsExecutor final Executor executor,
+      @EmailReviewCommentsExecutor ExecutorService executor,
       PatchSetInfoFactory patchSetInfoFactory,
       CommentSender.Factory commentSenderFactory,
       SchemaFactory<ReviewDb> schemaFactory,
@@ -94,7 +93,7 @@
     this.patchSet = patchSet;
     this.authorId = authorId;
     this.message = message;
-    this.comments = comments;
+    this.comments = PLC_ORDER.sortedCopy(comments);
   }
 
   void sendAsync() {
@@ -103,33 +102,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(authorId);
       cm.setPatchSet(patchSet, patchSetInfoFactory.get(change, patchSet));
       cm.setChangeMessage(message);
@@ -138,7 +114,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;
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..9ddb0ed 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
@@ -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()) {
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/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..eef0533 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
@@ -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) {
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..4fbb6a8 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,261 +14,173 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.ArrayListMultimap;
+import static com.google.gerrit.server.index.ChangeField.GROUP;
+
+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.ChangeUtil;
 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.WalkSorter.PatchSetData;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GroupCollector;
+import com.google.gerrit.server.index.IndexCollection;
 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.lib.Config;
 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.ArrayList;
 import java.util.Collection;
 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 GetRelatedByAncestors byAncestors;
   private final Provider<InternalChangeQuery> queryProvider;
+  private final Provider<WalkSorter> sorter;
+  private final IndexCollection indexes;
+  private final boolean byAncestorsOnly;
 
   @Inject
-  GetRelated(GitRepositoryManager gitMgr,
-      Provider<ReviewDb> db,
-      Provider<InternalChangeQuery> queryProvider) {
-    this.gitMgr = gitMgr;
-    this.dbProvider = db;
+  GetRelated(Provider<ReviewDb> db,
+      @GerritServerConfig Config cfg,
+      GetRelatedByAncestors byAncestors,
+      Provider<InternalChangeQuery> queryProvider,
+      Provider<WalkSorter> sorter,
+      IndexCollection indexes) {
+    this.db = db;
+    this.byAncestors = byAncestors;
     this.queryProvider = queryProvider;
+    this.sorter = sorter;
+    this.indexes = indexes;
+    byAncestorsOnly =
+        cfg.getBoolean("change", null, "getRelatedByAncestors", false);
   }
 
   @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;
+    List<String> thisPatchSetGroups = GroupCollector.getGroups(rsrc);
+    if (byAncestorsOnly
+        || thisPatchSetGroups == null
+        || !indexes.getSearchIndex().getSchema().hasField(GROUP)) {
+      return byAncestors.getRelated(rsrc);
     }
+    RelatedInfo relatedInfo = new RelatedInfo();
+    relatedInfo.changes = getRelated(rsrc, thisPatchSetGroups);
+    return relatedInfo;
   }
 
-  private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
-      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);
+  private List<ChangeAndCommit> getRelated(RevisionResource rsrc,
+      List<String> thisPatchSetGroups) throws OrmException, IOException {
+    if (thisPatchSetGroups.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(),
+            getAllGroups(rsrc.getChange().getId()));
+    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.
+    PatchSet.Id editBaseId = rsrc.getEdit().isPresent()
+        ? rsrc.getEdit().get().getBasePatchSet().getId()
+        : null;
+    for (PatchSetData d : sorter.get()
+        .includePatchSets(choosePatchSets(thisPatchSetGroups, cds))
+        .setRetainBody(true)
+        .sort(cds)) {
+      PatchSet ps = d.patchSet();
+      RevCommit commit;
+      if (ps.getId().equals(editBaseId)) {
+        // 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 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);
+      }
+    }
+    return result;
   }
 
-  private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
-      Collection<ChangeData> cds) throws OrmException {
-    Map<PatchSet.Id, PatchSet> r =
-        Maps.newHashMapWithExpectedSize(cds.size() * 2);
+  private static Set<PatchSet.Id> choosePatchSets(List<String> groups,
+      List<ChangeData> cds) throws OrmException {
+    // Prefer the latest patch set matching at least one group from this
+    // revision; otherwise, just use the latest patch set overall.
+    Set<PatchSet.Id> result = new HashSet<>();
     for (ChangeData cd : cds) {
-      for (PatchSet p : cd.patches()) {
-        r.put(p.getId(), p);
+      Collection<PatchSet> patchSets = cd.patchSets();
+      List<PatchSet> sameGroup = new ArrayList<>(patchSets.size());
+      for (PatchSet ps : patchSets) {
+        if (hasAnyGroup(ps, groups)) {
+          sameGroup.add(ps);
+        }
       }
+      result.add(ChangeUtil.PS_ID_ORDER.max(
+          !sameGroup.isEmpty() ? sameGroup : patchSets).getId());
     }
-
-    if (rsrc.getEdit().isPresent()) {
-      r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
-    }
-    return r;
+    return result;
   }
 
-  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));
-          }
-        }
-      }
+  private static boolean hasAnyGroup(PatchSet ps, List<String> groups) {
+    if (ps.getGroups() == null) {
+      return false;
     }
-    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());
+    // Expected size of each list is 1, so nested linear search is fine.
+    for (String g1 : ps.getGroups()) {
+      for (String g2 : groups) {
+        if (g1.equals(g2)) {
+          return true;
+        }
+      }
     }
     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;
-  }
-
   public static class RelatedInfo {
     public List<ChangeAndCommit> changes;
   }
@@ -279,6 +191,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 +203,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 +217,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/GetRelatedByAncestors.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java
new file mode 100644
index 0000000..09d1ef0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.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.server.change;
+
+import com.google.common.collect.ArrayListMultimap;
+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.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.change.GetRelated.ChangeAndCommit;
+import com.google.gerrit.server.change.GetRelated.RelatedInfo;
+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.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 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.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Implementation of {@link GetRelated} using {@link PatchSetAncestor}s. */
+class GetRelatedByAncestors {
+  private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
+
+  private final GitRepositoryManager gitMgr;
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
+
+  @Inject
+  GetRelatedByAncestors(GitRepositoryManager gitMgr,
+      Provider<ReviewDb> db,
+      Provider<InternalChangeQuery> queryProvider) {
+    this.gitMgr = gitMgr;
+    this.dbProvider = db;
+    this.queryProvider = queryProvider;
+  }
+
+  public RelatedInfo getRelated(RevisionResource rsrc)
+      throws RepositoryNotFoundException, IOException, OrmException {
+    try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
+        RevWalk rw = new RevWalk(git)) {
+      Ref ref = git.getRefDatabase().exactRef(rsrc.getChange().getDest().get());
+      RelatedInfo info = new RelatedInfo();
+      info.changes = walk(rsrc, rw, ref);
+      return info;
+    }
+  }
+
+  private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
+      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);
+    }
+
+    RevCommit rev = rw.parseCommit(ObjectId.fromString(
+        rsrc.getPatchSet().getRevision().get()));
+    rw.sort(RevSort.TOPO);
+    rw.markStart(rev);
+
+    if (ref != null && ref.getObjectId() != null) {
+      try {
+        rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+      } catch (IncorrectObjectTypeException notCommit) {
+        // Ignore and treat as new branch.
+      }
+    }
+
+    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) {
+        g = changes.get(p.getId().getParentKey()).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())) {
+        return Collections.emptyList();
+      }
+    }
+    return list;
+  }
+
+  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.patchSets()) {
+        r.put(p.getId(), p);
+      }
+    }
+
+    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;
+  }
+
+
+}
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..abbc640 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,63 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Strings;
+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.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+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.OrmRuntimeException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Map;
+
 @Singleton
-public class GetRevisionActions implements RestReadView<RevisionResource> {
+public class GetRevisionActions implements ETagView<RevisionResource> {
   private final ActionJson delegate;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final Config config;
 
   @Inject
-  GetRevisionActions(ActionJson delegate) {
+  GetRevisionActions(
+      ActionJson delegate,
+      Provider<InternalChangeQuery> queryProvider,
+      @GerritServerConfig Config config) {
     this.delegate = delegate;
+    this.queryProvider = queryProvider;
+    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) {
+    String topic = rsrc.getChange().getTopic();
+    if (!Submit.wholeTopicEnabled(config)
+        || Strings.isNullOrEmpty(topic)) {
+      return rsrc.getETag();
+    }
+    Hasher h = Hashing.md5().newHasher();
+    CurrentUser user = rsrc.getControl().getCurrentUser();
+    try {
+      for (ChangeData c : queryProvider.get().byTopicOpen(topic)) {
+        new ChangeResource(c.changeControl()).prepareETag(h, user);
+      }
+    } catch (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..9f7eb93 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,8 +14,6 @@
 
 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;
@@ -45,7 +43,8 @@
 
 @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;
@@ -69,7 +68,7 @@
 
   public static String cleanupHashtag(String hashtag) {
     hashtag = LEADER.trimLeadingFrom(hashtag);
-    hashtag = WHITESPACE.trimTrailingFrom(hashtag);
+    hashtag = CharMatcher.whitespace().trimTrailingFrom(hashtag);
     return hashtag;
   }
 
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..e63363e 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.getCurrentUser().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..2b5d7d9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.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.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.IdentifiedUser;
+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().getCurrentUser().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    IdentifiedUser user = (IdentifiedUser) rsrc.getControl().getCurrentUser();
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    List<PatchLineComment> drafts =
+        plcUtil.draftByChangeAuthor(db.get(), cd.notes(), user.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..e318162 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,8 +22,7 @@
 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;
@@ -63,6 +61,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 +89,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 +109,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 +117,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 +183,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)
+    public Boolean call()
         throws NoSuchProjectException, MergeException, IOException {
-      checkArgument(key.load != null, "Key cannot be loaded: %s", key);
       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 (RevWalk rw = CodeReviewCommit.newRevWalk(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,
+            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) {
@@ -264,7 +240,7 @@
       return accepted;
     }
 
-    private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
+    private CodeReviewCommit parse(RevWalk rw, ObjectId id)
         throws MissingObjectException, IncorrectObjectTypeException,
         IOException {
       return (CodeReviewCommit) rw.parseCommit(id);
@@ -280,10 +256,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,10 +271,9 @@
   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);
+      return cache.get(key, new Loader(key, dest, repo, db));
     } catch (ExecutionException e) {
       log.error(String.format("Error checking mergeability of %s into %s (%s)",
             key.commit.name(), key.into.name(), key.submitType.name()),
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..f92b13d 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
@@ -52,6 +52,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,6 +65,7 @@
     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);
 
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..7ff1c89e 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
@@ -16,6 +16,7 @@
 
 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.collect.SetMultimap;
 import com.google.gerrit.common.ChangeHooks;
@@ -35,6 +36,7 @@
 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.GroupCollector;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
@@ -108,9 +110,11 @@
   private SshInfo sshInfo;
   private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
   private boolean draft;
+  private Iterable<String> groups;
   private boolean runHooks;
   private boolean sendMail;
   private Account.Id uploader;
+  private boolean allowClosed;
 
   @Inject
   public PatchSetInserter(ChangeHooks hooks,
@@ -171,7 +175,15 @@
     return patchSet.getId();
   }
 
-  public PatchSetInserter setMessage(String message) throws OrmException {
+  public PatchSet getPatchSet() {
+    checkState(patchSet != null,
+        "getPatchSet() only valid after patch set is created");
+    return patchSet;
+  }
+
+  public PatchSetInserter setMessage(String message)
+      throws OrmException, IOException {
+    init();
     changeMessage = new ChangeMessage(
         new ChangeMessage.Key(
             ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
@@ -200,6 +212,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,6 +227,11 @@
     return this;
   }
 
+  public PatchSetInserter setAllowClosed(boolean allowClosed) {
+    this.allowClosed = allowClosed;
+    return this;
+  }
+
   public PatchSetInserter setUploader(Account.Id uploader) {
     this.uploader = uploader;
     return this;
@@ -239,12 +261,18 @@
 
     db.changes().beginTransaction(c.getId());
     try {
-      if (!db.changes().get(c.getId()).getStatus().isOpen()) {
+      updatedChange = db.changes().get(c.getId());
+      if (!updatedChange.getStatus().isOpen() && !allowClosed) {
         throw new InvalidChangeOperationException(String.format(
             "Change %s is closed", c.getId()));
       }
 
       ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+      if (groups != null) {
+        patchSet.setGroups(groups);
+      } else {
+        patchSet.setGroups(GroupCollector.getCurrentGroups(db, c));
+      }
       db.patchSets().insert(Collections.singleton(patchSet));
 
       SetMultimap<ReviewerState, Account.Id> oldReviewers = sendMail
@@ -255,13 +283,13 @@
           db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
             @Override
             public Change update(Change change) {
-              if (change.getStatus().isClosed()) {
+              if (change.getStatus().isClosed() && !allowClosed) {
                 return null;
               }
               if (!change.currentPatchSetId().equals(currentPatchSetId)) {
                 return null;
               }
-              if (change.getStatus() != Change.Status.DRAFT) {
+              if (change.getStatus() != Change.Status.DRAFT && !allowClosed) {
                 change.setStatus(Change.Status.NEW);
               }
               change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
@@ -293,7 +321,7 @@
         try {
           PatchSetInfo info = patchSetInfoFactory.get(commit, patchSet.getId());
           ReplacePatchSetSender cm =
-              replacePatchSetFactory.create(updatedChange);
+              replacePatchSetFactory.create(c.getId());
           cm.setFrom(user.getAccountId());
           cm.setPatchSet(patchSet, info);
           cm.setChangeMessage(changeMessage);
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..62520f4 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
@@ -20,6 +20,7 @@
 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.extensions.webui.UiAction;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -29,7 +30,8 @@
 import java.util.Set;
 
 @Singleton
-public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput> {
+public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput>,
+    UiAction<ChangeResource>{
   private HashtagsUtil hashtagsUtil;
 
   @Inject
@@ -38,12 +40,12 @@
   }
 
   @Override
-  public Response<? extends Set<String>> apply(ChangeResource req, HashtagsInput input)
+  public Response<Set<String>> apply(ChangeResource req, HashtagsInput input)
       throws AuthException, OrmException, IOException, BadRequestException,
       ResourceConflictException {
 
     try {
-      return Response.ok(hashtagsUtil.setHashtags(
+      return Response.<Set<String>> ok(hashtagsUtil.setHashtags(
           req.getControl(), input, true, true));
     } catch (IllegalArgumentException e) {
       throw new BadRequestException(e.getMessage());
@@ -51,4 +53,11 @@
       throw new ResourceConflictException(e.getMessage());
     }
   }
-}
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Edit Hashtags")
+      .setVisible(resource.getControl().canEditHashtags());
+  }
+}
\ No newline at end of file
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 8e989b6..e10077a 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
@@ -22,8 +22,6 @@
 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.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
@@ -180,11 +178,8 @@
       db.get().rollback();
     }
 
-    CheckedFuture<?, IOException> indexWrite;
     if (dirty) {
-      indexWrite = indexer.indexAsync(change.getId());
-    } else {
-      indexWrite = Futures.<Void, IOException> immediateCheckedFuture(null);
+      indexer.index(db.get(), change);
     }
     if (message != null && input.notify.compareTo(NotifyHandling.NONE) > 0) {
       email.create(
@@ -198,7 +193,6 @@
 
     Output output = new Output();
     output.labels = input.labels;
-    indexWrite.checkedGet();
     if (message != null) {
       fireCommentAddedHook(revision);
     }
@@ -351,7 +345,11 @@
 
     Map<String, PatchLineComment> drafts = Collections.emptyMap();
     if (!in.isEmpty() || draftsHandling != DraftHandling.KEEP) {
-      drafts = scanDraftComments(rsrc);
+      if (draftsHandling == DraftHandling.PUBLISH_ALL_REVISIONS) {
+        drafts = changeDrafts(rsrc);
+      } else {
+        drafts = patchSetDrafts(rsrc);
+      }
     }
 
     List<PatchLineComment> del = Lists.newArrayList();
@@ -376,8 +374,7 @@
         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());
+        setCommentRevId(e, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
         e.setMessage(c.message);
         if (c.range != null) {
           e.setRange(new CommentRange(
@@ -399,11 +396,11 @@
         del.addAll(drafts.values());
         break;
       case PUBLISH:
+      case PUBLISH_ALL_REVISIONS:
         for (PatchLineComment e : drafts.values()) {
           e.setStatus(PatchLineComment.Status.PUBLISHED);
           e.setWrittenOn(timestamp);
-          setCommentRevId(e, patchListCache, rsrc.getChange(),
-              rsrc.getPatchSet());
+          setCommentRevId(e, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
           ups.add(e);
         }
         break;
@@ -414,8 +411,18 @@
     return !del.isEmpty() || !ups.isEmpty();
   }
 
-  private Map<String, PatchLineComment> scanDraftComments(
-      RevisionResource rsrc) throws OrmException {
+  private Map<String, PatchLineComment> changeDrafts(RevisionResource rsrc)
+      throws OrmException {
+    Map<String, PatchLineComment> drafts = Maps.newHashMap();
+    for (PatchLineComment c : plcUtil.draftByChangeAuthor(
+        db.get(), rsrc.getNotes(), rsrc.getAccountId())) {
+      drafts.put(c.getKey().get(), c);
+    }
+    return drafts;
+  }
+
+  private Map<String, PatchLineComment> patchSetDrafts(RevisionResource rsrc)
+      throws OrmException {
     Map<String, PatchLineComment> drafts = Maps.newHashMap();
     for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(db.get(),
         rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes())) {
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..fc27010 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
@@ -255,8 +255,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) {
@@ -283,7 +283,7 @@
     }
     if (!toMail.isEmpty()) {
       try {
-        AddReviewerSender cm = addReviewerSenderFactory.create(change);
+        AddReviewerSender cm = addReviewerSenderFactory.create(change.getId());
         cm.setFrom(identifiedUser.getAccountId());
         cm.addReviewers(toMail);
         cm.send();
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 711cf18..b88931e 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,6 @@
 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.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -85,7 +84,7 @@
     @Override
     public Response<?> apply(ChangeResource rsrc, Publish.Input in)
         throws AuthException, ResourceConflictException, NoSuchChangeException,
-        IOException, InvalidChangeOperationException, OrmException {
+        IOException, OrmException {
       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..dd9a44d 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,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -82,13 +81,11 @@
 
     if (!updatedPatchSet.isDraft()
         || updatedChange.getStatus() == Change.Status.NEW) {
-      CheckedFuture<?, IOException> indexFuture =
-          indexer.indexAsync(updatedChange.getId());
+      indexer.index(dbProvider.get(), updatedChange);
       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());
     }
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/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index d2afd9a..d4a7bfc 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,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -26,10 +27,10 @@
 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.Project;
 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.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -38,188 +39,180 @@
 import com.google.inject.Provider;
 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 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 GitRepositoryManager repoManager;
   private final Provider<RebaseChange> rebaseChange;
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json,
+  public Rebase(GitRepositoryManager repoManager,
+      Provider<RebaseChange> rebaseChange,
+      ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider) {
+    this.repoManager = repoManager;
     this.rebaseChange = rebaseChange;
-    this.json = json
-        .addOption(ListChangesOption.CURRENT_REVISION)
-        .addOption(ListChangesOption.CURRENT_COMMIT);
+    this.json = json;
     this.dbProvider = dbProvider;
   }
 
   @Override
   public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
       throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, EmailException, OrmException {
+      ResourceConflictException, EmailException, OrmException, 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);
+    try (Repository repo = repoManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      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");
+      }
+      rebaseChange.get().rebase(repo, rw, rsrc, findBaseRev(rw, rsrc, input));
     } 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());
+    return json.create(OPTIONS).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());
-        }
-      }
+  private String findBaseRev(RevWalk rw, RevisionResource rsrc,
+      RebaseInput input) throws AuthException, ResourceConflictException,
+      OrmException, IOException {
+    if (input == null || input.base == null) {
+      return null;
     }
 
-    return false;
+    Change change = rsrc.getChange();
+    String base = input.base.trim();
+    if (base.equals("")) {
+      // remove existing dependency to other patch set
+      return change.getDest().get();
+    }
+
+    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 PatchSet parseBase(final String base) throws OrmException {
+  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) {
+    Project.NameKey project = resource.getChange().getProject();
+    boolean visible = resource.getChange().getStatus().isOpen()
+          && resource.isCurrent()
+          && resource.getControl().canRebase();
+    if (visible) {
+      try (Repository repo = repoManager.openRepository(project);
+          RevWalk rw = new RevWalk(repo)) {
+        visible = hasOneParent(rw, resource.getPatchSet());
+      } catch (IOException e) {
+        log.error("Failed to get ancestors of patch set "
+            + resource.getPatchSet().getId(), 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()));
+      .setVisible(visible);
     if (descr.isVisible()) {
       // Disable the rebase button in the RebaseDialog if
       // the change cannot be rebased.
@@ -240,7 +233,7 @@
     @Override
     public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
         throws AuthException, ResourceNotFoundException,
-        ResourceConflictException, EmailException, OrmException {
+        ResourceConflictException, EmailException, OrmException, IOException {
       PatchSet ps =
           rebase.dbProvider.get().patchSets()
               .get(rsrc.getChange().currentPatchSetId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
new file mode 100644
index 0000000..030aa92
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
@@ -0,0 +1,356 @@
+// 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.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+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.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.PatchSetInserter.ValidatePolicy;
+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.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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.TimeZone;
+
+@Singleton
+public class RebaseChange {
+  private static final Logger log = LoggerFactory.getLogger(RebaseChange.class);
+
+  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(ChangeControl.GenericFactory changeControlFactory,
+      Provider<ReviewDb> db,
+      @GerritPersonIdent PersonIdent myIdent,
+      GitRepositoryManager gitManager,
+      MergeUtil.Factory mergeUtilFactory,
+      PatchSetInserter.Factory patchSetInserterFactory) {
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.gitManager = gitManager;
+    this.serverTimeZone = myIdent.getTimeZone();
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+  }
+
+  /**
+   * Rebase the change of the given patch set.
+   * <p>
+   * If the patch set has no dependency to an open change, then the change is
+   * rebased on the tip of the destination branch.
+   * <p>
+   * If the patch set depends on an open change, it is rebased on the latest
+   * patch set of this change.
+   * <p>
+   * The rebased commit is added as new patch set to the change.
+   * <p>
+   * E-mail notification and triggering of hooks happens for the creation of the
+   * new patch set.
+   *
+   * @param git the repository.
+   * @param rw the RevWalk.
+   * @param rsrc revision to rebase.
+   * @param newBaseRev the commit that should be the new base.
+   * @throws NoSuchChangeException if the change to which the patch set belongs
+   *     does not exist or is not visible to the user.
+   * @throws EmailException if sending the e-mail to notify about the new patch
+   *     set fails.
+   * @throws OrmException if accessing the database fails.
+   * @throws IOException if accessing the repository fails.
+   * @throws InvalidChangeOperationException if rebase is not possible or not
+   *     allowed.
+   */
+  public void rebase(Repository git, RevWalk rw, RevisionResource rsrc,
+      String newBaseRev) throws NoSuchChangeException, EmailException,
+          OrmException, IOException, ResourceConflictException,
+          InvalidChangeOperationException {
+    Change change = rsrc.getChange();
+    PatchSet patchSet = rsrc.getPatchSet();
+    IdentifiedUser uploader = (IdentifiedUser) rsrc.getControl().getCurrentUser();
+
+    try (ObjectInserter inserter = git.newObjectInserter()) {
+      String baseRev = newBaseRev;
+      if (baseRev == null) {
+        baseRev = findBaseRevision(patchSet, change.getDest(), git, rw);
+      }
+
+      ObjectId baseObjectId = git.resolve(baseRev);
+      if (baseObjectId == null) {
+        throw new InvalidChangeOperationException(
+          "Cannot rebase: Failed to resolve baseRev: " + baseRev);
+      }
+
+      RevCommit baseCommit = rw.parseCommit(baseObjectId);
+      PersonIdent committerIdent =
+          uploader.newCommitterIdent(TimeUtil.nowTs(), serverTimeZone);
+
+      rebase(git, rw, inserter, change, patchSet.getId(),
+          uploader, baseCommit, mergeUtilFactory.create(
+              rsrc.getControl().getProjectControl().getProjectState(), true),
+          committerIdent, true, ValidatePolicy.GERRIT);
+    } catch (MergeConflictException e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+  }
+
+  /**
+   * 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 InvalidChangeOperationException if rebase is not possible or not
+   *     allowed.
+   * @throws IOException if accessing the repository fails.
+   * @throws OrmException if accessing the database fails.
+   */
+  private String findBaseRevision(PatchSet patchSet,
+      Branch.NameKey destBranch, Repository git, RevWalk rw)
+      throws InvalidChangeOperationException, IOException, OrmException {
+    String baseRev = null;
+    RevCommit commit = rw.parseCommit(
+        ObjectId.fromString(patchSet.getRevision().get()));
+
+    if (commit.getParentCount() > 1) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase a change with multiple parents.");
+    } else if (commit.getParentCount() == 0) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase a change without any parents"
+          + " (is this the initial commit?).");
+    }
+
+    RevId parentRev = new RevId(commit.getParent(0).name());
+
+    for (PatchSet depPatchSet : db.get().patchSets().byRevision(parentRev)) {
+      Change.Id depChangeId = depPatchSet.getId().getParentKey();
+      Change depChange = db.get().changes().get(depChangeId);
+      if (!depChange.getDest().equals(destBranch)) {
+        continue;
+      }
+
+      if (depChange.getStatus() == Status.ABANDONED) {
+        throw new InvalidChangeOperationException(
+            "Cannot rebase a change with an abandoned parent: "
+            + depChange.getKey());
+      }
+
+      if (depChange.getStatus().isOpen()) {
+        if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+          throw new InvalidChangeOperationException(
+              "Change is already based on the latest patch set of the"
+              + " dependent change.");
+        }
+        PatchSet latestDepPatchSet =
+            db.get().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 InvalidChangeOperationException(
+            "The destination branch does not exist: " + destBranch.get());
+      }
+      baseRev = destRef.getObjectId().getName();
+      if (baseRev.equals(parentRev.get())) {
+        throw new InvalidChangeOperationException(
+            "Change is already up to date.");
+      }
+    }
+    return baseRev;
+  }
+
+  /**
+   * Rebase the change of the given patch set on the given base commit.
+   * <p>
+   * The rebased commit is added as new patch set to the change.
+   * <p>
+   * E-mail notification and triggering of hooks is only done for the creation
+   * of the new patch set if {@code sendEmail} and {@code runHooks} are true,
+   * respectively.
+   *
+   * @param git the repository.
+   * @param inserter the object inserter.
+   * @param change the change to rebase.
+   * @param patchSetId the patch set ID to rebase.
+   * @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.
+   * @param rw the RevWalk.
+   * @return the new patch set, which is based on the given base commit.
+   * @throws NoSuchChangeException if the change to which the patch set belongs
+   *     does not exist or is not visible to the user.
+   * @throws OrmException if accessing the database fails.
+   * @throws IOException if rebase is not possible.
+   * @throws InvalidChangeOperationException if rebase is not possible or not
+   *     allowed.
+   */
+  public PatchSet rebase(Repository git, RevWalk rw,
+      ObjectInserter inserter, Change change, PatchSet.Id patchSetId,
+      IdentifiedUser uploader, RevCommit baseCommit, 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");
+    }
+    PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
+
+    RevCommit rebasedCommit;
+    ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
+    ObjectId newId = rebaseCommit(git, inserter, rw.parseCommit(oldId),
+        baseCommit, mergeUtil, committerIdent);
+
+    rebasedCommit = rw.parseCommit(newId);
+
+    ChangeControl changeControl =
+        changeControlFactory.validateFor(change, uploader);
+
+    PatchSetInserter patchSetInserter = patchSetInserterFactory
+        .create(git, rw, changeControl, rebasedCommit)
+        .setValidatePolicy(validate)
+        .setDraft(originalPatchSet.isDraft())
+        .setUploader(uploader.getAccountId())
+        .setSendMail(false)
+        .setRunHooks(runHooks);
+
+    PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
+    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,
+      InvalidChangeOperationException {
+    RevCommit parentCommit = original.getParent(0);
+
+    if (base.equals(parentCommit)) {
+      throw new InvalidChangeOperationException(
+          "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(RevisionResource r) {
+    return canRebase(r.getPatchSet(), r.getChange().getDest());
+  }
+
+  private boolean canRebase(PatchSet patchSet, Branch.NameKey dest) {
+    try (Repository git = gitManager.openRepository(dest.getParentKey());
+        RevWalk rw = new RevWalk(git)) {
+      findBaseRevision(patchSet, dest, git, rw);
+      return true;
+    } catch (InvalidChangeOperationException 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;
+    }
+  }
+}
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..c09aa3b 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,7 +15,6 @@
 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.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -54,7 +53,7 @@
   private final ChangeHooks hooks;
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
-  private final ChangeJson json;
+  private final ChangeJson.Factory json;
   private final ChangeIndexer indexer;
   private final ChangeMessagesUtil cmUtil;
   private final ChangeUpdate.Factory updateFactory;
@@ -63,7 +62,7 @@
   Restore(ChangeHooks hooks,
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
-      ChangeJson json,
+      ChangeJson.Factory json,
       ChangeIndexer indexer,
       ChangeMessagesUtil cmUtil,
       ChangeUpdate.Factory updateFactory) {
@@ -122,24 +121,22 @@
     }
     update.commit();
 
-    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
+    indexer.index(db, change);
 
     try {
-      ReplyToChangeSender cm = restoredSenderFactory.create(change);
+      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.getChangeId(), e);
     }
-    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;
+    return json.create(ChangeJson.NO_OPTIONS).format(change);
   }
 
   @Override
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..aaae6f0 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
@@ -44,12 +44,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;
@@ -69,18 +69,19 @@
       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);
+      revertedChangeId = changeUtil.revert(control,
+            change.currentPatchSetId(),
+            Strings.emptyToNull(input.message),
+            new PersonIdent(myIdent, TimeUtil.nowTs()),
+            new NoSshInfo());
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
     } 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 c4c9186..093506c 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
@@ -49,7 +49,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 +62,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 +90,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 {
@@ -147,9 +146,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..538f36b 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
@@ -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/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 935d448..6bba656 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,12 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.gerrit.common.data.SubmitRecord.Status.OK;
-
 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.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 +27,24 @@
 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.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.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,18 +52,12 @@
 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.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.List;
 import java.util.Map;
 
@@ -93,83 +68,79 @@
 
   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 CLICK_FAILURE_OTHER_TOOLTIP =
+      "Clicking the button would fail for other changes";
 
   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 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.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");
@@ -206,27 +177,22 @@
           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())));
-    }
+    ChangeSet submittedChanges = ChangeSet.create(change);
 
-    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, submittedChanges, 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 +205,65 @@
   }
 
   /**
-   * @param changes list of changes to be submitted at once
+   * @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,
-      IdentifiedUser identifiedUser) {
-    for (ChangeData c : changes) {
-      try {
-        ChangeControl changeControl = c.changeControl().forUser(
-            identifiedUser);
-        if (!changeControl.isVisible(dbProvider.get())) {
-          return BLOCKED_HIDDEN_TOPIC_TOOLTIP;
+  private String problemsForSubmittingChangeset(
+      ChangeSet cs, IdentifiedUser identifiedUser) {
+    try {
+      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;
+        // 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.
+        c.setMergeable(null);
+        Boolean mergeable = c.isMergeable();
+        if (mergeable == null) {
+          log.error("Ephemeral error checking if change is submittable");
+          return CLICK_FAILURE_TOOLTIP;
+        }
+        if (!mergeable) {
+          return CLICK_FAILURE_OTHER_TOOLTIP;
+        }
+        MergeOp.checkSubmitRule(c);
       }
+    } catch (ResourceConflictException e) {
+      return BLOCKED_SUBMIT_TOOLTIP;
+    } catch (NoSuchChangeException | OrmException 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 +272,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 +290,65 @@
         .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);
-      }
+
+    Boolean enabled;
+    try {
+      enabled = cd.isMergeable();
+    } catch (OrmException e) {
+      throw new OrmRuntimeException("Could not determine mergeability", e);
+    }
+
+    ChangeSet cs;
+    try {
+      cs = mergeSuperSet.completeChangeSet(db,
+          ChangeSet.create(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(cs,
+        resource.getUser());
+    if (submitProblems != null) {
+      return new UiAction.Description()
+        .setLabel(treatWithTopic ? submitTopicLabel : 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);
+        .setTitle(Strings.emptyToNull(tp.replace(params)))
+        .setVisible(true)
+        .setEnabled(Boolean.TRUE.equals(enabled));
     }
   }
 
@@ -345,288 +371,6 @@
         .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";
   }
@@ -654,16 +398,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 +437,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..1f6dfe4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.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.change;
+
+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.server.ReviewDb;
+import com.google.gerrit.server.git.ChangeSet;
+import com.google.gerrit.server.git.MergeSuperSet;
+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.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 MergeSuperSet mergeSuperSet;
+
+  @Inject
+  SubmittedTogether(ChangeJson.Factory json,
+      Provider<ReviewDb> dbProvider,
+      MergeSuperSet mergeSuperSet) {
+    this.json = json;
+    this.dbProvider = dbProvider;
+    this.mergeSuperSet = mergeSuperSet;
+  }
+
+  @Override
+  public List<ChangeInfo> apply(ChangeResource resource)
+      throws AuthException, BadRequestException,
+      ResourceConflictException, Exception {
+    try {
+      ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(),
+          ChangeSet.create(resource.getChange()));
+      if (cs.ids().size() > 1) {
+        return json.create(EnumSet.of(
+            ListChangesOption.CURRENT_REVISION,
+            ListChangesOption.CURRENT_COMMIT))
+          .format(cs.ids());
+      } else {
+        return Collections.emptyList();
+      }
+    } catch (OrmException | IOException e) {
+      log.error("Error on getting a ChangeSet", e);
+      throw e;
+    }
+  }
+}
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..31e34cf 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
@@ -106,7 +106,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..163fb10 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;
@@ -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,6 +88,8 @@
     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);
 
 
@@ -141,10 +156,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;
   }
@@ -194,6 +225,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,6 +299,14 @@
     return registerPageUrl;
   }
 
+  public String getRegisterUrl() {
+    return registerUrl;
+  }
+
+  public String getRegisterText() {
+    return registerText;
+  }
+
   public boolean isLdapAuthType() {
     return authType == AuthType.LDAP ||
         authType == AuthType.LDAP_BIND;
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..37a6869
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -0,0 +1,83 @@
+// 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.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 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;
+    }
+    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..e4d841a 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
@@ -31,15 +31,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);
     }
   }
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..789af9d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfirmEmail.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.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.IdentifiedUser;
+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();
+    }
+
+    try {
+      EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(input.token);
+      Account.Id accId = ((IdentifiedUser)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");
+    }
+  }
+}
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..4986989 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
@@ -16,12 +16,14 @@
 
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+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.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -31,6 +33,7 @@
 public class DownloadConfig {
   private final Set<DownloadScheme> downloadSchemes;
   private final Set<DownloadCommand> downloadCommands;
+  private final Set<ArchiveFormat> archiveFormats;
 
   @Inject
   DownloadConfig(@GerritServerConfig final Config cfg) {
@@ -45,6 +48,17 @@
             DownloadCommand.DEFAULT_DOWNLOADS);
     downloadCommands =
         Collections.unmodifiableSet(new HashSet<>(allCommands));
+
+    String v = cfg.getString("download", null, "archive");
+    if (v == null) {
+      archiveFormats = EnumSet.allOf(ArchiveFormat.class);
+    } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
+      archiveFormats = Collections.emptySet();
+    } else {
+      archiveFormats = new HashSet<>(ConfigUtil.getEnumList(cfg,
+          "download", null, "archive",
+          ArchiveFormat.TGZ));
+    }
   }
 
   /** Scheme used to download. */
@@ -56,4 +70,9 @@
   public Set<DownloadCommand> getDownloadCommands() {
     return downloadCommands;
   }
+
+  /** Archive formats for downloading. */
+  public Set<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..478febe 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,11 @@
 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.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -38,7 +41,6 @@
 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,24 +65,23 @@
 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.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;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.gpg.SignedPushModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
@@ -113,7 +114,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 +125,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 +140,6 @@
 import org.eclipse.jgit.transport.PreUploadHook;
 
 import java.util.List;
-import java.util.Set;
 
 
 /** Starts global state with standard dependencies. */
@@ -179,6 +179,7 @@
     install(new NoteDbModule());
     install(new PrologModule());
     install(new SshAddressesModule());
+    install(new SignedPushModule());
     install(ThreadLocalRequestContext.module());
 
     bind(AccountResolver.class);
@@ -187,29 +188,27 @@
     factory(AddReviewerSender.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,12 +220,12 @@
     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)
@@ -261,6 +260,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,6 +280,8 @@
     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);
@@ -297,9 +299,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..d481b44 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
@@ -18,8 +18,6 @@
 
 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 +32,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..2ebdadf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -0,0 +1,409 @@
+// 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.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.gerrit.server.git.gpg.SignedPushModule;
+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 final static String URL_ALIAS = "urlAlias";
+  private final static String KEY_MATCH = "match";
+  private final static 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;
+
+  @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) {
+    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;
+  }
+
+  @Override
+  public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
+    ServerInfo info = new ServerInfo();
+    info.auth = getAuthInfo(authConfig, realm);
+    info.change = getChangeInfo(config);
+    info.contactStore = getContactStoreInfo();
+    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(config);
+    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 ContactStoreInfo getContactStoreInfo() {
+    String url = config.getString("contactstore", null, "url");
+    if (url == null) {
+      return null;
+    }
+
+    ContactStoreInfo contactStore = new ContactStoreInfo();
+    contactStore.url = url;
+    return contactStore;
+  }
+
+  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);
+    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(Config cfg) {
+    ReceiveInfo info = new ReceiveInfo();
+    info.enableSignedPush = SignedPushModule.isEnabled(cfg);
+    return info;
+  }
+
+  private static Boolean toBoolean(boolean v) {
+    return v ? v : null;
+  }
+
+  public static class ServerInfo {
+    public AuthInfo auth;
+    public ChangeConfigInfo change;
+    public ContactStoreInfo contactStore;
+    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 ContactStoreInfo {
+    public String url;
+  }
+
+  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 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..48fe57f 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,7 +89,9 @@
   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;
@@ -186,7 +189,8 @@
     } catch (UnknownHostException e) {
     }
 
-    jvmSummary.currentWorkingDirectory = path(new File(".").getAbsoluteFile().getParentFile());
+    jvmSummary.currentWorkingDirectory =
+        path(Paths.get(".").toAbsolutePath().getParent());
     jvmSummary.site = path(sitePath);
     return jvmSummary;
   }
@@ -210,11 +214,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..65b0661 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")
@@ -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..44a70d4 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,9 +14,11 @@
 
 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;
@@ -31,7 +33,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 +54,7 @@
   }
 
   public static enum Operation {
-    FLUSH_ALL, FLUSH;
+    FLUSH_ALL, FLUSH
   }
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
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/ProjectOwnerGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
deleted file mode 100644
index 876c51f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
+++ /dev/null
@@ -1,30 +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.config;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.inject.BindingAnnotation;
-
-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.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface ProjectOwnerGroups {
-}
\ No newline at end of file
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/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/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index fbff7c4..a6a3e53 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,84 @@
   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 Path contact_information_pub;
 
-  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");
+    contact_information_pub = etc_dir.resolve("contact_information.pub");
 
-    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 +119,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/ContactStoreModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
index 6b195de..2d0f4f1 100644
--- 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
@@ -18,22 +18,19 @@
 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.util.BouncyCastleUtil;
 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;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /** Creates the {@link ContactStore} based on the configuration. */
 public class ContactStoreModule extends AbstractModule {
@@ -46,51 +43,30 @@
   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");
+    String url = config.getString("contactstore", null, "url");
     if (StringUtils.isEmptyOrNull(url)) {
       return new NoContactStore();
     }
 
-    if (!havePGP()) {
+    if (!BouncyCastleUtil.havePGP()) {
       throw new ProvisionException("BouncyCastle PGP not installed; "
           + " needed to encrypt contact information");
     }
 
-    final URL storeUrl;
+    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()) {
+    String storeAPPSEC = config.getString("contactstore", null, "appsec");
+    Path pubkey = site.contact_information_pub;
+    if (!Files.exists(pubkey)) {
       throw new ProvisionException("PGP public key file \""
-          + pubkey.getAbsolutePath() + "\" not found");
+          + pubkey.toAbsolutePath() + "\" 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
index 4048748..ad4652b 100644
--- 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
@@ -45,12 +45,12 @@
 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.nio.file.Files;
+import java.nio.file.Path;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.sql.Timestamp;
@@ -74,7 +74,7 @@
   private final ContactStoreConnection.Factory connFactory;
 
   EncryptedContactStore(final URL storeUrl, final String storeAPPSEC,
-      final File pubKey, final SchemaFactory<ReviewDb> schema,
+      final Path pubKey, final SchemaFactory<ReviewDb> schema,
       final ContactStoreConnection.Factory connFactory) {
     this.storeUrl = storeUrl;
     this.storeAPPSEC = storeAPPSEC;
@@ -106,8 +106,8 @@
     return true;
   }
 
-  private static PGPPublicKeyRingCollection readPubRing(final File pub) {
-    try (InputStream fin = new FileInputStream(pub);
+  private static PGPPublicKeyRingCollection readPubRing(Path pub) {
+    try (InputStream fin = Files.newInputStream(pub);
         InputStream in = PGPUtil.getDecoderStream(fin)) {
         return new BcPGPPublicKeyRingCollection(in);
     } catch (IOException | PGPException e) {
@@ -176,11 +176,10 @@
     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();
+    try (ArmoredOutputStream aout = new ArmoredOutputStream(buf);
+        OutputStream cout = cpk().open(aout, zText.length)) {
+      cout.write(zText);
+    }
 
     return buf.toByteArray();
   }
@@ -195,12 +194,13 @@
     }
 
     comdg = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
-    final OutputStream out =
+    try (OutputStream out =
         new PGPLiteralDataGenerator().open(comdg.open(buf),
-            PGPLiteralData.BINARY, fileName, len, fileDate);
-    out.write(plainText);
-    out.close();
-    comdg.close();
+            PGPLiteralData.BINARY, fileName, len, fileDate)) {
+      out.write(plainText);
+    } finally {
+      comdg.close(); // PGPCompressedDataGenerator doesn't implement Closable
+    }
     return buf.toByteArray();
   }
 
@@ -220,30 +220,25 @@
     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());
+    try (ReviewDb db = schema.open()) {
+      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(' ');
           }
-          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());
+          oistr.append(e.getEmailAddress());
         }
-      } finally {
-        db.close();
+        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());
       }
     } catch (OrmException e) {
       throw new ContactInformationStoreException(e);
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
index 471f6a2..ac500de 100644
--- 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
@@ -55,11 +55,8 @@
       throw new IOException("Connection failed: " + conn.getResponseCode());
     }
     final byte[] dst = new byte[2];
-    final InputStream in = conn.getInputStream();
-    try {
+    try (InputStream in = conn.getInputStream()) {
       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/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index 03441e7..c705a7d 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
@@ -156,17 +156,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 67de914..67bdd0b 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();
   }
@@ -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;
       }
     }
   }
@@ -389,7 +401,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 0a65527..7682b83 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,7 +17,6 @@
 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;
@@ -30,8 +29,11 @@
 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.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -50,7 +52,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
-import java.util.Map;
 
 /**
  * Utility functions to manipulate change edits.
@@ -63,20 +64,26 @@
   private final GitRepositoryManager gitManager;
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
+  private final ChangeIndexer indexer;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
+  private final ChangeKindCache changeKindCache;
 
   @Inject
   ChangeEditUtil(GitRepositoryManager gitManager,
       PatchSetInserter.Factory patchSetInserterFactory,
       ChangeControl.GenericFactory changeControlFactory,
+      ChangeIndexer indexer,
       Provider<ReviewDb> db,
-      Provider<CurrentUser> user) {
+      Provider<CurrentUser> user,
+      ChangeKindCache changeKindCache) {
     this.gitManager = gitManager;
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.changeControlFactory = changeControlFactory;
+    this.indexer = indexer;
     this.db = db;
     this.user = user;
+    this.changeKindCache = changeKindCache;
   }
 
   /**
@@ -109,16 +116,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,16 +139,13 @@
    * its parent.
    *
    * @param edit change edit to publish
-   * @throws AuthException
    * @throws NoSuchChangeException
    * @throws IOException
-   * @throws InvalidChangeOperationException
    * @throws OrmException
    * @throws ResourceConflictException
    */
-  public void publish(ChangeEdit edit) throws AuthException,
-      NoSuchChangeException, IOException, InvalidChangeOperationException,
-      OrmException, ResourceConflictException {
+  public void publish(ChangeEdit edit) throws NoSuchChangeException,
+      IOException, OrmException, ResourceConflictException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         RevWalk rw = new RevWalk(repo);
@@ -152,10 +156,16 @@
             "only edit for current patch set can be published");
       }
 
-      insertPatchSet(edit, change, repo, rw, basePatchSet,
-          squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
-      // TODO(davido): This should happen in the same BatchRefUpdate.
-      deleteRef(repo, edit);
+      try {
+        Change updatedChange =
+            insertPatchSet(edit, change, repo, rw, basePatchSet,
+                squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
+        // TODO(davido): This should happen in the same BatchRefUpdate.
+        deleteRef(repo, edit);
+        indexer.index(db.get(), updatedChange);
+      } catch (InvalidChangeOperationException e) {
+        throw new ResourceConflictException(e.getMessage());
+      }
     }
   }
 
@@ -168,12 +178,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)
@@ -183,8 +191,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);
     }
   }
@@ -201,7 +209,7 @@
     return writeSquashedCommit(rw, inserter, parent, edit);
   }
 
-  private void insertPatchSet(ChangeEdit edit, Change change,
+  private Change insertPatchSet(ChangeEdit edit, Change change,
       Repository repo, RevWalk rw, PatchSet basePatchSet, RevCommit squashed)
       throws NoSuchChangeException, InvalidChangeOperationException,
       OrmException, IOException {
@@ -211,17 +219,27 @@
     ps.setUploader(edit.getUser().getAccountId());
     ps.setCreatedOn(TimeUtil.nowTs());
 
-    PatchSetInserter insr =
+    StringBuilder message = new StringBuilder("Patch set ")
+      .append(ps.getPatchSetId())
+      .append(": ");
+
+    ChangeKind kind = changeKindCache.getChangeKind(db.get(), change, ps);
+    if (kind == ChangeKind.NO_CODE_CHANGE) {
+      message.append("Commit message was updated.");
+    } else {
+      message.append("Published edit on patch set ")
+        .append(basePatchSet.getPatchSetId())
+        .append(".");
+    }
+
+    PatchSetInserter inserter =
         patchSetInserterFactory.create(repo, rw,
             changeControlFactory.controlFor(change, edit.getUser()),
             squashed);
-    insr.setPatchSet(ps)
+    return inserter.setPatchSet(ps)
         .setDraft(change.getStatus() == Status.DRAFT ||
             basePatchSet.isDraft())
-        .setMessage(
-            String.format("Patch Set %d: Published edit on patch set %d",
-                ps.getPatchSetId(),
-                basePatchSet.getPatchSetId()))
+        .setMessage(message.toString())
         .insert();
   }
 
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..2b1e3dd 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
@@ -151,7 +151,7 @@
     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;
   }
 
@@ -229,39 +229,34 @@
   public void addDependencies(ChangeAttribute ca, Change change) {
     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));
-          }
+    try (ReviewDb db = schema.open()) {
+      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));
+      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) {
       // Squash DB exceptions and leave dependency lists partially filled.
@@ -401,38 +396,33 @@
     p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
     p.isDraft = patchSet.isDraft();
     final 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();
+    try (ReviewDb db = schema.open()) {
+      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);
     } catch (OrmException e) {
       log.error("Cannot load patch set data for " + patchSet.getId(), e);
     } catch (PatchSetInfoNotAvailableException e) {
@@ -489,6 +479,10 @@
    * @return object suitable for serialization to JSON
    */
   public AccountAttribute asAccountAttribute(final Account account) {
+    if (account == null) {
+      return null;
+    }
+
     AccountAttribute who = new AccountAttribute();
     who.name = account.getFullName();
     who.email = account.getPreferredEmail();
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/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index d8e56b3..eab2785 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
@@ -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();
       }
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..a18a3a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.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.git;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+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;
+
+/** A set of changes grouped together to be submitted atomically.*/
+@AutoValue
+public abstract class ChangeSet {
+  public static ChangeSet create(Iterable<Change> changes) {
+    ImmutableSet.Builder<Project.NameKey> pb = ImmutableSet.builder();
+    ImmutableSet.Builder<Branch.NameKey> bb = ImmutableSet.builder();
+    ImmutableSet.Builder<Change.Id> ib = ImmutableSet.builder();
+    ImmutableSet.Builder<PatchSet.Id> psb = ImmutableSet.builder();
+    ImmutableSetMultimap.Builder<Project.NameKey, Branch.NameKey> pbb =
+        ImmutableSetMultimap.builder();
+    ImmutableSetMultimap.Builder<Project.NameKey, Change.Id> pcb =
+        ImmutableSetMultimap.builder();
+    ImmutableSetMultimap.Builder<Branch.NameKey, Change.Id> cbb =
+        ImmutableSetMultimap.builder();
+
+    for (Change c : changes) {
+      Branch.NameKey branch = c.getDest();
+      Project.NameKey project = branch.getParentKey();
+      pb.add(project);
+      bb.add(branch);
+      ib.add(c.getId());
+      psb.add(c.currentPatchSetId());
+      pbb.put(project, branch);
+      pcb.put(project, c.getId());
+      cbb.put(branch, c.getId());
+    }
+
+    return new AutoValue_ChangeSet(pb.build(), bb.build(), ib.build(),
+        psb.build(), pbb.build(), pcb.build(), cbb.build());
+  }
+
+  public static ChangeSet create(Change change) {
+    return create(ImmutableList.of(change));
+  }
+
+  public abstract ImmutableSet<Project.NameKey> projects();
+  public abstract ImmutableSet<Branch.NameKey> branches();
+  public abstract ImmutableSet<Change.Id> ids();
+  public abstract ImmutableSet<PatchSet.Id> patchIds();
+  public abstract ImmutableSetMultimap<Project.NameKey, Branch.NameKey>
+      branchesByProject();
+  public abstract ImmutableSetMultimap<Project.NameKey, Change.Id>
+      changesByProject();
+  public abstract ImmutableSetMultimap<Branch.NameKey, Change.Id>
+      changesByBranch();
+
+  @Override
+  public int hashCode() {
+    return ids().hashCode();
+  }
+
+  public int size() {
+    return ids().size();
+  }
+}
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..7b1161c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -0,0 +1,119 @@
+// 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.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 Change.Id changeId;
+  private final Account.Id submitter;
+  private ReviewDb db;
+
+  @Inject
+  EmailMerge(@EmailReviewCommentsExecutor ExecutorService executor,
+      MergedSender.Factory mergedSenderFactory,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext requestContext,
+      @Assisted Change.Id changeId,
+      @Assisted @Nullable Account.Id submitter) {
+    this.sendEmailsExecutor = executor;
+    this.mergedSenderFactory = mergedSenderFactory;
+    this.schemaFactory = schemaFactory;
+    this.requestContext = requestContext;
+    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 getCurrentUser() {
+    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/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..d07572b 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, 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/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/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 633c3bb..5765452 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
@@ -51,7 +51,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;
@@ -128,8 +135,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 +160,7 @@
   }
 
   /** @return base directory under which all projects are stored. */
-  public File getBasePath() {
+  public Path getBasePath() {
     return basePath;
   }
 
@@ -163,12 +170,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
@@ -214,13 +221,13 @@
     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 +242,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 +302,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();
     }
   }
 
@@ -330,34 +334,27 @@
       final 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)) {
+      final 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);
+
+      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();
+      }
     } catch (IOException e) {
       log.error("Cannot update description for " + name, e);
     }
@@ -366,28 +363,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 +391,59 @@
     // 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);
+    }
 
-      } 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..d945f77 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,47 @@
 
 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.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.Table;
 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.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 +67,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;
@@ -87,15 +86,15 @@
 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.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 +115,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 +124,36 @@
   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 String logPrefix;
 
   private ProjectState destProject;
   private ReviewDb db;
   private Repository repo;
   private RevWalk 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 +163,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,219 +185,278 @@
     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 MergeException {
     destProject = projectCache.get(destBranch.getParentKey());
     if (destProject == null) {
       throw new MergeException("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();
+    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);
-        }
+        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 void checkSubmitRulesAndState(ChangeSet cs)
+      throws ResourceConflictException, OrmException {
+
+    StringBuilder msgbuf = new StringBuilder();
+    List<Change.Id> problemChanges = new ArrayList<>();
+    for (Change.Id id : cs.ids()) {
+      try {
+        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);
+    }
+  }
+
+  public void merge(ReviewDb db, ChangeSet changes, IdentifiedUser caller,
+      boolean checkSubmitRules) throws NoSuchChangeException,
+      OrmException, ResourceConflictException {
+    logPrefix = String.format("[%s]: ", String.valueOf(changes.hashCode()));
+    this.db = db;
+    logDebug("Beginning merge of {}", changes);
+    try {
+      ChangeSet cs = mergeSuperSet.completeChangeSet(db, changes);
+      logDebug("Calculated to merge {}", cs);
+      if (checkSubmitRules) {
+        logDebug("Checking submit rules and state");
+        checkSubmitRulesAndState(cs);
+      }
+      try {
+        integrateIntoHistory(cs, caller);
+      } catch (MergeException e) {
+        logError("Merge Conflict", e);
+        throw new ResourceConflictException("Merge Conflict", e);
+      }
+    } catch (IOException e) {
+      // Anything before the merge attempt is an error
+      throw new OrmException(e);
+    }
+  }
+
+  private void integrateIntoHistory(ChangeSet cs, IdentifiedUser caller)
+      throws MergeException, NoSuchChangeException, ResourceConflictException {
+    logDebug("Beginning merge attempt on {}", cs);
+    Map<Branch.NameKey, ListMultimap<SubmitType, ChangeData>> toSubmit =
+        new HashMap<>();
+    try {
+      logDebug("Perform the merges");
+      for (Project.NameKey project : cs.projects()) {
+        openRepository(project);
+        for (Branch.NameKey branch : cs.branchesByProject().get(project)) {
+          setDestProject(branch);
+
+          List<ChangeData> cds = new ArrayList<>();
+          for (Change.Id id : cs.changesByBranch().get(branch)) {
+            cds.add(changeDataFactory.create(db, id));
+          }
+          ListMultimap<SubmitType, ChangeData> submitting =
+              validateChangeList(cds);
+          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 : cs.projects()) {
+        openRepository(project);
+        for (Branch.NameKey branch : cs.branchesByProject().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, cs.branches());
+      checkState(pendingRefUpdates.isEmpty(), "programmer error: "
+          + "pending ref update list not emptied");
     } catch (NoSuchProjectException noProject) {
-      logWarn("Project " + destBranch.getParentKey() + " no longer exists,"
-          + " abandoning open changes");
-      abandonAllOpenChanges();
+      logWarn("Project " + noProject.project() + " no longer exists, "
+          + "abandoning open changes");
+      abandonAllOpenChanges(noProject.project());
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
+    } catch (IOException 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();
-      }
+      closeRepository();
     }
   }
 
-  private boolean containsMissingCommits(
-      ListMultimap<SubmitType, CodeReviewCommit> map, CodeReviewCommit commit) {
-    if (!isSubmitForMissingCommitsStillPossible(commit)) {
-      return false;
-    }
-
-    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) {
-      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;
-      }
-    }
-
-    return true;
-  }
-
   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 MergeException, 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)
+  private SubmitStrategy createStrategy(Branch.NameKey destBranch,
+      SubmitType submitType, CodeReviewCommit branchTip, IdentifiedUser caller)
       throws MergeException, 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 MergeException, NoSuchProjectException {
     try {
       repo = repoManager.openRepository(name);
     } catch (RepositoryNotFoundException notFound) {
@@ -439,10 +474,30 @@
     inserter = repo.newObjectInserter();
   }
 
-  private RefUpdate openBranch()
-      throws MergeException, OrmException, NoSuchChangeException {
+  private void closeRepository() {
+    if (inserter != null) {
+      inserter.close();
+    }
+    if (rw != null) {
+      rw.close();
+    }
+    if (repo != null) {
+      repo.close();
+    }
+  }
+
+  private RefUpdate getPendingRefUpdate(Branch.NameKey destBranch)
+      throws MergeException {
+
+    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());
@@ -450,23 +505,29 @@
         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 MergeException("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);
     }
   }
 
+  private CodeReviewCommit getBranchTip(Branch.NameKey destBranch)
+      throws MergeException {
+    if (openBranches.containsKey(destBranch)) {
+      return openBranches.get(destBranch);
+    } else {
+      getPendingRefUpdate(destBranch);
+      return openBranches.get(destBranch);
+    }
+  }
+
   private Set<RevCommit> getAlreadyAccepted(CodeReviewCommit branchTip)
       throws MergeException {
     Set<RevCommit> alreadyAccepted = new HashSet<>();
@@ -492,10 +553,10 @@
     return alreadyAccepted;
   }
 
-  private ListMultimap<SubmitType, Change> validateChangeList(
+  private ListMultimap<SubmitType, ChangeData> validateChangeList(
       List<ChangeData> submitted) throws MergeException {
     logDebug("Validating {} changes", submitted.size());
-    ListMultimap<SubmitType, Change> toSubmit = ArrayListMultimap.create();
+    ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create();
 
     Map<String, Ref> allRefs;
     try {
@@ -520,18 +581,18 @@
         throw new MergeException("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) {
@@ -541,7 +602,6 @@
           || 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 +612,6 @@
       } catch (IllegalArgumentException iae) {
         logError("Invalid revision on patch set " + ps.getId());
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -569,7 +628,6 @@
         logError("Revision " + idstr + " of patch set " + ps.getId()
             + " is not contained in any ref");
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
-        toUpdate.add(chg);
         continue;
       }
 
@@ -577,10 +635,8 @@
       try {
         commit = (CodeReviewCommit) 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;
       }
 
@@ -597,45 +653,20 @@
         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);
     }
     logDebug("Submitting on this run: {}", toSubmit);
     return toSubmit;
@@ -657,8 +688,13 @@
     }
   }
 
-  private RefUpdate updateBranch(SubmitStrategy strategy,
-      RefUpdate branchUpdate) throws MergeException {
+  private RefUpdate updateBranch(Branch.NameKey destBranch)
+      throws MergeException {
+    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)) {
@@ -713,17 +749,7 @@
           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 MergeException("Failed to lock " + branchUpdate.getName());
         default:
           throw new IOException(branchUpdate.getResult().name()
               + '\n' + branchUpdate);
@@ -733,11 +759,12 @@
     }
   }
 
-  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 +785,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> submitted,
+      Branch.NameKey destBranch, boolean dryRun, IdentifiedUser caller)
+      throws NoSuchChangeException, MergeException, ResourceConflictException,
+      OrmException {
+    if (!dryRun) {
+      logDebug("Updating change status for {} changes", submitted.size());
+    } else {
+      logDebug("Checking change state for {} changes in a dry run",
+          submitted.size());
+    }
+    MergeTip mergeTip = mergeTips.get(destBranch);
+    for (ChangeData cd : submitted) {
+      Change c = cd.change();
       CodeReviewCommit commit = commits.get(c.getId());
       CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
       if (s == null) {
@@ -773,6 +809,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,20 +824,27 @@
       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:
@@ -806,131 +857,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 MergeException("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 MergeException(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 MergeException(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,
+      Set<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 +933,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 +960,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,
@@ -1013,41 +1002,125 @@
     });
   }
 
-  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().delete(normalized.deleted());
+
+    try {
+      return saveToBatch(control, update, normalized, timestamp);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  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 +1128,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 +1143,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 +1166,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 +1186,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);
     }
   }
 
@@ -1320,13 +1260,15 @@
 
   private void logError(String msg, Throwable t) {
     if (log.isErrorEnabled()) {
-      log.error(logPrefix + msg, t);
+      if (t != null) {
+        log.error(logPrefix + msg, t);
+      } else {
+        log.error(logPrefix + 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/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
new file mode 100644
index 0000000..804417f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -0,0 +1,193 @@
+// 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.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, ChangeSet changes)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      OrmException {
+    if (Submit.wholeTopicEnabled(cfg)) {
+      return completeChangeSetIncludingTopics(db, changes);
+    } else {
+      return completeChangeSetWithoutTopic(db, changes);
+    }
+  }
+
+  private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      OrmException {
+    List<Change> ret = new ArrayList<>();
+
+    for (Project.NameKey project : changes.projects()) {
+      try (Repository repo = repoManager.openRepository(project);
+           RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+        for (Change.Id cId : changes.changesByProject().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.change());
+            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(cd.change().getDest(), hashes);
+
+            for (ChangeData chd : destChanges) {
+              Change chg = chd.change();
+              ret.add(chg);
+            }
+          }
+        }
+      }
+    }
+
+    return ChangeSet.create(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<Change> chgs = new ArrayList<>();
+      done = true;
+      for (Change.Id cId : newCs.ids()) {
+        // TODO(sbeller): Cache the change data here and in completeChangeSet
+        // There is no need to reread it a few times.
+        ChangeData cd = changeDataFactory.create(db, cId);
+        chgs.add(cd.change());
+
+        String topic = cd.change().getTopic();
+        if (!Strings.isNullOrEmpty(topic) && !topicsTraversed.contains(topic)) {
+          for (ChangeData addCd : queryProvider.get().byTopicOpen(topic)) {
+            chgs.add(addCd.change());
+          }
+          done = false;
+          topicsTraversed.add(topic);
+        }
+      }
+      changes = ChangeSet.create(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..27fb7f2 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;
@@ -44,8 +45,8 @@
    */
   public MergeTip(@Nullable CodeReviewCommit initial,
       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.mergeResults = Maps.newHashMap();
     this.branchTip = initial;
     // Assume fast-forward merge until opposite is proven.
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 84be4a1..c5ead54 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
@@ -25,13 +25,16 @@
 import com.google.gerrit.common.data.LabelType;
 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.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
@@ -68,16 +71,13 @@
 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.Set;
-import java.util.TimeZone;
 
 public class MergeUtil {
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
@@ -164,10 +164,6 @@
     return result;
   }
 
-  public PatchSetApproval getSubmitter(CodeReviewCommit c) {
-    return approvalsUtil.getSubmitter(db.get(), c.notes(), c.getPatchsetId());
-  }
-
   public RevCommit createCherryPickFromCommit(Repository repo,
       ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
       PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
@@ -195,7 +191,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 +213,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 +233,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 +299,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 +311,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,52 +340,6 @@
     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)
@@ -487,16 +444,15 @@
     };
   }
 
-  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, RevWalk rw,
+      ObjectInserter inserter, RevFlag canMergeFlag, Branch.NameKey destBranch,
+      CodeReviewCommit mergeTip, CodeReviewCommit n) throws MergeException {
     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);
       }
@@ -539,12 +495,12 @@
     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, RevWalk 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);
@@ -572,13 +528,11 @@
       }
     }
 
-    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 =
@@ -681,7 +635,7 @@
     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 {
     if (mergeTip == null) {
@@ -689,12 +643,10 @@
       // 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);
@@ -707,13 +659,8 @@
       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);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index c71c94f..840b167 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -148,6 +148,7 @@
   private final BatchRefUpdate batch;
   private final CommitBuilder commit;
   private boolean allowEmpty;
+  private boolean insertChangeId;
 
   @AssistedInject
   public MetaDataUpdate(GitReferenceUpdated gitRefUpdated,
@@ -180,6 +181,10 @@
     this.allowEmpty = allowEmpty;
   }
 
+  public void setInsertChangeId(boolean insertChangeId) {
+    this.insertChangeId = insertChangeId;
+  }
+
   /** @return batch in which to run the update, or {@code null} for no batch. */
   BatchRefUpdate getBatch() {
     return batch;
@@ -202,6 +207,10 @@
     return allowEmpty;
   }
 
+  boolean insertChangeId() {
+    return insertChangeId;
+  }
+
   public CommitBuilder getCommitBuilder() {
     return commit;
   }
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 95f5108..52932c4 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
@@ -118,6 +118,7 @@
   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 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,8 @@
     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.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
@@ -628,7 +635,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");
@@ -811,6 +818,8 @@
     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, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
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..0df866d
--- /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, 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/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 957f28f..0ae77b0 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;
@@ -63,6 +65,7 @@
 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;
@@ -256,17 +259,14 @@
     }
   }
 
-  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);
         }
       };
 
@@ -318,15 +318,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;
@@ -374,9 +375,9 @@
       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,
@@ -421,9 +422,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;
 
@@ -556,6 +557,7 @@
     commandProgress = progress.beginSubTask("refs", UNKNOWN);
 
     batch = repo.getRefDatabase().newBatchUpdate();
+    batch.setPushCertificate(rp.getPushCertificate());
     batch.setRefLogIdent(rp.getRefLogIdent());
     batch.setRefLogMessage("push", true);
 
@@ -597,6 +599,7 @@
       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 +615,8 @@
               case UPDATE:
               case UPDATE_NONFASTFORWARD:
                 autoCloseChanges(c);
+                branches.add(new Branch.NameKey(project.getNameKey(),
+                    c.getRefName()));
                 break;
 
               case DELETE:
@@ -641,8 +646,7 @@
             // 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(),
@@ -651,6 +655,16 @@
           }
         }
     }
+    // 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();
@@ -734,12 +748,7 @@
           if (replace.insertPatchSet().checkedGet() != null) {
             replace.inputCommand.setResult(OK);
           }
-        } catch (IOException err) {
-          reject(replace.inputCommand, "internal server error");
-          log.error(String.format(
-              "Cannot add patch set to %d of %s",
-              e.getKey().get(), project.getName()), err);
-        } catch (InsertException err) {
+        } catch (IOException | RestApiException err) {
           reject(replace.inputCommand, "internal server error");
           log.error(String.format(
               "Cannot add patch set to %d of %s",
@@ -788,9 +797,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());
         }
       }
@@ -799,13 +808,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");
@@ -1197,10 +1216,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) {
@@ -1358,7 +1375,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) {
@@ -1467,25 +1490,28 @@
 
   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(refsById, 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);
       }
 
@@ -1494,11 +1520,27 @@
       final int maxBatchChanges =
           receiveConfig.getEffectiveMaxBatchChangesLimit(currentUser);
       for (;;) {
-        final RevCommit c = walk.next();
+        final RevCommit c = rp.getRevWalk().next();
         if (c == null) {
           break;
         }
-        if (existing.contains(c)) { // Commit is already tracked.
+        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.
+          for (Ref ref : existingRefs) {
+            updateGroups.add(new UpdateGroupsRequest(ref, c));
+          }
           continue;
         }
 
@@ -1607,28 +1649,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;
         }
       }
     }
@@ -1656,6 +1705,7 @@
     final ReceiveCommand cmd;
     final ChangeInserter ins;
     boolean created;
+    Collection<String> groups;
 
     CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
         throws OrmException {
@@ -1672,22 +1722,20 @@
           ins.getPatchSet().getRefName());
     }
 
-    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, IOException,
+            ResourceConflictException {
           if (caller == Thread.currentThread()) {
             insertChange(db);
           } else {
-            ReviewDb db = schemaFactory.open();
-            try {
+            try (ReviewDb db = schemaFactory.open()) {
               insertChange(db);
-            } finally {
-              db.close();
             }
           }
           synchronized (newProgress) {
@@ -1699,8 +1747,9 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
-    private void insertChange(ReviewDb db) throws OrmException, IOException {
-      final PatchSet ps = ins.getPatchSet();
+    private void insertChange(ReviewDb db) throws OrmException, IOException,
+        ResourceConflictException {
+      final PatchSet ps = ins.setGroups(groups).getPatchSet();
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = commit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
@@ -1735,27 +1784,20 @@
   }
 
   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;
+    List<Change> changes = Lists.newArrayList(rsrc.getChange());
     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, ChangeSet.create(changes),
+          (IdentifiedUser) changeCtl.getCurrentUser(), false);
+    } catch (NoSuchChangeException e) {
+      throw new OrmException(e);
     }
-    if (c == null) {
-      addError("Submitting change " + changeCtl.getChange().getChangeId()
-          + " failed.");
-    } else {
-      addMessage("");
-      mergeQueue.merge(c.getDest());
+    addMessage("");
+    for (Change c : changes) {
       c = db.changes().get(c.getId());
       switch (c.getStatus()) {
-        case SUBMITTED:
-          addMessage("Change " + c.getChangeId() + " submitted.");
-          break;
         case MERGED:
           addMessage("Change " + c.getChangeId() + " merged.");
           break;
@@ -1858,6 +1900,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) {
@@ -2033,6 +2076,7 @@
       newPatchSet.setCreatedOn(TimeUtil.nowTs());
       newPatchSet.setUploader(currentUser.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
+      newPatchSet.setGroups(groups);
       if (magicBranch != null && magicBranch.draft) {
         newPatchSet.setDraft(true);
       }
@@ -2043,7 +2087,7 @@
           newPatchSet.getRefName());
     }
 
-    CheckedFuture<PatchSet.Id, InsertException> insertPatchSet()
+    CheckedFuture<PatchSet.Id, RestApiException> insertPatchSet()
         throws IOException {
       rp.getRevWalk().parseBody(newCommit);
 
@@ -2051,18 +2095,16 @@
       ListenableFuture<PatchSet.Id> future = changeUpdateExector.submit(
           requestScopePropagator.wrap(new Callable<PatchSet.Id>() {
         @Override
-        public PatchSet.Id call() throws OrmException, IOException, NoSuchChangeException {
+        public PatchSet.Id call() throws OrmException, IOException,
+            NoSuchChangeException, 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();
               }
             }
           } finally {
@@ -2105,7 +2147,8 @@
       return newPatchSet.getId();
     }
 
-    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException {
+    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException,
+        ResourceConflictException {
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = newCommit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
@@ -2137,6 +2180,9 @@
         }
 
         ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
+        if (newPatchSet.getGroups() == null) {
+          newPatchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
+        }
         db.patchSets().insert(Collections.singleton(newPatchSet));
 
         if (checkMergedInto) {
@@ -2219,7 +2265,7 @@
       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() {
@@ -2227,7 +2273,7 @@
           public void run() {
             try {
               ReplacePatchSetSender cm =
-                  replacePatchSetFactory.create(change);
+                  replacePatchSetFactory.create(change.getId());
               cm.setFrom(me);
               cm.setPatchSet(newPatchSet, info);
               cm.setChangeMessage(msg);
@@ -2248,7 +2294,6 @@
           }
         }));
       }
-      f.checkedGet();
 
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
           ObjectId.zeroId(), newCommit);
@@ -2266,6 +2311,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);
   }
@@ -2381,12 +2481,14 @@
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
-      Set<ObjectId> existing = Sets.newHashSet();
-      walk.markStart(walk.parseCommit(cmd.getNewId()));
-      markHeadsAsUninteresting(walk, existing, cmd.getRefName());
-
-      RevCommit c;
-      while ((c = walk.next()) != null) {
+      RevObject parsedObject = walk.parseAny(cmd.getNewId());
+      if (!(parsedObject instanceof RevCommit)) {
+        return;
+      }
+      walk.markStart((RevCommit)parsedObject);
+      markHeadsAsUninteresting(walk, cmd.getRefName());
+      Set<ObjectId> existing = changeRefsById().keySet();
+      for (RevCommit c; (c = walk.next()) != null;) {
         if (existing.contains(c)) {
           continue;
         } else if (!validCommit(ctl, cmd, c)) {
@@ -2496,21 +2598,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);
     }
   }
 
@@ -2608,12 +2699,13 @@
   }
 
   private void sendMergedEmail(final ReplaceRequest result) {
+    final Change.Id id = result.change.getId();
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
       @Override
       public void run() {
         try {
-          final MergedSender cm = mergedSenderFactory.create(result.changeCtl);
+          final MergedSender cm = mergedSenderFactory.create(id);
           cm.setFrom(currentUser.getAccountId());
           cm.setPatchSet(result.newPatchSet, result.info);
           cm.send();
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..b2d3632 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
@@ -154,9 +154,10 @@
     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) {
+        if (alreadySending.contains(c)
+            || toInclude.contains(c)
+            || c.getParentCount() > 1) {
+          // Do nothing
         } else if (toInclude.size() < base) {
           toInclude.add(c);
         } else {
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..365000b 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;
 
@@ -48,9 +49,12 @@
   @Provides
   @Singleton
   @EmailReviewCommentsExecutor
-  public WorkQueue.Executor createEmailReviewCommentsExecutor(
+  public ExecutorService createEmailReviewCommentsExecutor(
       @GerritServerConfig Config config, WorkQueue queues) {
     int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
+    if (poolSize == 0) {
+      return MoreExecutors.newDirectExecutorService();
+    }
     return queues.createQueue(poolSize, "EmailReviewComments");
   }
 
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/ScanningChangeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
index 65808fc..19e23b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
@@ -95,8 +95,8 @@
 
     @Override
     public List<Change> load(Project.NameKey key) throws Exception {
-      Repository repo = repoManager.openRepository(key);
-      try (ManualRequestContext ctx = requestContext.open()) {
+      try (Repository repo = repoManager.openRepository(key);
+          ManualRequestContext ctx = requestContext.open()) {
         ReviewDb db = ctx.getReviewDbProvider().get();
         Map<String, Ref> refs =
             repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES);
@@ -114,8 +114,6 @@
           Iterables.addAll(changes, db.changes().get(batch));
         }
         return changes;
-      } finally {
-        repo.close();
       }
     }
 
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..d83da3e 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,105 @@
 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());
+      ObjectId id = repo.resolve(destBranch.get());
+      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)))) {
-
+          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) ||
+              FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
         BlobBasedConfig bbc =
-            new BlobBasedConfig(null, db, mergeTip, GIT_MODULES);
+            new BlobBasedConfig(null, repo, commit, GIT_MODULES);
 
-        final String thisServer = new URI(urlProvider.get()).getHost();
+        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);
+        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 +179,31 @@
     }
   }
 
-  private void updateSuperProjects(final Branch.NameKey updatedBranch, RevWalk myRw,
-      final ObjectId mergedCommit, final String msg) throws SubmoduleException {
+  protected void updateSuperProjects(ReviewDb db,
+      Set<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 +211,117 @@
     }
   }
 
-  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;
 
-    final StringBuilder msgbuf = new StringBuilder();
-    msgbuf.append("Updated " + subscriber.getParentKey().get());
     Repository pdb = null;
     RevWalk recRw = null;
 
+    StringBuilder msgbuf = new StringBuilder("Updated git submodules\n\n");
     try {
       boolean sameAuthorForAll = true;
 
-      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());
-          }
-        } else {
-          msgbuf.append(c.getShortMessage());
-        }
-        msgbuf.append("\n");
-
-        if (author == null) {
-          author = c.getAuthorIdent();
-        } else if (!author.equals(c.getAuthorIdent())) {
-          sameAuthorForAll = false;
-        }
-      }
-
-      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());
+
+      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;
           }
-        });
+
+          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 = null;
+          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);
+
+              if (oldId != null) {
+                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;
+      }
+
       ObjectInserter oi = pdb.newObjectInserter();
       ObjectId tree = dc.writeTree(oi);
 
-      final CommitBuilder commit = new CommitBuilder();
+      ObjectId currentCommitId =
+          pdb.getRef(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,14 +343,12 @@
         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);
+      throw new SubmoduleException("Cannot update gitlinks for "
+          + subscriber.get(), e);
     } finally {
       if (recRw != null) {
         recRw.close();
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..74d8f2d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
@@ -0,0 +1,129 @@
+// 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.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 {
+  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,
+      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;
+      }
+
+      rows.add(new Row(s.substring(0, tab).trim(),
+          s.substring(tab + 1).trim()));
+    }
+    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();
+  }
+}
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 b905f67..17f51ef 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
@@ -43,6 +43,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.ChangeIdUtil;
 import org.eclipse.jgit.util.RawParseUtils;
 
 import java.io.BufferedReader;
@@ -95,7 +96,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);
   }
 
@@ -271,6 +272,14 @@
           commit.addParentId(src);
         }
 
+        if (update.insertChangeId()) {
+          ObjectId id =
+              ChangeIdUtil.computeChangeId(res, getRevision(),
+                  commit.getAuthor(), commit.getCommitter(),
+                  commit.getMessage());
+          commit.setMessage(ChangeIdUtil.insertId(commit.getMessage(), id));
+        }
+
         src = inserter.insert(commit);
         srcTree = res;
       }
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..a4684a1 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()) {
@@ -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..4a8163b 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
@@ -110,8 +110,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 +303,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() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/CheckResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/CheckResult.java
new file mode 100644
index 0000000..71321ba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/CheckResult.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.git.gpg;
+
+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 {
+  private final List<String> problems;
+
+  CheckResult(String... problems) {
+    this(Arrays.asList(problems));
+  }
+
+  CheckResult(List<String> problems) {
+    this.problems = Collections.unmodifiableList(new ArrayList<>(problems));
+  }
+
+  /**
+   * @return whether the result is entirely ok, i.e. has passed any verification
+   *     or validation checks.
+   */
+  public boolean isOk() {
+    return problems.isEmpty();
+  }
+
+  /** @return any problems encountered during checking. */
+  public List<String> getProblems() {
+    return problems;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(getClass().getSimpleName())
+        .append('[');
+    for (int i = 0; i < problems.size(); i++) {
+      if (i > 0) {
+        sb.append(", ");
+      }
+      sb.append(problems.get(i));
+    }
+    return sb.append(']').toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyChecker.java
new file mode 100644
index 0000000..5806e8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyChecker.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.server.git.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+
+import org.bouncycastle.openpgp.PGPPublicKey;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Checker for GPG public keys for use in a push certificate. */
+public class PublicKeyChecker {
+  /**
+   * Check a public key.
+   *
+   * @param key the public key.
+   * @param expectedKeyId the key ID that the caller expects.
+   */
+  public final CheckResult check(PGPPublicKey key, long expectedKeyId) {
+    List<String> problems = new ArrayList<>();
+    if (key.getKeyID() != expectedKeyId) {
+      problems.add(
+          "Public key does not match ID " + keyIdToString(expectedKeyId));
+    }
+    if (key.isRevoked()) {
+      // TODO(dborowitz): isRevoked is overeager:
+      // http://www.bouncycastle.org/jira/browse/BJB-45
+      problems.add("Key is revoked");
+    }
+
+    long validSecs = key.getValidSeconds();
+    if (validSecs != 0) {
+      long createdSecs = key.getCreationTime().getTime() / 1000;
+      long nowSecs = System.currentTimeMillis() / 1000;
+      if (nowSecs - createdSecs > validSecs) {
+        problems.add("Key is expired");
+      }
+    }
+    checkCustom(key, expectedKeyId, problems);
+    return new CheckResult(problems);
+  }
+
+  /**
+   * Perform custom checks.
+   * <p>
+   * Default implementation does nothing, but may be overridden by subclasses.
+   *
+   * @param key the public key.
+   * @param expectedKeyId the key ID that the caller expects.
+   * @param problems list to which any problems should be added.
+   */
+  public void checkCustom(PGPPublicKey key, long expectedKeyId,
+      List<String> problems) {
+    // Default implementation does nothing.
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyStore.java
new file mode 100644
index 0000000..7327c87
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PublicKeyStore.java
@@ -0,0 +1,170 @@
+// 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.gpg;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * 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 final Repository repo;
+  private ObjectReader reader;
+  private NoteMap notes;
+
+  /** @param repo repository to read keys from. */
+  public PublicKeyStore(Repository repo) {
+    this.repo = repo;
+  }
+
+  @Override
+  public void close() {
+    if (reader != null) {
+      reader.close();
+      reader = null;
+      notes = null;
+    }
+  }
+
+  private void load() throws IOException {
+    close();
+    reader = repo.newObjectReader();
+
+    Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_GPG_KEYS);
+    if (ref == null) {
+      return;
+    }
+    try (RevWalk rw = new RevWalk(reader)) {
+      notes = NoteMap.read(reader, rw.parseCommit(ref.getObjectId()));
+    }
+  }
+
+  /**
+   * 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 {
+    if (reader == null) {
+      load();
+    }
+    if (notes == null) {
+      return empty();
+    }
+    Note note = notes.getNote(keyObjectId(keyId));
+    if (note == null) {
+      return empty();
+    }
+
+    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) {
+          keys.add((PGPPublicKeyRing) obj);
+        }
+        checkState(!it.hasNext(),
+            "expected one PGP object per ArmoredInputStream");
+      }
+      return new PGPPublicKeyRingCollection(keys);
+    }
+  }
+
+  // TODO(dborowitz): put method.
+
+  private static PGPPublicKeyRingCollection empty()
+      throws PGPException, IOException {
+    return new PGPPublicKeyRingCollection(
+        Collections.<PGPPublicKeyRing> emptyList());
+  }
+
+  static String keyToString(PGPPublicKey key) {
+    @SuppressWarnings("unchecked")
+    Iterator<String> it = key.getUserIDs();
+    ByteBuffer buf = ByteBuffer.wrap(key.getFingerprint());
+    return String.format(
+        "%s %s(%04X %04X %04X %04X %04X  %04X %04X %04X %04X %04X)",
+        keyIdToString(key.getKeyID()),
+        it.hasNext() ? it.next() + " " : "",
+        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
+        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
+        buf.getShort(), buf.getShort());
+  }
+
+  static String keyIdToString(long keyId) {
+    // Match key ID format from gpg --list-keys.
+    return String.format("%08X", (int) keyId);
+  }
+
+  static ObjectId keyObjectId(long keyId) {
+    ByteBuffer buf = ByteBuffer.wrap(new byte[Constants.OBJECT_ID_LENGTH]);
+    buf.putLong(keyId);
+    return ObjectId.fromRaw(buf.array());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PushCertificateChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PushCertificateChecker.java
new file mode 100644
index 0000000..fcef3a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/PushCertificateChecker.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.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Checker for push certificates. */
+public abstract class PushCertificateChecker {
+  private final PublicKeyChecker publicKeyChecker;
+
+  protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
+    this.publicKeyChecker = publicKeyChecker;
+  }
+
+  /**
+   * Check a push certificate.
+   *
+   * @return result of the check.
+   * @throws PGPException if an error occurred during GPG checks.
+   * @throws IOException if an error occurred reading from the repository.
+   */
+  public final CheckResult check(PushCertificate cert) throws PGPException, IOException {
+    if (cert.getNonceStatus() != NonceStatus.OK) {
+      return new CheckResult("Invalid nonce");
+    }
+    PGPSignature sig = readSignature(cert);
+    if (sig == null) {
+      return new CheckResult("Invalid signature format");
+    }
+    Repository repo = getRepository();
+    List<String> problems = new ArrayList<>();
+    try (PublicKeyStore store = new PublicKeyStore(repo)) {
+      checkSignature(sig, cert, store.get(sig.getKeyID()), problems);
+      checkCustom(repo, problems);
+      return new CheckResult(problems);
+    } finally {
+      if (shouldClose(repo)) {
+        repo.close();
+      }
+    }
+  }
+
+  /**
+   * 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 does nothing, but may be overridden by subclasses.
+   *
+   * @param repo a repository previously returned by {@link #getRepository()}.
+   * @param problems list to which any problems should be added.
+   */
+  protected void checkCustom(Repository repo, List<String> problems) {
+    // Default implementation does nothing.
+  }
+
+  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 void checkSignature(PGPSignature sig,
+      PushCertificate cert, PGPPublicKeyRingCollection keys,
+      List<String> problems) {
+    List<String> deferredProblems = new ArrayList<>();
+    boolean anyKeys = false;
+    for (PGPPublicKeyRing kr : keys) {
+      PGPPublicKey k = kr.getPublicKey();
+      anyKeys = true;
+      try {
+        sig.init(new BcPGPContentVerifierBuilderProvider(), k);
+        sig.update(Constants.encode(cert.toText()));
+        if (!sig.verify()) {
+          // TODO(dborowitz): Privacy issues with exposing fingerprint/user ID
+          // of keys having the same ID as the pusher's key?
+          deferredProblems.add(
+              "Signature not valid with public key: " + keyToString(k));
+          continue;
+        }
+        CheckResult result = publicKeyChecker.check(k, sig.getKeyID());
+        if (result.isOk()) {
+          return;
+        }
+        StringBuilder err = new StringBuilder("Invalid public key (")
+            .append(keyToString(k))
+            .append("):");
+        for (int i = 0; i < result.getProblems().size(); i++) {
+          err.append('\n').append("  ").append(result.getProblems().get(i));
+        }
+        problems.add(err.toString());
+        return;
+      } catch (PGPException e) {
+        deferredProblems.add(
+            "Error checking signature with public key (" + keyToString(k)
+            + ": " + e.getMessage());
+      }
+    }
+    if (!anyKeys) {
+      problems.add(
+          "No public keys found for Key ID " + keyIdToString(sig.getKeyID()));
+    } else {
+      problems.addAll(deferredProblems);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushModule.java
new file mode 100644
index 0000000..6e7cc5f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushModule.java
@@ -0,0 +1,119 @@
+// 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.gpg;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.BouncyCastleUtil;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+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.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+
+public class SignedPushModule extends AbstractModule {
+  private static final Logger log =
+      LoggerFactory.getLogger(SignedPushModule.class);
+
+  public static boolean isEnabled(Config cfg) {
+    return cfg.getBoolean("receive", null, "enableSignedPush", false);
+  }
+
+  @Override
+  protected void configure() {
+    if (BouncyCastleUtil.havePGP()) {
+      DynamicSet.bind(binder(), ReceivePackInitializer.class)
+          .to(Initializer.class);
+    } else {
+      log.info("BouncyCastle PGP not installed; signed push verification is"
+          + " disabled");
+    }
+  }
+
+  @Singleton
+  private static class Initializer implements ReceivePackInitializer {
+    private final SignedPushConfig signedPushConfig;
+    private final SignedPushPreReceiveHook hook;
+    private final ProjectCache projectCache;
+
+    @Inject
+    Initializer(@GerritServerConfig Config cfg,
+        SignedPushPreReceiveHook hook,
+        ProjectCache projectCache) {
+      this.hook = hook;
+      this.projectCache = projectCache;
+
+      if (isEnabled(cfg)) {
+        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;
+      }
+      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(signedPushConfig);
+      rp.setPreReceiveHook(PreReceiveHookChain.newChain(Lists.newArrayList(
+          hook, rp.getPreReceiveHook())));
+    }
+  }
+
+  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-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushPreReceiveHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushPreReceiveHook.java
new file mode 100644
index 0000000..c6889b5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/gpg/SignedPushPreReceiveHook.java
@@ -0,0 +1,98 @@
+// 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.gpg;
+
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.eclipse.jgit.lib.Repository;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+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 {
+  private static final Logger log =
+      LoggerFactory.getLogger(SignedPushPreReceiveHook.class);
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+
+  @Inject
+  public SignedPushPreReceiveHook(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers) {
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+  }
+
+  @Override
+  public void onPreReceive(ReceivePack rp,
+      Collection<ReceiveCommand> commands) {
+    try {
+      PushCertificate cert = rp.getPushCertificate();
+      if (cert == null) {
+        return;
+      }
+      PushCertificateChecker checker = new PushCertificateChecker(
+          new PublicKeyChecker()) {
+            @Override
+            protected Repository getRepository() throws IOException {
+              return repoManager.openRepository(allUsers);
+            }
+
+            @Override
+            protected boolean shouldClose(Repository repo) {
+              return true;
+            }
+          };
+      CheckResult result = checker.check(cert);
+      if (!result.isOk()) {
+        for (String problem : result.getProblems()) {
+          rp.sendMessage(problem);
+        }
+        reject(commands, "invalid push cert");
+      }
+    } catch (PGPException | IOException e) {
+      log.error("Error checking push certificate", e);
+      reject(commands, "push cert error");
+    }
+  }
+
+  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-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..0e740f8 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
@@ -23,10 +23,10 @@
 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.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
@@ -131,14 +131,15 @@
       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,
+        PersonIdent myIdent = args.serverIdent.get();
+        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
+            myIdent, args.repo, args.rw, args.inserter,
             args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(), n);
         mergeTip.moveTipTo(result, n);
       }
-      PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(args.rw,
-          args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
-      setRefLogIdent(submitApproval);
+      args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+          mergeTip.getCurrentTip(), args.alreadyAccepted);
+      setRefLogIdent();
     } 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.
@@ -152,33 +153,19 @@
 
     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);
 
+    PersonIdent committer = args.caller.newCommitterIdent(
+        TimeUtil.nowTs(), args.serverIdent.get().getTimeZone());
     CodeReviewCommit newCommit =
         (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
-            args.inserter, mergeTip, n, cherryPickCommitterIdent,
-            cherryPickCmtMsg, args.rw);
+            args.inserter, mergeTip, n, committer, 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.setUploader(args.caller.getAccountId());
     ps.setRevision(new RevId(newCommit.getId().getName()));
 
     RefUpdate ru;
@@ -186,6 +173,7 @@
     args.db.changes().beginTransaction(n.change().getId());
     try {
       insertAncestors(args.db, ps.getId(), newCommit);
+      ps.setGroups(GroupCollector.getCurrentGroups(args.db, n.change()));
       args.db.patchSets().insert(Collections.singleton(ps));
       n.change()
           .setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
@@ -218,9 +206,9 @@
     newCommit.copyFrom(n);
     newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
     newCommit.setControl(
-        args.changeControlFactory.controlFor(n.change(), cherryPickUser));
+        args.changeControlFactory.controlFor(n.change(), args.caller));
     newCommits.put(newCommit.getPatchsetId().getParentKey(), newCommit);
-    setRefLogIdent(submitAudit);
+    setRefLogIdent();
     return newCommit;
   }
 
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..f7d8ab1 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,7 +14,6 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.MergeException;
@@ -43,10 +42,9 @@
       n.setStatusCode(CommitMergeStatus.NOT_FAST_FORWARD);
     }
 
-    PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTipCommit,
-            args.alreadyAccepted);
-    setRefLogIdent(submitApproval);
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+        newMergeTipCommit, args.alreadyAccepted);
+    setRefLogIdent();
 
     return mergeTip;
   }
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..d3a72e3 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,12 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -42,17 +43,19 @@
     }
     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);
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+        mergeTip.getCurrentTip(), args.alreadyAccepted);
+    setRefLogIdent();
 
     return mergeTip;
   }
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..688fa3e 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,12 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeTip;
 
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.util.Collection;
 import java.util.List;
 
@@ -48,18 +49,19 @@
     // 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);
-
+    args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
+        args.alreadyAccepted);
+    setRefLogIdent();
     return mergeTip;
   }
 
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 acd32c7..f9102ec 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
@@ -18,9 +18,8 @@
 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.RebaseChange;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.MergeConflictException;
@@ -33,6 +32,7 @@
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
 import java.util.Collection;
@@ -84,15 +84,11 @@
 
         } 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,
+                    n.change(), n.getPatchsetId(), args.caller,
                     mergeTip.getCurrentTip(), args.mergeUtil,
                     args.serverIdent.get(), false, ValidatePolicy.NONE);
-
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.approvalsUtil.byPatchSet(args.db,
                 n.getControl(), n.getPatchsetId())) {
@@ -109,13 +105,13 @@
                     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);
           } catch (NoSuchChangeException | OrmException | IOException
@@ -135,15 +131,15 @@
           if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
             mergeTip.moveTipTo(n, n);
           } else {
+            PersonIdent myIdent = args.serverIdent.get();
             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);
+          args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+              mergeTip.getCurrentTip(), args.alreadyAccepted);
+          setRefLogIdent();
         } catch (IOException e) {
           throw new MergeException("Cannot merge " + n.name(), e);
         }
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..4034abd 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,10 +14,11 @@
 
 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;
@@ -66,6 +67,7 @@
     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,
@@ -73,7 +75,7 @@
         RevWalk 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;
@@ -89,6 +91,7 @@
       this.mergeUtil = mergeUtil;
       this.indexer = indexer;
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
+      this.caller = caller;
     }
   }
 
@@ -115,6 +118,7 @@
   public final MergeTip run(final CodeReviewCommit currentTip,
       final Collection<CodeReviewCommit> toMerge) throws MergeException {
     refLogIdent = null;
+    checkState(args.caller != null);
     return _run(currentTip, toMerge);
   }
 
@@ -124,7 +128,7 @@
 
   /**
    * 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.
    *
@@ -156,7 +160,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 +186,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..b87499a 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,7 +20,7 @@
 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.change.RebaseChange;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeUtil;
@@ -88,14 +88,14 @@
   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)
+      final Branch.NameKey destBranch, final IdentifiedUser caller)
       throws MergeException, 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);
+            mergeUtilFactory.create(project), indexer, caller);
     switch (submitType) {
       case CHERRY_PICK:
         return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
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 d030a55..d8c3303 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
@@ -222,7 +222,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) {
@@ -387,7 +387,9 @@
       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();
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..5155e25 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,9 +20,11 @@
 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;
@@ -33,7 +35,6 @@
 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;
@@ -149,14 +150,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..0986584 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,9 +19,9 @@
 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;
@@ -32,100 +32,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)
+  public GroupInfo apply(TopLevelResource resource, GroupInput input)
       throws AuthException, 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 +135,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/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/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..bf5193f 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
@@ -22,22 +22,20 @@
 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.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.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 +49,80 @@
 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 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;
 
   @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 = "-o", usage = "Output options per group")
-  public void addOption(ListGroupsOption o) {
+  void addOption(ListGroupsOption o) {
     options.add(o);
   }
 
@@ -120,7 +145,10 @@
     this.userFactory = userFactory;
     this.accountGetGroups = accountGetGroups;
     this.json = json;
-    this.options = EnumSet.noneOf(ListGroupsOption.class);
+  }
+
+  public void setOptions(EnumSet<ListGroupsOption> options) {
+    this.options = options;
   }
 
   public Account.Id getUser() {
@@ -132,16 +160,16 @@
   }
 
   @Override
-  public Object apply(TopLevelResource resource) throws OrmException {
-    final Map<String, GroupInfo> output = Maps.newTreeMap();
+  public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
+      throws OrmException {
+    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 {
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..041e2fa 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
@@ -55,6 +55,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);
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..ba28cd1 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,8 +15,7 @@
 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;
@@ -25,12 +24,24 @@
 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 +49,92 @@
     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) {
+      ResourceNotFoundException, 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..5d7229a 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
@@ -43,7 +43,9 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -54,8 +56,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 +73,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 +158,49 @@
         }
       };
 
+  @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 {
-          Change c = input.change();
-          if (c == null) {
-            return null;
-          }
-          return firstNonNull(c.getTopic(), "");
+          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);
         }
       };
 
@@ -253,7 +301,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 +314,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 +356,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 +388,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
@@ -427,22 +491,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 +546,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/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 05bf9bd..4915a663 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,290 @@
       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);
+
+  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);
+
+
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
   }
@@ -150,9 +377,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/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..e79dc63 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,52 @@
  */
 @AutoValue
 public abstract class IndexConfig {
+  private static final int DEFAULT_MAX_PREFIX_TERMS = 100;
+
   public static IndexConfig createDefault() {
-    return create(Integer.MAX_VALUE);
+    return create(0, 0, 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, "maxPrefixTerms", DEFAULT_MAX_PREFIX_TERMS));
   }
 
+  public static IndexConfig create(int maxLimit, int maxPages,
+      int maxPrefixTerms) {
+    return new AutoValue_IndexConfig(
+        checkLimit(maxLimit, "maxLimit", Integer.MAX_VALUE),
+        checkLimit(maxPages, "maxPages", Integer.MAX_VALUE),
+        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 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..afb7c22 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,7 +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;
@@ -39,7 +38,7 @@
  */
 public class IndexModule extends LifecycleModule {
   public enum IndexType {
-    LUCENE, SOLR
+    LUCENE
   }
 
   /** Type of secondary index. */
@@ -69,7 +68,6 @@
   @Override
   protected void configure() {
     bind(ChangeQueryRewriter.class).to(IndexRewriteImpl.class);
-    bind(BasicChangeRewrites.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/IndexRewriteImpl.java
index 7fbddfb..27df8c2 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/IndexRewriteImpl.java
@@ -26,7 +26,6 @@
 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;
@@ -118,13 +117,10 @@
   }
 
   private final IndexCollection indexes;
-  private final BasicChangeRewrites basicRewrites;
 
   @Inject
-  IndexRewriteImpl(IndexCollection indexes,
-      BasicChangeRewrites basicRewrites) {
+  IndexRewriteImpl(IndexCollection indexes) {
     this.indexes = indexes;
-    this.basicRewrites = basicRewrites;
   }
 
   @Override
@@ -132,7 +128,6 @@
       int limit) throws QueryParseException {
     checkArgument(limit > 0, "limit must be positive: %s", limit);
     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;
@@ -207,7 +202,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(
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..81be0fd 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
@@ -85,8 +85,7 @@
 
   @Override
   public boolean hasChange() {
-    return index.getSchema().getFields()
-        .containsKey(ChangeField.CHANGE.getName());
+    return index.getSchema().hasField(ChangeField.CHANGE);
   }
 
   @Override
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 45b7c4d..31fbb40 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 0897d60..1bc6b05 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
@@ -213,7 +213,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) {
@@ -235,12 +235,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 : changeCache.get(project)) {
             Ref r = refs.get(c.currentPatchSetId().toRefName());
             if (r != null) {
@@ -256,16 +256,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/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/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/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index ac23455..9bb0816 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;
   }
 
@@ -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..592fbb0 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,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.base.MoreObjects;
+
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
@@ -23,6 +25,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 +33,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 +56,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) {
@@ -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;
@@ -135,6 +154,22 @@
       fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
       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/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
index 7e44877..31135d0 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,12 +21,14 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-class EmailSettings {
-  final boolean includeDiff;
-  final int maximumDiffSize;
+public class EmailSettings {
+  public final boolean allowRegisterNewEmail;
+  public final boolean includeDiff;
+  public final int maximumDiffSize;
 
   @Inject
   EmailSettings(@GerritServerConfig Config cfg) {
+    allowRegisterNewEmail = cfg.getBoolean("sendemail", "allowRegisterNewEmail", true);
     includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
     maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
   }
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 8a3133f..1e4fec7 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
@@ -254,7 +254,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();
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
index 53efa1e..80a5d24 100644
--- 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
@@ -100,7 +100,8 @@
             updatedPatchSet, info, recipients.getReviewers(),
             Collections.<Account.Id> emptySet());
         try {
-          CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
+          CreateChangeSender cm =
+              createChangeSenderFactory.create(updatedChange.getId());
           cm.setFrom(me);
           cm.setPatchSet(updatedPatchSet, info);
           cm.addReviewers(recipients.getReviewers());
@@ -119,7 +120,8 @@
                 updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
         msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
         try {
-          ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
+          ReplacePatchSetSender cm =
+              replacePatchSetFactory.create(updatedChange.getId());
           cm.setFrom(me);
           cm.setPatchSet(updatedPatchSet, info);
           cm.setChangeMessage(msg);
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/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..29bce1c 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
@@ -53,9 +53,7 @@
       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) {
+    } catch (XsrfException | UnsupportedEncodingException e) {
       throw new IllegalArgumentException(e);
     }
   }
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 2f8f75d..9baada5 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
@@ -192,8 +192,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 +201,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
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..101aaac 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
@@ -26,6 +26,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.file.Files;
 import java.util.Properties;
 
 /** Configures Velocity template engine for sending email. */
@@ -49,10 +50,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..96322d2 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
@@ -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..47c1731 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;
 
 /**
@@ -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 8cb72e9..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 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/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/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..2c4f30c 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,7 +15,7 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Cache;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -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/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 9867b11..b645e6c 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
@@ -23,7 +23,6 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -40,11 +39,7 @@
   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 +89,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());
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 878ff189..e22ea57 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
@@ -17,17 +17,17 @@
 
 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.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;
@@ -79,24 +79,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,13 +114,10 @@
   }
 
   @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();
     }
   }
 
@@ -161,25 +168,24 @@
       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,
@@ -212,7 +218,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()
@@ -325,7 +331,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());
     }
@@ -391,11 +397,8 @@
             fmt.formatMerge(buf, p, "BASE", oursName, theirsName, "UTF-8");
             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..ff4496f 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
@@ -142,9 +142,12 @@
       intralineDifferenceIsPossible = false;
     } else if (diffPrefs.isIntralineDifference()) {
       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.getIgnoreWhitespace() != Whitespace.IGNORE_NONE),
+              IntraLineDiffArgs.create(
+                a.src, b.src, edits, projectKey, bId, b.path));
       if (d != null) {
         switch (d.getStatus()) {
           case EDIT_LIST:
@@ -177,7 +180,7 @@
 
     boolean hugeFile = false;
     if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
-
+      // Do nothing
     } else if (a.src == b.src && a.size() <= context
         && content.getEdits().isEmpty()) {
       // Odd special case; the files are identical (100% rename or copy)
@@ -214,7 +217,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) {
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..802a837 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
@@ -93,7 +93,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;
@@ -145,7 +145,7 @@
     validatePatchSetId(psb);
 
     change = control.getChange();
-    projectKey = change.getProject();
+    project = change.getProject();
 
     aId = psa != null ? toObjectId(db, psa) : null;
     bId = toObjectId(db, psb);
@@ -155,51 +155,47 @@
       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.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);
+      }
     } 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.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
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..83856db 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
@@ -79,13 +79,8 @@
 
   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());
@@ -93,8 +88,6 @@
       return info;
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
-    } finally {
-      repo.close();
     }
   }
 
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..83643ff 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;
 
@@ -66,8 +65,6 @@
       } else {
         try {
           in = new URL(input.url).openStream();
-        } catch (MalformedURLException e) {
-          throw new BadRequestException(e.getMessage());
         } catch (IOException e) {
           throw new BadRequestException(e.getMessage());
         }
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 a8600fe..6eb336d 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..c2b28cb 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
@@ -25,7 +27,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 +69,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 +82,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 +107,7 @@
     return pluginUser;
   }
 
-  public File getSrcFile() {
+  public Path getSrcFile() {
     return srcFile;
   }
 
@@ -128,9 +130,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 +168,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.lastModified() != lastModified(jar);
   }
 }
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 3fbbfa9..6b458aa 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..4e651c2 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;
@@ -158,7 +162,7 @@
     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,26 +172,26 @@
     }
 
     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;
       }
 
@@ -195,21 +199,17 @@
     }
   }
 
-  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 +217,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 +241,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 +284,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 +304,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 +356,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 +370,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 +416,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 +449,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 +496,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 +510,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 +545,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 +562,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 +586,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 +614,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 +638,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 +679,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/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/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/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/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 1c6782c..1801712 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
@@ -31,8 +31,11 @@
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.gpg.SignedPushModule;
 import com.google.inject.util.Providers;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -45,6 +48,7 @@
   public InheritedBooleanInfo useSignedOffBy;
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
+  public InheritedBooleanInfo enableSignedPush;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
   public com.google.gerrit.extensions.client.ProjectState state;
@@ -54,7 +58,8 @@
   public Map<String, CommentLinkInfo> commentlinks;
   public ThemeInfo theme;
 
-  public ConfigInfo(ProjectControl control,
+  public ConfigInfo(Config gerritConfig,
+      ProjectControl control,
       TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
@@ -71,6 +76,7 @@
     InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
     InheritedBooleanInfo createNewChangeForAllNotInTarget =
         new InheritedBooleanInfo();
+    InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
 
     useContributorAgreements.value = projectState.isUseContributorAgreements();
     useSignedOffBy.value = projectState.isUseSignedOffBy();
@@ -86,6 +92,7 @@
     requireChangeId.configuredValue = p.getRequireChangeID();
     createNewChangeForAllNotInTarget.configuredValue =
         p.getCreateNewChangeForAllNotInTarget();
+    enableSignedPush.configuredValue = p.getEnableSignedPush();
 
     ProjectState parentState = Iterables.getFirst(projectState
         .parents(), null);
@@ -97,6 +104,7 @@
       requireChangeId.inheritedValue = parentState.isRequireChangeID();
       createNewChangeForAllNotInTarget.inheritedValue =
           parentState.isCreateNewChangeForAllNotInTarget();
+      enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
     }
 
     this.useContributorAgreements = useContributorAgreements;
@@ -104,6 +112,9 @@
     this.useContentMerge = useContentMerge;
     this.requireChangeId = requireChangeId;
     this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
+    if (SignedPushModule.isEnabled(gerritConfig)) {
+      this.enableSignedPush = enableSignedPush;
+    }
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
     maxObjectSizeLimit.value =
@@ -158,14 +169,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 b0ac201..c4b686c 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,8 +40,17 @@
 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.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;
@@ -44,8 +59,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)
@@ -54,40 +83,68 @@
     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 String name;
 
   @Inject
-  CreateProject(PerformCreateProject.Factory performCreateProjectFactory,
-      Provider<ProjectsCollection> projectsCollection,
+  CreateProject(Provider<ProjectsCollection> projectsCollection,
       Provider<GroupsCollection> groupsCollection, ProjectJson json,
       DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
       ProjectControl.GenericFactory projectControlFactory,
-      Provider<CurrentUser> currentUser, Provider<PutConfig> putConfig,
+      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,
       @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.name = name;
   }
 
   @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();
     }
@@ -95,8 +152,9 @@
       throw new BadRequestException("name must match URL");
     }
 
-    final CreateProjectArgs args = new CreateProjectArgs();
-    args.setProjectName(name);
+    CreateProjectArgs args = new CreateProjectArgs();
+    args.setProjectName(ProjectUtil.stripGitSuffix(name));
+
     if (!Strings.isNullOrEmpty(input.parent)) {
       args.newParent = projectsCollection.get().parse(input.parent).getControl();
     }
@@ -104,14 +162,16 @@
     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,
@@ -144,7 +204,7 @@
       }
     }
 
-    Project p = createProjectFactory.create(args).createProject();
+    Project p = createProject(args);
 
     if (input.pluginConfigValues != null) {
       try {
@@ -160,4 +220,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..40bbb12 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
@@ -106,11 +106,8 @@
     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..a5a96b1 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()) {
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..2ab10c9 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
@@ -18,15 +18,18 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
 @Singleton
 public class GetConfig implements RestReadView<ProjectResource> {
-
+  private final Config gerritConfig;
   private final TransferConfig config;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
@@ -34,11 +37,13 @@
   private final DynamicMap<RestView<ProjectResource>> views;
 
   @Inject
-  public GetConfig(TransferConfig config,
+  public GetConfig(@GerritServerConfig Config gerritConfig,
+      TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsNameProvider allProjects,
       DynamicMap<RestView<ProjectResource>> views) {
+    this.gerritConfig = gerritConfig;
     this.config = config;
     this.pluginConfigEntries = pluginConfigEntries;
     this.allProjects = allProjects;
@@ -48,7 +53,7 @@
 
   @Override
   public ConfigInfo apply(ProjectResource resource) {
-    return new ConfigInfo(resource.getControl(), config,
+    return new ConfigInfo(gerritConfig, resource.getControl(), config,
         pluginConfigEntries, cfgFactory, allProjects, views);
   }
 }
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..41d4f94 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());
@@ -108,8 +107,6 @@
         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/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index a8eda97..f7914e9 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
@@ -15,10 +15,11 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+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;
@@ -45,11 +46,11 @@
 
 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 +60,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,171 +96,156 @@
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
       throws ResourceNotFoundException, IOException, BadRequestException {
-    List<BranchInfo> branches = Lists.newArrayList();
+    FluentIterable<BranchInfo> branches = allBranches(rsrc);
+    branches = filterBranches(branches);
+    if (start > 0) {
+      branches = branches.skip(start);
+    }
+    if (limit > 0) {
+      branches = branches.limit(limit);
+    }
+    return branches.toList();
+  }
 
-    BranchInfo headBranch = null;
-    BranchInfo configBranch = null;
-    final Set<String> targets = Sets.newHashSet();
-
-    final Repository db;
-    try {
-      db = repoManager.openRepository(rsrc.getNameKey());
+  private FluentIterable<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 FluentIterable.from(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();
+    }
+
+    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 FluentIterable<BranchInfo> filterBranches(
+      FluentIterable<BranchInfo> branches) throws BadRequestException {
+    if (!Strings.isNullOrEmpty(matchSubstring)) {
+      branches = branches.filter(new SubstringPredicate(matchSubstring));
+    } else if (!Strings.isNullOrEmpty(matchRegex)) {
+      branches = branches.filter(new RegexPredicate(matchRegex));
     }
     return branches;
   }
 
+  private static class SubstringPredicate implements Predicate<BranchInfo> {
+    private final String substring;
+
+    private SubstringPredicate(String substring) {
+      this.substring = substring.toLowerCase(Locale.US);
+    }
+
+    @Override
+    public boolean apply(BranchInfo in) {
+      String ref = in.ref;
+      if (ref.startsWith(Constants.R_HEADS)) {
+        ref = ref.substring(Constants.R_HEADS.length());
+      }
+      ref = ref.toLowerCase(Locale.US);
+      return ref.contains(substring);
+    }
+  }
+
+  private static class RegexPredicate implements Predicate<BranchInfo> {
+    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(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()));
+      }
+    }
+  }
+
   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),
@@ -262,22 +261,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/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index a9851e4..04058f2 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
@@ -66,6 +66,7 @@
 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 +91,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());
@@ -236,12 +237,12 @@
     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) {
@@ -255,7 +256,7 @@
 
     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<>();
 
@@ -342,8 +343,7 @@
 
           try {
             if (!showBranch.isEmpty()) {
-              Repository git = repoManager.openRepository(projectName);
-              try {
+              try (Repository git = repoManager.openRepository(projectName)) {
                 if (!type.matches(git)) {
                   continue;
                 }
@@ -362,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();
               }
             }
 
@@ -510,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.getRef(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 b01e563..1c140e3 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
@@ -69,9 +69,7 @@
       ResourceNotFoundException {
     List<TagInfo> tags = Lists.newArrayList();
 
-    Repository repo = getRepository(resource.getNameKey());
-
-    try {
+    try (Repository repo = getRepository(resource.getNameKey())) {
       RevWalk rw = new RevWalk(repo);
       try {
         Map<String, Ref> all = visibleTags(resource.getControl(), repo,
@@ -82,8 +80,6 @@
       } finally {
         rw.dispose();
       }
-    } finally {
-      repo.close();
     }
 
     Collections.sort(tags, new Comparator<TagInfo>() {
@@ -104,7 +100,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/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..b782e3d 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
@@ -535,14 +535,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..455a42b 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,15 @@
     });
   }
 
+  public boolean isEnableSignedPush() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getEnableSignedPush();
+      }
+    });
+  }
+
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
     for (ProjectState s : treeInOrder()) {
@@ -488,25 +485,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/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index f22fb1e..7a4fb6e 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
@@ -47,6 +48,7 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,6 +63,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 +71,14 @@
     public InheritableBoolean useSignedOffBy;
     public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
+    public InheritableBoolean enableSignedPush;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
     public com.google.gerrit.extensions.client.ProjectState state;
     public Map<String, Map<String, ConfigValue>> pluginConfigValues;
   }
 
+  private final Config gerritConfig;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final GitRepositoryManager gitMgr;
@@ -87,7 +92,8 @@
   private final ChangeHooks hooks;
 
   @Inject
-  PutConfig(Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+  PutConfig(@GerritServerConfig Config gerritConfig,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       GitRepositoryManager gitMgr,
       ProjectState.Factory projectStateFactory,
@@ -98,6 +104,7 @@
       DynamicMap<RestView<ProjectResource>> views,
       ChangeHooks hooks,
       Provider<CurrentUser> currentUser) {
+    this.gerritConfig = gerritConfig;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.gitMgr = gitMgr;
@@ -161,6 +168,10 @@
         p.setRequireChangeID(input.requireChangeId);
       }
 
+      if (input.enableSignedPush != null) {
+        p.setEnableSignedPush(input.enableSignedPush);
+      }
+
       if (input.maxObjectSizeLimit != null) {
         p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
       }
@@ -203,8 +214,8 @@
       }
 
       ProjectState state = projectStateFactory.create(projectConfig);
-      return new ConfigInfo(state.controlFor(currentUser.get()), config,
-          pluginConfigEntries, cfgFactory, allProjects, views);
+      return new ConfigInfo(gerritConfig, state.controlFor(currentUser.get()),
+          config, pluginConfigEntries, cfgFactory, allProjects, views);
     } catch (ConfigInvalidException err) {
       throw new ResourceConflictException("Cannot read project " + projectName, err);
     } catch (IOException err) {
@@ -239,7 +250,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 +294,7 @@
           } else {
             if (oldValue != null) {
               validateProjectConfigEntryIsEditable(projectConfigEntry,
-                  projectState, e.getKey(), pluginName);
+                  projectState, v.getKey(), pluginName);
               cfg.unset(v.getKey());
             }
           }
@@ -305,7 +318,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..1d7c724 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,11 +59,11 @@
   }
 
   @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();
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 71c3104..cfabf13 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
@@ -259,6 +259,7 @@
     switch (getCurrentUser().getAccessPath()) {
       case REST_API:
       case JSON_RPC:
+      case UNKNOWN:
         owner = isOwner();
         admin = getCurrentUser().getCapabilities().canAdministrateServer();
         break;
@@ -327,17 +328,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",
@@ -364,18 +360,13 @@
     }
 
     switch (getCurrentUser().getAccessPath()) {
-      case REST_API:
-      case JSON_RPC:
-      case SSH_COMMAND:
-        return getCurrentUser().getCapabilities().canAdministrateServer()
-            || (isOwner() && !isForceBlocked(Permission.PUSH))
-            || canPushWithForce();
-
       case GIT:
         return canPushWithForce();
 
       default:
-        return false;
+        return getCurrentUser().getCapabilities().canAdministrateServer()
+            || (isOwner() && !isForceBlocked(Permission.PUSH))
+            || canPushWithForce();
     }
   }
 
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/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 4ff2b0f..3b031ee 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
@@ -60,11 +60,17 @@
   }
 
   @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();
-    validateParentUpdate(ctl, input.parent, true);
+    validateParentUpdate(ctl, input.parent, checkIfAdmin);
     IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
     try {
       MetaDataUpdate md = updateFactory.create(rsrc.getNameKey());
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 4df9831..e8e29c1 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;
   }
 
@@ -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/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/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
deleted file mode 100644
index 1053d92..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));
-
-  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 e4e94a1..0523d73 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
@@ -16,10 +16,11 @@
 
 import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 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;
@@ -31,6 +32,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;
@@ -67,14 +69,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());
@@ -95,71 +103,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();
+      }
     }
   }
 
@@ -172,12 +271,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));
@@ -204,7 +304,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<>();
@@ -215,6 +315,8 @@
   private List<SubmitRecord> submitRecords;
   private ChangedLines changedLines;
   private Boolean mergeable;
+  private Set<Account.Id> editsByUser;
+  private Set<Account.Id> reviewedBy;
 
   @AssistedInject
   private ChangeData(
@@ -450,6 +552,10 @@
     return change;
   }
 
+  public void setChange(Change c) {
+    change = c;
+  }
+
   public Change reloadChange() throws OrmException {
     change = db.changes().get(legacyId);
     return change;
@@ -468,7 +574,7 @@
       if (c == null) {
         return null;
       }
-      for (PatchSet p : patches()) {
+      for (PatchSet p : patchSets()) {
         if (p.getId().equals(c.currentPatchSetId())) {
           currentPatchSet = p;
           return p;
@@ -517,8 +623,7 @@
   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;
     }
@@ -536,23 +641,28 @@
    * @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;
+  }
+
+  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.
+   * @return patch set 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;
       }
@@ -602,7 +712,7 @@
     return submitRecords;
   }
 
-  public void setMergeable(boolean mergeable) {
+  public void setMergeable(Boolean mergeable) {
     this.mergeable = mergeable;
   }
 
@@ -619,10 +729,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) {
@@ -637,16 +745,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 = change.getId();
+      try (Repository repo = repoManager.openRepository(change.getProject())) {
+        for (String ref
+            : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).keySet()) {
+          if (Change.Id.fromEditRefPart(ref).equals(id)) {
+            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 c9d7e6c..88bb942 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
@@ -19,13 +19,12 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 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.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.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -34,8 +33,11 @@
 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.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;
@@ -57,9 +59,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;
@@ -88,11 +93,13 @@
   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_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
   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";
@@ -107,7 +114,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";
@@ -138,15 +147,16 @@
     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 boolean allowsDrafts;
+    final ChangeIndex index;
 
     private final Provider<CurrentUser> self;
 
@@ -165,6 +175,7 @@
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
+        AllUsersNameProvider allUsersName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
@@ -177,10 +188,11 @@
       this(db, queryProvider, rewriter, userFactory, self,
           capabilityControlFactory, changeControlGenericFactory,
           changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
-          allProjectsName, patchListCache, repoManager, projectCache,
-          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
-          trackingFooters,
-          cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
+          allProjectsName, allUsersName, patchListCache, repoManager,
+          projectCache, listChildProjects, submitStrategyFactory,
+          conflictsCache, trackingFooters,
+          cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true),
+          indexes != null ? indexes.getSearchIndex() : null);
     }
 
     private Arguments(
@@ -197,15 +209,16 @@
         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,
-        boolean allowsDrafts) {
+        boolean allowsDrafts,
+        ChangeIndex index) {
      this.db = db;
      this.queryProvider = queryProvider;
      this.rewriter = rewriter;
@@ -219,15 +232,16 @@
      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.allowsDrafts = allowsDrafts;
+     this.index = index;
     }
 
     Arguments asUser(CurrentUser otherUser) {
@@ -235,9 +249,9 @@
           Providers.of(otherUser),
           capabilityControlFactory, changeControlGenericFactory,
           changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
-          allProjectsName, patchListCache, repoManager, projectCache,
-          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
-          trackingFooters, allowsDrafts);
+          allProjectsName, allUsersName, patchListCache, repoManager,
+          projectCache, listChildProjects, submitStrategyFactory,
+          conflictsCache, trackingFooters, allowsDrafts, index);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -272,6 +286,10 @@
         throw new QueryParseException(NotSignedInException.MESSAGE, e);
       }
     }
+
+    Schema<ChangeData> getSchema() {
+      return index != null ? index.getSchema() : null;
+    }
   }
 
   private final Arguments args;
@@ -322,7 +340,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));
     }
@@ -339,14 +358,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);
     }
@@ -366,6 +384,9 @@
       return new HasDraftByPredicate(args, self());
     }
 
+    if ("edit".equalsIgnoreCase(value)) {
+      return new EditByPredicate(self());
+    }
     throw new IllegalArgumentException();
   }
 
@@ -384,7 +405,7 @@
     }
 
     if ("reviewed".equalsIgnoreCase(value)) {
-      return new IsReviewedPredicate();
+      return IsReviewedPredicate.create(args.getSchema());
     }
 
     if ("owner".equalsIgnoreCase(value)) {
@@ -396,7 +417,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
+      return new IsMergeablePredicate(args.fillArgs);
     }
 
     try {
@@ -410,7 +431,7 @@
 
   @Operator
   public Predicate<ChangeData> commit(String id) {
-    return new CommitPredicate(AbbreviatedObjectId.fromString(id));
+    return new CommitPredicate(args.getSchema(), id);
   }
 
   @Operator
@@ -426,8 +447,9 @@
 
   @Operator
   public Predicate<ChangeData> project(String name) {
-    if (name.startsWith("^"))
+    if (name.startsWith("^")) {
       return new RegexProjectPredicate(name);
+    }
     return new ProjectPredicate(name);
   }
 
@@ -444,15 +466,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
@@ -462,15 +479,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);
   }
 
@@ -553,8 +580,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
@@ -659,9 +685,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);
@@ -743,6 +772,52 @@
     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));
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
@@ -834,9 +909,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/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..e829e53 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
@@ -33,8 +33,8 @@
   public boolean match(ChangeData object) throws OrmException {
     try {
       for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(object.getId()), this), 0, 1)
-          .read()) {
+          Predicate.and(new LegacyChangeIdPredicate(
+              index.getSchema(), object.getId()), this), 0, 1).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..a63f80c 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,49 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.server.index.ChangeField.EXACT_COMMIT;
+
 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;
+import org.eclipse.jgit.lib.Constants;
 
 class CommitPredicate extends IndexPredicate<ChangeData> {
-  private final AbbreviatedObjectId abbrevId;
+  static FieldDef<ChangeData, ?> commitField(Schema<ChangeData> schema,
+      String id) {
+    if (id.length() == Constants.OBJECT_ID_STRING_LENGTH
+        && schema.hasField(EXACT_COMMIT)) {
+      return EXACT_COMMIT;
+    }
+    return ChangeField.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/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 2070746..8bea099 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
@@ -76,7 +76,7 @@
       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(
@@ -117,7 +117,7 @@
                 args.submitStrategyFactory.create(submitType,
                     db.get(), repo, rw, null, canMergeFlag,
                     getAlreadyAccepted(repo, rw, commit),
-                    otherChange.getDest());
+                    otherChange.getDest(), null);
             CodeReviewCommit otherCommit =
                 (CodeReviewCommit) rw.parseCommit(other);
             otherCommit.add(canMergeFlag);
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..5655154
--- /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), 0, 1).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 e08847a..56d26e5 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
@@ -15,16 +15,32 @@
 package com.google.gerrit.server.query.change;
 
 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.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
+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.server.index.ChangeField;
+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 java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -52,15 +68,22 @@
     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;
 
   @Inject
-  InternalChangeQuery(QueryProcessor queryProcessor) {
+  InternalChangeQuery(IndexConfig indexConfig,
+      QueryProcessor queryProcessor,
+      IndexCollection indexes) {
+    this.indexConfig = indexConfig;
     qp = queryProcessor.enforceVisibility(false);
+    this.indexes = indexes;
   }
 
   public InternalChangeQuery setLimit(int n) {
@@ -94,17 +117,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 +125,59 @@
         open()));
   }
 
+  public Iterable<ChangeData> byCommitsOnBranchNotMerged(Branch.NameKey branch,
+      List<String> hashes) throws OrmException {
+    Schema<ChangeData> schema = schema(indexes);
+    if (schema.hasField(ChangeField.EXACT_COMMIT)) {
+      return query(commitsOnBranchNotMerged(branch, commits(schema, hashes)));
+    } else {
+      return byCommitsOnBranchNotMerged(
+          schema, branch, hashes, indexConfig.maxPrefixTerms());
+    }
+  }
+
+  @VisibleForTesting
+  Iterable<ChangeData> byCommitsOnBranchNotMerged(Schema<ChangeData> schema,
+      Branch.NameKey branch, List<String> hashes, int batchSize)
+      throws OrmException {
+    List<Predicate<ChangeData>> commits = commits(schema, hashes);
+    int numBatches = (hashes.size() / batchSize) + 1;
+    List<Predicate<ChangeData>> queries = new ArrayList<>(numBatches);
+    for (List<Predicate<ChangeData>> batch
+        : Iterables.partition(commits, batchSize)) {
+      queries.add(commitsOnBranchNotMerged(branch, batch));
+    }
+    try {
+      return FluentIterable.from(qp.queryChanges(queries))
+        .transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
+          @Override
+          public List<ChangeData> apply(QueryResult in) {
+            return in.changes();
+          }
+        });
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  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;
+  }
+
+  private static Predicate<ChangeData> commitsOnBranchNotMerged(
+      Branch.NameKey branch, List<Predicate<ChangeData>> commits) {
+    return and(
+        ref(branch),
+        project(branch.getParentKey()),
+        not(status(Change.Status.MERGED)),
+        or(commits));
+  }
+
   public List<ChangeData> byProjectOpen(Project.NameKey project)
       throws OrmException {
     return query(and(project(project), open()));
@@ -120,7 +185,20 @@
 
   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> 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)));
   }
 
   private List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
@@ -130,4 +208,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/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..b990091 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;
@@ -37,10 +38,11 @@
     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/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%
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/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..72230f6 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
@@ -37,8 +37,8 @@
   public boolean match(ChangeData object) throws OrmException {
     try {
       for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(object.getId()), this), 0, 1)
-          .read()) {
+          Predicate.and(new LegacyChangeIdPredicate(
+              index.getSchema(), object.getId()), this), 0, 1).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 222c2bb..f73e0e4 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
@@ -209,11 +209,11 @@
 
           if (includePatchSets) {
             if (includeFiles) {
-              eventFactory.addPatchSets(c, d.patches(),
+              eventFactory.addPatchSets(c, d.patchSets(),
                 includeApprovals ? d.approvals().asMap() : null,
                 includeFiles, d.change(), labelTypes);
             } else {
-              eventFactory.addPatchSets(c, d.patches(),
+              eventFactory.addPatchSets(c, d.patchSets(),
                   includeApprovals ? d.approvals().asMap() : null,
                   labelTypes);
             }
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 299f0f1..bd6b297 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) {
@@ -141,7 +141,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/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 51d971d..6068dd0 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
@@ -132,6 +132,13 @@
       if (limit == getBackendSupportedLimit()) {
         limit--;
       }
+
+      int page = (start / limit) + 1;
+      if (page > indexConfig.maxPages()) {
+        throw new QueryParseException(
+            "Cannot go beyond page " + indexConfig.maxPages() + "of results");
+      }
+
       Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
       if (!(s instanceof ChangeDataSource)) {
         q = Predicate.and(open(), q);
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..de7005f 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(FuzzyTopicPredicate.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/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..b142bb0 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;
@@ -137,6 +139,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 +186,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 +194,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..3fa7986 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,6 +55,11 @@
     this.serverUser = serverUser;
   }
 
+  public AllUsersCreator setAdministrators(GroupReference admin) {
+    this.admin = admin;
+    return this;
+  }
+
   public void create() throws IOException, ConfigInvalidException {
     Repository git = null;
     try {
@@ -84,8 +94,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/DB2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java
new file mode 100644
index 0000000..4ad8e2f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.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.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();
+  }
+}
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..f50f123 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,7 @@
 
   @Override
   protected void configure() {
+    bind(DataSourceType.class).annotatedWith(Names.named("db2")).to(DB2.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/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/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/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 2b9d4b4..8da9e2a 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
@@ -90,16 +90,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 +109,6 @@
 
         updateSystemConfig(db);
       }
-    } finally {
-      db.close();
     }
   }
 
@@ -131,9 +126,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..7918df4 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_110> C = Schema_110.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..8cbf119
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
@@ -0,0 +1,161 @@
+// 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.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+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.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.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;
+
+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.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());
+      } 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) 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());
+      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);
+        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) throws OrmException {
+    SetMultimap<Project.NameKey, Change.Id> openByProject =
+        HashMultimap.create();
+    for (Change c : db.changes().all()) {
+      if (c.getStatus().isOpen()) {
+        openByProject.put(c.getProject(), c.getId());
+      }
+    }
+    return openByProject;
+  }
+
+  private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id)
+      throws IOException {
+    if (id == null) {
+      return null;
+    }
+    RevObject obj = rw.parseAny(id);
+    return (obj instanceof RevCommit) ? (RevCommit) obj : 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..c3d2356
--- /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-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..8ef32c0 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
@@ -73,8 +73,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 +106,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..7665c64 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
@@ -26,6 +26,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -35,8 +36,8 @@
 
   @Inject
   DefaultSecureStore(SitePaths site) {
-    File secureConfig = new File(site.etc_dir, "secure.config");
-    sec = new FileBasedConfig(secureConfig, FS.DETECTED);
+    Path secureConfig = site.etc_dir.resolve("secure.config");
+    sec = new FileBasedConfig(secureConfig.toFile(), FS.DETECTED);
     try {
       sec.load();
     } catch (Exception e) {
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..21634ac 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
@@ -109,21 +109,15 @@
 
   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) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/BouncyCastleUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/BouncyCastleUtil.java
new file mode 100644
index 0000000..ba87d58
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/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.server.util;
+
+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-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..ee74b35 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());
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/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 cf7f11f..32cdca5 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
@@ -33,8 +33,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 +56,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.setFile(resolve(logdir).resolve(name).toString());
     dst.setImmediateFlush(true);
     dst.setAppend(true);
     dst.setErrorHandler(new DieErrorHandler());
@@ -91,11 +91,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/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..a63b1e7 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
@@ -21,12 +21,12 @@
 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;
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/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index 3d8d271..2646fd0 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
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..be88bd4 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
 #
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..e658729 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,14 @@
 
 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 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 +77,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,11 +99,10 @@
   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(
@@ -112,8 +110,6 @@
       if (!env.execute(Prolog.BUILTIN, "consult_stream", pathTerm, inTerm)) {
         throw new CompileException("Cannot consult " + prologResource);
       }
-    } finally {
-      in.close();
     }
   }
 
@@ -173,20 +169,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/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index 8bedd17..e953cbf 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;
@@ -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();
   }
@@ -393,22 +404,6 @@
       }};
   }
 
-  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,
       Map<String, ? extends List<PatchLineComment>> expected) throws Exception {
     assertCommentMap(comments.list().apply(res), expected, true);
@@ -422,7 +417,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 +438,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..6100ffd 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(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-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/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..ba06770 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;
@@ -91,30 +88,30 @@
         ));
 
     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");
+    assertThat(ca.isRequireContactInformation()).isTrue();
 
     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 +128,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 +146,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 +162,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
@@ -205,7 +202,7 @@
     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 +214,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 +235,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,
@@ -274,15 +270,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/git/gpg/PublicKeyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyCheckerTest.java
new file mode 100644
index 0000000..a82619c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyCheckerTest.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.server.git.gpg;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class PublicKeyCheckerTest {
+  private PublicKeyChecker checker;
+
+  @Before
+  public void setUp() {
+    checker = new PublicKeyChecker();
+  }
+
+  @Test
+  public void validKey() throws Exception {
+    assertProblems(TestKey.key1());
+  }
+
+  @Test
+  public void wrongKeyId() throws Exception {
+    TestKey k = TestKey.key1();
+    long badId = k.getKeyId() + 1;
+    CheckResult result = checker.check(k.getPublicKey(), badId);
+    assertEquals(
+        Arrays.asList("Public key does not match ID 46328A8D"),
+        result.getProblems());
+  }
+
+  @Test
+  public void keyExpiringInFuture() throws Exception {
+    assertProblems(TestKey.key2());
+  }
+
+  @Test
+  public void expiredKey() throws Exception {
+    assertProblems(TestKey.key3(), "Key is expired");
+  }
+
+  @Test
+  public void selfRevokedKey() throws Exception {
+    assertProblems(TestKey.key4(), "Key is revoked");
+  }
+
+  private void assertProblems(TestKey tk, String... expected) throws Exception {
+    CheckResult result = checker.check(tk.getPublicKey(), tk.getKeyId());
+    assertEquals(Arrays.asList(expected), result.getProblems());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyStoreTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyStoreTest.java
new file mode 100644
index 0000000..f42e8b3
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PublicKeyStoreTest.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.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyObjectId;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+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.junit.Before;
+import org.junit.Test;
+
+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 = TestKey.key1().getPublicKey();
+    assertEquals("46328A8C", keyIdToString(key.getKeyID()));
+  }
+
+  @Test
+  public void testKeyToString() throws Exception {
+    PGPPublicKey key = TestKey.key1().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 = TestKey.key1().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 = TestKey.key1();
+    tr.branch(RefNames.REFS_GPG_KEYS)
+        .commit()
+        .add(keyObjectId(key1.getKeyId()).name(),
+          key1.getPublicKeyArmored())
+        .create();
+    TestKey key2 = TestKey.key2();
+    tr.branch(RefNames.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 = TestKey.key1();
+    TestKey key2 = TestKey.key2();
+    tr.branch(RefNames.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);
+  }
+
+  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);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PushCertificateCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PushCertificateCheckerTest.java
new file mode 100644
index 0000000..aa2a2c7
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/PushCertificateCheckerTest.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.server.git.gpg;
+
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.BCPGOutputStream;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureGenerator;
+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.junit.TestRepository;
+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.util.Arrays;
+
+public class PushCertificateCheckerTest {
+  private TestRepository<?> tr;
+  private SignedPushConfig signedPushConfig;
+  private PushCertificateChecker checker;
+
+  @Before
+  public void setUp() throws Exception {
+    TestKey key1 = TestKey.key1();
+    TestKey key3 = TestKey.key3();
+    tr = new TestRepository<>(new InMemoryRepository(
+        new DfsRepositoryDescription("repo")));
+    tr.branch(RefNames.REFS_GPG_KEYS).commit()
+        .add(PublicKeyStore.keyObjectId(key1.getPublicKey().getKeyID()).name(),
+            key1.getPublicKeyArmored())
+        .add(PublicKeyStore.keyObjectId(key3.getPublicKey().getKeyID()).name(),
+            key3.getPublicKeyArmored())
+        .create();
+    signedPushConfig = new SignedPushConfig();
+    signedPushConfig.setCertNonceSeed("sekret");
+    signedPushConfig.setCertNonceSlopLimit(60 * 24);
+
+    checker = new PushCertificateChecker(new PublicKeyChecker()) {
+      @Override
+      protected Repository getRepository() {
+        return tr.getRepository();
+      }
+
+      @Override
+      protected boolean shouldClose(Repository repo) {
+        return false;
+      }
+    };
+  }
+
+  @Test
+  public void validCert() throws Exception {
+    PushCertificate cert = newSignedCert(validNonce(), TestKey.key1());
+    assertProblems(cert);
+  }
+
+  @Test
+  public void invalidNonce() throws Exception {
+    PushCertificate cert = newSignedCert("invalid-nonce", TestKey.key1());
+    assertProblems(cert, "Invalid nonce");
+  }
+
+  @Test
+  public void missingKey() throws Exception {
+    TestKey key2 = TestKey.key2();
+    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 = TestKey.key3();
+    PushCertificate cert = newSignedCert(validNonce(), key3);
+    assertProblems(cert,
+        "Invalid public key (" + keyToString(key3.getPublicKey())
+          + "):\n  Key is expired");
+  }
+
+  private String validNonce() {
+    return signedPushConfig.getNonceGenerator()
+        .createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
+  }
+
+  private PushCertificate newSignedCert(String nonce, TestKey signingKey)
+      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));
+    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(tr.getRepository(), signedPushConfig);
+    return parser.parse(reader);
+  }
+
+  private void assertProblems(PushCertificate cert, String... expected)
+      throws Exception {
+    CheckResult result = checker.check(cert);
+    assertEquals(Arrays.asList(expected), result.getProblems());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/TestKey.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/TestKey.java
new file mode 100644
index 0000000..69362ab
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/gpg/TestKey.java
@@ -0,0 +1,496 @@
+// 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.gpg;
+
+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;
+
+class TestKey {
+  /**
+   * 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>
+   */
+  static TestKey key1() throws PGPException, IOException {
+    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>
+   */
+  static final TestKey key2() throws PGPException, IOException {
+    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>
+   */
+  static final TestKey key3() throws PGPException, IOException {
+    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>
+   */
+  static final TestKey key4() throws PGPException, IOException {
+    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");
+  }
+
+  // TODO(dborowitz): Figure out how to get gpg to revoke a key for someone
+  // else.
+
+  private final String pubArmored;
+  private final String secArmored;
+  private final PGPPublicKey pub;
+  private final PGPSecretKey sec;
+
+  private TestKey(String pubArmored, String secArmored)
+      throws PGPException, IOException {
+    this.pubArmored = pubArmored;
+    this.secArmored = secArmored;
+    BcKeyFingerprintCalculator fc = new BcKeyFingerprintCalculator();
+    this.pub = new PGPPublicKeyRing(newStream(pubArmored), fc).getPublicKey();
+    this.sec = new PGPSecretKeyRing(newStream(secArmored), fc).getSecretKey();
+  }
+
+  String getPublicKeyArmored() {
+    return pubArmored;
+  }
+
+  String getSecretKeyArmored() {
+    return secArmored;
+  }
+
+  PGPPublicKey getPublicKey() {
+    return pub;
+  }
+
+  PGPSecretKey getSecretKey() {
+    return sec;
+  }
+
+  long getKeyId() {
+    return pub.getKeyID();
+  }
+
+  String getFirstUserId() {
+    return (String) pub.getUserIDs().next();
+  }
+
+  PGPPrivateKey getPrivateKey() throws PGPException {
+    return sec.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-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 c9a2056..0c8625d 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
@@ -27,7 +27,7 @@
           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, indexes, 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
index 042459b..ac6d805 100644
--- 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
@@ -19,7 +19,6 @@
 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;
@@ -30,7 +29,6 @@
 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;
@@ -54,7 +52,7 @@
     indexes = new IndexCollection();
     indexes.setSearchIndex(index);
     queryBuilder = new FakeQueryBuilder(indexes);
-    rewrite = new IndexRewriteImpl(indexes, new BasicChangeRewrites());
+    rewrite = new IndexRewriteImpl(indexes);
   }
 
   @Test
@@ -94,9 +92,9 @@
   @Test
   public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
     Predicate<ChangeData> in =
-        parse("-status:abandoned (status:open OR status:merged)");
+        parse("-status:abandoned (file:a OR file:b)");
     assertEquals(
-        query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
+        query(in),
         rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT));
   }
 
@@ -184,17 +182,17 @@
   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),
+    assertEquals(EnumSet.of(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(none, status("(is:new is:draft) (is:merged)"));
+    assertEquals(none, status("(is:new is:draft) (is:merged)"));
 
-    assertEquals(EnumSet.of(MERGED, SUBMITTED),
-        status("(is:new is:draft) OR (is:merged OR is:submitted)"));
+    assertEquals(EnumSet.of(MERGED),
+        status("(is:new is:draft) OR (is:merged)"));
   }
 
   @Test
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..c7b81a2 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,72 @@
 
 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 org.junit.rules.ExpectedException;
 
 import java.io.UnsupportedEncodingException;
 
 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,59 +101,57 @@
     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) {
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/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 1308723..108c20f 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
@@ -203,16 +203,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..f19987f 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.getRef(update1.getRefName())).isNull();
 
       batch2 = update2.openUpdateInBatch(bru);
       batch2.write(update2, new CommitBuilder());
       batch2.commit();
-      assertNull(repo.getRef(update2.getRefName()));
+      assertThat(repo.getRef(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.getRef(update1.getRefName())).isNotNull();
+    assertThat(repo.getRef(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 4c123fd..85272d0 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;
@@ -44,12 +43,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");
@@ -104,8 +230,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);
@@ -123,8 +249,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);
@@ -133,8 +259,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);
@@ -153,13 +279,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
@@ -168,10 +290,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
@@ -179,12 +299,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
@@ -194,26 +312,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
@@ -221,8 +335,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
@@ -231,10 +344,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
@@ -244,10 +357,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
@@ -257,9 +370,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
@@ -269,34 +382,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
@@ -305,10 +419,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
@@ -316,9 +428,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
@@ -326,7 +439,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
@@ -336,7 +449,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
@@ -347,10 +460,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
@@ -364,10 +477,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
@@ -377,8 +490,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
@@ -387,7 +499,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
@@ -397,7 +509,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
@@ -407,7 +519,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
@@ -416,7 +528,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
@@ -425,7 +537,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
@@ -434,7 +546,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
@@ -444,7 +556,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
@@ -453,7 +565,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
@@ -462,7 +576,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
@@ -471,8 +587,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
@@ -481,8 +598,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
@@ -492,8 +610,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
@@ -503,8 +621,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
@@ -514,8 +632,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
@@ -527,35 +645,29 @@
     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);
   }
 }
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..6c3a39a 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,9 +21,12 @@
 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.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
@@ -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/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 34588fa..1372479 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,17 +33,17 @@
 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.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.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;
@@ -51,11 +52,11 @@
 import com.google.gerrit.server.account.AuthRequest;
 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.change.PatchSetInserter.ValidatePolicy;
+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.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
@@ -63,12 +64,12 @@
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
 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.revwalk.RevCommit;
@@ -78,35 +79,50 @@
 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.List;
 import java.util.concurrent.atomic.AtomicLong;
 
 @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 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 SchemaCreator schemaCreator;
   @Inject protected ThreadLocalRequestContext requestContext;
 
@@ -122,10 +138,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();
@@ -135,12 +151,13 @@
     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() {
@@ -189,58 +206,54 @@
 
   @Test
   public void byId() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, null, null).insert();
     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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, null, "branch").insert();
     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);
@@ -250,16 +263,16 @@
     change2.setStatus(Change.Status.MERGED);
     ins2.insert();
 
-    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);
@@ -273,31 +286,23 @@
     change3.setStatus(Change.Status.MERGED);
     ins3.insert();
 
-    List<ChangeInfo> results;
-    results = query("status:open");
-    assertThat(results).hasSize(2);
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-
-    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);
-
-    results = query("is: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);
   }
 
   @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);
@@ -311,29 +316,21 @@
     change3.setStatus(Change.Status.NEW);
     ins3.insert();
 
-    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);
@@ -343,109 +340,101 @@
     change2.setStatus(Change.Status.MERGED);
     ins2.insert();
 
-    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();
     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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     Change change2 = newChange(repo, null, null, user2, null).insert();
 
-    assertResultEquals(change1, queryOne("owner:" + userId.get()));
-    assertResultEquals(change2, queryOne("owner:" + user2));
+    assertQuery("owner:" + userId.get(), change1);
+    assertQuery("owner:" + user2, change2);
   }
 
   @Test
   public void byOwnerIn() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     Change change2 = newChange(repo, null, null, user2, null).insert();
 
-    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");
+    TestRepository<Repo> repo1 = createProject("repo1");
+    TestRepository<Repo> repo2 = createProject("repo2");
     Change change1 = newChange(repo1, null, null, null, null).insert();
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
-    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");
+    TestRepository<Repo> repo1 = createProject("repo1");
+    TestRepository<Repo> repo2 = createProject("repo2");
     Change change1 = newChange(repo1, null, null, null, null).insert();
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, "master").insert();
     Change change2 = newChange(repo, null, null, null, "branch").insert();
 
-    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");
@@ -456,30 +445,44 @@
     change2.setTopic("feature2");
     ins2.insert();
 
-    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");
+    ins3.insert();
 
-    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");
+    ins4.insert();
+
+    Change change5 = newChange(repo, null, null, null, null).insert();
+
+    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);
   }
 
   @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();
     RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    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();
@@ -487,65 +490,61 @@
         repo.parseBody(repo.commit().message("12346 67891").create());
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    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();
 
-    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);
+    gApi.changes().id(change.getId().get()).current()
+      .review(new ReviewInput().label("Code-Review", 1));
 
-    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();
+    assertQuery("label:Code-Review=-2");
+    assertQuery("label:Code-Review-2");
+    assertQuery("label:Code-Review=-1");
+    assertQuery("label:Code-Review-1");
+    assertQuery("label:Code-Review=0");
+    assertQuery("label:Code-Review=+1", change);
+    assertQuery("label:Code-Review=1", change);
+    assertQuery("label:Code-Review+1", change);
+    assertQuery("label:Code-Review=+2");
+    assertQuery("label:Code-Review=2");
+    assertQuery("label:Code-Review+2");
 
-    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();
+    assertQuery("label:Code-Review>=0", change);
+    assertQuery("label:Code-Review>0", change);
+    assertQuery("label:Code-Review>=1", change);
+    assertQuery("label:Code-Review>1");
+    assertQuery("label:Code-Review>=2");
 
-    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();
+    assertQuery("label: Code-Review<=2", change);
+    assertQuery("label: Code-Review<2", change);
+    assertQuery("label: Code-Review<=1", change);
+    assertQuery("label:Code-Review<1");
+    assertQuery("label:Code-Review<=0");
 
-    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"));
+    assertQuery("label:Code-Review=+1,anotheruser");
+    assertQuery("label:Code-Review=+1,user", change);
+    assertQuery("label:Code-Review=+1,user=user", change);
+    assertQuery("label:Code-Review=+1,Administrators", change);
+    assertQuery("label:Code-Review=+1,group=Administrators", change);
   }
 
   @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();
     }
 
-    List<ChangeInfo> results;
     for (int i = 1; i <= n + 2; i++) {
       int expectedSize;
       Boolean expectedMoreChanges;
@@ -556,86 +555,62 @@
         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());
     }
 
-    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());
     }
 
-    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 = newChange(repo, null, null, null, null).insert();
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     List<ChangeInserter> inserters = Lists.newArrayList();
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 5; i++) {
@@ -644,93 +619,68 @@
     }
 
     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");
+    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();
 
-    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();
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, userId.get(), null).insert();
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
@@ -738,88 +688,87 @@
       newChange(repo, null, null, user2, null).insert();
     }
 
-    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();
     }
 
-    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();
 
-    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();
 
-    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();
 
-    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();
 
-    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();
 
@@ -830,99 +779,74 @@
     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");
+    TestRepository<Repo> 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 now = TimeUtil.nowMs();
-    assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1)).isEqualTo(thirtyHours);
+    assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1))
+        .isEqualTo(thirtyHours);
     assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHours);
     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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0;
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0;
 
-    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(
@@ -934,32 +858,53 @@
     Change change1 = newChange(repo, commit1, null, null, null).insert();
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
@@ -977,34 +922,31 @@
   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();
 
@@ -1023,8 +965,7 @@
     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();
@@ -1033,24 +974,25 @@
 
     Change change6 = newChange(repo, null, null, null, "branch6").insert();
 
-    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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
@@ -1058,20 +1000,17 @@
     ins2.insert();
 
     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");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
@@ -1079,20 +1018,185 @@
     ins2.insert();
 
     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 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    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 = newChange(repo, null, null, null, null).insert();
+
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    ChangeInserter ins2 = newChange(repo, null, null, user2, null);
+    Change change2 = ins2.insert();
+
+    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 = newChange(repo, commit1, null, null, null).insert();
+    Change change2 = newChange(repo, commit2, null, null, null).insert();
+    Change change3 = newChange(repo, commit3, null, null, null).insert();
+    Change change4 = newChange(repo, commit4, null, null, null).insert();
+
+    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 {
+    clockStepMs = MILLISECONDS.convert(2, MINUTES);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change3 = newChange(repo, null, null, null, null).insert();
+
+    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);
+      ins.insert();
+      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(
+          indexes.getSearchIndex().getSchema(), 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 = "batch size " + i;
+      assertThat(ids).named(name).hasSize(n);
+      assertThat(ids).named(name)
+          .containsExactlyElementsIn(expectedIds);
+    }
   }
 
   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) {
@@ -1120,75 +1224,83 @@
 
     Change change = new Change(new Change.Key(key), id, ownerId,
         new Branch.NameKey(project, branch), TimeUtil.nowTs());
+    IdentifiedUser user = userFactory.create(Providers.of(db), ownerId);
     return changeFactory.create(
-        projectControlFactory.controlFor(project, userFactory.create(ownerId)),
+        projectControlFactory.controlFor(project, user),
         change,
         commit);
   }
 
-  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 Change newPatchSet(TestRepository<Repo> repo, Change c)
+      throws Exception {
+    // 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());
+    ChangeControl ctl = changeControlFactory.controlFor(c.getId(), user);
+    return patchSetFactory.create(
+          repo.getRepository(), repo.getRevWalk(), ctl, commit)
+        .setSendMail(false)
+        .setRunHooks(false)
+        .setValidatePolicy(ValidatePolicy.NONE)
+        .insert();
   }
 
   protected void assertBadQuery(Object query) throws Exception {
-    try {
-      query(query);
-      fail("expected BadRequestException for query: " + query);
-    } catch (BadRequestException e) {
-      // Expected.
-    }
+    assertBadQuery(newQuery(query));
   }
 
-  protected TestRepository<InMemoryRepository> createProject(String name)
-      throws Exception {
-    CreateProject create = projectFactory.create(name);
-    create.apply(TLR, new ProjectInput());
+  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..6122d65 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,7 +35,7 @@
 
   @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();
@@ -45,12 +43,12 @@
         repo.parseBody(repo.commit().message("one.two.three").create());
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    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..79e326f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
@@ -0,0 +1,109 @@
+// 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.MILLISECONDS;
+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.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.
+  }
+
+  @Test
+  public void isReviewed() throws Exception {
+    clockStepMs = MILLISECONDS.convert(2, MINUTES);
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    Change change3 = newChange(repo, null, null, null, null).insert();
+
+    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..7412b3f 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,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+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;
@@ -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/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..807f78d 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,7 +17,6 @@
 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;
@@ -25,7 +24,6 @@
 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;
@@ -46,11 +44,9 @@
 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.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 +69,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 {
@@ -93,12 +88,14 @@
     cfg.setString("gerrit", null, "allProjects", "Test-Projects");
     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 +114,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 +136,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,7 +183,7 @@
       }
     });
     install(new DefaultCacheFactory.Module());
-    install(new SmtpEmailSender.Module());
+    install(new FakeEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
 
     IndexType indexType = null;
@@ -204,10 +207,8 @@
   @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");
+  public ExecutorService createEmailReviewCommentsExecutor() {
+    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..ec53b29 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,16 +35,22 @@
     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));
     }
@@ -58,13 +64,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 +86,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 +101,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 +116,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-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 f4ff65b..dcff98e 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -29,6 +29,7 @@
     '//lib/mina:core',
     '//lib/mina:sshd',
     '//lib/jgit:jgit',
+    '//lib/jgit:jgit-archive',
   ],
   provided_deps = [
     '//lib/bouncycastle:bcprov',
@@ -51,8 +52,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..6d7ad0f 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
@@ -203,11 +203,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());
       }
@@ -338,8 +334,7 @@
       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()) {
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/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index cc7b637..1a6b2dd 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,10 @@
 
 package com.google.gerrit.sshd;
 
+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 +36,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 +68,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;
@@ -169,56 +172,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/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 39eb720..577eb55 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;
@@ -129,18 +130,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 +217,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 +240,7 @@
     initForwarding();
     initFileSystemFactory();
     initSubsystems();
-    initCompression();
+    initCompression(enableCompression);
     initUserAuth(userAuth, kerberosAuth, kerberosKeytab, kerberosPrincipal);
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
@@ -443,7 +449,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;
@@ -453,9 +460,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++) {
@@ -591,13 +598,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() {
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 439b8c8..4d6a790 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
@@ -220,7 +220,8 @@
 
     event.setProperty(P_SESSION, id(sd.getSessionId()));
 
-    String userName = "-", accountId = "-";
+    String userName = "-";
+    String accountId = "-";
 
     if (user != null && user.isIdentifiedUser()) {
       IdentifiedUser u = (IdentifiedUser) user;
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..b658440 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);
     }
   }
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/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..06b1cf5 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
@@ -81,17 +81,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 +119,6 @@
     command(gerrit, SetAccountCommand.class);
     command(gerrit, AdminSetParent.class);
 
-    command(gerrit, CreateAccountCommand.class);
     command(testSubmit, TestSubmitRuleCommand.class);
     command(testSubmit, TestSubmitTypeCommand.class);
 
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/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..ad72b13 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,6 +18,7 @@
 
 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.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
@@ -25,7 +26,6 @@
 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;
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/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..ff60410 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
@@ -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 3ac72a7..bc820a4 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
@@ -73,7 +73,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,
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..974b5c6 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
@@ -18,6 +18,7 @@
 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;
@@ -270,7 +271,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 {
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/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/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 889d3fb..2dcff16 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,7 @@
 
 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 com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.EventSource;
@@ -40,7 +40,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;
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..379ee4b
--- /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.equals(ObjectId.zeroId())) {
+        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..e770f60 100644
--- a/gerrit-util-http/BUCK
+++ b/gerrit-util-http/BUCK
@@ -12,6 +12,7 @@
     ':http',
     '//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..42fcb16 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,11 +14,11 @@
 
 package com.google.gerrit.util.http;
 
+import static com.google.common.truth.Truth.assertThat;
 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 org.junit.After;
 import org.junit.Before;
@@ -47,36 +47,36 @@
 
   @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(
+        mockRequest("/s/foo/bar", "", "/s"))).isEqualTo("/foo/bar");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        mockRequest("/s/foo%2Fbar", "", "/s"))).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(
+        mockRequest("/c/foo/bar", "", "/c"))).isEqualTo("/foo/bar");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/foo%2Fbar", "", "/c"))).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(
+        mockRequest("/c/s/foo/bar/", "/c", "/s"))).isEqualTo("/foo/bar/");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo/bar///", "/c", "/s"))).isEqualTo("/foo/bar/");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo%2Fbar/", "/c", "/s"))).isEqualTo("/foo%2Fbar/");
+    assertThat(RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo%2Fbar///", "/c", "/s"))).isEqualTo("/foo%2Fbar/");
   }
 
   @Test
   public void servletPathMatchesRequestPath() {
-    assertEquals(null, RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s", "/c", "/s")));
+    assertThat(RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s", "/c", "/s"))).isNull();
   }
 
   private HttpServletRequest mockRequest(String uri, String contextPath, String servletPath) {
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index 35f6084..1f82849 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -17,7 +17,6 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//gerrit-server/src/main/prolog:common',
-    '//gerrit-solr:solr',
     '//gerrit-sshd:sshd',
     '//lib:guava',
     '//lib:gwtorm',
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index f1b6f00..0aeccc7 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.3</version>
+  <version>2.12-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..50c822e 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
@@ -28,6 +28,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;
@@ -35,7 +36,6 @@
 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;
@@ -61,7 +61,6 @@
 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;
@@ -83,8 +82,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,7 +106,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(WebAppInitializer.class);
 
-  private File sitePath;
+  private Path sitePath;
   private Injector dbInjector;
   private Injector cfgInjector;
   private Injector sysInjector;
@@ -127,7 +127,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) {
@@ -216,7 +216,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 +268,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);
         }
       });
@@ -301,9 +301,6 @@
       case LUCENE:
         changeIndexModule = new LuceneIndexModule();
         break;
-      case SOLR:
-        changeIndexModule = new SolrIndexModule();
-        break;
       default:
         throw new IllegalStateException("unsupported index.type");
     }
@@ -315,7 +312,6 @@
       }
     });
     modules.add(SshKeyCacheImpl.module());
-    modules.add(new MasterNodeStartup());
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
@@ -323,6 +319,7 @@
       }
     });
     modules.add(new GarbageCollectionModule());
+    modules.add(new ChangeCleanupRunner.Module());
     return cfgInjector.createChildInjector(modules);
   }
 
diff --git a/lib/BUCK b/lib/BUCK
index 0797af7..1dbac0a 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -25,15 +25,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-16-gc4e356a',
+  bin_sha1 = '01225468065812bbe5f27972df6dafa9d796d833',
+  src_sha1 = '3622460ed58684cb33f786e3748637c8eea324f9',
   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',
@@ -45,15 +51,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-rc1',
+  sha1 = '0364538ac107b8943a1f4d68ac50f1b0421bb983',
   license = 'Apache2.0',
 )
 
@@ -72,15 +78,15 @@
 
 maven_jar(
   name = 'jsch',
-  id = 'com.jcraft:jsch:0.1.51',
-  sha1 = '6ceee2696b07cc320d0e1aaea82c7b40768aca0f',
+  id = 'com.jcraft:jsch:0.1.53',
+  sha1 = '658b682d5c817b27ae795637dfec047c63d29935',
   license = 'jsch',
 )
 
 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'],
 )
@@ -171,10 +177,10 @@
 
 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(
@@ -187,10 +193,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 66a12c1..f8feb63 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -43,8 +43,8 @@
 
 maven_jar(
   name = 'asciidoctor',
-  id = 'org.asciidoctor:asciidoctorj:1.5.0',
-  sha1 = '192df5660f72a0fb76966dcc64193b94fba65f99',
+  id = 'org.asciidoctor:asciidoctorj:1.5.2',
+  sha1 = '39d33f739ec1c46f6e908a725264eb74b23c9f99',
   license = 'Apache2.0',
   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..081cdd8 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -25,7 +25,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 +50,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 +81,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/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..a7244f2 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.5'
+SHA1 = 'd9cee6fe3de8e02372b1ac1e9a627224a4f649a7'
 
 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..8259252 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -64,6 +64,7 @@
   'php',
   'pig',
   'properties',
+  'puppet',
   'python',
   'r',
   'rst',
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 155baad..2ed62f6 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'],
 )
@@ -74,13 +73,6 @@
 )
 
 maven_jar(
-  name = 'io',
-  id = 'commons-io:commons-io:1.4',
-  sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
-  license = 'Apache2.0',
-)
-
-maven_jar(
   name = 'validator',
   id = 'commons-validator:commons-validator:1.4.1',
   sha1 = '2231238e391057a53f92bde5bbc588622c1956c3',
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/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..d02916f 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.12.v20150709'
 EXCLUDE = ['about.html']
 
 maven_jar(
   name = 'servlet',
   id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
-  sha1 = '1797875a3cc524d181733f323866a5f7bbca03a7',
+  sha1 = '50116cac18ad893c9628f0a1984390464b133921',
   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 = '9ace95998fbaae8425b2621c90230a229a554784',
   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 = 'a1f9e7874e1db2f664213f524463d12bd5ab5db4',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [
@@ -37,7 +37,7 @@
 maven_jar(
   name = 'server',
   id = 'org.eclipse.jetty:jetty-server:' + VERSION,
-  sha1 = 'd30a52e992c3484569f58763f55097a1da3202ee',
+  sha1 = '8c90ceffb6954385b024899d334192947d0e4077',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -49,7 +49,7 @@
 maven_jar(
   name = 'jmx',
   id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
-  sha1 = 'e0a9df505fbcc7c0481209325a106b922097468d',
+  sha1 = '8bc0288abba26dbbf4e9225d6fe6fa6348f8da05',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -61,7 +61,7 @@
 maven_jar(
   name = 'continuation',
   id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
-  sha1 = '476cae89c420170549b4851ed58dca25f349d16d',
+  sha1 = '0578cb87b78b71eeda91f5dfa3e8bfbafb55cced',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
@@ -69,7 +69,7 @@
 maven_jar(
   name = 'http',
   id = 'org.eclipse.jetty:jetty-http:' + VERSION,
-  sha1 = '8b30ddc8304df24a36efbfa267acc24b7403b692',
+  sha1 = '9a6c83f52c70c28e2272d83866b4111cd15ddbc5',
   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 = 'c02e9e303d231a589e0c8866c1ee89bcdeb40a55',
   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 = 'd99d38adfdb5ec677643f04fa862554b0bb8b42e',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index 3f30123..4e47cc7 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -1,13 +1,13 @@
 include_defs('//lib/maven.defs')
 
-REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.0.1.201506240215-r'
+REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
+VERS = '4.0.1.201506240215-r.94-g39dc898'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '3bdf2d666df1a5373f7ad291c075ab1329560afd',
-  src_sha1 = 'c8ab3011612a4680791df394e2ef1ab8debeaed6',
+  bin_sha1 = 'e076b3f71daaadd27e9cfc810778968deed04038',
+  src_sha1 = '255a8c836c1a85da9ffc371ba3e59e63c2bc1dee',
   license = 'jgit',
   repository = REPO,
   unsign = True,
@@ -22,7 +22,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = '8c73719477224802eda2a2da65bce8946d0fac6f',
+  sha1 = '4d4346164f89593a82670780fd041be358ab76c9',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit'],
@@ -36,7 +36,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '124e353f51adbbc1af12b143012cc1ebfa2c1012',
+  sha1 = '96408a19e7506bf19f25b55574ca212a3e789961',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit',
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = 'bddb62b8f532b6d46ac832d909fa46b73c40a126',
+  sha1 = '172e9b034bdd85b9611f3958258733fd74889cc7',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
@@ -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/log/BUCK b/lib/log/BUCK
index cadc7e7..b332f20 100644
--- a/lib/log/BUCK
+++ b/lib/log/BUCK
@@ -31,3 +31,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..6ab33d9 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.10.2'
+VERSION = '5.2.1'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'c01e3d675d277e0a93e7890d03cc3246b2cdecaa',
+  sha1 = 'a175590aa8b04e079eb1a136fd159f9163482ba4',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -16,8 +16,9 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = 'f977f8c443e8f4e9d1fd7fdfda80a6cf60b3e7c2',
+  sha1 = '33b7cc17d5a7c939af6fe3f67563f4709926d7f5',
   license = 'Apache2.0',
+  deps = [':core'],
   exclude = [
     'META-INF/LICENSE.txt',
     'META-INF/NOTICE.txt',
@@ -25,8 +26,37 @@
 )
 
 maven_jar(
-  name = 'query-parser',
-  id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'd70f54e1060d553ba7aeb4d49a71fd0c068499e8',
+  name = 'backward-codecs',
+  id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
+  sha1 = '603d1f06b133449272799d698e5118db65e523ba',
   license = 'Apache2.0',
+  deps = [':core'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'misc',
+  id = 'org.apache.lucene:lucene-misc:' + VERSION,
+  sha1 = 'be0a4f0ac06f0a2fa3689b4bf6cd1fe6847f9969',
+  license = 'Apache2.0',
+  deps = [':core'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'queryparser',
+  id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
+  sha1 = '73be0a2d4ab3e6b574be1938bfb27f7f730f0ad9',
+  license = 'Apache2.0',
+  deps = [':core'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
 )
diff --git a/lib/maven.defs b/lib/maven.defs
index 14be4e5..17f0e00 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -46,9 +46,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
@@ -62,7 +67,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,
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..677a9e2 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,7 +28,7 @@
   java_library(
     name = name + '__lib',
     srcs = [':' + name + '__pl2j'],
-    deps = ['//lib/prolog:prolog-cafe'] + deps,
+    deps = ['//lib/prolog:runtime'] + deps,
   )
   genrule(
     name = name + '__ln',
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/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 9553327..e49010b 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 955332734bd554527064f8af32907ee1c1561d2c
+Subproject commit e49010bbbed9d941c35a9f1eed1178cd909c7e34
diff --git a/plugins/download-commands b/plugins/download-commands
index 63e7cf5..99e61fb 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 63e7cf5f24045ede2ee9e5a220e594716b2b6ce4
+Subproject commit 99e61fb06a4505a9558c23a56213cb32ceaa9cca
diff --git a/plugins/replication b/plugins/replication
index 7a5a9f8..6a83800 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 7a5a9f88dc3d63d28627ae98a316761466aaa27f
+Subproject commit 6a83800e8fa959b69faa98111ad53437ee993378
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index f6403a1..d81e2d6 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit f6403a1ed35e908a4828fdc9168ed752fa178c69
+Subproject commit d81e2d6d3edc27c7aceea47518cbb03fb5590f11
diff --git a/tools/build.defs b/tools/build.defs
index 8b858cd..da07c1e 100644
--- a/tools/build.defs
+++ b/tools/build.defs
@@ -71,8 +71,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..fc94144 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,9 @@
     <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>
diff --git a/tools/default.defs b/tools/default.defs
index a6a65b3..543bf98 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -18,7 +18,11 @@
 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.
@@ -63,7 +67,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)
 
@@ -128,8 +133,10 @@
     manifest_entries = [],
     type = 'plugin',
     visibility = ['PUBLIC']):
-  from multiprocessing import cpu_count
-  mf_cmd = 'v=\$(git describe HEAD);'
+  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'
diff --git a/tools/download_all.py b/tools/download_all.py
index 241d20b..3b21882 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");
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/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..f3300fa 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");
@@ -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')
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..2584f2d 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',
 ]
 
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index 4d53783..fdc01a8 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -30,5 +30,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/pack_war.py b/tools/pack_war.py
index 7e7d895..cfa7e36 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");
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..e2d9ead 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]
 
+
+def replace_in_file(filename, src_pattern):
+  try:
+    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' % (filename, err), file=sys.stderr)
+
+
+src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$',
+                         re.MULTILINE)
 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')
-  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)
-  except IOError as err:
-    print('error updating %s: %s' % (pom, err), file=sys.stderr)
+  replace_in_file(pom, src_pattern)
 
-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"^(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 {