Merge branch 'stable-2.11'

* stable-2.11:
  Update 2.11.1 release notes
  PatchListCacheImpl: Catch LargeObjectException
  Update commons-validator to 1.4.1

Change-Id: I34f81ad8a0276d127b925422d6d97ac3c7b9bea4
diff --git a/.buckconfig b/.buckconfig
index e4a19f1..e3d0ffc 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -9,6 +9,7 @@
   docs = //Documentation:searchfree
   firefox = //:firefox
   gerrit = //:gerrit
+  headless = //:headless
   release = //:release
   safari = //:safari
   soyc = //gerrit-gwtui:ui_soyc
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.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..02ee883 100644
--- a/BUCK
+++ b/BUCK
@@ -1,6 +1,7 @@
 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')
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/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-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 7c664ac..f3bff7d 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-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 fdb9d18..95b6717 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -871,6 +871,30 @@
 +
 Default is "Submit patch set ${patchSet} into ${branch}".
 
+[[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 (…)
@@ -1188,7 +1212,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[
@@ -1428,7 +1452,7 @@
 [[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 +1460,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 +1478,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.
@@ -1800,6 +1839,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
@@ -2156,6 +2200,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
@@ -2581,6 +2655,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 +2747,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
@@ -2788,9 +2885,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::
 +
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..3887ff3 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -386,6 +386,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
 
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/dev-buck.txt b/Documentation/dev-buck.txt
index 2eb478c..89cd849 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:
@@ -593,14 +611,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
 ----
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 72d7ddf..b717f4e 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -334,6 +334,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-plugins.txt b/Documentation/dev-plugins.txt
index f9b66cb..9bb0007 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.10 \
+    -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
 
@@ -1519,7 +1523,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>
@@ -1717,17 +1721,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"));
 }
 ----
 
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..1b3e98f 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -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
 ----
 
@@ -352,9 +353,7 @@
 
 * 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
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index f5d5277..eb3c4fb 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -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/json.txt b/Documentation/json.txt
index feef1a1..8ccd03b 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -156,7 +156,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/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 1b9a705..138f85f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -310,6 +310,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
@@ -1146,6 +1153,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
 --
@@ -2586,7 +2696,7 @@
 ----
 
 [[list-drafts]]
-=== List Drafts
+=== List Revision Drafts
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/'
 --
@@ -2594,9 +2704,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 +2874,7 @@
 ----
 
 [[list-comments]]
-=== List Comments
+=== List Revision Comments
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/'
 --
@@ -3494,6 +3603,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. +
@@ -4013,7 +4125,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
@@ -4108,6 +4222,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]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 0ee6966..a8bbbfa 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -30,6 +30,84 @@
   "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"
+          }
+        },
+        "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"
+          }
+        },
+        "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"
+          }
+        }
+      ],
+      "archives": [
+        "tgz",
+        "tar",
+        "tbz2",
+        "txz"
+      ]
+    },
+    "gerrit": {
+      "all_projects": "All-Projects",
+      "all_users": "All-Users"
+    }
+  }
+----
+
+
 [[list-caches]]
 === List Caches
 --
@@ -822,6 +900,27 @@
 [[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`.
+|==========================================
+
 [[cache-info]]
 === CacheInfo
 The `CacheInfo` entity contains information about a cache.
@@ -878,6 +977,59 @@
 |`name`               |capability name
 |=================================
 
+[[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.
+|=================================
+
 [[entries-info]]
 === EntriesInfo
 The `EntriesInfo` entity contains information about the entries in a
@@ -896,6 +1048,21 @@
 `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,6"]
+|================================
+|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].
+|================================
+
 [[hit-ration-info]]
 === HitRatioInfo
 The `HitRatioInfo` entity contains information about the hit ratio of a
@@ -958,6 +1125,30 @@
 The number of open files.
 |============================
 
+[[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.
+|`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.
+|=======================================
+
 [[summary-info]]
 === SummaryInfo
 The `SummaryInfo` entity contains information about the current state
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index a909ab4..a25c7bb 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2116,6 +2116,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-inline-edit.txt b/Documentation/user-inline-edit.txt
index 5ad6b39..a89d0db 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.
 
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-search.txt b/Documentation/user-search.txt
index 3de45d2..ca29e44 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'::
 +
@@ -327,6 +333,19 @@
 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)`.
+
 
 == Argument Quoting
 
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
new file mode 100644
index 0000000..dfdcd37
--- /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://gerrit-releases.storage.googleapis.com/gerrit-2.12.war[
+https://gerrit-releases.storage.googleapis.com/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/VERSION b/VERSION
index 9791ceb..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.1'
+GERRIT_VERSION = '2.12-SNAPSHOT'
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index c7bea4e..b6a7b29 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -28,12 +28,14 @@
     '//lib:servlet-api-3_1',
     '//lib:truth',
 
+    '//lib/auto:auto-value',
     '//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 ff61899..0b40891 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,32 +14,36 @@
 
 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.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.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;
@@ -56,12 +60,16 @@
 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.Repository;
+import org.eclipse.jgit.transport.Transport;
+import org.junit.AfterClass;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -69,16 +77,23 @@
 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;
 
@@ -92,7 +107,7 @@
   protected GerritApi gApi;
 
   @Inject
-  private AcceptanceTestRequestScope atrScope;
+  protected AcceptanceTestRequestScope atrScope;
 
   @Inject
   private IdentifiedUser.GenericFactory identifiedUserFactory;
@@ -119,9 +134,20 @@
   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;
+
+  protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
   protected TestAccount user;
@@ -131,6 +157,9 @@
   protected ReviewDb db;
   protected Project.NameKey project;
 
+  private String resourcePrefix;
+  private List<Repository> toClose;
+
   @Rule
   public TestRule testRunner = new TestRule() {
     @Override
@@ -138,30 +167,24 @@
       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() {
@@ -184,9 +207,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);
@@ -196,56 +234,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)
@@ -265,9 +414,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 {
@@ -283,6 +431,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();
   }
@@ -300,10 +452,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);
   }
 
@@ -352,9 +525,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..f247463 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,9 +64,13 @@
     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 {
+    TestAccount account = accounts.get(username);
+    if (account != null) {
+      return account;
+    }
     ReviewDb db = reviewDbProvider.open();
     try {
       Account.Id id = new Account.Id(db.nextAccountId());
@@ -99,7 +111,10 @@
       accountCache.evictByUsername(username);
       byEmailCache.evict(email);
 
-      return new TestAccount(id, username, email, fullName, sshKey, httpPass);
+      account =
+          new TestAccount(id, username, email, fullName, sshKey, httpPass);
+      accounts.put(username, account);
+      return account;
     } finally {
       db.close();
     }
@@ -137,6 +152,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/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..a721221 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,22 @@
 
 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.git.SubmoduleOp;
 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 +54,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() {
@@ -65,10 +117,11 @@
         }
       }
     });
+    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 +131,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();
@@ -105,7 +157,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 {
@@ -138,9 +190,15 @@
     cfg.setBoolean("sshd", null, "testUseInsecureRandom", true);
     cfg.setString("cache", null, "directory", null);
     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 +208,10 @@
       protected void configure() {
         bind(AccountCreator.class);
         factory(PushOneCommit.Factory.class);
+        factory(SubmoduleOp.Factory.class);
+        install(InProcessProtocol.module());
+        install(new NoSshModule());
+        install(new AsyncReceiveCommits.Module());
       }
     };
     return sysInjector.createChildInjector(module);
@@ -167,6 +229,8 @@
     return InetAddress.getLoopbackAddress();
   }
 
+  private final Description desc;
+
   private Daemon daemon;
   private ExecutorService daemonService;
   private Injector testInjector;
@@ -174,8 +238,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 +272,10 @@
     return testInjector;
   }
 
+  Description getDescription() {
+    return desc;
+  }
+
   void stop() throws Exception {
     daemon.getLifecycleManager().stop();
     if (daemonService != null) {
@@ -216,4 +285,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/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 6cd8031..f2c51e1 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,10 +43,6 @@
 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";
@@ -65,11 +51,13 @@
   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 +65,7 @@
     PushOneCommit create(
         ReviewDb db,
         PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content,
@@ -106,7 +95,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 +104,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 +123,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 +137,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 +189,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 +204,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 +217,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 +229,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() {
@@ -282,10 +268,10 @@
 
     private void assertStatus(Status expectedStatus, String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
-      assertThat(expectedStatus)
+      assertThat(refUpdate.getStatus())
         .named(message(refUpdate))
-        .isEqualTo(refUpdate.getStatus());
-      assertThat(expectedMessage).isEqualTo(refUpdate.getMessage());
+        .isEqualTo(expectedStatus);
+      assertThat(refUpdate.getMessage()).isEqualTo(expectedMessage);
     }
 
     public void assertMessage(String expectedMessage) {
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..e06d31f 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
@@ -65,7 +65,20 @@
   }
 
   public RestResponse put(String endPoint, Object content) throws IOException {
+    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 {
     HttpPut put = new HttpPut(url + "/a" + endPoint);
+    if (header != null) {
+      put.addHeader(header);
+    }
     if (content != null) {
       put.addHeader(new BasicHeader("Content-Type", "application/json"));
       put.setEntity(new StringEntity(
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..e7b5834 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;
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..aaf6192 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
@@ -22,6 +22,8 @@
 
 import org.junit.Test;
 
+import java.util.List;
+
 public class AccountIT extends AbstractDaemonTest {
 
   @Test
@@ -49,14 +51,31 @@
   @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();
   }
 }
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 c13187f..9a58702 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,11 @@
 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.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 +27,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 +40,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 +61,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 +139,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 +148,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 +157,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 +165,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 +201,7 @@
         .id(r.getChangeId())
         .addReviewer(in);
 
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(user.id));
   }
 
@@ -204,7 +217,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 +225,7 @@
     gApi.changes()
         .id(r.getChangeId())
         .addReviewer(in);
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
   }
 
@@ -233,38 +246,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());
   }
@@ -283,7 +293,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());
   }
@@ -293,7 +304,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();
   }
@@ -307,13 +318,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();
   }
@@ -321,7 +332,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();
@@ -375,4 +387,86 @@
         .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 ChangeTable.
+          .withOption(ListChangesOption.LABELS)
+          .withOption(ListChangesOption.DETAILED_ACCOUNTS)
+          .get())
+        .hasSize(2);
+  }
 }
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/api/group/BUCK
similarity index 100%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK
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..8ed4165
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -0,0 +1,497 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.GroupInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+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.AccountGroup;
+import com.google.gerrit.server.group.SystemGroupBackend;
+
+import org.junit.Test;
+
+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 {
+    try {
+      gApi.groups().id("non-existing").addMembers("admin");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void removeFromNonExistingGroup_NotFound() throws Exception {
+    try {
+      gApi.groups().id("non-existing").removeMembers("admin");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
+  }
+
+  @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 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);
+    try {
+      gApi.groups().create(name("newGroup"));
+    } catch (AuthException expected) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void testCreateGroupWhenGroupAlreadyExists_Conflict()
+      throws Exception {
+    try {
+      gApi.groups().create("Administrators");
+    } catch (ResourceConflictException expected) {
+      // Expected.
+    }
+  }
+  @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 with name conflict
+    String other = name("other");
+    gApi.groups().create(other);
+    try {
+      gApi.groups().id(name).name(other);
+    } catch (ResourceConflictException expected) {
+      // Expected.
+    }
+
+    // set name to same name
+    gApi.groups().id(name).name(name);
+    assertThat(gApi.groups().id(name).name()).isEqualTo(name);
+
+    // rename
+    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();
+    try {
+      gApi.groups().id(name).get();
+    } catch (ResourceNotFoundException expected) {
+      // Expceted.
+    }
+  }
+
+  @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
+    try {
+      gApi.groups().id(name).owner("Non-Existing Group");
+    } catch (UnprocessableEntityException expected) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void listNonExistingGroupIncludes_NotFound() throws Exception {
+    try {
+      gApi.groups().id("non-existing").includedGroups();
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
+  }
+
+  @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");
+    try {
+      gApi.groups().id(gx).addGroups("non-existing");
+    } catch (UnprocessableEntityException expecetd) {
+      // Expected.
+    }
+  }
+
+  @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 {
+    try {
+      gApi.groups().id("non-existing").members();
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
+  }
+
+  @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()));
+  }
+
+  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..9ae95b5 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,35 +22,31 @@
 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.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);
   }
@@ -58,7 +54,7 @@
   @Test(expected = RestApiException.class)
   public void createProjectFooBar() throws Exception {
     ProjectInput in = new ProjectInput();
-    in.name = "foo";
+    in.name = name("foo");
     gApi.projects()
         .name("bar")
         .create(in);
@@ -67,12 +63,10 @@
   @Test(expected = ResourceConflictException.class)
   public void createProjectDuplicate() throws Exception {
     ProjectInput in = new ProjectInput();
-    in.name = "baz";
+    in.name = name("baz");
     gApi.projects()
-        .name("baz")
         .create(in);
     gApi.projects()
-        .name("baz")
         .create(in);
   }
 
@@ -86,43 +80,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..0d46ed7 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
@@ -46,14 +46,12 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 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.util.Collections;
 import java.util.HashMap;
@@ -75,7 +73,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 +106,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 +119,14 @@
   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 +151,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 +168,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 +206,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 +220,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 +232,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,12 +271,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);
 
     assertThat(cherry.get().subject).contains(in.message);
     cherry.current().review(ReviewInput.approve());
@@ -283,12 +302,13 @@
         .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);
@@ -300,12 +320,12 @@
 
   @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 +333,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 +346,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 +365,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 +525,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())
@@ -521,15 +554,14 @@
   }
 
   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 4726079..82f8c59 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,8 +16,6 @@
 
 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;
@@ -34,6 +32,8 @@
 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.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -54,12 +54,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;
@@ -109,12 +109,12 @@
   @Before
   public void setUp() throws Exception {
     db = reviewDbProvider.open();
-    changeId = newChange(git, admin.getIdent());
+    changeId = newChange(admin.getIdent());
     ps = getCurrentPatchSet(changeId);
-    amendChange(git, admin.getIdent(), changeId);
+    amendChange(admin.getIdent(), changeId);
     change = getChange(changeId);
     assertThat(ps).isNotNull();
-    changeId2 = newChange2(git, admin.getIdent());
+    changeId2 = newChange2(admin.getIdent());
     change2 = getChange(changeId2);
     assertThat(change2).isNotNull();
     ps2 = getCurrentPatchSet(changeId2);
@@ -245,9 +245,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);
   }
@@ -267,10 +267,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)))
@@ -455,6 +456,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();
@@ -624,10 +649,11 @@
   @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();
@@ -654,25 +680,25 @@
     assertThat(approvals.get(0).value).isEqualTo(1);
   }
 
-  private String newChange(Git git, PersonIdent ident) throws Exception {
+  private String newChange(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.to(git, "refs/for/master").getChangeId();
+    return push.to("refs/for/master").getChangeId();
   }
 
-  private String amendChange(Git git, PersonIdent ident, String changeId) throws Exception {
+  private String amendChange(PersonIdent ident, String changeId) throws Exception {
     PushOneCommit push =
-        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME2,
+        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 {
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 e65d51e..2323270 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,30 +16,24 @@
 
 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.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.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Set;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
@@ -52,6 +46,7 @@
   private NotesMigration notesMigration;
 
   protected enum Protocol {
+    // TODO(dborowitz): TEST.
     SSH, HTTP
   }
 
@@ -62,7 +57,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:
@@ -74,20 +69,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);
@@ -101,8 +94,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);
@@ -125,8 +117,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);
@@ -150,8 +141,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();
@@ -164,8 +154,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());
@@ -179,65 +168,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();
@@ -250,23 +233,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();
@@ -281,25 +263,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");
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..8bf4511
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.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.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;
+
+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);
+  }
+
+  protected void createSubscription(
+      TestRepository<?> repo, String branch, String subscribeToRepo,
+      String subscribeToBranch) throws Exception {
+    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 cfg = new Config();
+    cfg.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
+    cfg.setString("submodule", subscribeToRepo, "url", url);
+    cfg.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+
+    repo.branch("HEAD").commit().insertChangeId()
+      .message("subject: adding new subscription")
+      .add(".gitmodules", cfg.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..5066e66 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
@@ -32,7 +32,6 @@
 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;
@@ -55,7 +54,8 @@
   private ChangeNotes.Factory changeNotesFactory;
 
   @Inject
-  private @GerritPersonIdent PersonIdent serverIdent;
+  @GerritPersonIdent
+  private PersonIdent serverIdent;
 
   @Test
   public void submitOnPush() throws Exception {
@@ -73,9 +73,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 +90,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 +104,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,10 +116,9 @@
 
   @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 =
@@ -133,9 +131,9 @@
   @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 +196,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 +212,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,7 +253,7 @@
     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(
@@ -290,17 +288,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..3ffe07c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -0,0 +1,201 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 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;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+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
+  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, "master", "commit after unsubscribe");
+    pushChangeTo(subRepo, "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, "master", "commit after unsubscribe");
+    pushChangeTo(subRepo, "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 static AtomicInteger contentCounter = new AtomicInteger(0);
+
+  private ObjectId pushChangeTo(TestRepository<?> repo, String branch, String message)
+      throws Exception {
+
+    ObjectId ret = repo.branch("HEAD").commit().insertChangeId()
+      .message(message)
+      .add("a.txt", "a contents: " + contentCounter.addAndGet(1))
+      .create();
+
+    repo.git().push().setRemote("origin").setRefSpecs(
+        new RefSpec("HEAD:refs/heads/" + branch)).call();
+
+    return ret;
+  }
+
+  private ObjectId pushChangeTo(TestRepository<?> repo, String branch)
+      throws Exception {
+    return pushChangeTo(repo, branch, "some change");
+  }
+
+  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;
+    }
+  }
+
+}
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..7823e7d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.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.acceptance.git;
+
+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);
+  }
+}
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..0b07b87 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,12 +104,16 @@
         .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 {
@@ -126,10 +143,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 +160,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 +178,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 +190,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 +204,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);
+    }
   }
 
   /**
@@ -259,7 +280,7 @@
     }
 
     Splitter s = Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings();
-    assertThat(filtered).containsSequence(
-        Ordering.natural().sortedCopy(s.split(out)));
+    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/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 63c6493..af90d8f 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,51 @@
 
 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 static org.junit.Assert.fail;
 
 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
   public void getNonExistingAccount_NotFound() throws Exception {
-    assertThat(adminSession.get("/accounts/non-existing").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    try {
+      gApi.accounts().id("non-existing").get();
+      fail("Expected account to not exist");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
   }
 
   @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/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 dec3e65..a07bde7 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,23 +18,24 @@
 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;
@@ -50,19 +51,14 @@
 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.Ref;
@@ -116,7 +112,6 @@
       }
 
     }, listenerUser);
-    project = new Project.NameKey("p2");
   }
 
   @After
@@ -127,10 +122,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());
   }
@@ -138,85 +132,69 @@
   @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);
+    assertSubmitter(change1);
+    assertSubmitter(change2);
+    assertSubmitter(change3);
   }
 
-  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 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 {
+  protected void submitStatusOnly(String changeId) throws Exception {
     approve(changeId);
     Change c = queryProvider.get().byKeyPrefix(changeId).get(0).change();
     c.setStatus(Change.Status.SUBMITTED);
@@ -232,7 +210,7 @@
     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;
@@ -266,17 +244,9 @@
     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 =
@@ -291,11 +261,11 @@
     }
   }
 
-  protected void assertApproved(String changeId) throws IOException {
-    ChangeInfo c = getChange(changeId, DETAILED_LABELS);
+  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());
   }
 
@@ -309,17 +279,17 @@
     assertThat(submitter.getAccountId()).isEqualTo(admin.getId());
   }
 
-  protected void assertCherryPick(Git localGit, boolean contentMerge)
-      throws IOException {
-    assertRebase(localGit, contentMerge);
+  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(
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 afcd5d0..66c99f1 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,18 +18,15 @@
 
 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.gerrit.testutil.ConfigSuite;
-import com.google.gson.reflect.TypeToken;
 
-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 {
@@ -40,7 +37,7 @@
 
   @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");
@@ -49,27 +46,21 @@
 
   @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);
   }
 
   @Test
   public void revisionActionsTwoChangeChangesInTopic() throws Exception {
-    String changeId = createChangeWithTopic("foo2").getChangeId();
+    String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
     // create another change with the same topic
-    createChangeWithTopic("foo2").getChangeId();
+    createChangeWithTopic().getChangeId();
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
     if (isSubmitWholeTopicEnabled()) {
@@ -84,11 +75,42 @@
   }
 
   @Test
+  public void revisionActionsTwoChangeChangesInTopic_conflicting() throws Exception {
+    String changeId = createChangeWithTopic().getChangeId();
+    approve(changeId);
+
+    // create another change with the same topic
+    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 in the topic");
+    } else {
+      noSubmitWholeTopicAssertions(actions);
+    }
+  }
+
+  @Test
   public void revisionActionsTwoChangeChangesInTopicReady() throws Exception {
-    String changeId = createChangeWithTopic("foo2").getChangeId();
+    String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
     // create another change with the same topic
-    String changeId2 = createChangeWithTopic("foo2").getChangeId();
+    String changeId2 = createChangeWithTopic().getChangeId();
     approve(changeId2);
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
@@ -103,15 +125,6 @@
     }
   }
 
-  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());
-  }
-
   private void noSubmitWholeTopicAssertions(Map<String, ActionInfo> actions) {
     ActionInfo info = actions.get("submit");
     assertThat(info.enabled).isTrue();
@@ -127,18 +140,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..d92270c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -16,18 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static org.junit.Assert.fail;
 
 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 +42,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 +51,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");
+    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 +91,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 +102,18 @@
     assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
   }
 
+  private void assertCreateFails(ChangeInfo in,
+      Class<? extends RestApiException> errType, String errSubstring)
+      throws Exception {
+    try {
+      gApi.changes().create(in);
+      fail("Expected " + errType.getSimpleName());
+    } catch (RestApiException expected) {
+      assertThat(expected).isInstanceOf(errType);
+      assertThat(expected.getMessage()).contains(errSubstring);
+    }
+  }
+
   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..834b873 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
@@ -45,7 +45,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,7 +60,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);
@@ -74,7 +74,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 +90,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/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..c00a260 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,28 +39,26 @@
 
   @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);
@@ -70,22 +68,21 @@
   }
 
   @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,18 +92,17 @@
   }
 
   @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());
@@ -115,19 +111,18 @@
 
   @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,17 +133,16 @@
 
   @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());
@@ -157,17 +151,16 @@
 
   @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());
@@ -191,12 +184,11 @@
 
   @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 +212,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
@@ -244,14 +235,13 @@
 
   @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);
+    createChange("Change 2", "b", "b");
+    PushOneCommit.Result change3 = createChange("Change 3", "c", "c");
+    createChange("Change 4", "d", "d");
+    PushOneCommit.Result change5 = createChange("Change 5", "e", "e");
 
     // Out of the above, only submit 3 and 5.
     submitStatusOnly(change3.getChangeId());
@@ -271,17 +261,16 @@
 
   @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");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change2 = createChange("Change 2", "b", "b1");
     submit(change2.getChangeId());
 
-    checkout(git, initialHead.getId().getName());
-    PushOneCommit.Result change3 = createChange(git, "Change 3", "b", "b2");
+    testRepo.reset(initialHead);
+    PushOneCommit.Result change3 = createChange("Change 3", "b", "b2");
     assertThat(change3.getCommit().getParent(0)).isEqualTo(initialHead);
-    PushOneCommit.Result change4 = createChange(git, "Change 3", "c", "c3");
+    PushOneCommit.Result change4 = createChange("Change 3", "c", "c3");
 
     submitStatusOnly(change3.getChangeId());
     submitStatusOnly(change4.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..12df6fc 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());
@@ -45,16 +45,23 @@
 
   @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..ebd3d3c 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,9 +33,8 @@
 
   @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);
@@ -48,17 +45,16 @@
 
   @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());
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..95aef9e 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,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;
 
@@ -21,9 +19,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());
@@ -33,17 +30,16 @@
 
   @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());
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..bb26a30 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());
@@ -46,19 +46,19 @@
   }
 
   @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());
@@ -68,22 +68,21 @@
   }
 
   @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,18 +92,17 @@
   }
 
   @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);
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/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/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/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/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..d4365c5 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
@@ -18,6 +18,7 @@
     '//lib:guava',
     '//lib:junit',
     '//lib:truth',
+    '//gerrit-extension-api:api',
     '//gerrit-server:server',
   ],
 )
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..25a226d 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
@@ -18,17 +18,25 @@
 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 static org.junit.Assert.fail;
 
 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 +47,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 +80,39 @@
       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 {
+    try {
+      branch().create(new BranchInput());
+      fail("Expected " + errType.getSimpleName());
+    } catch (RestApiException expected) {
+      assertThat(expected).isInstanceOf(errType);
+    }
+  }
 }
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..6347874 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
@@ -17,15 +17,23 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
+import static org.junit.Assert.fail;
 
 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 +41,7 @@
 import com.google.gerrit.server.project.ProjectState;
 
 import org.apache.http.HttpStatus;
+import org.apache.http.message.BasicHeader;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
@@ -47,30 +56,8 @@
 
 public class CreateProjectIT extends AbstractDaemonTest {
   @Test
-  public void testCreateProjectApi() throws Exception {
-    final String newProjectName = "newProject";
-    ProjectInfo p = gApi.projects().name(newProjectName).create().get();
-    assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertThat(projectState).isNotNull();
-    assertProjectInfo(projectState.getProject(), p);
-    assertHead(newProjectName, "refs/heads/master");
-  }
-
-  @Test
-  public void testCreateProjectApiWithGitSuffix() throws Exception {
-    final String newProjectName = "newProject";
-    ProjectInfo p = gApi.projects().name(newProjectName + ".git").create().get();
-    assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertThat(projectState).isNotNull();
-    assertProjectInfo(projectState.getProject(), p);
-    assertHead(newProjectName, "refs/heads/master");
-  }
-
-  @Test
-  public void testCreateProject() throws Exception {
-    final String newProjectName = "newProject";
+  public void testCreateProjectHttp() throws Exception {
+    String newProjectName = name("newProject");
     RestResponse r = adminSession.put("/projects/" + newProjectName);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
@@ -82,11 +69,49 @@
   }
 
   @Test
-  public void testCreateProjectWithGitSuffix() throws Exception {
-    final String newProjectName = "newProject";
-    RestResponse r = adminSession.put("/projects/" + newProjectName + ".git");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+  public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict()
+      throws Exception {
+    RestResponse r = adminSession.put("/projects/" + allProjects.get());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+  }
+
+  @Test
+  public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed()
+      throws Exception {
+    RestResponse r = adminSession.putWithHeader("/projects/" + allProjects.get(),
+        new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_PRECONDITION_FAILED);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testCreateProjectHttpWithUnreasonableName_BadRequest()
+      throws Exception {
+    RestResponse r = adminSession.put("/projects/" + Url.encode(name("invalid/../name")));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = name("otherName");
+    RestResponse r = adminSession.put("/projects/" + name("someName"), in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProjectHttpWithInvalidRefName_BadRequest()
+      throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.branches = Collections.singletonList(name("invalid ref name"));
+    RestResponse r = adminSession.put("/projects/" + name("newProject"), in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProject() throws Exception {
+    String newProjectName = name("newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName).get();
     assertThat(p.name).isEqualTo(newProjectName);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNotNull();
@@ -95,25 +120,28 @@
   }
 
   @Test
-  public void testCreateProjectWithNameMismatch_BadRequest() throws Exception {
-    ProjectInput in = new ProjectInput();
-    in.name = "otherName";
-    RestResponse r = adminSession.put("/projects/someName", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  public void testCreateProjectWithGitSuffix() throws Exception {
+    String newProjectName = name("newProject");
+    ProjectInfo p = gApi.projects().create(newProjectName + ".git").get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
   }
 
   @Test
   public void testCreateProjectWithProperties() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.description = "Test description";
     in.submitType = SubmitType.CHERRY_PICK;
     in.useContributorAgreements = InheritableBoolean.TRUE;
     in.useSignedOffBy = InheritableBoolean.TRUE;
     in.useContentMerge = InheritableBoolean.TRUE;
     in.requireChangeId = InheritableBoolean.TRUE;
-    RestResponse r = adminSession.put("/projects/" + newProjectName, in);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    ProjectInfo p = gApi.projects().create(in).get();
     assertThat(p.name).isEqualTo(newProjectName);
     Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
     assertProjectInfo(project, p);
@@ -127,13 +155,16 @@
 
   @Test
   public void testCreateChildProject() throws Exception {
-    final String parentName = "parent";
-    RestResponse r = adminSession.put("/projects/" + parentName);
-    r.consume();
-    final String childName = "child";
+    String parentName = name("parent");
     ProjectInput in = new ProjectInput();
+    in.name = parentName;
+    gApi.projects().create(in);
+
+    String childName = name("child");
+    in = new ProjectInput();
+    in.name = childName;
     in.parent = parentName;
-    r = adminSession.put("/projects/" + childName, in);
+    gApi.projects().create(in);
     Project project = projectCache.get(new Project.NameKey(childName)).getProject();
     assertThat(project.getParentName()).isEqualTo(in.parent);
   }
@@ -142,21 +173,22 @@
   public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
       throws Exception {
     ProjectInput in = new ProjectInput();
+    in.name = name("newProjectName");
     in.parent = "non-existing-project";
-    RestResponse r = adminSession.put("/projects/child", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    assertCreateFails(in, UnprocessableEntityException.class);
   }
 
   @Test
   public void testCreateProjectWithOwner() throws Exception {
-    final String newProjectName = "newProject";
+    String newProjectName = name("newProject");
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.owners = Lists.newArrayListWithCapacity(3);
     in.owners.add("Anonymous Users"); // by name
     in.owners.add(SystemGroupBackend.REGISTERED_USERS.get()); // by UUID
     in.owners.add(Integer.toString(groupCache.get(
         new AccountGroup.NameKey("Administrators")).getId().get())); // by ID
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
     expectedOwnerIds.add(SystemGroupBackend.ANONYMOUS_USERS);
@@ -169,39 +201,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 +244,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) {
@@ -251,4 +289,14 @@
       }
     }
   }
+
+  private void assertCreateFails(ProjectInput in,
+      Class<? extends RestApiException> errType) throws Exception {
+    try {
+      gApi.projects().create(in);
+      fail("Expected " + errType.getSimpleName());
+    } catch (RestApiException expected) {
+      assertThat(expected).isInstanceOf(errType);
+    }
+  }
 }
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..a026d76 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,25 @@
 
 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 static org.junit.Assert.fail;
 
 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 +40,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 +72,43 @@
       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();
+    try {
+      branch().get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
+  }
+
+  private void assertDeleteForbidden() throws Exception {
+    try {
+      branch().delete();
+      fail("Expected AuthException");
+    } catch (AuthException expected) {
+      // Expected.
+    }
+    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..d3677ab 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());
@@ -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..3793c82 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,87 @@
 
 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 static org.junit.Assert.fail;
 
-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);
+    try {
+      gApi.projects().name("non-existing").branches().get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
   }
 
   @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);
+    try {
+      gApi.projects().name(project.get()).branches().get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
   }
 
   @Test
+  @TestProjectInput(createEmptyCommit = false)
   public void listBranchesOfEmptyProject() throws Exception {
-    Project.NameKey emptyProject = new Project.NameKey("empty");
-    createProject(sshSession, emptyProject.get(), null, false);
-    RestResponse r = adminSession.get("/projects/" + emptyProject.get() + "/branches");
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("refs/meta/config",  null, false),
-        new BranchInfo[] {
-          new BranchInfo("HEAD", null, false)
-        });
-    assertBranches(expected, toBranchInfoList(r));
+    assertBranches(ImmutableList.of(
+          branch("HEAD", null, false),
+          branch("refs/meta/config",  null, false)),
+        list().get());
   }
 
   @Test
   public void listBranches() throws Exception {
-    pushTo("refs/heads/master");
-    String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    pushTo("refs/heads/dev");
-    String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    RestResponse r = adminSession.get("/projects/" + project.get() + "/branches");
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("refs/meta/config",  null, false),
-        new BranchInfo[] {
-          new BranchInfo("HEAD", "master", false),
-          new BranchInfo("refs/heads/master", masterCommit, false),
-          new BranchInfo("refs/heads/dev", devCommit, true)
-        });
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertBranches(expected, result);
-
-    // verify correct sorting
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/dev");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/master");
+    String master = pushTo("refs/heads/master").getCommit().name();
+    String dev = pushTo("refs/heads/dev").getCommit().name();
+    assertBranches(ImmutableList.of(
+          branch("HEAD", "master", false),
+          branch("refs/meta/config",  null, false),
+          branch("refs/heads/dev", dev, true),
+          branch("refs/heads/master", master, false)),
+        list().get());
   }
 
   @Test
   public void listBranchesSomeHidden() throws Exception {
     blockRead(project, "refs/heads/dev");
-    pushTo("refs/heads/master");
-    String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+    String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
-    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
+    setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("HEAD", "master", false),
-        new BranchInfo[] {
-          new BranchInfo("refs/heads/master", masterCommit, false),
-        });
-    assertBranches(expected, toBranchInfoList(r));
+    assertBranches(ImmutableList.of(
+          branch("HEAD", "master", false),
+          branch("refs/heads/master", master, false)),
+        list().get());
   }
 
   @Test
   public void listBranchesHeadHidden() throws Exception {
     blockRead(project, "refs/heads/master");
     pushTo("refs/heads/master");
-    pushTo("refs/heads/dev");
-    String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
+    String dev = pushTo("refs/heads/dev").getCommit().name();
+    setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    assertBranches(Collections.singletonList(new BranchInfo("refs/heads/dev",
-        devCommit, false)), toBranchInfoList(r));
+    assertBranches(ImmutableList.of(branch("refs/heads/dev", dev, false)),
+        list().get());
   }
 
   @Test
@@ -119,47 +104,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 +147,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/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 6cd39ab..0dcba72 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,16 +31,33 @@
 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;
 
@@ -50,140 +66,158 @@
 
   @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));
   }
 
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
@@ -198,7 +232,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 +244,38 @@
     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;
+    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 d598b06..57b00bc 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)
     List<PatchListEntry>  entries = getPatches(a, b);
@@ -151,29 +164,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);
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..9d44ad6 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 {
@@ -356,6 +356,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..bab8e33
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+  @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..4f9f190 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
@@ -97,7 +93,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-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-common/BUCK b/gerrit-common/BUCK
index 88e503e..484136a 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -51,6 +51,7 @@
     '//lib:guava',
     '//lib/jgit:jgit',
     '//lib/joda:joda-time',
+    '//lib/log:api',
   ],
   visibility = ['PUBLIC'],
 )
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/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/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
index d911390..045591c 100644
--- 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
@@ -16,10 +16,7 @@
 
 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;
@@ -36,17 +33,11 @@
   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;
@@ -138,30 +129,10 @@
     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;
   }
@@ -170,22 +141,6 @@
     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;
   }
@@ -216,22 +171,6 @@
     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;
   }
@@ -261,9 +200,9 @@
   }
 
   public boolean siteHasUsernames() {
-    if (getAuthType() == AuthType.CUSTOM_EXTENSION
+    if (authType == AuthType.CUSTOM_EXTENSION
         && getHttpPasswordUrl() != null
-        && !canEdit(FieldName.USER_NAME)) {
+        && !editableAccountFields.contains(FieldName.USER_NAME)) {
       return false;
     }
     return true;
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
index 8219d27..0aff539 100644
--- 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
@@ -47,10 +47,6 @@
       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;
     }
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..fa3d7a8 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
@@ -30,6 +30,7 @@
   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/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/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-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 38b802a..d0204e4 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.11.1</version>
+  <version>2.12-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
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/api/GerritApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
index cc5807b..1435d9e 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,14 @@
 
 import com.google.gerrit.extensions.api.accounts.Accounts;
 import com.google.gerrit.extensions.api.changes.Changes;
+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 Groups groups();
   public Projects projects();
 
   /**
@@ -40,6 +42,11 @@
     }
 
     @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/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/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 06f0a75..6781af2 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 {
@@ -106,6 +108,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 +270,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();
     }
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/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index dd2ce92..2d2e4e9 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
@@ -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..3d1e3bd 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,6 +14,7 @@
 
 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;
@@ -49,11 +50,16 @@
   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;
 
+  Map<String, ActionInfo> actions() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -145,6 +151,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 +179,10 @@
     public CommentApi comment(String id) 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/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
new file mode 100644
index 0000000..417e371
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
new file mode 100644
index 0000000..28665fe
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupInput.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+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-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
new file mode 100644
index 0000000..7ea9fb6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/PutDescriptionInput.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class PutDescriptionInput {
+  @DefaultInput
+  public String description;
+  public String commitMessage;
+}
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..5caa903 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 {
@@ -58,7 +59,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 +91,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/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/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-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupOptionsInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupOptionsInfo.java
new file mode 100644
index 0000000..074e1a4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupOptionsInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+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..7c71ba3 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
@@ -29,4 +29,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/events/GarbageCollectorListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
new file mode 100644
index 0000000..eb223c6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
@@ -0,0 +1,40 @@
+// 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();
+
+    /**
+     * 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..7de740dc 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,9 @@
    * <p>
    * Items must be defined in a Guice module before they can be bound:
    * <pre>
+   * {@code
    *   DynamicSet.itemOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   * }
    * </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..b777899 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,10 +67,12 @@
    * Maps must be defined in a Guice module before they can be bound:
    *
    * <pre>
+   * {@code
    * DynamicMap.mapOf(binder(), new TypeLiteral<Thing<Bar>>(){});
    * bind(new TypeLiteral<Thing<Bar>>() {})
    *   .annotatedWith(Exports.named(&quot;foo&quot;))
    *   .to(Impl.class);
+   * }
    * </pre>
    *
    * @param binder a new binder created in the module.
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..8bc39a5 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,9 @@
    * <p>
    * Sets must be defined in a Guice module before they can be bound:
    * <pre>
+   * {@code
    *   DynamicSet.setOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   * }
    * </pre>
    *
    * @param binder a new binder created in the module.
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/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
index b015274..191ffd7 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-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/BUCK b/gerrit-gwtui/BUCK
index aad5e0b..9eb0bf6 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -2,6 +2,7 @@
 include_defs('//tools/gwt-constants.defs')
 from multiprocessing import cpu_count
 
+CPU_COUNT = cpu_count()
 DEPS = GWT_COMMON_DEPS + [
   '//gerrit-gwtexpui:CSS',
   '//lib:gwtjsonrpc',
@@ -29,8 +30,8 @@
   name = 'ui_opt',
   modules = [MODULE],
   module_deps = [':ui_module'],
-  deps = DEPS + [':ui_dbg'],
-  local_workers = cpu_count(),
+  deps = DEPS + ([':ui_dbg'] if CPU_COUNT < 8 else []),
+  local_workers = CPU_COUNT,
   strict = True,
   experimental_args = GWT_COMPILER_ARGS,
   vm_args = GWT_JVM_ARGS,
@@ -41,7 +42,7 @@
   modules = [MODULE],
   module_deps = [':ui_module'],
   deps = DEPS + [':ui_dbg'],
-  local_workers = cpu_count(),
+  local_workers = CPU_COUNT,
   strict = True,
   experimental_args = GWT_COMPILER_ARGS + ['-compileReport'],
   vm_args = GWT_JVM_ARGS,
@@ -54,7 +55,7 @@
   optimize = 0,
   module_deps = [':ui_module'],
   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/gwt.defs b/gerrit-gwtui/gwt.defs
index cd206c0..0e5928d 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -17,7 +17,7 @@
   'firefox',
   'gecko1_8',
   'safari',
-  'msie', 'ie6', 'ie8', 'ie9',
+  'msie', 'ie8', 'ie9',
 ]
 ALIASES = {
   'chrome': 'safari',
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..0f5065f 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
@@ -40,5 +40,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..7f17f4f 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
@@ -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) {
@@ -121,7 +121,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/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index e2bf142..e43139c 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
@@ -341,7 +341,7 @@
 
   private static String legacyChange(final String token) {
     final String s = skip(token);
-    final String t[] = s.split(",", 2);
+    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])));
     }
@@ -695,7 +695,7 @@
         }
 
         if (matchExact(SETTINGS_AGREEMENTS, token)
-            && Gerrit.getConfig().isUseContributorAgreements()) {
+            && Gerrit.info().auth().useContributorAgreements()) {
           return new MyAgreementsScreen();
         }
 
@@ -707,11 +707,13 @@
           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));
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..30980e1 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
@@ -144,8 +144,8 @@
     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(")");
+    } else if (info._accountId() > 0) {
+      b.append(" (").append(info._accountId()).append(")");
     }
     return b.toString();
   }
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..c176a09 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
@@ -28,6 +28,7 @@
 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.config.ServerInfo;
 import com.google.gerrit.client.extensions.TopMenu;
 import com.google.gerrit.client.extensions.TopMenuItem;
 import com.google.gerrit.client.extensions.TopMenuList;
@@ -49,7 +50,6 @@
 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;
@@ -101,11 +101,12 @@
       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}";
 
   private static String myHost;
   private static GerritConfig myConfig;
+  private static ServerInfo myServerInfo;
   private static HostPageData.Theme myTheme;
   private static Account myAccount;
   private static String defaultScreenToken;
@@ -288,6 +289,11 @@
     return myConfig;
   }
 
+  /** Get the public configuration data used by this Gerrit instance. */
+  public static ServerInfo info() {
+    return myServerInfo;
+  }
+
   public static GitwebLink getGitwebLink() {
     GitwebConfig gw = getConfig().getGitwebLink();
     return gw != null && gw.type != null ? new GitwebLink(gw) : null;
@@ -426,8 +432,16 @@
     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();
+    ConfigServerApi.serverInfo(cbg.add(new GerritCallback<ServerInfo>() {
+      @Override
+      public void onSuccess(ServerInfo info) {
+        myServerInfo = info;
+      }
+    }));
+    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();
@@ -444,7 +458,7 @@
         }
         onModuleLoad2(result);
       }
-    });
+    }));
   }
 
   private static void initHostname() {
@@ -538,7 +552,6 @@
     };
     gBody.add(body);
 
-    RpcStatus.INSTANCE = new RpcStatus();
     JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
     JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
@@ -579,6 +592,7 @@
       AccountApi.self().view("preferences").get(cbg.add(createMyMenuBarCallback()));
     }
     PluginLoader.load(hpd.plugins,
+        hpd.pluginsLoadTimeout,
         cbg.addFinal(new GerritCallback<VoidResult>() {
           @Override
           public void onSuccess(VoidResult result) {
@@ -704,9 +718,9 @@
     }
 
     if (signedIn) {
-      whoAmI(cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP);
+      whoAmI(!info().auth().isClientSslCertLdap());
     } else {
-      switch (cfg.getAuthType()) {
+      switch (info().auth().authType()) {
         case CLIENT_SSL_CERT_LDAP:
           break;
 
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/RelativeDateFormatter.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
index 5fc8cb3..443a6a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -47,7 +47,9 @@
     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 Util.C.inTheFuture();
+    }
 
     // seconds
     if (ageMillis < upperLimit(MINUTE_IN_MILLIS)) {
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 d5805ef..94198cb 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
@@ -58,11 +58,12 @@
     final SuggestBox suggestBox = new SuggestBox(
         new RemoteSuggestOracle(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/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 90348db..c8bb731 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
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.client.account.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;
@@ -53,8 +52,8 @@
     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) {
+      } 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/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 1127374..36fa98d 100644
--- 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
@@ -18,7 +18,7 @@
 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 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; }-*/;
@@ -29,7 +29,7 @@
    *         available, such as when no plugin is installed. This method returns
    *         false if the server did not check on avatars for the account.
    */
-  public final native boolean has_avatar_info()
+  public final native boolean hasAvatarInfo()
   /*-{ return this.hasOwnProperty('avatars') }-*/;
 
   public final AvatarInfo avatar(int sz) {
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..33d11a8 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
@@ -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);
@@ -116,7 +116,7 @@
   @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..2d9aa73 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
@@ -23,7 +23,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 +47,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,7 +100,7 @@
     }
 
     int row = 0;
-    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
+    if (!Gerrit.info().auth().canEdit(FieldName.USER_NAME)
         && Gerrit.getConfig().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
@@ -167,11 +167,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() {
@@ -274,7 +274,7 @@
           @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();
@@ -324,7 +324,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);
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/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 803ac55..6867bab 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
@@ -117,7 +117,8 @@
 
     FlowPanel dateTimePanel = new FlowPanel();
 
-    final int labelIdx, fieldIdx;
+    final int labelIdx;
+    final int fieldIdx;
     if (LocaleInfo.getCurrentLocale().isRTL()) {
       labelIdx = 1;
       fieldIdx = 0;
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..ff6fffb 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
@@ -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
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..5f1e383 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
@@ -70,7 +70,7 @@
     formBody.add(contactGroup);
 
     if (Gerrit.getUserAccount().getUserName() == null
-        && Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+        && 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()));
@@ -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..ca4ac20 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
@@ -34,7 +34,7 @@
     }
     link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
     link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
+    if (Gerrit.info().auth().useContributorAgreements()) {
       link(Util.C.tabAgreements(), PageLinks.SETTINGS_AGREEMENTS);
     }
   }
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..e440d55 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
@@ -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() {
@@ -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/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/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index cf3e940..1e3f918 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
@@ -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/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 4446354..affbe61 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,7 +20,7 @@
 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
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..9092508 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
@@ -44,7 +44,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/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
index 86a31ee..0fac957 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
@@ -32,7 +32,7 @@
           @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/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.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..a8dd9c5 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
@@ -444,8 +444,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();
@@ -484,8 +485,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..08ab2c0 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
@@ -38,7 +38,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;
@@ -265,7 +264,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 +305,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 +322,7 @@
         box.removeItem(inheritedIndex);
       } else {
         box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
-            + inheritedBoolean.inherited_value() + ")");
+            + inheritedBoolean.inheritedValue() + ")");
       }
     }
   }
@@ -342,20 +341,20 @@
 
   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());
+    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);
     }
@@ -674,19 +673,16 @@
 
   public 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()));
-          }
+        commands.add(cmdLinkfactory.new CloneCommandLink());
+        if (Gerrit.getConfig().getSshdAddress() != null && hasUserName()) {
+          commands.add(
+              cmdLinkfactory.new CloneWithCommitMsgHookCommandLink(getProjectKey()));
         }
       }
     }
@@ -698,7 +694,7 @@
         && 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..0dff684 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
@@ -186,7 +186,7 @@
 
       private void addWebLinks(int row, ProjectInfo k) {
         GitwebLink gitWebLink = Gerrit.getGitwebLink();
-        List<WebLinkInfo> webLinks = Natives.asList(k.web_links());
+        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);
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 2b40954..b7307be 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
@@ -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/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
index a5243ae..e831e8b 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
@@ -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..c092375 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
@@ -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..f968fd2 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
@@ -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/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
index 931a04d..6acb420 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
@@ -61,11 +61,18 @@
       screen: function(p,c){G._screen(this.name,p,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/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
index fb489cc..d708e8c 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
@@ -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/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..10d77cb 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
@@ -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);
@@ -65,7 +64,8 @@
   private String message;
   private String branch;
   private String key;
-  private boolean canSubmit;
+
+  private boolean rebaseParentNotCurrent = true;
 
   Actions() {
     initWidget(uiBinder.createAndBindUi(this));
@@ -78,20 +78,25 @@
     boolean hasUser = Gerrit.isSignedIn();
     RevisionInfo revInfo = info.revision(revision);
     CommitInfo commit = revInfo.commit();
-    changeId = info.legacy_id();
+    changeId = info.legacyId();
     project = info.project();
     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 +112,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,10 +156,6 @@
     return ids;
   }
 
-  void setSubmitEnabled() {
-    submit.setVisible(canSubmit);
-  }
-
   @UiHandler("followUp")
   void onFollowUp(@SuppressWarnings("unused") ClickEvent e) {
     if (followUpAction == null) {
@@ -181,16 +183,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 f74ebf6..3a3ffe2 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,16 +1,16 @@
-//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;
 
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..09de2c8 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,16 +1,16 @@
-//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;
 
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/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 4d7dac2..4133c13 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
@@ -113,6 +113,7 @@
     String label_need();
     String replyBox();
     String selected();
+    String highlight();
     String hashtagName();
   }
 
@@ -131,11 +132,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;
@@ -248,7 +248,7 @@
   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 +256,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 +293,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()) {
+    if (!info.revision(revision).isEdit()) {
       reply.setTitle(Gerrit.getConfig().getReplyTitle());
       reply.setHTML(new SafeHtmlBuilder()
         .openDiv()
         .append(Gerrit.getConfig().getReplyLabel())
         .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 +324,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 +336,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 +344,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 +355,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 +379,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 +402,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 +423,7 @@
     branchLink.setTargetHistoryToken(
         PageLinks.toChangeQuery(
             BranchLink.query(
-                info.project_name_key(),
+                info.projectNameKey(),
                 info.status(),
                 info.branch(),
                 null)));
@@ -505,7 +453,7 @@
         reviewMode.setVisible(false);
       }
 
-      if (rev.is_edit()) {
+      if (rev.isEdit()) {
         if (info.hasEditBasedOnCurrentPatchSet()) {
           publishEdit.setVisible(true);
         } else {
@@ -517,11 +465,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 +514,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 +781,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 +801,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 +819,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 +833,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 +841,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.getUserAccountInfo()._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 +908,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 +930,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);
@@ -901,6 +957,7 @@
           @Override
           public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
             r.add(result);
+            hasDraftComments = !result.isEmpty();
           }
 
           @Override
@@ -914,7 +971,7 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       return;
     }
 
@@ -922,7 +979,7 @@
         group.add(new AsyncCallback<CommitInfo>() {
           @Override
           public void onSuccess(CommitInfo info) {
-            rev.set_commit(info);
+            rev.setCommit(info);
           }
 
           @Override
@@ -933,7 +990,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 +1019,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 +1036,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");
     }
   }
@@ -1043,18 +1099,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 {
-      statusText.setInnerText(Util.toLongString(info.status()));
-    }
     labels.set(info);
 
     renderOwner(info);
@@ -1063,7 +1108,6 @@
     initReplyButton(info, revision);
     initIncludedInAction(info);
     initChangeAction(info);
-    initRevisionsAction(info, revision);
     initDownloadAction(info, revision);
     initProjectLinks(info);
     initBranchLink(info);
@@ -1072,7 +1116,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);
@@ -1083,23 +1127,45 @@
       setVisible(hashtagTableRow, false);
     }
 
+    StringBuilder sb = new StringBuilder();
+    sb.append(Util.M.changeScreenTitleId(info.idAbbreviated()));
+    if (info.subject() != null) {
+      sb.append(": ");
+      sb.append(info.subject());
+    }
+    setWindowTitle(sb.toString());
+
+    // Although this is related to the revision, we can process it early to
+    // render it faster.
+    if (!info.status().isOpen()
+        || !revision.equals(info.currentRevision())
+        || 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 {
+      statusText.setInnerText(Util.toLongString(info.status()));
+    }
+
     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);
 
@@ -1108,16 +1174,15 @@
       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) {
@@ -1138,7 +1203,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) {
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..830369d 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;
 
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..357f04c 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
@@ -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..f75f6a1 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
@@ -19,13 +19,11 @@
 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.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,15 +125,10 @@
     }
 
     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" : "");
   }
@@ -148,7 +141,7 @@
           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());
@@ -198,7 +191,7 @@
       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 ee94564..ce17013 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,16 +1,16 @@
-//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;
 
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..aa2b6f0 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,16 +1,16 @@
-//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;
 
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..34e67dd 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
@@ -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..0303a88 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
@@ -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));
@@ -268,7 +268,7 @@
     if (scheme != null && scheme != pref.getDownloadUrl()) {
       pref.setDownloadUrl(scheme);
       PreferenceInput in = PreferenceInput.create();
-      in.download_scheme(scheme);
+      in.downloadScheme(scheme);
       AccountApi.self().view("preferences")
           .put(in, new AsyncCallback<JavaScriptObject>() {
             @Override
@@ -303,11 +303,11 @@
       return createObject().cast();
     }
 
-    final void download_scheme(DownloadScheme s) {
-      download_scheme0(s.name());
+    final void downloadScheme(DownloadScheme s) {
+      downloadScheme0(s.name());
     }
 
-    private final native void download_scheme0(String n) /*-{
+    private final native void downloadScheme0(String n) /*-{
       this.download_scheme = n;
     }-*/;
 
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..dd8df36 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
@@ -87,7 +87,7 @@
     String deltaColumn2();
     String inserted();
     String deleted();
-    String removeButton();
+    String restoreDelete();
   }
 
   public static enum Mode {
@@ -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();
@@ -657,10 +663,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 +739,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 +759,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 +792,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..30394d6 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
@@ -39,7 +39,7 @@
         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..d2afbcf 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
@@ -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..47a870b 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,8 +14,6 @@
 
 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;
@@ -23,9 +21,7 @@
 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.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
index 59b05f0..e59420c 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;
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..f192a71 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
@@ -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..2d5dce0 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,30 @@
   interface Binder extends UiBinder<HTMLPanel, LineComment> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
+  @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()));
+    } else {
+      ps = defaultPs;
+      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..2890832 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,16 @@
       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='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..bc1ac30 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
@@ -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);
     }
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/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
index 665eff5..f3ab35e 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
@@ -128,7 +128,7 @@
         @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,7 +187,7 @@
     if (r.draft()) {
       sb.append(Resources.C.draft()).append(' ');
     }
-    if (r.has_draft_comments()) {
+    if (r.hasDraftComments()) {
       sb.openSpan()
         .addStyleName(style.draft_comment())
         .setAttribute("title", Resources.C.draftCommentsTooltip())
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/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
index 6638dbe..105a3c8 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
@@ -19,6 +19,7 @@
 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.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/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 0c78a67..0020c50 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
@@ -59,6 +59,7 @@
     String pointer();
     String row();
     String subject();
+    String submittable();
     String tabPanel();
   }
 
@@ -171,6 +172,7 @@
     getTab(Tab.CHERRY_PICKS).setShowBranches(true);
     getTab(Tab.SAME_TOPIC).setShowBranches(true);
     getTab(Tab.SAME_TOPIC).setShowProjects(true);
+    getTab(Tab.SAME_TOPIC).setShowSubmittable(true);
   }
 
   void set(final ChangeInfo info, final String revision) {
@@ -178,7 +180,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,8 +190,8 @@
 
     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),
@@ -199,9 +201,11 @@
       StringBuilder topicQuery = new StringBuilder();
       topicQuery.append("status:open");
       topicQuery.append(" ").append(op("topic", info.topic()));
-      topicQuery.append(" ").append(op("-change", info.legacy_id().get()));
       ChangeList.query(topicQuery.toString(),
-          EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
+          EnumSet.of(ListChangesOption.CURRENT_REVISION,
+                     ListChangesOption.CURRENT_COMMIT,
+                     ListChangesOption.DETAILED_LABELS,
+                     ListChangesOption.LABELS),
           new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
     }
   }
@@ -211,7 +215,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 +323,16 @@
     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());
           arr.push(c);
         }
       }
@@ -350,56 +355,60 @@
     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 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; }-*/;
+
     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..1a09fc5 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
@@ -68,7 +68,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 +87,7 @@
 
   private boolean showBranches;
   private boolean showProjects;
+  private boolean showSubmittable;
   private boolean showIndirectAncestors;
   private boolean registerKeys;
   private int maxHeight;
@@ -111,6 +113,10 @@
     this.showProjects = showProjects;
   }
 
+  void setShowSubmittable(boolean submittable) {
+    this.showSubmittable = submittable;
+  }
+
   void setShowIndirectAncestors(boolean showIndirectAncestors) {
     this.showIndirectAncestors = showIndirectAncestors;
   }
@@ -296,19 +302,23 @@
 
       sb.openSpan();
       GitwebLink gw = Gerrit.getGitwebLink();
-      if (gw != null && (!info.has_change_number() || !info.has_revision_number())) {
+      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 (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,8 +328,8 @@
     }
 
     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());
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 1f11e65..d4b6c42 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,16 +1,16 @@
-//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;
 
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..d6ab9ae 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,16 +1,16 @@
-//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;
 
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 6e40979..cccab34 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
@@ -33,6 +33,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;
@@ -47,23 +48,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();
   }
 
@@ -111,11 +114,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..aec617a 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
@@ -21,9 +21,9 @@
 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.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
@@ -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/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
index 47d6cad..fe0e6d0 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
@@ -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/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 18e5e87..ce35747 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
@@ -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.getUserAccountInfo()._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.getUserAccountInfo()._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..f45b983 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
@@ -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..b3eff93 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
@@ -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..2daa9ea 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
@@ -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/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..c0c828a 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
@@ -68,7 +68,8 @@
 .current,
 .gitweb,
 .indirect,
-.notCurrent {
+.notCurrent,
+.submittable {
   display: inline-block;
   text-align: center;
   vertical-align: top;
@@ -87,3 +88,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..9974532 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
@@ -18,7 +18,9 @@
 import com.google.gerrit.client.NotFoundScreen;
 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 +30,16 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
+import java.util.Set;
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
+  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 +71,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 +87,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 +168,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..7a5e239 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
@@ -51,7 +51,7 @@
     input.project(emptyToNull(project));
     input.branch(emptyToNull(branch));
     input.subject(emptyToNull(subject));
-    input.base_change(emptyToNull(base));
+    input.baseChange(emptyToNull(base));
 
     if (Gerrit.getConfig().isAllowDraftChanges()) {
       input.status(Change.Status.DRAFT.toString());
@@ -95,6 +95,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 +187,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);
   }
 
@@ -236,7 +251,7 @@
     public final native void branch(String b) /*-{ if(b)this.branch=b; }-*/;
     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 +281,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..efaa7c2 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
@@ -22,6 +22,7 @@
   String statusLongMerged();
   String statusLongAbandoned();
   String statusLongDraft();
+  String submittable();
   String readyToSubmit();
   String mergeConflict();
   String notCurrent();
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..40c6d24 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
@@ -3,6 +3,7 @@
 statusLongMerged = Merged
 statusLongAbandoned = Abandoned
 statusLongDraft = Draft
+submittable = Submittable
 readyToSubmit = Ready to Submit
 mergeConflict = Merge Conflict
 notCurrent = Not Current
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/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index fff792f..9052fef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -34,30 +34,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 +66,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,14 +85,14 @@
   }
 
   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 String changeId() /*-{ return this.change_id; }-*/;
   public final native boolean mergeable() /*-{ return this.mergeable || false; }-*/;
   public final native int insertions() /*-{ return this.insertions; }-*/;
   public final native int deletions() /*-{ return this.deletions; }-*/;
@@ -102,35 +103,92 @@
   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; }-*/;
+  private final native void setSubmittable(boolean x) /*-{ this.submittable = x; }-*/;
   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()
   /*-{ return this._more_changes ? true : false; }-*/;
 
+  public final boolean submittable() {
+    init();
+    getMissingLabelIndex();
+    return _submittable();
+  }
+
+  private final native boolean _submittable()
+  /*-{ return this.submittable ? true : false; }-*/;
+
+  /**
+   * As a side effect this.submittable is evaluated and set accordingly.
+   *
+   * @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
+            setSubmittable(false);
+            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:
+          setSubmittable(false);
+          return -1;
+      }
+    }
+    setSubmittable(ret == -1);
+    return ret;
+  }
+
   protected ChangeInfo() {
   }
 
@@ -155,10 +213,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,7 +227,7 @@
     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; }-*/;
@@ -182,11 +240,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 +266,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 +275,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 +307,19 @@
     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 hasDraftComments() /*-{ return this.has_draft_comments || false; }-*/;
+    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 +331,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 +340,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 +381,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 +415,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() {
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..5fcaf24 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
@@ -20,52 +20,68 @@
 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/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 8b71448..c52ac32 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
@@ -27,6 +27,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 +46,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;
@@ -118,13 +127,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 +217,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,8 +238,8 @@
       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()
@@ -447,7 +456,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..c69ee57 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
@@ -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/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 488b34b..dddbd61 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
@@ -54,7 +54,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 +74,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/config/AuthInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/AuthInfo.java
new file mode 100644
index 0000000..5e66d83
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/AuthInfo.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.client.config;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuthInfo extends JavaScriptObject {
+  public final AuthType authType() {
+    return AuthType.valueOf(authTypeRaw());
+  }
+
+  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 (AccountFieldNameInfo f : Natives.asList(_editableAccountFields())) {
+      fields.add(f.get());
+    }
+    return fields;
+  }
+
+  public final native boolean useContributorAgreements()
+  /*-{ return this.use_contributor_agreements || false; }-*/;
+  private final native String authTypeRaw() /*-{ return this.auth_type; }-*/;
+  private final native JsArray<AccountFieldNameInfo> _editableAccountFields()
+  /*-{ return this.editable_account_fields; }-*/;
+
+  protected AuthInfo() {
+  }
+
+  private static class AccountFieldNameInfo extends JavaScriptObject {
+    final Account.FieldName get() {
+      return Account.FieldName.valueOf(getRaw());
+    }
+
+    private final native String getRaw() /*-{ return this; }-*/;
+
+    protected AccountFieldNameInfo() {
+    }
+  }
+}
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..5d79390 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
@@ -37,4 +37,8 @@
   public static void defaultPreferences(AsyncCallback<Preferences> cb) {
     new RestApi("/config/server/preferences").get(cb);
   }
+
+  public static void serverInfo(AsyncCallback<ServerInfo> cb) {
+    new RestApi("/config/server/info").get(cb);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/DownloadInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/DownloadInfo.java
new file mode 100644
index 0000000..e97d472
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/DownloadInfo.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.config;
+
+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.HashSet;
+import java.util.Set;
+
+public class DownloadInfo extends JavaScriptObject {
+  public final Set<String> schemes() {
+    return Natives.keys(_schemes());
+  }
+  public final native DownloadSchemeInfo scheme(String n) /*-{ return this.schemes[n]; }-*/;
+  private final native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
+
+  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);
+    }
+
+    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]; }-*/;
+    private final native NativeMap<NativeString> _commands() /*-{ return this.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/config/GerritInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java
new file mode 100644
index 0000000..33036ad
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.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.client.config;
+
+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; }-*/;
+
+  protected GerritInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
new file mode 100644
index 0000000..a1e8dbc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ServerInfo extends JavaScriptObject {
+  public final native AuthInfo auth() /*-{ return this.auth; }-*/;
+  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 boolean hasContactStore() {
+    return contactStore() != null;
+  }
+
+  protected ServerInfo() {
+  }
+
+  public static class ContactStoreInfo extends JavaScriptObject {
+    public final native String url() /*-{ return this.url; }-*/;
+
+    protected ContactStoreInfo() {
+    }
+  }
+}
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/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..f7f4528 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
@@ -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 1105476..b188c59 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.
@@ -151,13 +151,13 @@
 
   void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
       boolean editExists, int currentPatchSet, 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,
         currentPatchSet, open, binary);
-    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b(), editExists,
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.metaB(), editExists,
         currentPatchSet, 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/FileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
index c4459b6..77b28d4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
@@ -25,9 +25,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; }-*/;
 
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..42ae61d 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
@@ -67,7 +67,7 @@
   }
 
   private static enum ReviewedState {
-    AUTO_REVIEW, LOADED;
+    AUTO_REVIEW, LOADED
   }
 
   @UiField CheckBox reviewed;
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 aed2218..e597398 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.
@@ -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..a32d7b8 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.
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 d06b59e..a8b6065 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
@@ -238,11 +238,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));
         }
-        int currentPatchSet = info.revision(info.current_revision())._number();
+        int currentPatchSet = info.revision(info.currentRevision())._number();
         JsArray<RevisionInfo> list = info.revisions().values();
         RevisionInfo.sortRevisionInfoByNumber(list);
         diffTable.set(prefs, list, diff, edit != null, currentPatchSet,
@@ -570,8 +570,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);
@@ -606,7 +606,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) {
@@ -654,7 +654,7 @@
   }
 
   DiffInfo.IntraLineStatus getIntraLineStatus() {
-    return diff.intraline_status();
+    return diff.intralineStatus();
   }
 
   boolean canEnableRenderEntireFile(DiffPreferences prefs) {
@@ -663,7 +663,7 @@
   }
 
   String getContentType() {
-    return getContentType(diff.meta_b());
+    return getContentType(diff.metaB());
   }
 
   void setThemeStyles(boolean d) {
@@ -716,8 +716,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()));
           }
         }
 
@@ -923,7 +923,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;
     }
 
@@ -983,8 +983,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;
@@ -992,8 +992,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);
   }
 
@@ -1043,8 +1043,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());
           }
 
@@ -1085,8 +1085,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/download/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
index cea9106..d4237f9 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,8 +15,6 @@
 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.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -25,89 +23,15 @@
 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 CheckoutCommandLink extends DownloadCommandLink {
-      public CheckoutCommandLink () {
-        super(DownloadCommand.CHECKOUT, "checkout");
-      }
-
-      @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");
+        super("clone");
       }
 
       @Override
@@ -121,7 +45,7 @@
       private final Project.NameKey project;
 
       public CloneWithCommitMsgHookCommandLink(Project.NameKey project) {
-        super(DownloadCommand.CHECKOUT, "clone with commit-msg hook");
+        super("clone with commit-msg hook");
         this.project = project;
       }
 
@@ -175,12 +99,8 @@
     }
   }
 
-  final DownloadCommand cmdType;
-
-  public DownloadCommandLink(DownloadCommand cmdType,
-      String text) {
+  public DownloadCommandLink(String text) {
     super(text);
-    this.cmdType = cmdType;
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
     addClickHandler(this);
@@ -192,28 +112,6 @@
     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() {
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..5b7d015 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,8 +15,6 @@
 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;
@@ -34,7 +32,7 @@
     return getWidgetCount() == 0;
   }
 
-  public void select(AccountGeneralPreferences.DownloadCommand cmdType) {
+  public void select() {
     DownloadCommandLink first = null;
 
     for (Widget w : this) {
@@ -43,10 +41,6 @@
         if (first == null) {
           first = d;
         }
-        if (d.cmdType == cmdType) {
-          d.select();
-          return;
-        }
       }
     }
 
@@ -70,9 +64,6 @@
   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..19c65a9 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
@@ -16,32 +16,24 @@
 
 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.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
-import java.util.Set;
-
 public abstract class DownloadPanel extends FlowPanel {
   protected String projectName;
 
-  protected Set<DownloadScheme> allowedSchemes =
-      Gerrit.getConfig().getDownloadSchemes();
-  protected Set<DownloadCommand> allowedCommands =
-      Gerrit.getConfig().getDownloadCommands();
   protected DownloadCommandLink.CopyableCommandLinkFactory cmdLinkfactory;
 
   protected DownloadCommandPanel commands = new DownloadCommandPanel();
   protected DownloadUrlPanel urls = new DownloadUrlPanel(commands);
   protected CopyableLabel copyLabel = new CopyableLabel("");
 
-  public DownloadPanel(String project, String ref, boolean allowAnonymous) {
+  public DownloadPanel(String project, boolean allowAnonymous) {
     this.projectName = project;
 
     copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
-    urls.add(DownloadUrlLink.createDownloadUrlLinks(project, ref, allowAnonymous));
+    urls.add(DownloadUrlLink.createDownloadUrlLinks(project, allowAnonymous));
     cmdLinkfactory = new DownloadCommandLink.CopyableCommandLinkFactory(
         copyLabel, urls);
 
@@ -58,7 +50,7 @@
         pref = new AccountGeneralPreferences();
         pref.resetToDefaults();
       }
-      commands.select(pref.getDownloadCommand());
+      commands.select();
       urls.select(pref.getDownloadUrl());
 
       FlowPanel p = new FlowPanel();
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..275b918 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
@@ -17,7 +17,6 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -33,28 +32,9 @@
 import java.util.Set;
 
 public class DownloadUrlLink extends Anchor implements ClickHandler {
-  public static class DownloadRefUrlLink extends DownloadUrlLink {
-    protected String projectName;
-    protected String ref;
-
-    protected DownloadRefUrlLink(DownloadScheme urlType,
-        String text, String project, String ref) {
-      super(urlType, text);
-      this.projectName = project;
-      this.ref = ref;
-    }
-
-    protected void appendRef(StringBuilder r) {
-      if (ref != null) {
-        r.append(" ");
-        r.append(ref);
-      }
-    }
-  }
-
-  public static class AnonGitLink extends DownloadRefUrlLink {
-    public AnonGitLink(String project, String ref) {
-      super(DownloadScheme.ANON_GIT, Util.M.anonymousDownload("Git"), project, ref);
+  public static class AnonGitLink extends DownloadUrlLink {
+    public AnonGitLink(String project) {
+      super(DownloadScheme.ANON_GIT, Util.M.anonymousDownload("Git"), project);
     }
 
     @Override
@@ -62,14 +42,13 @@
       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);
+  public static class AnonHttpLink extends DownloadUrlLink {
+    public AnonHttpLink(String project) {
+      super(DownloadScheme.ANON_HTTP, Util.M.anonymousDownload("HTTP"), project);
     }
 
     @Override
@@ -81,14 +60,13 @@
         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);
+  public static class SshLink extends DownloadUrlLink {
+    public SshLink(String project) {
+      super(DownloadScheme.SSH, "SSH", project);
     }
 
     @Override
@@ -107,16 +85,15 @@
       r.append(sshAddr);
       r.append("/");
       r.append(projectName);
-      appendRef(r);
       return r.toString();
     }
   }
 
-  public static class HttpLink extends DownloadRefUrlLink {
+  public static class HttpLink extends DownloadUrlLink {
     protected boolean anonymous;
 
-    public HttpLink(String project, String ref, boolean anonymous) {
-      super(DownloadScheme.HTTP, "HTTP", project, ref);
+    public HttpLink(String project, boolean anonymous) {
+      super(DownloadScheme.HTTP, "HTTP", project);
       this.anonymous = anonymous;
     }
 
@@ -145,46 +122,41 @@
         r.append(base.substring(s));
       }
       r.append(projectName);
-      appendRef(r);
       return r.toString();
     }
   }
 
   public static boolean siteReliesOnHttp() {
     return Gerrit.getConfig().getGitHttpUrl() != null
-        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+        && Gerrit.info().auth().isCustomExtension()
         && !Gerrit.getConfig().siteHasUsernames();
   }
 
   public static List<DownloadUrlLink> createDownloadUrlLinks(String project,
-      String ref, boolean allowAnonymous) {
+      boolean allowAnonymous) {
     List<DownloadUrlLink> urls = new ArrayList<>();
-    Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
+    Set<String> allowedSchemes = Gerrit.info().download().schemes();
 
     if (allowAnonymous
         && Gerrit.getConfig().getGitDaemonUrl() != null
-        && (allowedSchemes.contains(DownloadScheme.ANON_GIT) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.AnonGitLink(project, ref));
+        && allowedSchemes.contains("git")) {
+      urls.add(new DownloadUrlLink.AnonGitLink(project));
     }
 
     if (allowAnonymous
-        && (allowedSchemes.contains(DownloadScheme.ANON_HTTP) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.AnonHttpLink(project, ref));
+        && allowedSchemes.contains("anonymous http")) {
+      urls.add(new DownloadUrlLink.AnonHttpLink(project));
     }
 
     if (Gerrit.getConfig().getSshdAddress() != null
         && hasUserName()
-        && (allowedSchemes.contains(DownloadScheme.SSH) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.SshLink(project, ref));
+        && allowedSchemes.contains("ssh")) {
+      urls.add(new DownloadUrlLink.SshLink(project));
     }
 
     if ((hasUserName() || siteReliesOnHttp())
-        && (allowedSchemes.contains(DownloadScheme.HTTP)
-            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.HttpLink(project, ref, allowAnonymous));
+        && allowedSchemes.contains("http")) {
+      urls.add(new DownloadUrlLink.HttpLink(project, allowAnonymous));
     }
     return urls;
   }
@@ -196,21 +168,11 @@
   }
 
   protected DownloadScheme urlType;
+  protected String projectName;
   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(DownloadScheme urlType, String text, String project) {
     super(text);
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
@@ -219,6 +181,8 @@
     if (!hostPageUrl.endsWith("/")) {
       hostPageUrl += "/";
     }
+    this.urlType = urlType;
+    this.projectName = project;
   }
 
   public String getUrlData() {
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..e463607 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
@@ -185,7 +185,7 @@
         .get(group1.add(new AsyncCallback<DiffInfo>() {
           @Override
           public void onSuccess(DiffInfo diffInfo) {
-            diffLinks = diffInfo.web_links();
+            diffLinks = diffInfo.webLinks();
           }
 
           @Override
@@ -377,7 +377,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..0ee9fba 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;
@@ -297,13 +301,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 +358,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
    */
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..e2b1112 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
@@ -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,7 +187,7 @@
     } 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);
     }
@@ -235,7 +235,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 +249,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/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index efeb2ec..63823ea 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.
@@ -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..8642556 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
@@ -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..352ade7 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
@@ -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/UnifiedPatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
index a5c1484..2479322 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
@@ -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..0284aa3 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
@@ -17,22 +17,20 @@
 import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.actions.ActionInfo;
 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..b91c5de 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
@@ -35,23 +35,23 @@
   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 SubmitType submitType() {
+    return SubmitType.valueOf(submitTypeRaw());
   }
 
   public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
@@ -63,7 +63,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 +75,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 +131,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 +152,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 +179,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 +189,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..e000a97 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
@@ -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..d81dfe5 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);
     }
@@ -317,6 +317,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..029e59b 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
@@ -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..2633e3b 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
@@ -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/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/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
index 021f39c..a2b4aa8 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,7 +19,6 @@
 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;
@@ -82,7 +81,7 @@
     return newChange.getText();
   }
 
-  class BranchSuggestion implements Suggestion {
+  static class BranchSuggestion implements Suggestion {
     private BranchInfo branch;
 
     public BranchSuggestion(BranchInfo branch) {
@@ -91,10 +90,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/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..2744877 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -29,6 +30,7 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -51,10 +53,10 @@
         String query = request.getQuery().toLowerCase();
         LinkedList<ChangeSuggestion> suggestions = new LinkedList<>();
         for (final ChangeInfo ci : changes) {
-          if (changeId.equals(ci.legacy_id())) {
+          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
@@ -76,8 +78,10 @@
       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,
+          ChangeList.query(
+              "project:" + project + " AND branch:" + branch
+                  + " AND is:open NOT age:90d",
+              Collections.<ListChangesOption> emptySet(),
               new GerritCallback<ChangeList>() {
                 @Override
                 public void onSuccess(ChangeList result) {
@@ -136,12 +140,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/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 91fb6af..378c4d5 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
@@ -52,8 +52,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
index 4b0e46c..eadc536 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -16,22 +16,18 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
 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;
@@ -40,8 +36,6 @@
 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;
@@ -50,42 +44,32 @@
   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) {
+  GerritConfigProvider(Realm r,
+      @GerritServerConfig Config gsc,
+      AuthConfig ac,
+      GitWebConfig gwc,
+      SshInfo si,
+      ServletContext sc,
+      GetArchive.AllowedFormats af,
+      @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()) {
@@ -118,15 +102,9 @@
         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);
@@ -134,8 +112,18 @@
     config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
         cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
     config.setLargeChangeSize(cfg.getInt("change", "largeChange", 500));
+
+    // 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.
     config.setArchiveFormats(Lists.newArrayList(Iterables.transform(
-        archiveFormats.getAllowed(),
+        Iterables.filter(
+            archiveFormats.getAllowed(),
+            new Predicate<ArchiveFormat>() {
+              @Override
+              public boolean apply(ArchiveFormat format) {
+                return (format != ArchiveFormat.ZIP);
+              }
+            }),
         new Function<ArchiveFormat, String>() {
           @Override
           public String apply(ArchiveFormat in) {
@@ -146,17 +134,7 @@
     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);
+    config.setEditableAccountFields(realm.getEditableFields());
 
     if (gitWebConfig.getUrl() != null) {
       config.setGitwebLink(new GitwebConfig(gitWebConfig.getUrl(), gitWebConfig
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
index 9d47977..7dc0c17 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.httpd;
 
+import static java.nio.file.Files.isExecutable;
+import static java.nio.file.Files.isRegularFile;
+
 import com.google.gerrit.common.data.GitWebType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -23,16 +26,17 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 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 final Path gitweb_cgi;
+  private final Path gitweb_css;
+  private final Path gitweb_js;
+  private final Path git_logo_png;
   private GitWebType type;
 
   @Inject
@@ -117,20 +121,20 @@
       return;
     }
 
-    final File pkgCgi = new File("/usr/lib/cgi-bin/gitweb.cgi");
+    final Path pkgCgi = Paths.get("/usr/lib/cgi-bin/gitweb.cgi");
     String[] resourcePaths = {"/usr/share/gitweb/static", "/usr/share/gitweb",
         "/var/www/static", "/var/www"};
-    File cgi;
+    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 (!cgi.isFile()) {
+      if (!isRegularFile(cgi)) {
         throw new IllegalStateException("Cannot find gitweb.cgi: " + cgi);
       }
-      if (!cgi.canExecute()) {
+      if (!isExecutable(cgi)) {
         throw new IllegalStateException("Cannot execute gitweb.cgi: " + cgi);
       }
 
@@ -138,11 +142,11 @@
         // Assume the administrator pointed us to the distribution,
         // which also has the corresponding CSS and logo file.
         //
-        String absPath = cgi.getParentFile().getAbsolutePath();
+        String absPath = cgi.getParent().toAbsolutePath().toString();
         resourcePaths = new String[] {absPath + "/static", absPath};
       }
 
-    } else if (pkgCgi.isFile() && pkgCgi.canExecute()) {
+    } else if (isRegularFile(pkgCgi) && isExecutable(pkgCgi)) {
       // Use the OS packaged CGI.
       //
       log.debug("Assuming gitweb at " + pkgCgi);
@@ -154,13 +158,15 @@
       resourcePaths = new String[] {};
     }
 
-    File css = null, js = null, logo = null;
+    Path css = null;
+    Path js = null;
+    Path 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()) {
+      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;
       }
     }
@@ -191,22 +197,22 @@
   }
 
   /** @return local path to the CGI executable; null if we shouldn't execute. */
-  public File getGitwebCGI() {
+  public Path getGitwebCGI() {
     return gitweb_cgi;
   }
 
   /** @return local path of the {@code gitweb.css} matching the CGI. */
-  public File getGitwebCSS() {
+  public Path getGitwebCSS() {
     return gitweb_css;
   }
 
   /** @return local path of the {@code gitweb.js} for the CGI. */
-  public File getGitwebJS() {
+  public Path getGitwebJS() {
     return gitweb_js;
   }
 
   /** @return local path of the {@code git-logo.png} for the CGI. */
-  public File getGitLogoPNG() {
+  public Path getGitLogoPNG() {
     return git_logo_png;
   }
 
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..400b146 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
@@ -186,7 +186,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/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index b8a8092..c1c3b2b 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,7 +102,7 @@
         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 {
@@ -120,14 +118,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,7 +128,7 @@
 
     } else {
       rsp.setContentType("text/html");
-      rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+      rsp.setCharacterEncoding(HtmlDomUtil.ENC.name());
       final Writer out = rsp.getWriter();
       out.write("<html>");
       out.write("<body>");
@@ -155,12 +145,6 @@
     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();
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 19c8342..b5400b2 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
@@ -110,7 +110,7 @@
 
       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 {
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/gitweb/GitLogoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
index 41aa552..3396f2b 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 static com.google.gerrit.common.FileUtil.lastModified;
+
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.httpd.GitWebConfig;
 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(GitWebConfig gitWebConfig) throws IOException {
     byte[] png;
-    final File src = gitWebConfig.getGitLogoPNG();
+    Path src = gitWebConfig.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;
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
index 4a39b97..5625334 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,6 +14,8 @@
 
 package com.google.gerrit.httpd.gitweb;
 
+import static com.google.gerrit.common.FileUtil.lastModified;
+
 import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.SitePaths;
@@ -22,8 +24,8 @@
 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;
@@ -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 {
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
index d71732a..6926afd 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 static com.google.gerrit.common.FileUtil.lastModified;
+
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.httpd.GitWebConfig;
 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;
@@ -40,14 +43,14 @@
   @Inject
   GitWebJavaScriptServlet(final GitWebConfig gitWebConfig) throws IOException {
     byte[] png;
-    final File src = gitWebConfig.getGitwebJS();
+    Path src = gitWebConfig.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;
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
index 573725c..cf43041 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,6 +29,8 @@
 
 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;
@@ -55,7 +57,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 +64,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;
@@ -84,7 +87,7 @@
 
   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;
@@ -145,28 +148,30 @@
 
   private void makeSiteConfig(final SitePaths site,
       final GerritConfig gerritConfig) throws IOException {
-    if (!site.tmp_dir.exists()) {
-      site.tmp_dir.mkdirs();
+    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 +179,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 +197,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");
       }
 
@@ -294,15 +299,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 +331,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 "''";
     }
@@ -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()) {
@@ -535,7 +539,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());
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..405a861 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 {
@@ -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,25 +626,15 @@
   }
 
   private static String getJsPluginPath(Plugin plugin) {
-    return String.format("/plugins/%s/static/%s", plugin.getName(), plugin.getSrcFile()
-        .getName());
+    return String.format("/plugins/%s/static/%s", plugin.getName(),
+        plugin.getSrcFile().getFileName());
   }
 
-  private void writeToResponse(HttpServletResponse res, InputStream in)
+  private void writeToResponse(HttpServletResponse res, InputStream inputStream)
       throws IOException {
-    try {
-      OutputStream out = res.getOutputStream();
-      try {
-        byte[] tmp = new byte[1024];
-        int n;
-        while ((n = in.read(tmp)) > 0) {
-          out.write(tmp, 0, n);
-        }
-      } finally {
-        out.close();
-      }
-    } finally {
-      in.close();
+    try (OutputStream out = res.getOutputStream();
+        InputStream in = inputStream) {
+      ByteStreams.copy(in, out);
     }
   }
 
@@ -674,9 +666,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/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 90c5ff4..e0d4b51 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,6 +14,8 @@
 
 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;
@@ -30,6 +32,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 +50,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,8 +72,8 @@
 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;
@@ -84,6 +88,7 @@
   private final boolean refreshHeaderFooter;
   private final StaticServlet staticServlet;
   private final boolean isNoteDbEnabled;
+  private final Integer pluginsLoadTimeout;
   private volatile Page page;
 
   @Inject
@@ -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,54 @@
     }
 
     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);
-      if (in != null) {
-        Hasher md = Hashing.md5().newHasher();
+    InputStream in = servletContext.getResourceAsStream("/" + src);
+    if (in != null) {
+      Hasher md = Hashing.md5().newHasher();
+      try {
         try {
-          try {
-            final byte[] buf = new byte[1024];
-            int n;
-            while ((n = in.read(buf)) > 0) {
-              md.putBytes(buf, 0, n);
-            }
-          } finally {
-            in.close();
+          final byte[] buf = new byte[1024];
+          int n;
+          while ((n = in.read(buf)) > 0) {
+            md.putBytes(buf, 0, n);
           }
-        } catch (IOException e) {
-          throw new IOException("Failed reading " + src, e);
+        } finally {
+          in.close();
         }
-        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);
       }
+      src += "?content=" + md.hash().toString();
+    } else {
+      log.debug("No " + src + " in webapp root; keeping noncache.js URL");
     }
 
     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;
   }
@@ -216,7 +222,7 @@
 
     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 {
@@ -288,16 +294,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);
     }
   }
 
@@ -319,6 +325,7 @@
       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 +371,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 +383,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 +393,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..00568f0 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,7 +68,7 @@
 
     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 {
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..b85cdf0 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
@@ -40,12 +40,14 @@
  * <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")
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..e690334 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
@@ -209,29 +208,22 @@
   }
 
   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 +231,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 +246,7 @@
     }
 
     boolean isStale() {
-      return lastModified != src.lastModified();
+      return lastModified != lastModified(src);
     }
   }
 }
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..0045649 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());
@@ -384,10 +388,10 @@
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
     } finally {
-      globals.auditService.dispatch(new HttpAuditEvent(globals.webSession.get()
-          .getSessionId(), globals.currentUser.get(), req.getRequestURI(),
-          auditStartTs, params, req.getMethod(), inputRequestBody, status,
-          result));
+      globals.auditService.dispatch(new ExtendedHttpAuditEvent(globals.webSession.get()
+          .getSessionId(), globals.currentUser.get(), req,
+          auditStartTs, params, inputRequestBody, status,
+          result, rsrc, viewData == null ? null : viewData.view));
     }
   }
 
@@ -656,7 +660,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);
@@ -781,7 +785,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)) {
@@ -958,7 +962,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);
@@ -1053,10 +1058,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,7 +1076,7 @@
 
   private static BinaryResult compress(BinaryResult bin)
       throws IOException {
-    TemporaryBuffer.Heap buf = heap(20 << 20);
+    TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, 20 << 20);
     GZIPOutputStream gz = new GZIPOutputStream(buf);
     bin.writeTo(gz);
     gz.close();
@@ -1083,8 +1093,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/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index e1c9e3c..a12d8d5 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
@@ -201,8 +201,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/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..d2a333e 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" />
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..e3e6d48 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,11 +88,15 @@
 
     // 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 "";
@@ -122,20 +126,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 +172,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();
@@ -201,10 +210,16 @@
           final ZipEntry ze = e.nextElement();
           if (ze.isDirectory()) {
             continue;
-          } else if (ze.getName().startsWith("WEB-INF/lib/")) {
+          }
+
+          String name = ze.getName();
+          if (name.startsWith("WEB-INF/lib/")) {
             extractJar(zf, ze, jars);
-          } else if (ze.getName().startsWith("WEB-INF/pgm-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 {
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..64da702 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
@@ -14,7 +14,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;
@@ -30,7 +31,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
 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 +39,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 +52,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -64,9 +66,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 +81,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;
@@ -119,54 +128,20 @@
   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_SORT_FIELD =
+      sortFieldName(ChangeField.LEGACY_ID);
   private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
+  private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.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);
+
   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 +154,10 @@
     }
   }
 
+  private static String sortFieldName(FieldDef<?, ?> f) {
+    return f.getName() + "_SORT";
+  }
+
   static interface Factory {
     LuceneChangeIndex create(Schema<ChangeData> schema, String base);
   }
@@ -187,12 +166,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,12 +203,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;
 
+  /**
+   * 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(
       @GerritServerConfig Config cfg,
@@ -245,15 +235,8 @@
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.schema = schema;
+    this.useDocValuesForSorting = schema.getVersion() >= 15;
 
-    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 +246,44 @@
         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();
+    }
+    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) 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);
@@ -358,12 +366,18 @@
     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));
+  private Sort getSort() {
+    if (useDocValuesForSorting) {
+      return new Sort(
+          new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
+          new SortField(ID_SORT_FIELD, 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 {
@@ -402,7 +416,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 +476,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);
@@ -494,6 +509,19 @@
     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)) {
@@ -509,6 +537,16 @@
     FieldType<?> type = values.getField().getType();
     Store store = store(values.getField());
 
+    if (useDocValuesForSorting) {
+      if (values.getField() == ChangeField.LEGACY_ID) {
+        int v = (Integer) getOnlyElement(values.getValues());
+        doc.add(new NumericDocValuesField(ID_SORT_FIELD, v));
+      } else if (values.getField() == 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 +573,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 672a7c9..35d6636 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);
     install(new IndexModule(threads));
     if (singleVersion == null && base == null) {
@@ -54,7 +56,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() {
       factory(OnlineReindexer.Factory.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 c3570a1..109525a 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,9 +94,11 @@
   private final LuceneChangeIndex.Factory indexFactory;
   private final IndexCollection indexes;
   private final OnlineReindexer.Factory reindexerFactory;
+  private final boolean onlineUpgrade;
 
   @Inject
   LuceneVersionManager(
+      @GerritServerConfig Config cfg,
       SitePaths sitePaths,
       LuceneChangeIndex.Factory indexFactory,
       IndexCollection indexes,
@@ -101,6 +107,7 @@
     this.indexFactory = indexFactory;
     this.indexes = indexes;
     this.reindexerFactory = reindexerFactory;
+    this.onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
   }
 
   @Override
@@ -114,10 +121,10 @@
       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");
     }
 
@@ -130,7 +137,7 @@
       if (v.schema == null) {
         continue;
       }
-      if (write.isEmpty()) {
+      if (write.isEmpty() && onlineUpgrade) {
         write.add(v);
       }
       if (v.ready) {
@@ -159,7 +166,7 @@
     }
 
     int latest = write.get(0).version;
-    if (latest != search.version) {
+    if (onlineUpgrade && latest != search.version) {
       reindexerFactory.create(latest).start();
     }
   }
@@ -167,29 +174,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..218bb71 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
@@ -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));
@@ -249,8 +249,4 @@
   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..981f909
--- /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);
+      // 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-pgm/BUCK b/gerrit-pgm/BUCK
index 9a317cd..10d6dda 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',
@@ -106,7 +107,9 @@
     '//lib:args4j',
     '//lib:gwtorm',
     '//lib:servlet-api-3_1',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:cafeteria',
+    '//lib/prolog:compiler',
+    '//lib/prolog:runtime',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
@@ -125,7 +128,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/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 2b9af2f..1afa243 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;
@@ -93,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;
 
@@ -148,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;
 
@@ -184,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) {
@@ -195,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() {
@@ -207,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();
         }
@@ -216,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);
         }
@@ -264,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);
     }
@@ -280,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();
@@ -325,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());
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/Ls.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Ls.java
index 7e7b602..2bcd6ea 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
@@ -39,6 +39,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/")) {
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/RebuildNotedb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
index ab20f53..a29a8e0 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
@@ -244,7 +244,7 @@
     }
   }
 
-  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/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index 17a54d4..8f1b354 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;
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/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 907624d..80d69f1 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;
@@ -525,11 +527,13 @@
         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());
@@ -559,8 +563,9 @@
   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();
     }
   }
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..534ef050 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);
@@ -132,7 +137,7 @@
       throw failure;
     }
 
-    System.err.println("Initialized " + getSitePath().getCanonicalPath());
+    System.err.println("Initialized " + getSitePath().toRealPath().normalize());
     afterInit(run);
     return 0;
   }
@@ -208,7 +213,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 +233,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 +292,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 +413,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/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..e0cb4c4 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 {
@@ -70,8 +76,9 @@
           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,6 +105,10 @@
               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 {
@@ -105,12 +116,47 @@
     }
   }
 
-  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/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/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..a714ac9 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,11 +170,11 @@
     }
   }
 
-  private static String getVersion(final File plugin) throws IOException {
-    final JarFile jarFile = new JarFile(plugin);
+  private static String getVersion(Path plugin) throws IOException {
+    JarFile jarFile = new JarFile(plugin.toFile());
     try {
-      final Manifest manifest = jarFile.getManifest();
-      final Attributes main = manifest.getMainAttributes();
+      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/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index 4bf1c88..00c7c58 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,120 @@
 
   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) {
+      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..10d93ee 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,11 +69,11 @@
   }
 
   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 {
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 80%
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..ca8b183 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 {
@@ -222,51 +223,51 @@
   }
 
   /** 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()) {
+          FileInputStream in = new FileInputStream(f);
+          try {
+            while (true) {
+              int nRead = in.read(buffer, 0, buffer.length);
+              if (nRead <= 0) {
+                break;
               }
-            } finally {
-              in.close();
+              out.write(buffer, 0, nRead);
             }
+          } finally {
+            in.close();
           }
-          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..ee0a0e6 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();
@@ -47,14 +51,12 @@
     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 +71,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/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/pom.xml b/gerrit-plugin-api/pom.xml
index 17cf79a..eb2ec31 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.11.1</version>
+  <version>2.12-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 8f9684a..1da5d51 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
-  <version>2.11.1</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>
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-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index f384bc1..bd4d738 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
-  <version>2.11.1</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>
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-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index e4fc192..fa261be 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.11.1</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>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index 559d9de..11d4d83 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
-  <version>2.11.1</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>
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-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/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/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/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/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 072982a..4154913 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,19 @@
   /** Suffix of a meta ref in the notedb. */
   public static final String META_SUFFIX = "/meta";
 
+  public static String fullName(String ref) {
+    return (ref.startsWith(REFS) ? "" : 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) {
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/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 7202dc3..5cda4dd 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,6 +60,7 @@
     '//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',
@@ -66,7 +68,7 @@
     '//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,19 @@
     ':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/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',
   ],
@@ -134,7 +139,7 @@
     '//gerrit-common:server',
     '//lib:junit',
     '//lib/guice:guice',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
 )
 
@@ -155,7 +160,7 @@
     '//lib:truth',
     '//lib/jgit:jgit',
     '//lib/guice:guice',
-    '//lib/prolog:prolog-cafe',
+    '//lib/prolog:runtime',
   ],
 )
 
@@ -217,7 +222,7 @@
     '//lib/joda:joda-time',
     '//lib:parboiled-core',
     '//lib:parboiled-java',
-    '//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..19c3145 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());
@@ -906,7 +958,7 @@
       } 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 +966,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 +1001,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 +1018,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 +1031,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 +1040,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/ReductionLimitException.java b/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
deleted file mode 100644
index 2c27240..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
+++ /dev/null
@@ -1,24 +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.rules;
-
-/** Thrown by {@link PrologEnvironment} if a script runs too long. */
-public class ReductionLimitException extends RuntimeException {
-  private static final long serialVersionUID = 1L;
-
-  ReductionLimitException(int limit) {
-    super(String.format("exceeded reduction limit of %d", limit));
-  }
-}
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..658bb35 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,13 +28,18 @@
 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;
@@ -44,10 +50,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 +60,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 +101,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 +113,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 +158,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 +183,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,12 +209,60 @@
           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;
@@ -237,7 +290,6 @@
 
   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 +306,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..c9359b4 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;
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..c31a411 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
@@ -142,7 +142,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/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 9faa516..305b66d 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
@@ -17,9 +17,11 @@
 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;
@@ -95,6 +97,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.
    *
@@ -104,7 +117,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();
@@ -316,7 +330,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();
@@ -456,7 +470,7 @@
           throw new IOException("Failed to delete ref " + patch.getRefName() +
               " in " + repo.getDirectory() + ": " + update.getResult());
       }
-      gitRefUpdated.fire(change.getProject(), update);
+      gitRefUpdated.fire(change.getProject(), update, ReceiveCommand.Type.DELETE);
     } finally {
       repo.close();
     }
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..88f034e 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,33 +322,45 @@
     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 {
@@ -356,7 +393,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/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index 580897f..465c9ba 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
@@ -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/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/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/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/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/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/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 07936d9..d181c35 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";
 
@@ -50,9 +50,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) {
@@ -63,7 +64,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/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 9b32ca9..4a652b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -73,6 +73,9 @@
 
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+    if (uuid == null) {
+      return null;
+    }
     GroupBackend b = backend(uuid);
     if (b == null) {
       log.warn("Unknown GroupBackend for UUID: " + uuid);
@@ -121,6 +124,9 @@
 
    @Override
    public boolean contains(AccountGroup.UUID uuid) {
+     if (uuid == null) {
+       return false;
+     }
      GroupMembership m = membership(uuid);
      if (m == null) {
        log.warn("Unknown GroupMembership for UUID: " + uuid);
@@ -134,6 +140,9 @@
       Multimap<GroupMembership, AccountGroup.UUID> lookups =
           ArrayListMultimap.create();
       for (AccountGroup.UUID uuid : uuids) {
+        if (uuid == null) {
+          continue;
+        }
         GroupMembership m = membership(uuid);
         if (m == null) {
           log.warn("Unknown GroupMembership for UUID: " + uuid);
@@ -161,6 +170,9 @@
       Multimap<GroupMembership, AccountGroup.UUID> lookups =
           ArrayListMultimap.create();
       for (AccountGroup.UUID uuid : uuids) {
+        if (uuid == null) {
+          continue;
+        }
         GroupMembership m = membership(uuid);
         if (m == null) {
           log.warn("Unknown GroupMembership for UUID: " + uuid);
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..ee7e1a5 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,26 @@
 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.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 Groups groups;
   private final Projects projects;
 
   @Inject
   GerritApiImpl(Accounts accounts,
       Changes changes,
+      Groups groups,
       Projects projects) {
     this.accounts = accounts;
     this.changes = changes;
+    this.groups = groups;
     this.projects = projects;
   }
 
@@ -47,6 +51,11 @@
   }
 
   @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..c686415 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,9 @@
   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.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..7051617 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
@@ -28,7 +28,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);
   }
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..78b3f10 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,6 +41,8 @@
 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;
@@ -54,13 +59,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;
@@ -75,11 +82,14 @@
   private final Provider<ChangeJson> 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,
@@ -92,9 +102,12 @@
       Provider<ChangeJson> 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;
@@ -108,6 +121,8 @@
     this.changeJson = changeJson;
     this.postHashtags = postHashtags;
     this.getHashtags = getHashtags;
+    this.listComments = listComments;
+    this.listDrafts = listDrafts;
     this.check = check;
     this.editDetail = editDetail;
     this.change = change;
@@ -244,6 +259,10 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        ((IdentifiedUser) u).clearStarredChanges();
+      }
       return changeJson.get().addOptions(s).format(change);
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
@@ -289,6 +308,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..1b420f7 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,6 +27,7 @@
 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;
@@ -40,16 +41,17 @@
 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.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 +62,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);
   }
@@ -80,13 +82,14 @@
   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,
@@ -103,13 +106,14 @@
       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;
@@ -132,6 +136,7 @@
     this.draftFactory = draftFactory;
     this.comments = comments;
     this.commentFactory = commentFactory;
+    this.revisionActions = revisionActions;
     this.revision = r;
   }
 
@@ -188,7 +193,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 +298,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 +316,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 +337,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 +356,9 @@
       throw new RestApiException("Cannot retrieve comment", 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/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
new file mode 100644
index 0000000..b7c8cd9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -0,0 +1,261 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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 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,
+      @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.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);
+    }
+  }
+
+}
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/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 9060108..3607e34 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
@@ -134,7 +134,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);
   }
 
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..a73f8ba 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;
@@ -123,17 +122,15 @@
     }
     update.commit();
 
-    CheckedFuture<?, IOException> indexFuture =
-        indexer.indexAsync(change.getId());
+    indexer.index(db, change);
     try {
-      ReplyToChangeSender cm = abandonedSenderFactory.create(change);
+      ReplyToChangeSender cm = abandonedSenderFactory.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);
     }
-    indexFuture.checkedGet();
     hooks.doChangeAbandonedHook(change,
         caller.getAccount(),
         db.patchSets().get(change.currentPatchSetId()),
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..180b9ad 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;
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/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..28614f7 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;
@@ -31,12 +32,14 @@
 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;
@@ -65,7 +68,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,10 +82,7 @@
 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;
@@ -92,13 +91,13 @@
 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.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 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;
@@ -107,10 +106,16 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.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;
@@ -130,9 +135,11 @@
   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 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;
@@ -154,9 +161,11 @@
       LabelNormalizer ln,
       Provider<CurrentUser> user,
       AnonymousUser au,
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       IdentifiedUser.GenericFactory uf,
       ChangeData.Factory cdf,
-      PatchSetInfoFactory psi,
       FileInfoJson fileInfoJson,
       AccountLoader.Factory ailf,
       DynamicMap<DownloadScheme> downloadSchemes,
@@ -171,9 +180,11 @@
     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.mergeUtilFactory = mergeUtilFactory;
     this.fileInfoJson = fileInfoJson;
     this.accountLoaderFactory = ailf;
     this.downloadSchemes = downloadSchemes;
@@ -238,9 +249,11 @@
       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);
     }
@@ -298,7 +311,8 @@
       if (i == null) {
         try {
           i = toChangeInfo(cd, reviewed, Optional.<PatchSet.Id> absent());
-        } catch (OrmException | RuntimeException e) {
+        } catch (PatchListNotAvailableException | OrmException | IOException
+            | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
           } else {
@@ -341,7 +355,8 @@
   }
 
   private ChangeInfo toChangeInfo(ChangeData cd, Set<Change.Id> reviewed,
-      Optional<PatchSet.Id> limitToPsId) throws OrmException {
+      Optional<PatchSet.Id> limitToPsId)
+      throws PatchListNotAvailableException, OrmException, IOException {
     ChangeInfo out = new ChangeInfo();
 
     if (has(CHECK)) {
@@ -412,7 +427,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) {
@@ -830,14 +845,15 @@
     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 +863,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 +887,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 +899,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))
@@ -919,34 +945,35 @@
     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 +1029,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 23039aa..8fdd445 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
@@ -317,7 +317,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..265cb49 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;
@@ -61,18 +60,16 @@
     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));
-
     byte[] buf = new byte[20];
     ObjectId noteId;
     try {
@@ -87,6 +84,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..6540ef2 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;
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..f4d1e0a 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
@@ -102,6 +102,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..b8e1178 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;
@@ -182,9 +183,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..87a1ca6 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,14 @@
 
 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.collect.Collections2;
 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.Nullable;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ProblemInfo;
@@ -210,17 +211,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 {
@@ -307,8 +297,7 @@
       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(),
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 4dffd67..ac5d3c6 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
@@ -33,6 +33,7 @@
 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.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -55,7 +56,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;
@@ -140,11 +140,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();
@@ -161,6 +157,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) {
@@ -176,6 +173,7 @@
             new PatchSet.Id(change.getId(),
             change.currentPatchSetId().get()));
         parentCommit = ObjectId.fromString(ps.getRevision().get());
+        groups = ps.getGroups();
       } else {
         Ref destRef = git.getRef(refName);
         if (destRef == null) {
@@ -183,6 +181,7 @@
               "Branch %s does not exist.", refName));
         }
         parentCommit = destRef.getObjectId();
+        groups = null;
       }
       RevCommit mergeTip = rw.parseCommit(parentCommit);
 
@@ -212,6 +211,7 @@
 
       change.setTopic(input.topic);
       ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
+      ins.setGroups(groups);
       ins.insert();
 
       return Response.created(json.format(change.getId()));
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/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..ccc7645 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
@@ -18,16 +18,15 @@
 import com.google.common.collect.ImmutableMap;
 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 +35,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 +46,25 @@
     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));
+      allowed = cfg.getArchiveFormats();
     }
 
     public Set<ArchiveFormat> getAllowed() {
       return allowed;
     }
+
+    public ImmutableMap<String, ArchiveFormat> getExtensions() {
+      return extensions;
+    }
   }
 
   private final GitRepositoryManager repoManager;
@@ -93,8 +80,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,6 +89,9 @@
     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());
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..0f0a6a2 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,47 @@
 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 GitRepositoryManager repoManager;
   private final ChangeJson json;
 
   @Option(name = "--links", usage = "Add weblinks")
   private boolean addLinks;
 
   @Inject
-  GetCommit(ChangeJson json) {
+  GetCommit(GitRepositoryManager repoManager,
+      ChangeJson 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);
+      Response<CommitInfo> r = Response.ok(
+          json.toCommit(rsrc.getControl(), rw, commit, addLinks));
+      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/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 6cdae44..df93278e 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,258 +14,166 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.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.ChangeField;
+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(ChangeField.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()));
+    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) {
-        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())) {
+    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;
   }
@@ -277,6 +185,9 @@
     public Integer _revisionNumber;
     public Integer _currentRevisionNumber;
 
+    public ChangeAndCommit() {
+    }
+
     ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
       if (change != null) {
         changeId = change.getKey().get();
@@ -297,5 +208,28 @@
       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)
+          .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..119de7e
--- /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.getRef(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..3ae6f6c 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,65 @@
 
 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;
 
-@Singleton
-public class GetRevisionActions implements RestReadView<RevisionResource> {
-  private final ActionJson delegate;
+import org.eclipse.jgit.lib.Config;
 
+import java.util.Map;
+
+@Singleton
+public class GetRevisionActions implements ETagView<RevisionResource> {
+  private final ActionJson delegate;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final Config config;
+  private final RebaseChange rebaseChange;
   @Inject
-  GetRevisionActions(ActionJson delegate) {
+  GetRevisionActions(
+      ActionJson delegate,
+      Provider<InternalChangeQuery> queryProvider,
+      @GerritServerConfig Config config,
+      RebaseChange rebaseChange) {
     this.delegate = delegate;
+    this.queryProvider = queryProvider;
+    this.config = config;
+    this.rebaseChange = rebaseChange;
   }
 
   @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(), rebaseChange).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/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/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index d0e4c99..b73af931 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);
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..db9c6d0 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
@@ -35,6 +35,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,6 +109,7 @@
   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;
@@ -200,6 +202,11 @@
     return this;
   }
 
+  public PatchSetInserter setGroups(Iterable<String> groups) {
+    this.groups = groups;
+    return this;
+  }
+
   public PatchSetInserter setRunHooks(boolean runHooks) {
     this.runHooks = runHooks;
     return this;
@@ -239,12 +246,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()) {
         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
@@ -293,7 +306,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/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..5aacef7 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,27 +39,32 @@
 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;
-
 @Singleton
 public class Rebase implements RestModifyView<RevisionResource, RebaseInput>,
     UiAction<RevisionResource> {
 
-  private static final Logger log =
-      LoggerFactory.getLogger(Rebase.class);
+  private static final Logger log = LoggerFactory.getLogger(Rebase.class);
 
+  private final GitRepositoryManager repoManager;
   private final Provider<RebaseChange> rebaseChange;
   private final ChangeJson json;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json,
+  public Rebase(GitRepositoryManager repoManager,
+      Provider<RebaseChange> rebaseChange,
+      ChangeJson json,
       Provider<ReviewDb> dbProvider) {
+    this.repoManager = repoManager;
     this.rebaseChange = rebaseChange;
     this.json = json
         .addOption(ListChangesOption.CURRENT_REVISION)
@@ -69,68 +75,24 @@
   @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, change, rsrc.getPatchSet().getId(),
+          rsrc.getUser(), 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());
     }
@@ -138,88 +100,118 @@
     return json.format(change.getId());
   }
 
-  private boolean isDescendantOf(Change.Id child, RevId ancestor)
-      throws OrmException {
-    ReviewDb db = dbProvider.get();
-
-    ArrayList<RevId> parents = new ArrayList<>();
-    parents.add(ancestor);
-    while (!parents.isEmpty()) {
-      RevId parent = parents.remove(0);
-      // get direct descendants of change
-      for (PatchSetAncestor desc : db.patchSetAncestors().descendantsOf(parent)) {
-        PatchSet descPatchSet = db.patchSets().get(desc.getPatchSet());
-        Change.Id descChangeId = descPatchSet.getId().getParentKey();
-        if (child.equals(descChangeId)) {
-          PatchSet.Id descCurrentPatchSetId =
-              db.changes().get(descChangeId).currentPatchSetId();
-          // it's only bad if the descendant patch set is current
-          return descPatchSet.getId().equals(descCurrentPatchSetId);
-        } else {
-          // process indirect descendants as well
-          parents.add(descPatchSet.getRevision());
-        }
-      }
+  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 +232,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..96b513e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
@@ -0,0 +1,387 @@
+// 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.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.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.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.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 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>
+   * It is verified that the current user is allowed to do the rebase.
+   * <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 change the change to rebase.
+   * @param patchSetId the patch set ID to rebase.
+   * @param uploader the user that creates the rebased patch set.
+   * @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, Change change,
+      PatchSet.Id patchSetId, IdentifiedUser uploader, String newBaseRev)
+      throws NoSuchChangeException, EmailException, OrmException, IOException,
+      InvalidChangeOperationException {
+    Change.Id changeId = patchSetId.getParentKey();
+    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);
+    }
+    try (ObjectInserter inserter = git.newObjectInserter()) {
+      String baseRev = newBaseRev;
+      if (baseRev == null) {
+        baseRev = findBaseRevision(
+            patchSetId, db.get(), 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, patchSetId,
+          uploader, baseCommit, mergeUtilFactory.create(
+              changeControl.getProjectControl().getProjectState(), true),
+          committerIdent, true, ValidatePolicy.GERRIT);
+    } catch (MergeConflictException e) {
+      throw new IOException(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 patchSetId patch set ID for which the new base commit should be
+   *     found.
+   * @param db the ReviewDb.
+   * @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 static String findBaseRevision(PatchSet.Id patchSetId,
+      ReviewDb db, Branch.NameKey destBranch, Repository git, RevWalk rw)
+      throws InvalidChangeOperationException, IOException, OrmException {
+    String baseRev = null;
+
+    PatchSet patchSet = db.patchSets().get(patchSetId);
+    if (patchSet == null) {
+      throw new InvalidChangeOperationException(
+          "Patch set " + patchSetId + " not found");
+    }
+    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.patchSets().byRevision(parentRev)) {
+      Change.Id depChangeId = depPatchSet.getId().getParentKey();
+      Change depChange = db.changes().get(depChangeId);
+      if (!depChange.getDest().equals(destBranch)) {
+        continue;
+      }
+
+      if (depChange.getStatus() == Status.ABANDONED) {
+        throw new 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.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 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(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 (RevWalk rw = new RevWalk(git)) {
+      findBaseRevision(patchSetId, db.get(), branch, git, rw);
+      return true;
+    } catch (InvalidChangeOperationException e) {
+      return false;
+    } catch (OrmException | IOException e) {
+      log.warn("Error checking if patch set " + patchSetId + " on " + branch
+          + " can be rebased", e);
+      return false;
+    } finally {
+      git.close();
+    }
+  }
+}
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..46d73d7 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;
@@ -122,17 +121,16 @@
     }
     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()),
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..9e243b0 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
@@ -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 5f67e6e..109d96f 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
@@ -99,6 +99,10 @@
       "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";
+  private static final String CLICK_FAILURE_OTHER_TOOLTIP =
+      "Clicking the button would fail for other changes in the topic";
+  private static final String CLICK_FAILURE_TOOLTIP =
+      "Clicking the button would fail.";
 
   public enum Status {
     SUBMITTED, MERGED
@@ -169,7 +173,7 @@
     this.titlePattern = new ParameterizedString(MoreObjects.firstNonNull(
         cfg.getString("change", null, "submitTooltip"),
         DEFAULT_TOOLTIP));
-    submitWholeTopic = cfg.getBoolean("change", null, "submitWholeTopic" , false);
+    submitWholeTopic = wholeTopicEnabled(cfg);
     this.submitTopicLabel = MoreObjects.firstNonNull(
         Strings.emptyToNull(cfg.getString("change", null, "submitTopicLabel")),
         "Submit whole topic");
@@ -206,17 +210,19 @@
           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())));
-    }
+    List<Change> submittedChanges = submit(rsrc, caller, false);
 
     if (input.waitForMerge) {
-      mergeQueue.merge(change.getDest());
+      for (Change c : submittedChanges) {
+        // TODO(sbeller): We should make schedule return a Future, then we
+        // could do these all in parallel and still block until they're done.
+        mergeQueue.merge(c.getDest());
+      }
       change = dbProvider.get().changes().get(change.getId());
     } else {
-      mergeQueue.schedule(change.getDest());
+      for (Change c : submittedChanges) {
+        mergeQueue.schedule(c.getDest());
+      }
     }
 
     if (change == null) {
@@ -239,14 +245,15 @@
   }
 
   /**
-   * @param changes list of changes to be submitted at once
+   * @param changeList list of changes to be submitted at once
    * @param identifiedUser the user who is checking to submit
    * @return a reason why any of the changes is not submittable or null
    */
-  private String problemsForSubmittingChanges(List<ChangeData> changes,
+  private String problemsForSubmittingChanges(
+      List<ChangeData> changeList,
       IdentifiedUser identifiedUser) {
-    for (ChangeData c : changes) {
-      try {
+    try {
+      for (ChangeData c : changeList) {
         ChangeControl changeControl = c.changeControl().forUser(
             identifiedUser);
         if (!changeControl.isVisible(dbProvider.get())) {
@@ -255,13 +262,26 @@
         if (!changeControl.canSubmit()) {
           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;
+        }
         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;
       }
+    } catch (ResourceConflictException e) {
+      return BLOCKED_TOPIC_TOOLTIP;
+    } catch (OrmException e) {
+      log.error("Error checking if change is submittable", e);
+      throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
     return null;
   }
@@ -274,12 +294,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 {
+      checkSubmitRule(cd, cd.currentPatchSet(), false);
+    } 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,13 +312,21 @@
         .setTitle("")
         .setVisible(false);
     }
+
+    Boolean enabled;
+    try {
+      enabled = cd.isMergeable();
+    } catch (OrmException e) {
+      throw new OrmRuntimeException("Could not determine mergeability", e);
+    }
+
+    List<ChangeData> changesByTopic = null;
     if (submitWholeTopic && !Strings.isNullOrEmpty(topic)) {
-      List<ChangeData> changesByTopic = null;
-      try {
-        changesByTopic = queryProvider.get().byTopicOpen(topic);
-      } catch (OrmException e) {
-        throw new OrmRuntimeException(e);
-      }
+      changesByTopic = getChangesByTopic(topic);
+    }
+    if (submitWholeTopic
+        && !Strings.isNullOrEmpty(topic)
+        && changesByTopic.size() > 1) {
       Map<String, String> params = ImmutableMap.of(
           "topicSize", String.valueOf(changesByTopic.size()));
       String topicProblems = problemsForSubmittingChanges(changesByTopic,
@@ -311,7 +343,7 @@
           .setTitle(Strings.emptyToNull(
               submitTopicTooltip.replace(params)))
           .setVisible(true)
-          .setEnabled(true);
+          .setEnabled(Boolean.TRUE.equals(enabled));
       }
     } else {
       RevId revId = resource.getPatchSet().getRevision();
@@ -322,7 +354,8 @@
       return new UiAction.Description()
         .setLabel(label)
         .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
-        .setVisible(true);
+        .setVisible(true)
+        .setEnabled(Boolean.TRUE.equals(enabled));
     }
   }
 
@@ -345,9 +378,10 @@
         .orNull();
   }
 
-  private Change submitToDatabase(ReviewDb db, Change.Id changeId,
-      final Timestamp timestamp) throws OrmException {
-    return db.changes().atomicUpdate(changeId,
+  private Change submitToDatabase(final ReviewDb db, final Change.Id changeId,
+      final Timestamp timestamp) throws OrmException,
+      ResourceConflictException {
+    Change ret = db.changes().atomicUpdate(changeId,
       new AtomicUpdate<Change>() {
         @Override
         public Change update(Change change) {
@@ -359,6 +393,12 @@
           return null;
         }
       });
+    if (ret != null) {
+      return ret;
+    } else {
+      throw new ResourceConflictException("change " + changeId + " is "
+          + status(db.changes().get(changeId)));
+    }
   }
 
   private Change submitThisChange(RevisionResource rsrc, IdentifiedUser caller,
@@ -376,13 +416,11 @@
 
     db.changes().beginTransaction(change.getId());
     try {
-      BatchMetaDataUpdate batch = approve(rsrc, update, caller, timestamp);
+      BatchMetaDataUpdate batch = approve(rsrc.getPatchSet().getId(),
+          cd.changeControl(), 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();
@@ -391,7 +429,7 @@
     return change;
   }
 
-  private Change submitWholeTopic(RevisionResource rsrc, IdentifiedUser caller,
+  private List<Change> submitWholeTopic(RevisionResource rsrc, IdentifiedUser caller,
       boolean force, String topic) throws ResourceConflictException, OrmException,
       IOException {
     Preconditions.checkNotNull(topic);
@@ -415,45 +453,45 @@
 
     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;
-        }
+        BatchMetaDataUpdate batch = approve(c.currentPatchSet().getId(),
+            c.changeControl(), update, caller, timestamp);
+        // Write update commit after all normalized label commits.
+        batch.write(update, new CommitBuilder());
+        submitToDatabase(db, c.getId(), timestamp);
       }
       db.commit();
     } finally {
       db.rollback();
     }
     List<Change.Id> ids = new ArrayList<>(changesByTopic.size());
+    List<Change> ret = new ArrayList<>(changesByTopic.size());
     for (ChangeData c : changesByTopic) {
       ids.add(c.getId());
+      ret.add(c.change());
     }
     indexer.indexAsync(ids).checkedGet();
-    return change;
+
+    return ret;
   }
 
-  public Change submit(RevisionResource rsrc, IdentifiedUser caller,
+  public List<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);
+      return Arrays.asList(submitThisChange(rsrc, caller, force));
     }
   }
 
-  private BatchMetaDataUpdate approve(RevisionResource rsrc,
+  private BatchMetaDataUpdate approve(PatchSet.Id psId, ChangeControl control,
       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)) {
+        approvalsUtil.byPatchSet(dbProvider.get(), control, psId)) {
       if (!byKey.containsKey(psa.getKey())) {
         byKey.put(psa.getKey(), psa);
       }
@@ -464,7 +502,7 @@
         || !submit.getAccountId().equals(caller.getAccountId())) {
       submit = new PatchSetApproval(
           new PatchSetApproval.Key(
-              rsrc.getPatchSet().getId(),
+              psId,
               caller.getAccountId(),
               LabelId.SUBMIT),
           (short) 1, TimeUtil.nowTs());
@@ -479,7 +517,7 @@
     // 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());
+        labelNormalizer.normalize(control, byKey.values());
 
     // TODO(dborowitz): Don't use a label in notedb; just check when status
     // change happened.
@@ -489,13 +527,13 @@
     dbProvider.get().patchSetApprovals().delete(normalized.deleted());
 
     try {
-      return saveToBatch(rsrc, update, normalized, timestamp);
+      return saveToBatch(control, update, normalized, timestamp);
     } catch (IOException e) {
       throw new OrmException(e);
     }
   }
 
-  private BatchMetaDataUpdate saveToBatch(RevisionResource rsrc,
+  private BatchMetaDataUpdate saveToBatch(ChangeControl ctl,
       ChangeUpdate callerUpdate, LabelNormalizer.Result normalized,
       Timestamp timestamp) throws IOException {
     Table<Account.Id, String, Optional<Short>> byUser = HashBasedTable.create();
@@ -507,7 +545,6 @@
       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())) {
@@ -654,6 +691,18 @@
     return new RevisionResource(changes.parse(target), rsrc.getPatchSet());
   }
 
+  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;
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..0ccf991
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.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, 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);
+      rw.sort(RevSort.TOPO);
+      Multimap<RevCommit, PatchSetData> byCommit = byCommit(rw, in);
+      if (byCommit.isEmpty()) {
+        return ImmutableList.of();
+      }
+
+      // 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.
+      for (RevCommit c : byCommit.keySet()) {
+        rw.markStart(c);
+      }
+
+      int expected = byCommit.keySet().size();
+      int found = 0;
+      RevCommit c;
+      List<PatchSetData> result = new ArrayList<>(expected);
+      while (found < expected && (c = rw.next()) != null) {
+        Collection<PatchSetData> psds = byCommit.get(c);
+        if (!psds.isEmpty()) {
+          found++;
+          for (PatchSetData psd : psds) {
+            result.add(psd);
+          }
+        }
+      }
+      return result;
+    }
+  }
+
+  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());
+  }
+
+  @AutoValue
+  static abstract 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/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index d7138b3..abcd441 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
@@ -44,6 +44,7 @@
   private final boolean enableRunAs;
   private final boolean userNameToLowerCase;
   private final boolean gitBasicAuth;
+  private final boolean useContributorAgreements;
   private final String loginUrl;
   private final String logoutUrl;
   private final String openIdSsoUrl;
@@ -75,6 +76,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);
 
 
@@ -194,6 +197,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:
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/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..8e9d7e8 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
+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 +39,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,8 +63,6 @@
 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;
@@ -113,7 +111,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 +122,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 +137,6 @@
 import org.eclipse.jgit.transport.PreUploadHook;
 
 import java.util.List;
-import java.util.Set;
 
 
 /** Starts global state with standard dependencies. */
@@ -195,21 +192,17 @@
     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);
@@ -261,6 +254,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);
@@ -297,6 +291,7 @@
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
+    factory(SubmoduleSectionParser.Factory.class);
 
     bind(AccountManager.class);
     bind(ChangeUserName.CurrentUser.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..4b1236b 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,10 +44,11 @@
 
   @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);
     }
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..fe81dfd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+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.change.ArchiveFormat;
+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.Locale;
+import java.util.Map;
+
+public class GetServerInfo implements RestReadView<ConfigResource> {
+  private final Config config;
+  private final AuthConfig authConfig;
+  private final Realm realm;
+  private final DownloadConfig downloadConfig;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
+  private final DynamicMap<DownloadCommand> downloadCommands;
+  private final AllProjectsName allProjectsName;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  public GetServerInfo(
+      @GerritServerConfig Config config,
+      AuthConfig authConfig,
+      Realm realm,
+      DownloadConfig downloadConfig,
+      DynamicMap<DownloadScheme> downloadSchemes,
+      DynamicMap<DownloadCommand> downloadCommands,
+      AllProjectsName allProjectsName,
+      AllUsersName allUsersName) {
+    this.config = config;
+    this.authConfig = authConfig;
+    this.realm = realm;
+    this.downloadConfig = downloadConfig;
+    this.downloadSchemes = downloadSchemes;
+    this.downloadCommands = downloadCommands;
+    this.allProjectsName = allProjectsName;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
+    ServerInfo info = new ServerInfo();
+    info.auth = new AuthInfo(authConfig, realm);
+    info.contactStore = getContactStoreInfo();
+    info.download =
+        new DownloadInfo(downloadConfig, downloadSchemes, downloadCommands);
+    info.gerrit = new GerritInfo(allProjectsName, allUsersName);
+    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 static Boolean toBoolean(boolean v) {
+    return v ? v : null;
+  }
+
+  public static class ServerInfo {
+    public AuthInfo auth;
+    public ContactStoreInfo contactStore;
+    public DownloadInfo download;
+    public GerritInfo gerrit;
+  }
+
+  public static class AuthInfo {
+    public AuthType authType;
+    public Boolean useContributorAgreements;
+    public List<Account.FieldName> editableAccountFields;
+
+    public AuthInfo(AuthConfig cfg, Realm realm) {
+      authType = cfg.getAuthType();
+      useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
+      editableAccountFields = new ArrayList<>(realm.getEditableFields());
+    }
+  }
+
+  public static class ContactStoreInfo {
+    public String url;
+  }
+
+  public static class DownloadInfo {
+    public Map<String, DownloadSchemeInfo> schemes;
+    public List<String> archives;
+
+    public DownloadInfo(DownloadConfig downloadConfig,
+        DynamicMap<DownloadScheme> downloadSchemes,
+        DynamicMap<DownloadCommand> downloadCommands) {
+      schemes = new HashMap<>();
+      for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
+        DownloadScheme scheme = e.getProvider().get();
+        if (scheme.isEnabled() && scheme.getUrl("${project}") != null) {
+          schemes.put(e.getExportName(),
+              new DownloadSchemeInfo(scheme, downloadCommands));
+        }
+      }
+      archives =
+          Lists.transform(new ArrayList<>(downloadConfig.getArchiveFormats()),
+              new Function<ArchiveFormat, String>() {
+                @Override
+                public String apply(ArchiveFormat archiveFormat) {
+                  return archiveFormat.name().toLowerCase(Locale.US);
+                }
+              });
+    }
+  }
+
+  public static class DownloadSchemeInfo {
+    public String url;
+    public Boolean isAuthRequired;
+    public Boolean isAuthSupported;
+    public Map<String, String> commands;
+
+    public DownloadSchemeInfo(DownloadScheme scheme,
+        DynamicMap<DownloadCommand> downloadCommands) {
+      url = scheme.getUrl("${project}");
+      isAuthRequired = toBoolean(scheme.isAuthRequired());
+      isAuthSupported = toBoolean(scheme.isAuthSupported());
+
+      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) {
+          commands.put(commandName, c);
+        }
+      }
+    }
+  }
+
+  public static class GerritInfo {
+    public String allProjects;
+    public String allUsers;
+
+    public GerritInfo(AllProjectsName allProjectsName, AllUsersName allUsersName) {
+      allProjects = allProjectsName.get();
+      allUsers = allUsersName.get();
+    }
+  }
+}
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..2d19f65 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,6 +32,8 @@
 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;
@@ -43,7 +44,7 @@
 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/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..006419b 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
@@ -41,7 +41,7 @@
   private final DynamicMap<Cache<?, ?>> cacheMap;
 
   public static enum OutputFormat {
-    LIST, TEXT_LIST;
+    LIST, TEXT_LIST
   }
 
   @Option(name = "--format", usage = "output format")
@@ -85,7 +85,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/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 64848ba..31fdc1e 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,6 +35,7 @@
     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);
   }
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..a437085 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,35 @@
 
   /**
    * 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);
+      log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
     } catch (ConfigInvalidException e) {
-      log.warn("Failed to load " + pluginConfigFile.getAbsolutePath(), e);
+      log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
     }
 
     return cfg;
@@ -271,15 +275,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 +294,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 +311,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 +328,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 +341,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 +358,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..2bbb731 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
@@ -52,7 +52,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/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/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..f6e08b8 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
@@ -28,11 +28,12 @@
 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.nio.file.Files;
+import java.nio.file.Path;
 import java.security.Security;
 
 /** Creates the {@link ContactStore} based on the configuration. */
@@ -46,7 +47,7 @@
   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();
     }
@@ -56,18 +57,18 @@
           + " 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);
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..fedc909 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) {
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..70107c2 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
@@ -389,7 +389,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..79c6593 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
@@ -183,8 +183,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);
     }
   }
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..4a61e3e 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;
   }
 
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/BranchOrderSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
index c447d31..f044342 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,22 +34,14 @@
     } 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));
+    int i = order.indexOf(RefNames.fullName(branch));
     if (0 <= i) {
       return order.subList(i + 1, order.size());
     } else {
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..a915a79 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(
@@ -74,15 +89,17 @@
       Repository repo = null;
       try {
         repo = repoManager.openRepository(p);
-        logGcConfiguration(p, repo);
+        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(
@@ -102,6 +119,27 @@
     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 +161,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/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
index 575ad52..cf18de0 100644
--- 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
@@ -1,16 +1,16 @@
-//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.
-//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.server.git;
 
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..717b393 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 {
@@ -366,28 +373,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 +401,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..96207fa 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
@@ -24,7 +24,6 @@
 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.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
@@ -814,6 +813,19 @@
             potentiallyStillSubmittable.add(commit);
             break;
 
+          case REVISION_GONE:
+            logDebug("Commit not found for change {}", c.getId());
+            ChangeMessage 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");
+            sendMergeFail(commit.notes(), msg, false);
+            break;
+
           default:
             setNew(commit,
                 message(c, "Unspecified merge failure: " + s.name()));
@@ -1032,7 +1044,7 @@
         }
 
         try {
-          MergedSender cm = mergedSenderFactory.create(changeControl(c));
+          MergedSender cm = mergedSenderFactory.create(c.getId());
           if (from != null) {
             cm.setFrom(from.getAccountId());
           }
@@ -1174,12 +1186,7 @@
       update.commit();
     }
 
-    CheckedFuture<?, IOException> indexFuture;
-    if (change != null) {
-      indexFuture = indexer.indexAsync(change.getId());
-    } else {
-      indexFuture = null;
-    }
+    indexer.index(db, change);
     final PatchSetApproval from = submitter;
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
@@ -1199,7 +1206,7 @@
         }
 
         try {
-          MergeFailSender cm = mergeFailSenderFactory.create(c);
+          MergeFailSender cm = mergeFailSenderFactory.create(c.getId());
           if (from != null) {
             cm.setFrom(from.getAccountId());
           }
@@ -1217,14 +1224,6 @@
       }
     }));
 
-    if (indexFuture != null) {
-      try {
-        indexFuture.checkedGet();
-      } catch (IOException e) {
-        logError("Failed to index new change message", e);
-      }
-    }
-
     if (submitter != null) {
       try {
         hooks.doMergeFailedHook(c,
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 295ad52..da38a58 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;
@@ -195,7 +198,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 +220,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 +240,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 +306,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 +318,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();
     }
   }
@@ -667,7 +677,7 @@
       }
 
       @Override
-      public void release() {
+      public void close() {
       }
     });
     return (ThreeWayMerger) m;
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/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 020c94a..99e61d6 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
@@ -305,6 +305,10 @@
     return notifySections.values();
   }
 
+  public void putNotifyConfig(String name, NotifyConfig nc) {
+    notifySections.put(name, nc);
+  }
+
   public Map<String, LabelType> getLabelSections() {
     return labelSections;
   }
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 f83bab6..464e267 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
@@ -44,6 +44,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;
@@ -641,8 +642,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(),
@@ -1468,6 +1468,10 @@
   private void selectNewAndReplacedChangesFromMagicBranch() {
     newChanges = Lists.newArrayList();
     final RevWalk walk = rp.getRevWalk();
+
+    Set<ObjectId> existing = changeRefsById().keySet();
+    GroupCollector groupCollector = new GroupCollector(refsById, db);
+
     walk.reset();
     walk.sort(RevSort.TOPO);
     walk.sort(RevSort.REVERSE, true);
@@ -1487,7 +1491,6 @@
             magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
       }
 
-      Set<ObjectId> existing = changeRefsById().keySet();
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<>();
       final int maxBatchChanges =
@@ -1497,7 +1500,17 @@
         if (c == null) {
           break;
         }
+        groupCollector.visit(c);
         if (existing.contains(c)) { // Commit is already tracked.
+          // TODO(dborowitz): Corner case where an existing commit might need a
+          // new group:
+          // 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.
           continue;
         }
 
@@ -1606,8 +1619,20 @@
       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);
+      }
+    } catch (OrmException e) {
+      log.error("Error collecting groups for changes", e);
+      reject(magicBranch.cmd, "internal server error");
+      return;
     }
   }
 
@@ -1647,6 +1672,7 @@
     final ReceiveCommand cmd;
     final ChangeInserter ins;
     boolean created;
+    Collection<String> groups;
 
     CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
         throws OrmException {
@@ -1691,7 +1717,7 @@
     }
 
     private void insertChange(ReviewDb db) throws OrmException, IOException {
-      final PatchSet ps = ins.getPatchSet();
+      final PatchSet ps = ins.setGroups(groups).getPatchSet();
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = commit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
@@ -1729,18 +1755,15 @@
       throws OrmException, IOException {
     Submit submit = submitProvider.get();
     RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
-    Change c;
+    List<Change> changes;
     try {
       // Force submit even if submit rule evaluation fails.
-      c = submit.submit(rsrc, currentUser, true);
+      changes = submit.submit(rsrc, currentUser, true);
     } catch (ResourceConflictException e) {
       throw new IOException(e);
     }
-    if (c == null) {
-      addError("Submitting change " + changeCtl.getChange().getChangeId()
-          + " failed.");
-    } else {
-      addMessage("");
+    addMessage("");
+    for (Change c : changes) {
       mergeQueue.merge(c.getDest());
       c = db.changes().get(c.getId());
       switch (c.getStatus()) {
@@ -1849,6 +1872,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) {
@@ -2024,6 +2048,7 @@
       newPatchSet.setCreatedOn(TimeUtil.nowTs());
       newPatchSet.setUploader(currentUser.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
+      newPatchSet.setGroups(groups);
       if (magicBranch != null && magicBranch.draft) {
         newPatchSet.setDraft(true);
       }
@@ -2128,6 +2153,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) {
@@ -2210,7 +2238,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() {
@@ -2218,7 +2246,7 @@
           public void run() {
             try {
               ReplacePatchSetSender cm =
-                  replacePatchSetFactory.create(change);
+                  replacePatchSetFactory.create(change.getId());
               cm.setFrom(me);
               cm.setPatchSet(newPatchSet, info);
               cm.setChangeMessage(msg);
@@ -2239,7 +2267,6 @@
           }
         }));
       }
-      f.checkedGet();
 
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
           ObjectId.zeroId(), newCommit);
@@ -2372,9 +2399,12 @@
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
-      walk.markStart(walk.parseCommit(cmd.getNewId()));
+      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)) {
@@ -2598,12 +2628,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/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 3cf9fed..0f66da4 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
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+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;
@@ -61,6 +62,7 @@
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -93,17 +95,25 @@
   private final Set<Branch.NameKey> updatedSubscribers;
   private final Account account;
   private final ChangeHooks changeHooks;
+  private final SubmoduleSectionParser.Factory subSecParserFactory;
 
   @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) {
+  public SubmoduleOp(@Assisted Branch.NameKey destBranch,
+      @Assisted RevCommit mergeTip,
+      @Assisted RevWalk rw,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      SchemaFactory<ReviewDb> sf,
+      @Assisted Repository db,
+      @Assisted Project destProject,
+      @Assisted List<Change> submitted,
+      @Assisted Map<Change.Id,
+      CodeReviewCommit> commits,
+      @GerritPersonIdent PersonIdent myIdent,
+      GitRepositoryManager repoManager,
+      GitReferenceUpdated gitRefUpdated,
+      @Nullable @Assisted Account account,
+      ChangeHooks changeHooks,
+      SubmoduleSectionParser.Factory subSecParserFactory) {
     this.destBranch = destBranch;
     this.mergeTip = mergeTip;
     this.rw = rw;
@@ -118,6 +128,7 @@
     this.gitRefUpdated = gitRefUpdated;
     this.account = account;
     this.changeHooks = changeHooks;
+    this.subSecParserFactory = subSecParserFactory;
 
     updatedSubscribers = new HashSet<>();
   }
@@ -128,7 +139,7 @@
 
       updateSubmoduleSubscriptions();
       updateSuperProjects(destBranch, rw, mergeTip.getId().toObjectId(), null);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       throw new SubmoduleException("Cannot open database", e);
     } finally {
       if (schema != null) {
@@ -145,42 +156,47 @@
     }
 
     try {
-      final TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
-      if (tw != null
-          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) || FileMode.EXECUTABLE_FILE
-              .equals(tw.getRawMode(0)))) {
+      Set<SubmoduleSubscription> oldSubscriptions =
+          Sets.newHashSet(schema.submoduleSubscriptions()
+              .bySuperProject(destBranch));
 
+      Set<SubmoduleSubscription> newSubscriptions;
+      TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
+      if (tw != null
+          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) ||
+              FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
         BlobBasedConfig bbc =
             new BlobBasedConfig(null, db, mergeTip, GIT_MODULES);
 
-        final String thisServer = new URI(urlProvider.get()).getHost();
+        String thisServer = new URI(urlProvider.get()).getHost();
 
-        final Branch.NameKey target =
+        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();
+        newSubscriptions = subSecParserFactory.create(bbc, thisServer, target)
+            .parseAllSections();
+      } else {
+        newSubscriptions = Collections.emptySet();
+      }
 
-        final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-        for (SubmoduleSubscription s : newSubscriptions) {
-          if (oldSubscriptions.contains(s)) {
-            alreadySubscribeds.add(s);
-          }
+      Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
+      for (SubmoduleSubscription s : newSubscriptions) {
+        if (oldSubscriptions.contains(s)) {
+          alreadySubscribeds.add(s);
         }
+      }
 
-        oldSubscriptions.removeAll(newSubscriptions);
-        newSubscriptions.removeAll(alreadySubscribeds);
+      oldSubscriptions.removeAll(newSubscriptions);
+      newSubscriptions.removeAll(alreadySubscribeds);
 
-        if (!oldSubscriptions.isEmpty()) {
-          schema.submoduleSubscriptions().delete(oldSubscriptions);
-        }
+      if (!oldSubscriptions.isEmpty()) {
+        schema.submoduleSubscriptions().delete(oldSubscriptions);
+      }
+      if (!newSubscriptions.isEmpty()) {
         schema.submoduleSubscriptions().insert(newSubscriptions);
       }
+
     } catch (OrmException e) {
       logAndThrowSubmoduleException(
           "Database problem at update of subscriptions table from "
@@ -201,7 +217,8 @@
   }
 
   private void updateSuperProjects(final Branch.NameKey updatedBranch, RevWalk myRw,
-      final ObjectId mergedCommit, final String msg) throws SubmoduleException {
+      final ObjectId mergedCommit, final String msg) throws SubmoduleException,
+      IOException {
     try {
       final List<SubmoduleSubscription> subscribers =
           schema.submoduleSubscriptions().bySubmodule(updatedBranch).toList();
@@ -227,6 +244,7 @@
                 && (c.getStatusCode() == CommitMergeStatus.CLEAN_MERGE
                     || c.getStatusCode() == CommitMergeStatus.CLEAN_PICK
                     || c.getStatusCode() == CommitMergeStatus.CLEAN_REBASE)) {
+              myRw.parseBody(c);
               sb.append("\n")
                 .append(c.getFullMessage());
             }
@@ -278,8 +296,7 @@
       throws SubmoduleException {
     PersonIdent author = null;
 
-    final StringBuilder msgbuf = new StringBuilder();
-    msgbuf.append("Updated " + subscriber.getParentKey().get());
+    final StringBuilder msgbuf = new StringBuilder("Updated git submodules\n");
     Repository pdb = null;
     RevWalk recRw = null;
 
@@ -351,6 +368,7 @@
       commit.setCommitter(myIdent);
       commit.setMessage(msgbuf.toString());
       oi.insert(commit);
+      oi.flush();
 
       ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
 
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..37df726 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;
@@ -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/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/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 60af3003..2d229a9 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
@@ -27,6 +27,7 @@
 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;
@@ -186,6 +187,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()));
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..dd981ad 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
@@ -20,7 +20,7 @@
 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;
@@ -89,7 +89,7 @@
                     .getSubmitter(n).getAccountId());
             PatchSet newPatchSet =
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.getPatchsetId(), n.change(), uploader,
+                    n.change(), n.getPatchsetId(), uploader,
                     mergeTip.getCurrentTip(), args.mergeUtil,
                     args.serverIdent.get(), false, ValidatePolicy.NONE);
 
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..ffe351b 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;
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..f3fb2a9 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,6 +20,7 @@
 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;
@@ -33,7 +34,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;
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..134d31c 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
@@ -45,8 +45,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 +78,7 @@
     }
   }
 
+  private final Provider<IdentifiedUser> self;
   private final AccountManager accountManager;
   private final AuthType authType;
   private final AccountsCollection accounts;
@@ -85,7 +89,8 @@
   private final AuditService auditService;
 
   @Inject
-  AddMembers(AccountManager accountManager,
+  AddMembers(Provider<IdentifiedUser> self,
+      AccountManager accountManager,
       AuthConfig authConfig,
       AccountsCollection accounts,
       AccountResolver accountResolver,
@@ -93,6 +98,7 @@
       AccountLoader.Factory infoFactory,
       Provider<ReviewDb> db,
       AuditService auditService) {
+    this.self = self;
     this.accountManager = accountManager;
     this.auditService = auditService;
     this.authType = authConfig.getAuthType();
@@ -114,11 +120,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 +132,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 +164,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 +202,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 {
     }
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/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/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/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/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 2993739..ebbc951 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
@@ -44,6 +44,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -54,6 +55,10 @@
  * {@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 {
   /** Legacy change ID. */
@@ -141,18 +146,26 @@
         }
       };
 
+  @Deprecated
   /** Topic, a short annotation on the branch. */
-  public static final FieldDef<ChangeData, String> TOPIC =
+  public static final FieldDef<ChangeData, String> LEGACY_TOPIC =
       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);
+        }
+      };
+
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> TOPIC =
+      new FieldDef.Single<ChangeData, String>(
+          "topic3", FieldType.PREFIX, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getTopic(input);
         }
       };
 
@@ -253,7 +266,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) {
@@ -274,7 +287,7 @@
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
           Set<String> revisions = Sets.newHashSet();
-          for (PatchSet ps : input.patches()) {
+          for (PatchSet ps : input.patchSets()) {
             if (ps.getRevision() != null) {
               revisions.add(ps.getRevision().get());
             }
@@ -293,7 +306,7 @@
           try {
             List<FooterLine> footers = input.commitFooters();
             if (footers == null) {
-              return null;
+              return ImmutableSet.of();
             }
             return Sets.newHashSet(
                 args.trackingFooters.extract(footers).values());
@@ -498,6 +511,71 @@
         }
       };
 
+  /** 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();
+
+  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..b6b702c 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
@@ -38,7 +38,7 @@
         ChangeField.PROJECT,
         ChangeField.PROJECTS,
         ChangeField.REF,
-        ChangeField.TOPIC,
+        ChangeField.LEGACY_TOPIC,
         ChangeField.UPDATED,
         ChangeField.FILE_PART,
         ChangeField.PATH,
@@ -68,7 +68,7 @@
       ChangeField.PROJECT,
       ChangeField.PROJECTS,
       ChangeField.REF,
-      ChangeField.TOPIC,
+      ChangeField.LEGACY_TOPIC,
       ChangeField.UPDATED,
       ChangeField.FILE_PART,
       ChangeField.PATH,
@@ -88,6 +88,7 @@
       ChangeField.DELTA,
       ChangeField.HASHTAG);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V14 = schema(
       ChangeField.LEGACY_ID,
       ChangeField.ID,
@@ -95,6 +96,62 @@
       ChangeField.PROJECT,
       ChangeField.PROJECTS,
       ChangeField.REF,
+      ChangeField.LEGACY_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.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      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_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.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY);
+
+  static final Schema<ChangeData> V16 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
       ChangeField.TOPIC,
       ChangeField.UPDATED,
       ChangeField.FILE_PART,
@@ -113,7 +170,67 @@
       ChangeField.ADDED,
       ChangeField.DELETED,
       ChangeField.DELTA,
-      ChangeField.HASHTAG);
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY);
+
+  static final Schema<ChangeData> V17 = 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.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET);
+
+  static final Schema<ChangeData> V18 = 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.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP);
 
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
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..08f4748 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>
@@ -28,13 +30,30 @@
 @AutoValue
 public abstract class IndexConfig {
   public static IndexConfig createDefault() {
-    return create(Integer.MAX_VALUE);
+    return create(0, 0);
   }
 
-  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));
+  }
+
+  public static IndexConfig create(int maxLimit, int maxPages) {
+    return new AutoValue_IndexConfig(
+        checkLimit(maxLimit, "maxLimit"),
+        checkLimit(maxPages, "maxPages"));
+  }
+
+  private static int checkLimit(int limit, String name) {
+    if (limit == 0) {
+      return Integer.MAX_VALUE;
+    }
+    checkArgument(limit > 0, "%s must be positive: %s", name, limit);
+    return limit;
   }
 
   public abstract int maxLimit();
+
+  public abstract int maxPages();
 }
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..689c596 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,8 +402,9 @@
       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 {
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..8147cff 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;
   }
@@ -175,7 +177,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/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/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/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/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/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..43c232b 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,8 +160,7 @@
     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) {
@@ -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..149325d9 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,11 @@
 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.Map;
 
 /**
  * Utility functions to parse PatchLineComments out of a note byte array and
@@ -86,8 +90,7 @@
 
   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);
@@ -99,20 +102,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 +149,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)
@@ -449,7 +442,7 @@
     PatchLineComment first = comments.get(0);
 
     short side = first.getSide();
-    PatchSet.Id psId = getCommentPsId(first);
+    PatchSet.Id psId = PatchLineCommentsUtil.getCommentPsId(first);
     appendHeaderField(writer, side == 0
         ? BASE_PATCH_SET
         : PATCH_SET,
@@ -459,7 +452,7 @@
     String currentFilename = null;
 
     for (PatchLineComment c : comments) {
-      PatchSet.Id currentPsId = getCommentPsId(c);
+      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 "
@@ -524,19 +517,47 @@
     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/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 40a51aa..b8caa62 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
@@ -87,6 +87,7 @@
   private final ExecutorService diffExecutor;
   private final long timeoutMillis;
 
+
   @Inject
   PatchListLoader(GitRepositoryManager mgr,
       PatchListCache plc,
@@ -337,7 +338,7 @@
         }
 
         @Override
-        public void release() {
+        public void close() {
         }
       });
 
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..5b70730 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
@@ -177,7 +177,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 +214,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/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/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..0f4aa6c 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
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..8da8cc1 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,7 +51,7 @@
   @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();
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..6b84c21 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;
   }
 
@@ -168,7 +170,7 @@
 
   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..2887a00 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
@@ -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..28d57b2 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 {
@@ -229,45 +232,11 @@
   }
 
   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);
   }
 
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/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index ac40df0..cb94063 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 + "\"");
     }
@@ -151,7 +150,7 @@
           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:
@@ -174,7 +173,11 @@
           }
         }
 
-        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;
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..0abffde 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,181 @@
 
     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);
+      Repository repo = repoManager.createRepository(nameKey);
+      try {
+        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);
+          }
+        }
+
+        RefUpdate u = repo.updateRef(Constants.HEAD);
+        u.disableRefLog();
+        u.link(head);
+
+        createProjectConfig(args);
+
+        if (!args.permissionsOnly
+            && args.createEmptyCommit) {
+          createEmptyCommits(repo, nameKey, args.branch);
+        }
+
+        return projectCache.get(nameKey).getProject();
+      } finally {
+        repo.close();
+      }
+    } 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 (IllegalStateException err) {
+      try {
+        Repository repo = repoManager.openRepository(nameKey);
+        try {
+          if (repo.getObjectDatabase().exists()) {
+            throw new ResourceConflictException("project \"" + nameKey + "\" exists");
+          }
+          throw err;
+        } finally {
+          repo.close();
+        }
+      } catch (IOException ioErr) {
+        String msg = "Cannot create " + nameKey;
+        log.error(msg, err);
+        throw ioErr;
+      }
+    } 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/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 4aba333..202dc0e 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;
@@ -113,7 +114,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());
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..fc34917 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
@@ -165,8 +165,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/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/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index a8eda97..8195c2a 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,163 @@
   @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);
+      addRef(db, refs, Constants.HEAD);
+      addRef(db, refs, RefNames.REFS_CONFIG);
+      addRef(db, refs, RefNames.REFS_USERS_DEFAULT);
     } 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 static void addRef(Repository db, List<Ref> refs, String name)
+      throws IOException {
+    Ref ref = db.getRef(name);
+    if (ref != null) {
+      refs.add(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 +268,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..2d8757e 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;
@@ -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<>();
 
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 689920b..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 {
-        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);
-          }
-        }
-
-        final RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-
-        createProjectConfig();
-
-        if (!createProjectArgs.permissionsOnly
-            && createProjectArgs.createEmptyCommit) {
-          createEmptyCommits(repo, nameKey, createProjectArgs.branch);
-        }
-
-        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/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 558b572..10b64a0 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;
@@ -205,10 +205,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);
@@ -488,25 +488,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/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..e507de7 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;
@@ -364,18 +365,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..07c0162 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;
@@ -43,9 +44,9 @@
 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,10 +72,7 @@
     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 {
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..b3c6aeb 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
@@ -370,9 +370,6 @@
       } catch (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/VariablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
index d6d0f9c..e298e5f 100644
--- 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
@@ -81,8 +81,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     if (getClass() == other.getClass()) {
       final VariablePredicate<?> v = (VariablePredicate<?>) other;
       return getName().equals(v.getName())
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
index 48f3898..981051b 100644
--- 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
@@ -45,8 +45,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     if (getClass() == other.getClass()) {
       final WildPatternPredicate<?> p = (WildPatternPredicate<?>) other;
       return getOperator().equals(p.getOperator());
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
index 1053d92..d167860 100644
--- 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
@@ -29,7 +29,7 @@
           new InvalidProvider<InternalChangeQuery>(),
           new InvalidProvider<ChangeQueryRewriter>(),
           null, null, null, null, null, null, null, null, null, null, null,
-          null, null, null, null, null, null, null, null));
+          null, null, null, null, null, null, null, null, null));
 
   private static final QueryRewriter.Definition<ChangeData, BasicChangeRewrites> mydef =
       new QueryRewriter.Definition<>(BasicChangeRewrites.class, BUILDER);
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..5c023e2 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
@@ -119,7 +119,7 @@
   public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes)
       throws OrmException {
     for (ChangeData cd : changes) {
-      cd.patches();
+      cd.patchSets();
     }
   }
 
@@ -127,7 +127,7 @@
       throws OrmException {
     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);
       }
     }
@@ -172,12 +172,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 +205,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<>();
@@ -450,6 +451,10 @@
     return change;
   }
 
+  public void setChange(Change c) {
+    change = c;
+  }
+
   public Change reloadChange() throws OrmException {
     change = db.changes().get(legacyId);
     return change;
@@ -468,7 +473,7 @@
       if (c == null) {
         return null;
       }
-      for (PatchSet p : patches()) {
+      for (PatchSet p : patchSets()) {
         if (p.getId().equals(c.currentPatchSetId())) {
           currentPatchSet = p;
           return p;
@@ -536,23 +541,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 +612,7 @@
     return submitRecords;
   }
 
-  public void setMergeable(boolean mergeable) {
+  public void setMergeable(Boolean mergeable) {
     this.mergeable = mergeable;
   }
 
@@ -619,9 +629,7 @@
         if (ps == null || !changeControl().isPatchVisible(ps, db)) {
           return null;
         }
-        Repository repo = null;
-        try {
-          repo = repoManager.openRepository(c.getProject());
+        try (Repository repo = repoManager.openRepository(c.getProject())) {
           Ref ref = repo.getRef(c.getDest().get());
           SubmitTypeRecord rec = new SubmitRuleEvaluator(this)
               .getSubmitType();
@@ -637,10 +645,6 @@
               ref, rec.type, mergeStrategy, c.getDest(), repo, db);
         } catch (IOException e) {
           throw new OrmException(e);
-        } finally {
-          if (repo != null) {
-            repo.close();
-          }
         }
       }
     }
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..e64b53f 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,13 @@
 import com.google.inject.ProvisionException;
 import com.google.inject.util.Providers;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 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,6 +94,7 @@
   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";
@@ -107,6 +114,7 @@
   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_REVIEWER = "reviewer";
   public static final String FIELD_REVIEWERIN = "reviewerin";
@@ -138,15 +146,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 +174,7 @@
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
+        AllUsersNameProvider allUsersName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
@@ -177,10 +187,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 +208,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 +231,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 +248,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 +285,10 @@
         throw new QueryParseException(NotSignedInException.MESSAGE, e);
       }
     }
+
+    Schema<ChangeData> getSchema() {
+      return index != null ? index.getSchema() : null;
+    }
   }
 
   private final Arguments args;
@@ -339,8 +356,7 @@
 
   @Operator
   public Predicate<ChangeData> comment(String value) {
-    ChangeIndex index = args.indexes.getSearchIndex();
-    return new CommentPredicate(index, value);
+    return new CommentPredicate(args.index, value);
   }
 
   @Operator
@@ -396,7 +412,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
+      return new IsMergeablePredicate(args.getSchema(), args.fillArgs);
     }
 
     try {
@@ -426,8 +442,9 @@
 
   @Operator
   public Predicate<ChangeData> project(String name) {
-    if (name.startsWith("^"))
+    if (name.startsWith("^")) {
       return new RegexProjectPredicate(name);
+    }
     return new ProjectPredicate(name);
   }
 
@@ -444,15 +461,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 +474,17 @@
 
   @Operator
   public Predicate<ChangeData> topic(String name) {
-    if (name.startsWith("^"))
-      return new RegexTopicPredicate(name);
-    return new TopicPredicate(name);
+    if (name.startsWith("^")) {
+      return new RegexTopicPredicate(args.getSchema(), name);
+    }
+    return new TopicPredicate(args.getSchema(), name);
   }
 
   @Operator
   public Predicate<ChangeData> ref(String ref) {
-    if (ref.startsWith("^"))
+    if (ref.startsWith("^")) {
       return new RegexRefPredicate(ref);
+    }
     return new RefPredicate(ref);
   }
 
@@ -553,8 +567,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 +672,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 +759,46 @@
     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);
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
@@ -834,9 +890,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/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 14daa4d..3dd7c61 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
@@ -32,7 +32,7 @@
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
-    for (PatchSet p : object.patches()) {
+    for (PatchSet p : object.patchSets()) {
       if (p.getRevision() != null && p.getRevision().get() != null) {
         final ObjectId id = ObjectId.fromString(p.getRevision().get());
         if (abbrevId.prefixCompare(id) == 0) {
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/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..15b1d8c 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,26 @@
 package com.google.gerrit.server.query.change;
 
 import static com.google.gerrit.server.query.Predicate.and;
+import static com.google.gerrit.server.query.Predicate.or;
 import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
 
+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.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+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.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -52,15 +62,18 @@
     return new ChangeStatusPredicate(status);
   }
 
-  private static Predicate<ChangeData> topic(String topic) {
-    return new TopicPredicate(topic);
+  private static Predicate<ChangeData> commit(AbbreviatedObjectId id) {
+    return new CommitPredicate(id);
   }
 
   private final QueryProcessor qp;
+  private final IndexCollection indexes;
 
   @Inject
-  InternalChangeQuery(QueryProcessor queryProcessor) {
+  InternalChangeQuery(QueryProcessor queryProcessor,
+      IndexCollection indexes) {
     qp = queryProcessor.enforceVisibility(false);
+    this.indexes = indexes;
   }
 
   public InternalChangeQuery setLimit(int n) {
@@ -73,6 +86,10 @@
     return this;
   }
 
+  private Predicate<ChangeData> topic(String topic) {
+    return new TopicPredicate(schema(indexes), topic);
+  }
+
   public List<ChangeData> byKey(Change.Key key) throws OrmException {
     return byKeyPrefix(key.get());
   }
@@ -123,6 +140,23 @@
     return query(and(topic(topic), open()));
   }
 
+  public List<ChangeData> byCommitPrefix(String prefix) throws OrmException {
+    return query(commit(AbbreviatedObjectId.fromString(prefix)));
+  }
+
+  public List<ChangeData> byCommit(ObjectId id) throws OrmException {
+    return query(commit(AbbreviatedObjectId.fromObjectId(id)));
+  }
+
+  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 {
     try {
       return qp.queryChanges(p).changes();
@@ -130,4 +164,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/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/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..7d5f1dc 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(TopicPredicate.topicField(schema), re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
deleted file mode 100644
index 0947fae..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-/**
- * Predicate which creates Repository, RevWalk objects and properly
- * closes them. Git based operators should extend this predicate.
- *
- */
-public abstract class RevWalkPredicate extends OperatorPredicate<ChangeData> {
-  private static final Logger log =
-      LoggerFactory.getLogger(RevWalkPredicate.class);
-
-  public static class Arguments {
-    public final PatchSet patchSet;
-    public final RevId revision;
-    public final AnyObjectId objectId;
-    public final Change change;
-    public final Project.NameKey projectName;
-
-    public Arguments(PatchSet patchSet,
-        RevId revision,
-        AnyObjectId objectId,
-        Change change,
-        Project.NameKey projectName) {
-      this.patchSet = patchSet;
-      this.revision = revision;
-      this.objectId = objectId;
-      this.change = change;
-      this.projectName = projectName;
-    }
-  }
-
-  public final Provider<ReviewDb> db;
-  public final GitRepositoryManager repoManager;
-
-  public RevWalkPredicate(Provider<ReviewDb> db,
-      GitRepositoryManager repoManager, String operator, String ref) {
-    super(operator, ref);
-    this.db = db;
-    this.repoManager = repoManager;
-  }
-
-  @Override
-  public boolean match(ChangeData object) throws OrmException {
-    final PatchSet patchSet = object.currentPatchSet();
-    if (patchSet == null) {
-      return false;
-    }
-
-    final RevId revision = patchSet.getRevision();
-    if (revision == null) {
-      return false;
-    }
-
-    final AnyObjectId objectId = ObjectId.fromString(revision.get());
-    if (objectId == null) {
-      return false;
-    }
-
-    Change change = object.change();
-    if (change == null) {
-      return false;
-    }
-
-    final Project.NameKey projectName = change.getProject();
-    if (projectName == null) {
-      return false;
-    }
-
-    Arguments args = new Arguments(patchSet, revision, objectId, change, projectName);
-
-    try (Repository repo = repoManager.openRepository(projectName);
-        RevWalk rw = new RevWalk(repo)) {
-      return match(repo, rw, args);
-    } catch (RepositoryNotFoundException e) {
-      log.error("Repository \"" + projectName.get() + "\" unknown.", e);
-    } catch (IOException e) {
-      log.error(projectName.get() + " cannot be read as a repository", e);
-    }
-    return false;
-  }
-
-  public abstract boolean match(Repository repo, RevWalk rw, Arguments args);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index 07a6714..7196c9f 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/TopicPredicate.java
@@ -18,12 +18,26 @@
 
 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 TopicPredicate extends IndexPredicate<ChangeData> {
-  TopicPredicate(String topic) {
-    super(ChangeField.TOPIC, topic);
+  @SuppressWarnings("deprecation")
+  static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_TOPIC;
+    }
+    FieldDef<ChangeData, ?> f = schema.getFields().get(TOPIC.getName());
+    if (f != null) {
+      return f;
+    }
+    return schema.getFields().get(ChangeField.LEGACY_TOPIC.getName());
+  }
+
+  TopicPredicate(Schema<ChangeData> schema, String topic) {
+    super(topicField(schema), topic);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AclUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AclUtil.java
new file mode 100644
index 0000000..ced8cd0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AclUtil.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.server.git.ProjectConfig;
+
+public class AclUtil {
+  public static void grant(ProjectConfig config, AccessSection section,
+      String permission, GroupReference... groupList) {
+    grant(config, section, permission, false, groupList);
+  }
+
+  public static void grant(ProjectConfig config, AccessSection section,
+      String permission, boolean force, GroupReference... groupList) {
+    Permission p = section.getPermission(permission, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setForce(force);
+        p.add(r);
+      }
+    }
+  }
+
+  public static void grant(ProjectConfig config,
+      AccessSection section, LabelType type,
+      int min, int max, GroupReference... groupList) {
+    String name = Permission.LABEL + type.getName();
+    Permission p = section.getPermission(name, true);
+    for (GroupReference group : groupList) {
+      if (group != null) {
+        PermissionRule r = rule(config, group);
+        r.setRange(min, max);
+        p.add(r);
+      }
+    }
+  }
+
+  public static PermissionRule rule(ProjectConfig config, GroupReference group) {
+    return new PermissionRule(config.resolve(group));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 1eefbf9..1198176 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;
@@ -183,41 +185,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 +193,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/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..fe5b992 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
@@ -131,9 +131,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..c1be4f8 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_108> C = Schema_108.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..c809af4 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
@@ -58,14 +58,14 @@
           throw new ProvisionException("Schema not yet initialized."
               + "  Run init to initialize the schema:\n"
               + "$ java -jar gerrit.war init -d "
-              + site.site_path.getAbsolutePath());
+              + 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.getAbsolutePath() + " init -d "
-              + site.site_path.getAbsolutePath());
+              + "$ 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
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/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/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 8970425..195a3e0 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) {
@@ -91,8 +101,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();
@@ -106,12 +114,10 @@
               projectName = projectName.substring(0, //
                   projectName.length() - Constants.DOT_GIT_EXT.length());
             }
-
-            if (repoManager.list().contains(new Project.NameKey(projectName))) {
-              return new SubmoduleSubscription(
-                  superProjectBranch,
-                  new Branch.NameKey(new Project.NameKey(projectName), branch),
-                  path);
+            Project.NameKey projectKey = new Project.NameKey(projectName);
+            if (projectCache.get(projectKey) != null) {
+              return new SubmoduleSubscription(superProjectBranch,
+                  new Branch.NameKey(projectKey, branch), path);
             }
           }
         }
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/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/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index f5dca09..74ab572 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
@@ -33,7 +33,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;
@@ -112,7 +113,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"
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..027d043 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
@@ -22,7 +22,7 @@
 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;
@@ -185,8 +185,8 @@
 
   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..a437477 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
@@ -106,7 +106,8 @@
   public Config config;
 
   @ConfigSuite.Config
-  public static @GerritServerConfig Config noteDbEnabled() {
+  @GerritServerConfig
+  public static Config noteDbEnabled() {
     return NotesMigration.allEnabledConfig();
   }
 
@@ -240,24 +241,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);
@@ -371,7 +371,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();
   }
@@ -422,7 +422,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 +443,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
index 358620f..8caba88 100644
--- 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
@@ -25,6 +25,7 @@
 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.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -36,10 +37,10 @@
 import com.google.gerrit.testutil.FakeAccountByEmailCache;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
 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;
@@ -52,12 +53,13 @@
 import java.util.List;
 
 public class ConsistencyCheckerTest {
+  private LifecycleManager lifecycle;
   private InMemoryDatabase schemaFactory;
   private ReviewDb db;
   private InMemoryRepositoryManager repoManager;
   private ConsistencyChecker checker;
 
-  private TestRepository<InMemoryRepository> repo;
+  private TestRepository<Repo> repo;
   private Project.NameKey project;
   private Account.Id userId;
   private RevCommit tip;
@@ -65,7 +67,9 @@
   @Before
   public void setUp() throws Exception {
     FakeAccountByEmailCache accountCache = new FakeAccountByEmailCache();
-    schemaFactory = InMemoryDatabase.newDatabase();
+    lifecycle = new LifecycleManager();
+    schemaFactory = InMemoryDatabase.newDatabase(lifecycle);
+    lifecycle.start();
     schemaFactory.create();
     db = schemaFactory.open();
     repoManager = new InMemoryRepositoryManager();
@@ -88,6 +92,9 @@
     if (db != null) {
       db.close();
     }
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
     if (schemaFactory != null) {
       InMemoryDatabase.drop(schemaFactory);
     }
@@ -335,7 +342,8 @@
 
   @Test
   public void missingDestRef() throws Exception {
-    RefUpdate ru = repo.getRepository().updateRef("refs/heads/master");
+    String ref = "refs/heads/master";
+    RefUpdate ru = repo.getRepository().updateRef(ref);
     ru.setForceUpdate(true);
     assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
     Change c = insertChange();
@@ -344,7 +352,7 @@
     updatePatchSetRef(ps);
     db.patchSets().insert(singleton(ps));
 
-    assertProblems(c, "Destination ref not found (may be new branch): master");
+    assertProblems(c, "Destination ref not found (may be new branch): " + ref);
   }
 
   @Test
@@ -356,7 +364,8 @@
 
     assertProblems(c,
         "Patch set 1 (" + rev + ") is not merged into destination ref"
-        + " master (" + tip.name() + "), but change status is MERGED");
+        + " refs/heads/master (" + tip.name()
+        + "), but change status is MERGED");
   }
 
   @Test
@@ -370,7 +379,8 @@
 
     assertProblems(c,
         "Patch set 1 (" + commit.name() + ") is merged into destination ref"
-        + " master (" + commit.name() + "), but change status is NEW");
+        + " refs/heads/master (" + commit.name()
+        + "), but change status is NEW");
   }
 
   @Test
@@ -387,7 +397,8 @@
     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");
+        + " 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");
 
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..7b7983c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java
@@ -0,0 +1,267 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.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 seriesOfChangesAtSameTimestamp() 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();
+  }
+
+  private ChangeData newChange(TestRepository<Repo> tr, ObjectId id)
+      throws Exception {
+    Project.NameKey project = tr.getRepository().getDescription().getProject();
+    Change c = TestChanges.newChange(project, userId);
+    ChangeData cd = ChangeData.createForTest(c.getId(), 1);
+    cd.setChange(c);
+    cd.currentPatchSet().setRevision(new RevId(id.name()));
+    cd.setPatchSets(ImmutableList.of(cd.currentPatchSet()));
+    return cd;
+  }
+
+  private PatchSet addPatchSet(ChangeData cd, ObjectId id) throws Exception {
+    TestChanges.incrementPatchSet(cd.change());
+    PatchSet ps = new PatchSet(cd.change().currentPatchSetId());
+    ps.setRevision(new RevId(id.name()));
+    List<PatchSet> patchSets = new ArrayList<>(cd.patchSets());
+    patchSets.add(ps);
+    cd.setPatchSets(patchSets);
+    return ps;
+  }
+
+  private TestRepository<Repo> newRepo(String name)
+      throws Exception {
+    return new TestRepository<>(
+        repoManager.createRepository(new Project.NameKey(name)));
+  }
+
+  private static PatchSetData patchSetData(ChangeData cd, RevCommit commit)
+      throws Exception {
+    return PatchSetData.create(cd, cd.currentPatchSet(), commit);
+  }
+
+  private static PatchSetData patchSetData(ChangeData cd, int psId,
+      RevCommit commit) throws Exception {
+    return PatchSetData.create(
+        cd, cd.patchSet(new PatchSet.Id(cd.getId(), psId)), commit);
+  }
+
+  private static void assertSorted(WalkSorter sorter, List<ChangeData> changes,
+      List<PatchSetData> expected) throws Exception {
+    for (List<ChangeData> list : permutations(changes)) {
+      assertThat(sorter.sort(list))
+          .containsExactlyElementsIn(expected).inOrder();
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/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..5533d53 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
@@ -25,88 +25,89 @@
 
 import org.junit.Test;
 
-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 {
   @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());
+      Files.createFile(root);
       try {
         new SitePaths(root);
         fail("Did not throw exception");
-      } catch (FileNotFoundException e) {
-        assertEquals("Not a directory: " + root.getPath(), e.getMessage());
+      } catch (NotDirectoryException e) {
+        // Expected.
       }
     } 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..181ba15 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();
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/QueryListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java
new file mode 100644
index 0000000..d022d3e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class QueryListTest extends TestCase {
+  public static final String Q_P = "project:foo";
+  public static final String Q_B = "branch:bar";
+  public static final String Q_COMPLEX = "branch:bar AND peers:'is:open\t'";
+
+  public static final String N_FOO = "foo";
+  public static final String N_BAR = "bar";
+
+  public static final String L_FOO = N_FOO + "\t" + Q_P + "\n";
+  public static final String L_BAR = N_BAR + "\t" + Q_B + "\n";
+  public static final String L_FOO_PROP = N_FOO + "   \t" + Q_P + "\n";
+  public static final String L_BAR_PROP = N_BAR + "   \t" + Q_B + "\n";
+  public static final String L_FOO_PAD_F = " " + N_FOO + "\t" + Q_P + "\n";
+  public static final String L_FOO_PAD_E = N_FOO + " \t" + Q_P + "\n";
+  public static final String L_BAR_PAD_F = N_BAR + "\t " + Q_B + "\n";
+  public static final String L_BAR_PAD_E = N_BAR + "\t" + Q_B + " \n";
+  public static final String L_COMPLEX = N_FOO + "\t" + Q_COMPLEX + "\t \n";
+  public static final String L_BAD = N_FOO + "\n";
+
+  public static final String HEADER = "# Name\tQuery\n";
+  public static final String C1 = "# A Simple Comment\n";
+  public static final String C2 = "# Comment with a tab\t and multi # # #\n";
+
+  public static final String F_SIMPLE = L_FOO + L_BAR;
+  public static final String F_PROPER = L_BAR_PROP + L_FOO_PROP; // alpha order
+  public static final String F_PAD_F = L_FOO_PAD_F + L_BAR_PAD_F;
+  public static final String F_PAD_E = L_FOO_PAD_E + L_BAR_PAD_E;
+
+  @Test
+  public void testParseSimple() throws Exception {
+    QueryList ql = QueryList.parse(F_SIMPLE, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseWHeader() throws Exception {
+    QueryList ql = QueryList.parse(HEADER + F_SIMPLE, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseWComments() throws Exception {
+    QueryList ql = QueryList.parse(C1 + F_SIMPLE + C2, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseFooComment() throws Exception {
+    QueryList ql = QueryList.parse("#" + L_FOO + L_BAR, null);
+    assertThat(ql.getQuery(N_FOO)).isNull();
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParsePaddedFronts() throws Exception {
+    QueryList ql = QueryList.parse(F_PAD_F, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParsePaddedEnds() throws Exception {
+    QueryList ql = QueryList.parse(F_PAD_E, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseComplex() throws Exception {
+    QueryList ql = QueryList.parse(L_COMPLEX, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_COMPLEX);
+  }
+
+  @Test(expected = IOException.class)
+  public void testParseBad() throws Exception {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    QueryList.parse(L_BAD, sink);
+  }
+
+  @Test
+  public void testAsText() throws Exception {
+    String expectedText = HEADER + "#\n" + F_PROPER;
+    QueryList ql = QueryList.parse(F_SIMPLE, null);
+    String asText = ql.asText();
+    assertThat(asText).isEqualTo(expectedText);
+
+    ql = QueryList.parse(asText, null);
+    asText = ql.asText();
+    assertThat(asText).isEqualTo(expectedText);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
deleted file mode 100644
index c2f3f6f..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ /dev/null
@@ -1,1017 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.capture;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.junit.Assert.assertEquals;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.ResultSet;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StandardKeyEncoder;
-import com.google.inject.Provider;
-
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.easymock.IMocksControl;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.dircache.DirCacheBuilder;
-import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
-  static {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
-  private static final String newLine = System.getProperty("line.separator");
-
-  private IMocksControl mockMaker;
-  private SchemaFactory<ReviewDb> schemaFactory;
-  private SubmoduleSubscriptionAccess subscriptions;
-  private ReviewDb schema;
-  private Provider<String> urlProvider;
-  private GitRepositoryManager repoManager;
-  private GitReferenceUpdated gitRefUpdated;
-  private ChangeHooks changeHooks;
-
-  @SuppressWarnings("unchecked")
-  @Override
-  @Before
-  public void setUp() throws Exception {
-    super.setUp();
-
-    mockMaker = EasyMock.createStrictControl();
-    schemaFactory = mockMaker.createMock(SchemaFactory.class);
-    schema = mockMaker.createMock(ReviewDb.class);
-    subscriptions = mockMaker.createMock(SubmoduleSubscriptionAccess.class);
-    urlProvider = mockMaker.createMock(Provider.class);
-    repoManager = mockMaker.createMock(GitRepositoryManager.class);
-    gitRefUpdated = mockMaker.createMock(GitReferenceUpdated.class);
-    changeHooks = mockMaker.createMock(ChangeHooks.class);
-  }
-
-  private void doReplay() {
-    mockMaker.replay();
-  }
-
-  private void doVerify() {
-    mockMaker.verify();
-  }
-
-  /**
-   * It tests Submodule.update in the scenario a merged commit is an empty one
-   * (it does not have a .gitmodules file) and the project the commit was merged
-   * is not a submodule of other project.
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testEmptyCommit() throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository realDb = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git git = new Git(realDb);
-
-      final RevCommit mergeTip = git.commit().setMessage("test").call();
-
-      final Branch.NameKey branchNameKey =
-          new Branch.NameKey(new Project.NameKey("test-project"), "test-branch");
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080");
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> emptySubscriptions =
-          new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
-      expect(subscriptions.bySubmodule(branchNameKey)).andReturn(
-          emptySubscriptions);
-
-      schema.close();
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(branchNameKey, mergeTip, new RevWalk(realDb), urlProvider,
-              schemaFactory, realDb, null, new ArrayList<Change>(), null, null,
-              null, null, null, null);
-
-      submoduleOp.update();
-
-      doVerify();
-    }
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project"</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = .
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. The row inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "a" on branch "refs/heads/master"</li>
-   * <li>path "a"</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionToDotBranchValue() throws Exception {
-    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", ".").toString(), "refs/heads/master");
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project"</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = refs/heads/master
-   * </pre>
-   *
-   * <p>
-   * It expects to insert a new row in subscriptions table. The row inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source" on branch "refs/heads/master"</li>
-   * <li>path "source"</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionToSameBranch() throws Exception {
-    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", "refs/heads/master").toString(),
-        "refs/heads/master");
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project"</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = refs/heads/test
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. The row inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source" on branch "refs/heads/test"</li>
-   * <li>path "source"</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionToDifferentBranch() throws Exception {
-    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", "refs/heads/test").toString(),
-        "refs/heads/test");
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source-a"]
-   *       path = source-a
-   *       url = http://localhost:8080/source-a
-   *       branch = .
-   *
-   *     [submodule "source-b"]
-   *       path = source-b
-   *       url = http://localhost:8080/source-b
-   *       branch = .
-   * </pre>
-   * <p>
-   * It expects to insert new rows in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
-   * <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionsWithDotBranchValue() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("source-a", "source-a",
-            "http://localhost:8080/source-a", ".");
-    sb.append(buildSubmoduleSection("source-b", "source-b",
-        "http://localhost:8080/source-b", "."));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
-
-    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
-        subscriptionsToInsert);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source-a"]
-   *       path = source-a
-   *       url = http://localhost:8080/source-a
-   *       branch = .
-   *
-   *     [submodule "source-b"]
-   *       path = source-b
-   *       url = http://localhost:8080/source-b
-   *       branch = refs/heads/master
-   * </pre>
-   * <p>
-   * It expects to insert new rows in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
-   * <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionsDotAndSameBranchValues() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("source-a", "source-a",
-            "http://localhost:8080/source-a", ".");
-    sb.append(buildSubmoduleSection("source-b", "source-b",
-        "http://localhost:8080/source-b", "refs/heads/master"));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
-
-    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
-        subscriptionsToInsert);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>no subscriptions existing to destination project</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   *
-   * <pre>
-   *     [submodule "source-a"]
-   *       path = source-a
-   *       url = http://localhost:8080/source-a
-   *       branch = refs/heads/test-a
-   *
-   *     [submodule "source-b"]
-   *       path = source-b
-   *       url = http://localhost:8080/source-b
-   *       branch = refs/heads/test-b
-   * </pre>
-   *
-   * <p>
-   * It expects to insert new rows in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source-a" on branch "refs/heads/test-a" with "source-a" path</li>
-   * <li>source "source-b" on branch "refs/heads/test-b" with "source-b" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testNewSubscriptionsSpecificBranchValues() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("source-a", "source-a",
-            "http://localhost:8080/source-a", "refs/heads/test-a");
-    sb.append(buildSubmoduleSection("source-b", "source-b",
-        "http://localhost:8080/source-b", "refs/heads/test-b"));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/test-a"), "source-a"));
-    subscriptionsToInsert
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/test-b"), "source-b"));
-
-    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
-        subscriptionsToInsert);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>one subscription existing to destination project/branch</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "source"]
-   *       path = source
-   *       url = http://localhost:8080/source
-   *       branch = refs/heads/master
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. The rows inserted
-   * specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "source" on branch "refs/heads/master" with "source" path</li>
-   * </ul>
-   * </p>
-   * <p>
-   * It also expects to remove the row in subscriptions table specifying another
-   * project/branch subscribed to merged branch. This one to be removed is:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "old-source" on branch "refs/heads/master" with "old-source"
-   * path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testSubscriptionsInsertOneRemoveOne() throws Exception {
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("source"), "refs/heads/master"),
-        "source"));
-
-    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
-    oldOnesToMergedBranch.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("old-source"),
-            "refs/heads/master"), "old-source"));
-
-    doOnlySubscriptionTableOperations(buildSubmoduleSection("source", "source",
-        "http://localhost:8080/source", "refs/heads/master").toString(),
-        mergedBranch, subscriptionsToInsert, oldOnesToMergedBranch);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering:
-   * <ul>
-   * <li>one subscription existing to destination project/branch with a source
-   * called old on refs/heads/master branch</li>
-   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
-   * <li>
-   * commit contains .gitmodules file with content</li>
-   * </ul>
-   *
-   * <pre>
-   *     [submodule "new"]
-   *       path = new
-   *       url = http://localhost:8080/new
-   *       branch = refs/heads/master
-   *
-   *     [submodule "old"]
-   *       path = old
-   *       url = http://localhost:8080/old
-   *       branch = refs/heads/master
-   * </pre>
-   * <p>
-   * It expects to insert a new row in subscriptions table. It should not remove
-   * any row. The rows inserted specifies:
-   * <ul>
-   * <li>target "dest-project" on branch "refs/heads/master"</li>
-   * <li>source "new" on branch "refs/heads/master" with "new" path</li>
-   * </ul>
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testSubscriptionAddedAndMantainPreviousOne() throws Exception {
-    final StringBuilder sb =
-        buildSubmoduleSection("new", "new", "http://localhost:8080/new",
-            "refs/heads/master");
-    sb.append(buildSubmoduleSection("old", "old", "http://localhost:8080/old",
-        "refs/heads/master"));
-
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    final SubmoduleSubscription old =
-        new SubmoduleSubscription(mergedBranch, new Branch.NameKey(new Project.NameKey(
-            "old"), "refs/heads/master"), "old");
-
-    List<SubmoduleSubscription> extractedsubscriptions = new ArrayList<>();
-    extractedsubscriptions.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("new"), "refs/heads/master"),
-        "new"));
-    extractedsubscriptions.add(old);
-
-    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
-    oldOnesToMergedBranch.add(old);
-
-    doOnlySubscriptionTableOperations(sb.toString(), mergedBranch,
-        extractedsubscriptions, oldOnesToMergedBranch);
-
-    doVerify();
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering an empty .gitmodules
-   * file is part of a commit to a destination project/branch having two sources
-   * subscribed.
-   * <p>
-   * It expects to remove the subscriptions to destination project/branch.
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testRemoveSubscriptions() throws Exception {
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> extractedsubscriptions = new ArrayList<>();
-    List<SubmoduleSubscription> oldOnesToMergedBranch = new ArrayList<>();
-    oldOnesToMergedBranch
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
-    oldOnesToMergedBranch
-        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
-            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
-
-    doOnlySubscriptionTableOperations("", mergedBranch, extractedsubscriptions,
-        oldOnesToMergedBranch);
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering no .gitmodules file
-   * in a merged commit to a destination project/branch that is a source one to
-   * one called "target-project".
-   * <p>
-   * It expects to update the git link called "source-project" to be in target
-   * repository.
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testOneSubscriberToUpdate() throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository sourceRepository = createWorkRepository();
-        Repository targetRepository = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git sourceGit = new Git(sourceRepository);
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git targetGit = new Git(targetRepository);
-
-      addRegularFileToIndex("file.txt", "test content", sourceRepository);
-
-      final RevCommit sourceMergeTip =
-          sourceGit.commit().setMessage("test").call();
-
-      final Branch.NameKey sourceBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("source-project"),
-              "refs/heads/master");
-
-      final CodeReviewCommit codeReviewCommit =
-          new CodeReviewCommit(sourceMergeTip.toObjectId());
-      final Change submittedChange = new Change(
-          new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
-          new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
-
-      final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
-      mergedCommits.put(submittedChange.getId(), codeReviewCommit);
-
-      final List<Change> submitted = new ArrayList<>();
-      submitted.add(submittedChange);
-
-      addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
-
-      targetGit.commit().setMessage("test").call();
-
-      final Branch.NameKey targetBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("target-project"),
-              sourceBranchNameKey.get());
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080");
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> subscribers =
-          new ListResultSet<>(Collections
-              .singletonList(new SubmoduleSubscription(targetBranchNameKey,
-                  sourceBranchNameKey, "source-project")));
-      expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
-          subscribers);
-
-      expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
-          .andReturn(targetRepository).anyTimes();
-
-      Capture<RefUpdate> ruCapture = new Capture<>();
-      gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
-          capture(ruCapture));
-      changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
-          anyObject(RefUpdate.class), EasyMock.<Account>isNull());
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> emptySubscriptions =
-          new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
-      expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
-          emptySubscriptions);
-
-      schema.close();
-
-      final PersonIdent myIdent =
-          new PersonIdent("test-user", "test-user@email.com");
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
-              sourceRepository), urlProvider, schemaFactory, sourceRepository,
-              new Project(sourceBranchNameKey.getParentKey()), submitted,
-              mergedCommits, myIdent, repoManager, gitRefUpdated, null,
-              changeHooks);
-
-      submoduleOp.update();
-
-      doVerify();
-      RefUpdate ru = ruCapture.getValue();
-      assertEquals(ru.getName(), targetBranchNameKey.get());
-    }
-  }
-
-  /**
-   * It tests SubmoduleOp.update in a scenario considering established circular
-   * reference in submodule_subscriptions table.
-   * <p>
-   * In the tested scenario there is no .gitmodules file in a merged commit to a
-   * destination project/branch that is a source one to one called
-   * "target-project".
-   * <p>
-   * submodule_subscriptions table will be incorrect due source appearing as a
-   * subscriber or target-project: according to database target-project has as
-   * source the source-project, and source-project has as source the
-   * target-project.
-   * <p>
-   * It expects to update the git link called "source-project" to be in target
-   * repository and ignoring the incorrect row in database establishing the
-   * circular reference.
-   * </p>
-   *
-   * @throws Exception If an exception occurs.
-   */
-  @Test
-  public void testAvoidingCircularReference() throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository sourceRepository = createWorkRepository();
-        Repository targetRepository = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git sourceGit = new Git(sourceRepository);
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git targetGit = new Git(targetRepository);
-
-      addRegularFileToIndex("file.txt", "test content", sourceRepository);
-
-      final RevCommit sourceMergeTip =
-          sourceGit.commit().setMessage("test").call();
-
-      final Branch.NameKey sourceBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("source-project"),
-              "refs/heads/master");
-
-      final CodeReviewCommit codeReviewCommit =
-          new CodeReviewCommit(sourceMergeTip.toObjectId());
-      final Change submittedChange = new Change(
-          new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
-          new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
-
-      final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
-      mergedCommits.put(submittedChange.getId(), codeReviewCommit);
-
-      final List<Change> submitted = new ArrayList<>();
-      submitted.add(submittedChange);
-
-      addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
-
-      targetGit.commit().setMessage("test").call();
-
-      final Branch.NameKey targetBranchNameKey =
-          new Branch.NameKey(new Project.NameKey("target-project"),
-              sourceBranchNameKey.get());
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080");
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> subscribers =
-          new ListResultSet<>(Collections
-              .singletonList(new SubmoduleSubscription(targetBranchNameKey,
-                  sourceBranchNameKey, "source-project")));
-      expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
-          subscribers);
-
-      expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
-          .andReturn(targetRepository).anyTimes();
-
-      Capture<RefUpdate> ruCapture = new Capture<>();
-      gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
-          capture(ruCapture));
-      changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
-            anyObject(RefUpdate.class), EasyMock.<Account>isNull());
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
-          new ListResultSet<>(Collections
-              .singletonList(new SubmoduleSubscription(sourceBranchNameKey,
-                  targetBranchNameKey, "target-project")));
-      expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
-          incorrectSubscriptions);
-
-      schema.close();
-
-      final PersonIdent myIdent =
-          new PersonIdent("test-user", "test-user@email.com");
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
-              sourceRepository), urlProvider, schemaFactory, sourceRepository,
-              new Project(sourceBranchNameKey.getParentKey()), submitted,
-              mergedCommits, myIdent, repoManager, gitRefUpdated, null, changeHooks);
-
-      submoduleOp.update();
-
-      doVerify();
-      RefUpdate ru = ruCapture.getValue();
-      assertEquals(ru.getName(), targetBranchNameKey.get());
-    }
-  }
-
-  /**
-   * It calls SubmoduleOp.update considering only one insert on Subscriptions
-   * table.
-   * <p>
-   * It considers a commit containing a .gitmodules file was merged in
-   * refs/heads/master of a dest-project.
-   * </p>
-   * <p>
-   * The .gitmodules file content should indicate a source project called
-   * "source".
-   * </p>
-   *
-   * @param gitModulesFileContent The .gitmodules file content. During the test
-   *        this file is created, so the commit containing it.
-   * @param sourceBranchName The branch name of source project "pointed by"
-   *        .gitmodules file.
-   * @throws Exception If an exception occurs.
-   */
-  private void doOneSubscriptionInsert(final String gitModulesFileContent,
-      final String sourceBranchName) throws Exception {
-    final Branch.NameKey mergedBranch =
-        new Branch.NameKey(new Project.NameKey("dest-project"),
-            "refs/heads/master");
-
-    List<SubmoduleSubscription> subscriptionsToInsert = new ArrayList<>();
-    subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
-        new Branch.NameKey(new Project.NameKey("source"), sourceBranchName),
-        "source"));
-
-    doOnlySubscriptionInserts(gitModulesFileContent, mergedBranch,
-        subscriptionsToInsert);
-  }
-
-  /**
-   * It calls SubmoduleOp.update method considering scenario only inserting new
-   * subscriptions.
-   * <p>
-   * In this test a commit is created and considered merged to
-   * {@code mergedBranch} branch.
-   * </p>
-   * <p>
-   * The destination project the commit was merged is not considered to be a
-   * source of another project (no subscribers found to this project).
-   * </p>
-   *
-   * @param gitModulesFileContent The .gitmodules file content.
-   * @param mergedBranch The {@code Branch.NameKey} instance representing the
-   *        project/branch the commit was merged.
-   * @param extractedSubscriptions The subscription rows extracted from
-   *        gitmodules file.
-   * @throws Exception If an exception occurs.
-   */
-  private void doOnlySubscriptionInserts(final String gitModulesFileContent,
-      final Branch.NameKey mergedBranch,
-      final List<SubmoduleSubscription> extractedSubscriptions) throws Exception {
-    doOnlySubscriptionTableOperations(gitModulesFileContent, mergedBranch,
-        extractedSubscriptions, new ArrayList<SubmoduleSubscription>());
-  }
-
-  /**
-   * It calls SubmoduleOp.update method considering scenario only updating
-   * Subscriptions table.
-   * <p>
-   * In this test a commit is created and considered merged to
-   * {@code mergedBranch} branch.
-   * </p>
-   * <p>
-   * The destination project the commit was merged is not considered to be a
-   * source of another project (no subscribers found to this project).
-   * </p>
-   *
-   * @param gitModulesFileContent The .gitmodules file content.
-   * @param mergedBranch The {@code Branch.NameKey} instance representing the
-   *        project/branch the commit was merged.
-   * @param extractedSubscriptions The subscription rows extracted from
-   *        gitmodules file.
-   * @param previousSubscriptions The subscription rows to be considering as
-   *        existing and pointing as target to the {@code mergedBranch}
-   *        before updating the table.
-   * @throws Exception If an exception occurs.
-   */
-  private void doOnlySubscriptionTableOperations(
-      final String gitModulesFileContent, final Branch.NameKey mergedBranch,
-      final List<SubmoduleSubscription> extractedSubscriptions,
-      final List<SubmoduleSubscription> previousSubscriptions) throws Exception {
-    expect(schemaFactory.open()).andReturn(schema);
-
-    try (Repository realDb = createWorkRepository()) {
-      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
-      @SuppressWarnings("resource")
-      final Git git = new Git(realDb);
-
-      addRegularFileToIndex(".gitmodules", gitModulesFileContent, realDb);
-
-      final RevCommit mergeTip = git.commit().setMessage("test").call();
-
-      expect(urlProvider.get()).andReturn("http://localhost:8080").times(2);
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      expect(subscriptions.bySuperProject(mergedBranch)).andReturn(
-          new ListResultSet<>(previousSubscriptions));
-
-      SortedSet<Project.NameKey> existingProjects = new TreeSet<>();
-
-      for (SubmoduleSubscription extracted : extractedSubscriptions) {
-        existingProjects.add(extracted.getSubmodule().getParentKey());
-      }
-
-      for (int index = 0; index < extractedSubscriptions.size(); index++) {
-        expect(repoManager.list()).andReturn(existingProjects);
-      }
-
-      final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-      for (SubmoduleSubscription s : extractedSubscriptions) {
-        if (previousSubscriptions.contains(s)) {
-          alreadySubscribeds.add(s);
-        }
-      }
-
-      final Set<SubmoduleSubscription> subscriptionsToRemove =
-          new HashSet<>(previousSubscriptions);
-      final List<SubmoduleSubscription> subscriptionsToInsert =
-          new ArrayList<>(extractedSubscriptions);
-
-      subscriptionsToRemove.removeAll(subscriptionsToInsert);
-      subscriptionsToInsert.removeAll(alreadySubscribeds);
-
-      if (!subscriptionsToRemove.isEmpty()) {
-        expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-        subscriptions.delete(subscriptionsToRemove);
-      }
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      subscriptions.insert(subscriptionsToInsert);
-
-      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      expect(subscriptions.bySubmodule(mergedBranch)).andReturn(
-          new ListResultSet<>(new ArrayList<SubmoduleSubscription>()));
-
-      schema.close();
-
-      doReplay();
-
-      final SubmoduleOp submoduleOp =
-          new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb),
-              urlProvider, schemaFactory, realDb, new Project(mergedBranch
-                  .getParentKey()), new ArrayList<Change>(), null, null,
-              repoManager, null, null, null);
-
-      submoduleOp.update();
-    }
-  }
-
-  /**
-   * It creates and adds a regular file to git index of a repository.
-   *
-   * @param fileName The file name.
-   * @param content File content.
-   * @param repository The Repository instance.
-   * @throws IOException If an I/O exception occurs.
-   */
-  private void addRegularFileToIndex(final String fileName,
-      final String content, final Repository repository) throws IOException {
-    final ObjectInserter oi = repository.newObjectInserter();
-    AnyObjectId objectId =
-        oi.insert(Constants.OBJ_BLOB, Constants.encode(content));
-    oi.flush();
-    addEntryToIndex(fileName, FileMode.REGULAR_FILE, objectId, repository);
-  }
-
-  /**
-   * It creates and adds a git link to git index of a repository.
-   *
-   * @param fileName The file name.
-   * @param objectId The sha-1 value of git link.
-   * @param repository The Repository instance.
-   * @throws IOException If an I/O exception occurs.
-   */
-  private void addGitLinkToIndex(final String fileName,
-      final AnyObjectId objectId, final Repository repository)
-      throws IOException {
-    addEntryToIndex(fileName, FileMode.GITLINK, objectId, repository);
-  }
-
-  /**
-   * It adds an entry to index.
-   *
-   * @param path The entry path.
-   * @param fileMode The entry file mode.
-   * @param objectId The ObjectId value of the entry.
-   * @param repository The repository instance.
-   * @throws IOException If an I/O exception occurs.
-   */
-  private void addEntryToIndex(final String path, final FileMode fileMode,
-      final AnyObjectId objectId, final Repository repository)
-      throws IOException {
-    final DirCacheEntry e = new DirCacheEntry(path);
-    e.setFileMode(fileMode);
-    e.setObjectId(objectId);
-
-    final DirCacheBuilder dirCacheBuilder = repository.lockDirCache().builder();
-    dirCacheBuilder.add(e);
-    dirCacheBuilder.commit();
-  }
-
-  private static StringBuilder buildSubmoduleSection(final String name,
-      final String path, final String url, final String branch) {
-    final StringBuilder sb = new StringBuilder();
-
-    sb.append("[submodule \"");
-    sb.append(name);
-    sb.append("\"]");
-    sb.append(newLine);
-
-    sb.append("\tpath = ");
-    sb.append(path);
-    sb.append(newLine);
-
-    sb.append("\turl = ");
-    sb.append(url);
-    sb.append(newLine);
-
-    sb.append("\tbranch = ");
-    sb.append(branch);
-    sb.append(newLine);
-
-    return sb;
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/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/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/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/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index aea966a..6067442 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
@@ -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
@@ -327,16 +328,16 @@
     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,17 +440,17 @@
       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());
+      assertThat(approvals1).isEmpty();
+      assertThat(notesWithComments.comments).hasSize(1);
       notesWithComments.close();
 
       ChangeNotesParser notesWithApprovals =
@@ -455,8 +458,8 @@
       notesWithApprovals.parseAll();
       ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
           notesWithApprovals.buildApprovals();
-      assertEquals(1, approvals2.size());
-      assertEquals(1, notesWithApprovals.commentsForBase.size());
+      assertThat(approvals2).hasSize(1);
+      assertThat(notesWithApprovals.comments).hasSize(1);
       notesWithApprovals.close();
     } 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..49b61cc 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"
@@ -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
@@ -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..c7aabc9 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
@@ -285,7 +285,8 @@
   public void testUsernamePatternNonRegex() {
     allow(local, READ, DEVS, "refs/sb/${username}/heads/*");
 
-    ProjectControl u = util.user(local, "u", DEVS), d = util.user(local, "d", DEVS);
+    ProjectControl u = util.user(local, "u", DEVS);
+    ProjectControl 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());
   }
@@ -294,7 +295,8 @@
   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);
+    ProjectControl u = util.user(local, "d.v", DEVS);
+    ProjectControl 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());
   }
@@ -316,7 +318,8 @@
     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);
+    ProjectControl u = util.user(local, DEVS);
+    ProjectControl 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());
   }
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..32bc9c6 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,11 @@
 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.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
@@ -81,15 +83,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 +142,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;
   }
 
@@ -197,7 +217,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..fd36097 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,7 +15,6 @@
 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 java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
@@ -23,7 +22,9 @@
 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,12 +33,11 @@
 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;
@@ -51,11 +51,7 @@
 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.notedb.NotesMigration;
-import com.google.gerrit.server.project.CreateProject;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
@@ -63,12 +59,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;
@@ -81,32 +77,37 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+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());
+  }
+
+  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 GerritApi gApi;
-  @Inject protected IdentifiedUser.RequestFactory userFactory;
+  @Inject protected IdentifiedUser.GenericFactory userFactory;
   @Inject protected InMemoryDatabase schemaFactory;
   @Inject protected InMemoryRepositoryManager repoManager;
   @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 +123,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 +136,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);
+    final CurrentUser requestUser =
+        userFactory.create(Providers.of(db), requestUserId);
     return new RequestContext() {
       @Override
       public CurrentUser getCurrentUser() {
@@ -189,58 +191,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 +248,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 +271,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 +301,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 +325,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");
@@ -458,28 +432,29 @@
 
     Change change3 = newChange(repo, null, null, null, null).insert();
 
-    assertThat(query("topic:foo")).isEmpty();
-    assertResultEquals(change1, queryOne("topic:feature1"));
-    assertResultEquals(change2, queryOne("topic:feature2"));
-    assertResultEquals(change3, queryOne("topic:\"\""));
+    assertQuery("topic:foo");
+    assertQuery("topic:feature1", change1);
+    assertQuery("topic:feature2", change2);
+    assertQuery("topic:feature", change2, change1);
+    assertQuery("topic:\"\"", change3, change2, change1);
   }
 
   @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 +462,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 +527,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 +591,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 +660,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 +751,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 +830,32 @@
     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:3", change1);
+    assertQuery("added:>2", change1);
+    assertQuery("added:>=3", change1);
+    assertQuery("added:<1", change2);
+    assertQuery("added:<=0", change2);
 
-    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("deleted:>3");
+    assertQuery("deleted:2", change2);
+    assertQuery("deleted:>1", change2);
+    assertQuery("deleted:>=2", change2);
+    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 +873,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 +916,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 +925,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 +951,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 +969,98 @@
     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());
   }
 
   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 +1088,69 @@
 
     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 void assertBadQuery(Object query) throws Exception {
+    assertBadQuery(newQuery(query));
+  }
+
+  protected void assertBadQuery(QueryRequest query) throws Exception {
     try {
-      query(query);
+      query.get();
       fail("expected BadRequestException for query: " + query);
     } catch (BadRequestException e) {
       // Expected.
     }
   }
 
-  protected TestRepository<InMemoryRepository> createProject(String name)
-      throws Exception {
-    CreateProject create = projectFactory.create(name);
-    create.apply(TLR, new ProjectInput());
+  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 void assertQuery(Object query, Change... changes)
+      throws Exception {
+    assertQuery(newQuery(query), changes);
   }
 
-  protected List<ChangeInfo> query(Object query) throws Exception {
+  protected void assertQuery(QueryRequest query, Change... changes)
+      throws Exception {
+    assertThat(query(query)).named(query.toString())
+        .containsExactlyElementsIn(ids(changes)).inOrder();
+  }
+
+  protected List<Integer> query(Object query) throws Exception {
     return query(newQuery(query));
   }
 
-  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 List<Integer> query(QueryRequest query) throws Exception {
+    return FluentIterable.from(query.get())
+        .transform(new Function<ChangeInfo, Integer>() {
+          @Override
+          public Integer apply(ChangeInfo in) {
+            return in._number;
+          }
+        }).toList();
+  }
+
+  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 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..0feb800
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+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.
+  }
+}
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..d4398cd 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);
   }
 
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/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index d87888f..bd672f3 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,15 +184,16 @@
     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);
   }
 
   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());
 
@@ -214,13 +218,12 @@
                 projectNameCandidate.length() - Constants.DOT_GIT_EXT.length());
           }
           if (projectNameCandidate.equals(reposToBeFound.get(id))) {
-            expect(repoManager.list()).andReturn(
-                new TreeSet<>(Collections.singletonList(
-                    new Project.NameKey(projectNameCandidate))));
+            expect(projectCache.get(new Project.NameKey(projectNameCandidate)))
+                .andReturn(createNiceMock(ProjectState.class));
             break;
           } else {
-            expect(repoManager.list()).andReturn(
-                new TreeSet<>(Collections.<Project.NameKey> emptyList()));
+            expect(projectCache.get(new Project.NameKey(projectNameCandidate)))
+                .andReturn(null);
           }
         }
       }
@@ -229,10 +232,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..1f5b6cd 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
@@ -16,6 +16,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+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;
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..2219d28 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,12 @@
     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.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);
   }
 
   private final Config cfg;
@@ -117,6 +112,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 +134,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 +181,7 @@
       }
     });
     install(new DefaultCacheFactory.Module());
-    install(new SmtpEmailSender.Module());
+    install(new FakeEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
 
     IndexType indexType = null;
@@ -204,10 +205,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/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
index ddb86c3..0faa691 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
@@ -25,8 +25,8 @@
 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;
 import java.util.Map;
 
 class IndexVersionCheck implements LifecycleListener {
@@ -34,8 +34,8 @@
       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");
+  public static Path solrIndexConfig(SitePaths sitePaths) {
+    return sitePaths.index_dir.resolve("gerrit_index.config");
   }
 
   private final SitePaths sitePaths;
@@ -48,9 +48,9 @@
   @Override
   public void start() {
     // TODO Query schema version from a special meta-document
-    File file = solrIndexConfig(sitePaths);
+    Path path = solrIndexConfig(sitePaths);
     try {
-      FileBasedConfig cfg = new FileBasedConfig(file, FS.detect());
+      FileBasedConfig cfg = new FileBasedConfig(path.toFile(), FS.detect());
       cfg.load();
       for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
         int schemaVersion = cfg.getInt("index", e.getKey(), "schemaVersion", 0);
@@ -61,9 +61,9 @@
         }
       }
     } catch (IOException e) {
-      throw new ProvisionException("unable to read " + file);
+      throw new ProvisionException("unable to read " + path);
     } catch (ConfigInvalidException e) {
-      throw new ProvisionException("invalid config file " + file);
+      throw new ProvisionException("invalid config file " + path);
     }
   }
 
@@ -75,6 +75,6 @@
   private final String upgrade() {
     return "\nRun reindex to rebuild the index:\n"
         + "$ java -jar gerrit.war reindex -d "
-        + sitePaths.site_path.getAbsolutePath();
+        + sitePaths.site_path.toAbsolutePath();
   }
 }
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
index 78f5265..b9e47954 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
@@ -319,7 +319,7 @@
         doc.addField(name, value);
       }
     } else {
-      throw QueryBuilder.badFieldType(type);
+      throw FieldType.badFieldType(type);
     }
   }
 
@@ -327,7 +327,7 @@
   public void markReady(boolean ready) throws IOException {
     // TODO Move the schema version information to a special meta-document
     FileBasedConfig cfg = new FileBasedConfig(
-        solrIndexConfig(sitePaths),
+        solrIndexConfig(sitePaths).toFile(),
         FS.detect());
     for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
       cfg.setInt("index", e.getKey(), "schemaVersion",
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
index 38de6ee..0133e33 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
@@ -50,7 +50,6 @@
 
   @Override
   protected void configure() {
-    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     install(new IndexModule(threads));
     bind(ChangeIndex.class).to(SolrChangeIndex.class);
     listener().to(SolrChangeIndex.class);
@@ -61,6 +60,12 @@
 
   @Provides
   @Singleton
+  IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+    return IndexConfig.fromConfig(cfg);
+  }
+
+  @Provides
+  @Singleton
   public SolrChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 4774cb3..7bee47e 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -28,6 +28,7 @@
     '//lib/mina:core',
     '//lib/mina:sshd',
     '//lib/jgit:jgit',
+    '//lib/jgit:jgit-archive',
   ],
   provided_deps = [
     '//lib/bouncycastle:bcprov',
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..c38394a 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
@@ -338,8 +338,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..e453a4b 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,50 @@
   }
 
   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 e) {
+            logBadKey(path, line, e);
+          } catch (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..33ffb47 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
@@ -135,12 +135,14 @@
  * <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
@@ -443,7 +445,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 +456,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++) {
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/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..7dc558e 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
@@ -109,7 +109,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,7 +125,8 @@
   @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;
@@ -181,7 +183,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 f75eb2b..2d071c7 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
@@ -70,15 +70,23 @@
     // Honor the legacy hyphenated forms as aliases for the non-hyphenated forms
     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
+    // The following commands can only be run 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 {
       command("git-receive-pack").to(Commands.key(git, "receive-pack"));
       command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
       command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
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..d02fb4c 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
@@ -46,6 +46,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 +88,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/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..15e030f 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;
@@ -110,7 +110,7 @@
               db, true).filter(repo.getRefDatabase().getRefs(ALL), false);
 
       for (final String ref : refsMap.keySet()) {
-        if (!onlyRefsHeads || ref.startsWith(Branch.R_HEADS)) {
+        if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
           stdout.println(ref);
         }
       }
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/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 8f26bba..6a71176 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
@@ -758,7 +758,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/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/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
new file mode 100644
index 0000000..929f7ea
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -0,0 +1,226 @@
+// 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");
+      }
+
+      try {
+        // The archive is sent in DATA sideband channel
+        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();
+        sidebandOut.close();
+      } catch (GitAPIException e) {
+        throw new Failure(7, "fatal: git api exception, " + e);
+      }
+    } catch (Failure f) {
+      // Report the error in ERROR sideband channel
+      SideBandOutputStream sidebandError =
+          new SideBandOutputStream(SideBandOutputStream.CH_ERROR,
+              SideBandOutputStream.MAX_BUF, out);
+      sidebandError.write(f.getMessage().getBytes(UTF_8));
+      sidebandError.flush();
+      sidebandError.close();
+      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-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/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-war/pom.xml b/gerrit-war/pom.xml
index 0fb1c9a..17e8286 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.11.1</version>
+  <version>2.12-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
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..60f389e 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,18 +37,18 @@
   }
 
   @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 {
       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:
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 b365e76..3bc8b58 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
@@ -82,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;
@@ -105,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;
@@ -126,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) {
@@ -215,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);
@@ -267,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);
         }
       });
diff --git a/lib/BUCK b/lib/BUCK
index a880f06..d91ea26 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -45,8 +45,8 @@
 
 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',
 )
 
@@ -188,8 +188,8 @@
 
 maven_jar(
   name = 'truth',
-  id = 'com.google.truth:truth:0.25',
-  sha1 = '503ba892e8482976b81eb2b2df292858fbac3782',
+  id = 'com.google.truth:truth:0.26',
+  sha1 = 'b5802815625d82f39c33219299771f3d64301b06',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':guava',
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 7eb70c1..f06c662 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")
@@ -99,9 +96,9 @@
       UnsupportedEncodingException, FileNotFoundException {
     RAMDirectory directory = new RAMDirectory();
     IndexWriterConfig config = new IndexWriterConfig(
-        LUCENE_VERSION,
         new StandardAnalyzer(CharArraySet.EMPTY_SET));
     config.setOpenMode(OpenMode.CREATE);
+    config.setCommitOnClose(true);
     IndexWriter iwriter = new IndexWriter(directory, config);
     for (String inputFile : inputFiles) {
       File file = new File(inputFile);
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..57d93b3 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'],
 )
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index dcefb64..9dc2e73 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.0.201505050340-m2'
+REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
+VERS = '4.0.0.201505191015-rc1.19-g1773002'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '1cc3120d39ed2b55584e631634e65c5d2e6c1cf7',
-  src_sha1 = '425f578cc9d5ccb8f3b050a5ab1e2d7a0becb25d',
+  bin_sha1 = '4db24b39dab8dc0e889807383728032945f461be',
+  src_sha1 = '1723a2855f50493b7c0b216aae97909a7ea59962',
   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 = '2a9f55d1d92afef795542b995db6ab261007857f',
+  sha1 = '7bfdbddea56a87f3f2687ae6abf2c5bdae649f0c',
   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 = 'ee3954753067818f8f734981a01c13ac33425f2c',
+  sha1 = '08fce6b89f6d1e78f99869d542d70899f3be9c9f',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit',
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '6cc19f8f0a1791e26d4225625ecba6a31d9b830e',
+  sha1 = 'a54c16076e6cbdb9113565a82cffa5f268ae8e3b',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
diff --git a/lib/joda/BUCK b/lib/joda/BUCK
index d45ce78..2401870 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.7',
+  sha1 = '5599707a3eaad13e889f691b3af78c8c03842195',
   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..68d579d 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.10.2'
+VERSION = '5.1.0'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'c01e3d675d277e0a93e7890d03cc3246b2cdecaa',
+  sha1 = '93e64c67106f9a50e6ea01cfcfd6ac692ab3a41a',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -16,8 +16,33 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = 'f977f8c443e8f4e9d1fd7fdfda80a6cf60b3e7c2',
+  sha1 = '54770d9b792536dff25ae1d70cd8af822c0079a3',
   license = 'Apache2.0',
+  deps = [':core'],
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'backward-codecs',
+  id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
+  sha1 = '5f0c5bb10ac3facace6b314bb02a6b572795b3c9',
+  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 = '3b700fa57f5d444da0e58cc1855042e6c5a18640',
+  license = 'Apache2.0',
+  deps = [':core'],
   exclude = [
     'META-INF/LICENSE.txt',
     'META-INF/NOTICE.txt',
@@ -27,6 +52,11 @@
 maven_jar(
   name = 'query-parser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'd70f54e1060d553ba7aeb4d49a71fd0c068499e8',
+  sha1 = '53f0b3f0e700a8ec484195d3370688171e830634',
   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 4edba9c..cc45212 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/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..0cbe10e 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;
@@ -63,7 +63,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 +75,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 +84,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/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 17b63c1..a93641d 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 17b63c160498d02fb1c511c5b43b02f538b29558
+Subproject commit a93641db50f52e24421f75671bb1d4df268dd722
diff --git a/plugins/download-commands b/plugins/download-commands
index baa09c2..1cf6921 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit baa09c2e265a2b264a5fb4571e7eefda04def0c4
+Subproject commit 1cf69212a7489e88d8c73377f0f77f8a5965db75
diff --git a/plugins/replication b/plugins/replication
index 53ee1b8..f03316c 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 53ee1b8ec4de5de4d710233eda2230b5380f1390
+Subproject commit f03316c26b5991cba96280de59a4119619a18a58
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 603b7b3..b5c6f81 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 603b7b3885a1d2953ca891f58d2a6095e72e5313
+Subproject commit b5c6f81d979e78fbd734d946b18270ec8319eaf6
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 30518c4..14bc9ce 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -18,6 +18,7 @@
 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
 
 # Set defaults on java rules:
@@ -63,7 +64,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)
 
@@ -129,7 +131,7 @@
     type = 'plugin',
     visibility = ['PUBLIC']):
   from multiprocessing import cpu_count
-  mf_cmd = 'v=\$(git describe HEAD);'
+  mf_cmd = 'v=%s;' % git_describe(name)
   if manifest_file:
     mf_src = [manifest_file]
     mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT'
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..0247e32 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(plugin = 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 plugin or plugin == '${pluginName}':
+    p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+  else:
+    p = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = 'plugins/%s' % plugin)
   v = p.communicate()[0].strip()
   r = p.returncode
   if r != 0: